Zpět | Obsah | Další |
Dnes se podíváme trochu blíž na to, jak se v Kakau vytváří grafické uživatelské rozhraní, a seznámíme se s nesmírně pohodlným prostředkem, který k tomu slouží — s InterfaceBuilderem. Zběžně jsme si jej ukázali již v jednom z předcházejících dílů; tentokrát si však řekneme daleko více podrobností.
Nejprve si vysvětlíme základy, na nichž je vazba mezi kódem aplikace a jejím uživatelským rozhraním postavena: jde jen o dvě kouzelná slůvka — "outlet" a "akce". O co jde? Je to vlastně hrozně jednoduché:
Outlet je odkaz na jiný objekt, tj. de facto jakákoli proměnná typu id nebo ukazatel na nějakou třídu. Deklarujeme jej jednoduše — prostě v interface třídy vytvoříme odpovídající proměnnou, a to je vše:
@interface MyClass:...
{
id text;
IBOutlet NSWindow *window;
...
}
...
Speciální slovo IBOutlet nemá v jazyce Objective C žádný význam (ve standardních headerech je prostě definováno jako prázné makro, takto:
#define IBOutlet
slouží jen pro InterfaceBuilder, aby snadno rozeznal outlety od ostatních proměnných).
Smyslem "outletů" je to, že je můžeme snadno — a bez jakéhokoli programování! — navázat na libovolné prvky grafického uživatelského rozhraní. Za chvilku si ukážeme jak se to doopravdy dělá; nejprve si blíž vysvětlíme jejich použití.
V našem příkladu jsme definovali dva outlety, text — který může obsahovat odkaz na objekt libovolné třídy — a window, který by měl obsahovat odkaz na nějaké okno. Dejme tomu, že text navážeme na nějaké editační pole. Pak již můžeme s oběma prvky uživatelského rozhraní přímo a bez nejmenších obtíží pracovat — třeba takto:
...
[text setStringValue:@"Ahoj, napiš sem svoje jméno"]; // nastavíme obsah pole
[window makeKeyAndOrderFront:self]; // okno "vytáhneme" do popředí
...
user=[text stringValue]; // načteme momentální obsah pole
...
Zatímco outlety nabízejí velmi pohodlný přístup z kódu k libovolným prvkům uživatelského rozhraní, úkolem akcí je zabezpečit opačný směr — převést "události" uživatelského rozhraní (stisknutí tlačítka, volbu položky menu apod.) na vhodné události v kódu. Je tedy zřejmé, že akce budou nejspíš metody, a skutečně je tomu tak — jedná se o metody, jež mají právě jeden argument typu "objekt" (id). Stejně jako outlety, i akce prostě jen deklarujeme v hlavičkovém souboru:
@interface MyClass:...
...
-(void)showLoginPanel:sender;
-(void)login:sender;
...
Nyní můžeme — opět bez jakéhokoli programování! — vytvořit třeba položku menu "Login", tu připojit k akci showLoginPanel:, a do okna z minulého příkladu přidat tlačítko, jež připojíme k akci login:. Podobně, jako propojení outletů s objekty zajistí, že proměnná (representující outlet) je automaticky inicializována správnou hodnotou, připojení akce zařídí, že kdykoli uživatel aktivuje daný prvek, automaticky se vyvolá patřičná metoda. Pokud tedy např. připravíme implementaci
@implementation MyClass
...
-(void)showLoginPanel:sender {
[window makeKeyAndOrderFront:self];
}
-(void)login:sender {
user=[text stringValue];
[window performClose:self];
}
...
@end
máme vlastně naprogramované jednoduché UI: kdykoli uživatel zvolí položku menu "Login", zobrazí se v popředí okno. V něm je textové pole, jehož obsah může měnit, a tlačítko; jakmile stiskne toto tlačítko, obsah textového pole se uloží do proměnné user, a okno se opět zavře (jelikož se provede metoda login:).
Je načase si ukázat, jak se navazují outlety a akce na prvky uživatelského rozhraní. Je to opravdu jednoduchoučké — slouží k tomu natahování "drátů" uvnitř aplikace InterfaceBuilder. Podívejte se na obrázky — nejprve připojení outletu text:
Prostě jsme myší "natáhli drát" od objektu třídy MyClass na požadované textové pole v okně "Login Window", a v inspektoru u pravého okraje obrázku jsme určili, že toto propojení se týká outletu text. Přesně stejně bychom propojili druhý outlet s oknem: "natáhneme drát" opět z objektu MyClass, skončíme tentokrát ne nad textovým polem, ale nad oknem, a v inspektoru zvolíme outlet window. To je celé.
Akce se "drátují" velmi podobně — podívejte se na další obrázek:
Drát jsme tentokrát "vytáhli" z položky menu "Login", a skončili jsme na objektu třídy MyClass; v inspektoru jsme vybrali požadovanou akci (showLoginPanel:)— a je to. V levém sloupci inspektoru je, mimochodem, seznam všech možných "událostí" objektu uživatelského rozhraní, nad kterým jsme začali "drátovat". U jednoduchých objektů jako je tlačítko menu je zde jediná možnost, jež se obvykle jmenuje target, složitější objekty však mohou vyvolat "událostí" více — a my můžeme různé události spojit s různými akcemi.
Opět stejným způsobem připojíme tlačítko v okně k akci login:, jistě je zřejmé jak: natáhneme "drát" od tlačítka nad objekt MyClass, a v inspektoru vybereme akci. Tím jsme hotovi — pokud nyní aplikaci spustíme, bude vše korektně fungovat. V příštích několika odstavcích si odpovíme na několik otázek, jež asi pozorný čtenář klade...
Čtenář, který si dobře pamatuje základy Objective C, určitě má pocit, že tu něco chybí: kde, kdy a jak vznikne ten objekt třídy MyClass, na který jsme vše "drátovali"? Objekty přece vznikají tak, že zavoláme patřičnou metodu odpovídající třídy — my ale nic takového nedělali, máme jen interface a implementaci třídy MyClass a pár obrázků v InterfaceBuilderu, to přece nestačí?!?
Inu, stačí. Museli jsme udělat jen dvě věci:
Na vše ostatní stačí dynamický objektový systém — v C++ by to dost dobře nešlo, ale v Objective C (nebo v Javě, kterou InterfaceBuilder také podporuje) není nic snazšího: po spuštění aplikace se automaticky zavede objektová síť, připravená v InterfaceBuilderu. Její součástí je také informace "tady má být objekt třídy jménem 'MyClass', jeho outlet jménem 'window' se má inicializovat na odkaz na tohle okno,...". V dynamickém objektovém systému není problém za běhu vytvořit objekt třídy, jejíž jméno známe, nebo zapsat hodnotu do property daného jména — a přesně to se stane. Tak se vytvoří všechny potřebné objekty a naváží všechny vazby, dříve než se aplikace rozběhne (tj. než se spustí event loop, dobře skrytý uvnitř standardních knihoven, takže se o něj nemusíme nijak starat).
Stojí za to si uvědomit zásadní rozdíl mezi tímto přístupem, a na první pohled trochu podobnými službami "resource editorů", známých např. z Mac OS 9-. Ani InterfaceBuilder ani standardní knihovny neobsahují vůbec žádnou podporu pro práci s objekty třídy MyClass; stačilo určit jméno třídy, jména outletů a jména akcí. O vše ostatní se postará dynamický objektový systém. Pokud bychom však chtěli v klasickém resource editoru pracovat s objekty nějaké třídy, museli bychom
jako např. okno, menu, tlačítko, textové pole? Krátká odpověď: nalezli jsme je v "paletě" — okénku InterfaceBuilderu, obsahujícím standardní objekty GUI, a myší jsme je "naházeli" tam, kam to bylo zapotřebí.
Nu dobrá, myslíte si asi, ale co to je paleta? Žádný zázrak — jde jen o balíček, který obsahuje pro libovolné množství tříd
Z hlediska koncepce InterfaceBuilderu je podstatný vlastně jen první bod, ten odlišuje InterfaceBuilder od všech rádobydynamických editorů všech ostatních systémů tzv. visuálního programování. Druhý a třetí bod nepřinášejí nic principiálně nového; v praxi jsou ovšem nesmírně důležité, protože tvorbu uživatelského rozhraní mnohonásobně usnadňují.
Tak se patrně zeptá uživatel VisualBASICu: k čemu to? Vždyť bychom přeci mohli rovnou u tlačítka implementovat jeho metodu click:, namísto psaní metody v nějakém objektu MyClass a drátování!
Inu, mohli bychom, jenže bychom si tím (a) dost podstatně pokazili strukturu aplikace — a to by se nám zle vymstilo ve složitějších případech — a (b) bychom se připravili o nesmírně flexibilní nástroj. Podívejme se na oba body postupně:
Rozumná struktura (víceméně jakéhokoli) objektového systému se dá vyjádřit zkratkou MVC — Model, View, Controller. Model representuje data, View jsou prvky uživatelského rozhraní, a Controller je logika aplikace, jež svazuje "model" a "view" dohromady. To je ale přesně to, co děláme v InterfaceBuilderu: navazujeme prvky "view" pomocí "drátů" na outlety a akce "controlleru", jímž je zde právě objekt MyClass (a který už sám pracuje s "modelem" podle potřeby). "VisualBasicový" přístup toto rozlišení neumožňuje — v něm jsou prvky "contolleru" promíchány s prvky "view", jinými slovy, metoda click: je implementována přímo jako součást tlačítka.
A ta flexibilita? Ta je dána tím, že můžeme "natáhnout" drát jakkoli potřebujeme, a třeba i více drátů může "skončit" na jediné akci. Dejme tomu, že by se login panel měl otvírat nejen příkazem menu, ale také tlačítkem v nějakém toolbaru. "VisualBasicově" bychom museli programovat dvě různé metody click:, jednu u položky menu, druhou u tlačítka toolbaru; v InterfaceBuilderu neprogramujeme vůbec nic, jen natáhneme dva dráty — jeden z položky menu, druhý z tlačítka, oba skončí na "controlleru" a vyberou stejnou akci, showLoginPanel:.
"Drátovací" logika InterfaceBuilderu nám kromě toho velmi často dokáže programování úplně uspořit! Podívejme se znovu na náš triviální příklad — celou metodu showLoginPanel: jsme programovali úplně zbytečně, a pohodlně bychom se obešli bez ní. Nic nám přeci nebrání "drát" natáhnout od položky menu (a klidně i od tlačítka toolbaru a čehokoli dalšího) přímo na okno, a rovnou zvolit v inspektoru metodu makeKeyAndOrderFront: — místo abychom její odeslání programovali. Podívejte se na další obrázek:
Tím jsme se, mimochodem, dostali k odpovědi na další otázku, která zřejmě pozorným čtenářům už nějakou dobu vrtá hlavou: k čemu jsou dobré u "akcí" ty argumenty sender? Nu, právě pro rozlišení akcí, odeslaných "po různých drátech". Představte si, že v okně máme dvě různá tlačítka, třeba "Login" a "Login special"; obě dělají skoro totéž, ale ne úplně totéž. Samozřejmě, mohli bychom pro každé vytvořit samostatnou akci, a obě tyto akce by volaly nějaký společný kód, nějak takto:
@implementation MyClass
...
-(void)_login:(BOOL)special {
...
}
-(void)login:sender {
[self _login:NO];
}
-(void)loginSpecial:sender {
[self _login:YES];
}
...
@end
Většinou se to tak skutečně dělá, ale jsou případy, kdy to není vhodné, a potřebujeme namísto toho "nadrátovat" obě tlačítka na jednu společnou akci, jež sama rozliší, které z tlačítek ji vyvolalo. Díky argumentu "sender", který obsahuje odesilatele zprávy, je to snadné:
@implementation MyClass
...
-(void)login:sender {
BOOL special=[[sender title] isEqual:@"Login special"];
...
}
...
@end
Poznamenejme, že v praxi bychom se samozřejmě neptali na titulek tlačítka (protože to by nám zkomplikovalo lokalizaci aplikace), ale použili bychom některou z mnoha jiných možností identifikace, jež systém s InterfaceBuilderem nabízí — na jejich podrobný popis nemáme místo, ale aspoň jednu si stručně ukázat můžeme. Mohli bychom snadno připravit ještě jeden outlet specialButton pro "speciální" tlačítko, a v metodě login: prostě napsat "special=sender==specialButton"...
Dnešní díl uzavřeme tím, že si ještě vysvětlíme, kam a jak se vlastně ty "obrázky" z InterfaceBuilderu ukládají. Nejprve "kam": do tzv. NIB souborů; NIB je zkratka za "NeXT Interface Builder". Tyto soubory jsou součástí projektu, a při buildování jsou bez jakékoli změny uloženy přímo do vytvořené aplikace.
Standardní aplikační kód — o který se nemusíme nijak starat, dostaneme jej "zadarmo" na systémových knihovnách — najde základní aplikační NIB, a před vlastním spuštěním aplikace jej automaticky zavede. Runtime systému Cocoa obsahuje poměrně komplikovanou sadu služeb, jež umožňují autmaticky volit různé NIBy podle toho, na jakém počítači a v jakém operačním systému aplikace běží, podle toho, jaký jazyk si uživatel zvolil pro komunikaci a podobně; tím se však prozatím nemusíme podrobně zabývat. Kdykoli v průběhu práce aplikace si pak můžeme programově vyžádat zavedení kteréhokoli dalšího NIBu, podle potřeby.
Co to přesně znamená "zavedení NIBu" už vlastně víme, ale pro lepší přehled si to zopakujme:
Po zavedení základního aplikačního NIBu se už jen spustí event loop — a to je vlastně všechno: event loop se postará o to, aby dejme tomu stisknutí tlačítka myši nad položkou menu vyvolalo událost "aktivace položky"; navázání této položky na vhodnou akci zajistí zavolání správné metody... a aplikace pracuje jak má (samozřejmě, zatím jsme neřešili takové věci jako psaní textu do editačních polí a podobně — to si blíže popíšeme až později).
Zpět | Obsah | Další |
Copyright © Chip, O. Čada 2000-2003