Zpět Obsah Další

GUI a InterfaceBuilder — příklad


V minulé části našeho volného seriálu o programování v API Cocoa jsme dokončili přehledný popis InterfaceBuilderu a slíbili si trochu složitější příklad. Dnes si jej tedy ukážeme, spolu s využitím speciálních "pseudoobjektů", jako je First Responder nebo File's Owner: připravíme velmi jednoduchý editor, který dokáže využívat doplňování podle slovníku.

Editor

Nejprve připravíme samotný editor. Díky flexibilitě a síle standardních knihovních služeb Cocoa to je práce na pár minut a na několik programových řádků. My se zde samozřejmě podrobně soustředíme na práci v InterfaceBuilderu, a ostatní činnosti popíšeme jen natolik, aby bylo jasné co se vlastně děje.

Nejprve spustíme ProjectBuilder a vyžádáme si vytvoření nového projektu typu "Cocoa Document-based Application". ProjectBuilder nám ušetří spoustu práce tím, že automaticky vytvoří základní kostru aplikace a umístí do ní potřebné soubory — včetně dvou NIBů: MainMenu.nib obsahuje hlavní menu a je centrálním NIBem celé aplikace; MyDocument.nib obsahuje GUI vztahující se k dokumentu — především tedy okno, ve kterém dokument bude zobrazován.

Malá odbočka: proč jsou NIBy navrženy právě takto? To je jednoduché: hlavní NIB se zavede hned při spuštění aplikace; dokumentový NIB se naproti tomu zavede vždy, když otvíráme nový dokument. Tak vlastně dostaneme úpně "zadarmo" to, že pro každý dokument se vytvoří jeho vlastní sada GUI objektů.

"File's Owner" pro dokumentový NIB je vždy instance speciální třídy, která representuje controller dokumentu (připomeňme strukturu "MVC", o které jsme se bavili v předminulém dílu: model representuje data, view jsou prvky uživatelského rozhraní uložené v NIBu, a controller je logika aplikace, jež svazuje model a view dohromady). Tuto třídu pro nás ProjectBuilder už také připravil a nazval ji MyDocument — samozřejmě ji můžeme podle libosti přejmenovat. Protože budeme v dokumentu potřebovat textový editor, přidáme do interface třídy nový outlet "text" — to vidíme na prvním obrázku, spolu s oknem ProjectBuilderu.

Povšimněte si v levé části okna seznamu souborů, jež jsou součástí projektu: vedle zdrojových textů třídy MyDocument zde vidíme oba NIBy a pár dalších pomocných souborů. Jen pro zajímavost — např. soubor Credits.rtf usnadňuje tvorbu standardního panelu "o aplikaci": součástí menu v MainMenu.nib už je odpovídající příkaz, nastavený tak, že otevře standardní panel, v němž se zobrazí ikona aplikace, její jméno a verse, a v samostatném poli obsah formátovaného textového souboru Credits.rtf: žádné programování není zapotřebí...

Zpět k naší práci: po přidání outletu "text" otevřeme poklepáním MyDocument.nib, a v InterfaceBuilderu do jeho okna přetáhneme z palety připravený objekt třídy NSTextView; to vidíme na druhém obrázku.

Za povšimnutí stojí modré čáry, jimiž InterfaceBuilder automaticky vyznačí ideální polohu objektu v okně, aby vzhled aplikace odpovídal standardům Mac OS X: samozřejmě, že můžeme tuto "nápovědu" ignorovat a objekt umístit kamkoli, ale za normálních okolností je to významná pomoc.

Z okna ProjectBuilderu vhodíme soubor MyDocument.h do okna InterfaceBuilderu (aby věděl o novém outletu). Pak textový objekt roztáhneme přes celé okno, určíme jeho atributy pomocí inspektoru, a technikou, již známe už z minulého dílu, jej navážeme na outlet "text" ve File's Owneru:

