Zpět Obsah Další

GUI a InterfaceBuilder


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í.

"Outlety" a "akce"

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:).

Jednoduché drátování

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:

IB_outlet1

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:

IB_action1

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...

Kde se vzal objekt "MyClass"?

Č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

Kde se vzaly ostatní objekty?

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í.

Proč vůbec "drátovat"?

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:

IB_action2

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"...

Co je to NIB

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).

Shrnutí

Dnes jsme se seznámili se základy vazby kódu aplikace na grafické uživatelské rozhraní, a ukázali jsme si postavení InterfaceBuilderu a základní operace. Příště se na to na všechno podíváme podrobněji, a ukážeme si více zajímavých detailů.


Zpět Obsah Další

Copyright © Chip, O. Čada 2000-2003