Jak už víme, vlastníkem dokumentového NIBu je vždy instance třídy, jež je controller daného typu dokumentů, v našem případě tedy právě MyDocument.

Pro dokončení editoru už stačí přidat jen šest programových řádků, v nichž určíme způsob, jakým se budou dokumenty ukládat na disk a načítat z disku. Prvním je pomocná proměnná "loaded" typu NSString*. kterou přidáme mezi proměnné instance MyDocument; zbývajících pět doplníme do připravených metod ve zdrojovém souboru MyDocument.m — přidané řádky jsou označeny tučně:

- (NSData *)dataRepresentationOfType:(NSString *)aType { return [[text string] dataUsingEncoding:NSUnicodeStringEncoding]; } - (BOOL)loadDataRepresentation:(NSData *)data ofType:(NSString *)aType { loaded=[[NSString alloc] initWithData:data encoding:NSUnicodeStringEncoding]; return loaded!=nil; } - (void)windowControllerDidLoadNib:(NSWindowController *) aController { [super windowControllerDidLoadNib:aController]; if (loaded) [text setString:[loaded autorelease]]; }

Podrobnostem třídy NSDocument, již zde vlastně využíváme (podívejte se znovu na obrázek

kde je vidět, že MyDocument je její podtřída) se budeme věnovat později; zatím proto jen telegraficky:

Tím jsme dokončili prakticky kompletní textový editor (jediná ze základních služeb, jež zatím nefunguje, je vyhledávání; jinak díky skvěle navrženému objektovému systému Cocoa funguje vše, od práce se soubory a se schránkou přes korektor pravopisu až po Services).

Slovník

Nyní k editoru přidáme slovník pro automatické doplňování. Nejprve si připravíme model slovníku, tj. implementaci jeho služeb: přidáme do projektu soubory Dict.h a Dict.m, a začneme tím, že v souboru Dict.h navrhneme interface, tj. seznam služeb. Vzhledem k tomu, že jde o velmi jednoduchou úlohu, bude i rozhraní triviální — ani nebudeme připravovat instanci, sestavíme rozhraní jen z metod třídy:

 @interface Dict : NSObject
 +(NSString*)wordForPrefix:(NSString*)prefix; // najít slovo pro daný prefix
 +(void)addWord:(NSString*)word; // přidat do slovníku nové slovo
 +(void)removeWord:(NSString*)word; // odebrat ze slovníku slovo
 @end

Konkrétní implementace není pro náš příklad důležitá; vzhledem k tomu, že včetně ukládání slovníku do aplikačních předvoleb nezabere ani třicet programových řádků, umístili jsme ji pro případné zájemce až na konec textu. Klidně ji však můžete ignorovat — z hlediska toho, čím se v tomto článku zabýváme, není nikterak důležitá.

Vazba slovníku na GUI

Ačkoli slovník jako takový máme hotový, je třeba ještě napsat pár řádků, které zajistí jeho vazbu na grafické rozhraní: asi budeme chtít, aby se automaticky hledalo slovo k prefixu, který jsme právě napsali — je tedy zapotřebí se v objektu "text" podívat kde je kursor, najít (částečné) slovo vlevo od něj, a to použít jako hledaný prefix... Kromě toho musíme do menu přidat příkazy "Complete" a "Add Word", a nějakým způsobem je připojit k odpovídajícím službám.

Na to právě využijeme "First Responder": otevřeme hlavní NIB, pomocí inspektoru tříd do First Responderu přidáme služby dictWordAdd: a dictWordComplete:, a do menu — standardním "nataháním" z palety — přidáme nové podmenu s patřičnými příkazy. Pak už jen "nadrátujeme" položky menu na odpovídající služby First Responderu; spojení položky "Add Word" se službou dictWordAdd: ilustruje čtvrtý obrázek:

Uvědomme si, co jsme právě zařídili: kdykoli uživatel vybere položku menu "Add Word", najde systém view (ve smyslu MVC) objekt, ve kterém je právě kursor (tj. First Responder), a pokusí se mu poslat zprávu dictWordAdd:. Pokud to není možné (protože objekt, ve kterém je kursor, takové zprávě nerozumí), zkouší systém postupně další objekty — především jeho controller. V našem případě se tedy systém pokusí nejprve předat zprávu objektu NSTextView, který jsme umístili do okna v InterfaceBuilderu; pokud se to nepodaří, předá zprávu odpovídajícímu objektu MyDocument.

Pro dokončení editoru tedy stačí implementovat metody dictWordAdd: a dictWordComplete: ve třídě MyDocument. Možností je samozřejmě řada; my jsme zvolili nekomplikovanou implementaci, využívající pomocné metody _currentWord, která nalezne vhodné slovo:

 -(NSString*)_currentWord {
     NSRange selection=[text selectedRange];
     if (selection.length==0) {
         if (selection.location>0) selection.location--;
         selection=[[text textStorage] doubleClickAtIndex:selection.location];
         [text setSelectedRange:selection];
     }
     if (selection.length==0) return nil;
     return [[text string] substringWithRange:selection];
 }

Metoda je poměrně jednoduchá: nejprve zjistíme je-li již nějaký text označen. Není-li tomu tak (selection.length==0), pokusíme se najít slovo vlevo od kursoru: vyžádáme si od textového systému Cocoa (textStorage) vyhledání slova, které by bylo označeno, kdybychom myší poklepali vlevo vedle kursoru (doubleClickAtIndex:; pro "vlevo" je tam selection.location--). Nalezené slovo označíme (setSelectedRange:). Pak již jen vrátíme označené slovo, je-li jaké — ať již jsme jej označili programově, nebo bylo označeno uživatelem (substringWithRange:).

Implementace vlastních akcí dictWordAdd: a dictWordComplete: je pak už triviální, a měla by být srozumitelná i bez bližšího vysvětlení:

 -(void)dictWordComplete:sender {
     NSString *s=[self _currentWord];
     if (s && (s=[Dict wordForPrefix:s])) // mám prefix && slovo ze slovníku
         [text replaceCharactersInRange:[text selectedRange] withString:s];
 }
 -(void)dictWordAdd:sender {
     NSString *s=[self _currentWord];
     if (s) [Dict addWord:s];
 }

To je všechno — můžeme aplikaci zbuildovat a vyzkoušet. Editor normálně funguje; libovolné slovo můžeme přidat do slovníku tak, že jej označíme, a vyvoláme příkaz "Add Word" z menu. Pak stačí jen napsat prefix některého ze slov ze slovníku a vyvolat příkaz "Complete", a slovo se korektně doplní.

Díky tomu, že jsme příkazy v hlavním NIBu navázali na First Responder, to funguje korektně v kterémkoli dokumentovém okně, ve kterém je právě kursor, ať jich máme otevřených najednou sebevíc: prostřednictvím First Responderu se zprávy dictWordAdd: a dictWordComplete: pošlou vždy patřičné instanci třídy MyDocument.

First Responder a Cocoa toho umějí ještě víc!

Doplňování nám tedy funguje korektně ve všech dokumentech. Ale co jinde? Dejme tomu, že někdo otevře standardní panel korektoru pravopisu, a pokusí se použít doplňování podle slovníku v jeho textovém poli:

máme problém, tam to nefunguje! Totéž platí pro textová pole panelů pro otvírání a ukládání souborů; podobný problém také nastane, pokud aplikaci rozšíříme o další panely, třeba o panel pro vyhledávání, nebo panel předvoleb...

Je vůbec možné zařídit, aby služby "Add Word" a "Complete" fungovaly kdekoli, kde zrovna vkládáme text — i kdyby to náhodou bylo textové pole, které je součástí standardního systémového kódu (jako třeba panel pro otvírání souborů, nebo panel korektoru)? Inu, v klasických API postavených na statických jazycích jako C++ nebo Java by to asi byl neřešitelný problém. V Cocoa je to ale díky dynamické podstatě Objective C a díky skvělému designu Frst Responderu snadné: dokážeme to za dvě minutky!

Především si znovu uvědomíme, co jsme si říkali v minulém odstavci: "... kdykoli uživatel vybere položku menu "Add Word", najde systém view (ve smyslu MVC) objekt, ve kterém je právě kursor (tj. First Responder), a pokusí se mu poslat zprávu dictWordAdd:...". Pokud tedy dokážeme zařídit, aby standardní systémový view objekt NSTextView — kdekoli je použit — rozuměl zprávám dictWordAdd: a dictWordComplete:, máme vyhráno.

To ale v Objective C není nic těžkého — vzpomeňme si na třetí díl našeho seriálu, ve kterém jsme si ukazovali možnosti jazyka Objective C; tehdy jsme si také ukázali kategorie, které umožňují přidávat další, nové služby k již existujícím třídám. Úplně proto stačí připravit kategorii třídy NSTextView, pomocí příkazů "Cut" a "Paste" do ní přenést implementaci metod _currentWord, dictWordAdd: a dictWordComplete: ze třídy MyDocument, a příkazem "Find" nahradit odkazy na outlet "text" odkazem na sebe sama (self):

 @implementation NSTextView (MyWordCompletionCategory)
 -(NSString*)_currentWord {
     NSRange selection=[self selectedRange];
     if (selection.length==0) {
         if (selection.location>0) selection.location--;
         selection=[[self textStorage] doubleClickAtIndex:selection.location];
         [self setSelectedRange:selection];
     }
     if (selection.length==0) return nil;
     return [[self string] substringWithRange:selection];
 }
 -(void)dictWordComplete:sender {
     NSString *s=[self _currentWord];
     if (s && (s=[Dict wordForPrefix:s])) // mám prefix && slovo ze slovníku
         [self replaceCharactersInRange:[self selectedRange] withString:s];
 }
 - (void)dictWordAdd:sender {
     NSString *s=[self _currentWord];
     if (s) [Dict addWord:s];
 }
 @end

To je celé — stačí aplikaci znovu zbuildovat, a vše funguje: doplňovat můžeme kdekoli, v kterémkoli panelu, v jakémkoli textovém poli. Samozřejmě, že stále funguje doplňování i v dokumentech — i ty využívají standardní třídy NSTextView.

To je pro dnešek vše...

...příště se pustíme do přehledu tříd Application Kitu.

Implementace třídy Dict:

 @implementation Dict
 static NSMutableSet *_set=nil;
 +(void)_aboutToQuit {
     [[NSUserDefaults standardUserDefaults] setObject:[_set allObjects] forKey:@"Dictionary"];
 }
 +(NSMutableSet*)_sharedSet {
     if (!_set) {
         NSUserDefaults *df=[NSUserDefaults standardUserDefaults];
         _set=[[NSMutableSet setWithArray:[df arrayForKey:@"Dictionary"]] retain];
         // zajistíme, aby se slovník uložil před ukončením aplikace:
         [[NSNotificationCenter defaultCenter] addObserver:self
            selector:@selector(_aboutToQuit)
            name:NSApplicationWillTerminateNotification object:NSApp];
     }
     return _set;
 }
 +(void)addWord:(NSString*)word {
     [[self _sharedSet] addObject:word];
 }
 +(void)removeWord:(NSString*)word {
     [[self _sharedSet] removeObject:word];
 }
 +(NSString*)wordForPrefix:(NSString*)prefix {
     NSEnumerator *en=[[self _sharedSet] objectEnumerator];
     NSString *s;
     while (s=[en nextObject])
         if ([s hasPrefix:prefix]) return s;
     return nil;
 }
 @end


Zpět Obsah Další

Copyright © Chip, O. Čada 2000-2003