Zpět Obsah

Práce s více dokumenty


Aplikační knihovna Cocoa AppKit nabízí nesmírně bohatou paletu nejrůznějších služeb; v našem zběžném kursu programování v Cocoa už ale není dostatek místa na to, abychom se jimi zabývali. V dnešním, posledním dílu se proto soustředíme na asi nejdůležitější věc z toho, o čem jsme se dosud nebavili — na podporu práce s více dokumenty. Zároveň si ukážeme alespoň základy práce s grafikou: to je další nesmírně důležitá věc, jíž jsme se až dosud příliš nevěnovali. Předvedeme si také praktické využití notifikací a další služby — někdy i za cenu ne právě optimální implementace.

Zamyslíme-li se nad tím, uvědomíme si, že aplikace, jež pracují s více dokumenty, mají hodně společného: celá struktura příkazů "Nový dokument" / "Uložit" / "Uložit jako" / "Otevřít" a jejich funkce, služby typu "Návrat k naposledy uložené versi", koneckonců i takové věci jako správa oken, jež dokumenty representují či podpora služby Undo jsou stále stejné. Kvalitní vývojářský systém by je proto měl zajišťovat prostřednictvím standardních knihoven, tak, aby programátor nebyl nucen znovu psát (nebo kopírovat, to je jedno) jeden a ten samý kód pro každou aplikaci.

V prostředí Cocoa tomu tak skutečně je: kombinace standardních tříd NSDocumentController, NSDocument a několika dalších, méně významných, se zcela automaticky postará o vše podstatné.

Ukázková aplikace

Stejně jako v minulých dílech našeho seriálu i nyní si vše potřebné ukážeme na konkrétní aplikaci: připravíme jednoduché zobrazování grafů funkcí. "Dokument" bude prostě funkce (respektive, aby aplikace nebyla úplně triviální, sada několika funkcí); dokumentové okno bude obsahovat jak editor těchto funkcí, tak i pole, ve kterém se zobrazí jejich grafy.

Je zřejmé, že z hlediska vlastní správy dokumentů je úplně lhostejné, jestli se grafy funkcí zobrazují nebo ne. My toho využijeme k rozdělení aplikace i jejího popisu do dvou kroků: v prvém připravíme pouze triviální editor "dokumentů", jimiž budou právě seznamy funkcí; potom se zvlášť postaráme o jejich zobrazení.

Opět samozřejmě využijeme služeb ProjectBuilderu — vyžádáme si vytvoření nového projektu typu "Cocoa Document-based Application", a ProjectBuilder pro nás automaticky připraví kostru projektu i se základními zdrojovými soubory:

Stojí za povšimnutí, že ProjectBuilder připravil automaticky dva NIBy: "MainMenu" se zavede automaticky ihned při spuštění aplikace a obsahuje především její hlavní nabídku; můžeme do něj umísťovat také pomocné objekty na aplikační úrovni (třeba panel s jednoduchou nápovědou). "MyDocument" naproti tomu representuje "view" dokumentu — zavede se znovu pro každý otevřený (nebo nově vytvořený) dokument, a vytvoří jeho vlastní grafické uživatelské rozhraní. ProjectBuilder nám také vytvořil základ pro dokumentový controller, zdrojové soubory "MyDocument.h" a "MyDocument.m".

(Při tvorbě skutečné komerční aplikace by asi hned první věc, již uděláme, byla přejmenování "MyDocument" na nějaké jméno lépe odpovídající tomu, co aplikace dělá; v tomto ukázkovém projektu si to můžeme nechat od cesty.)

Už minule jsme se seznámili se souborem Info.plist, který obsahuje všechny důležité informace o aplikaci. Dnes se na něj podíváme znovu, protože mezi podstatné informace samozřejmě patří také to, že jde o aplikaci která pracuje s dokumenty, a o jaké dokumenty jde. Operační systém tyto informace spravuje a na jejich základě např. aplikaci automaticky spustí ve chvíli, kdy otevřeme její dokument.

První důležitá informace, již bychom měli do souboru Info.plist vložit, je identifikátor aplikace: jde o jednoznačné jméno, podle nějž se aplikace dá kdykoli rozeznat. Pro jednoznačnost je ideální využít internetová doménová jména: ta samozřejmě jednoznačná jsou, a navíc jde o konvenci, již využívá a doporučuje sama firma Apple:

Druhá věc, již je nutné pro dokumentovou aplikaci určit, je právě seznam typů dokumentů, se kterými aplikace pracuje: jistě, může jich být libovolně mnoho, ačkoli většina běžných aplikací pracuje jen s dokumenty jednoho typu. K tomu slouží další panel:

Jméno ("Name") by bylo důležité pro rozlišení v případě, že bychom pracovali s několika různými typy dokumentů; takto jej můžeme ignorovat. "Role" může být "Editor", "Viewer" (prohlížeč) a "None" — aplikace s dokumenty tohoto typu neumí pracovat, avšak přináší o nich informaci do systému. Důležitá je přípona souboru ("Extensions"), podle níž se pozná, že dokument patří právě této aplikaci; přípon může být i víc (např. pro prohlížeč obrázků bychom zaregistrovali "jpg" i "jpeg"). Velmi důležité pole je také "Document Class": objekt této třídy totiž knihovny Cocoa automaticky vytvoří jako controller dokumentu kdykoli je to zapotřebí.

Zbývající pole jsou triviální: "OS types" je přežitek Mac OS 9 a HFS, který můžeme pokojně ignorovat, a v "Icon File" bychom mohli určit jméno souboru, obsahujícího ikonu, s níž budou dokumenty této aplikace zobrazeny např. ve Finderu či v doku.

Model, view, controller?

Samozřejmě, že i dokumentová aplikace je založena na paradigmatu MVC, jímž jsme se zabývali minule. Tentokrát je ovšem situace malinko složitější: aplikace jako celek má jednoduchý controller, který se stará o základní služby jako je správa nabídek a správné předávání požadavků aktuálnímu dokumentu. O to se vůbec nemusíme starat — tyto služby zajistí automaticky NSDocumentController.

Sami však musíme připravit model, view a controller pro jeden dokument — o správu více dokumentů se postarají knihovny Cocoa. Jak jsme se už zmínili, základy nám automaticky připravil ProjectBuilder: dokumentové view je v NIBu "MyDocument.nib", controller (jak je určeno v souboru Info.plist) bude objekt třídy MyDocument, jejíž zdrojové soubory — "MyDocument.h" a "MyDocument.m" — už také máme připraveny.

Model

Vždy je však vhodné si nejprve připravit model; tím tedy začneme. V našem případě je model triviální a v praxi bychom jeho služby patrně zaintegrovali do controlleru; pro lepší ilustraci paradigmatu MVC pro něj ale schválně připravíme samostatnou třídu... nazvěme ji nápaditě Model.

Připomeňme, že model pro nás representuje několik funkcí. Na funkci se budeme dívat jako na prostý text (jak si ukážeme níže, nebudeme se ani obtěžovat jej sami interpretovat — využijeme na to standardní kalkulátor "bc", který máme díky unixovému dědictví v Mac OS X volně k dispozici). Základní API modelu by tedy mohlo vypadat např. takto:

 @interface Model:NSObject { ... properties doplníme později ... }
 -(int)numberOfFunctions;
 -(NSString*)functionAtIndex:(int)n;
 -(void)setFunction:(NSString*)fnc atIndex:(int)n;
 -(void)removeFunctionAtIndex:(int)n;
 -(void)addFunction:(NSString*)fnc;
 ...

Kromě toho potřebujeme model ukládat do souborů a opět z nich načítat. Existuje řada možných přístupů, jež Cocoa podporuje; asi nejobecnější a nejjednodušší je, dokáže-li model načíst a zapsat svůj obsah do objektu třídy NSData (obecná binární data); jak uvidíme, o vše ostatní se opět automaticky postarají standardní třídy Cocoa. Přidáme tedy k modelu ještě následující dvě služby:

 ...
 +(Model*)modelWithData:(NSData*)data; // vytvoří model na základě dat; pokud to nejde, vrátí nil
 -(NSData*)contents; // vrátí obsah modelu ve formě obecných dat
 ...

Jako úplný základ by to stačilo; brzy však uvidíme, že pro controller je vhodné, může-li k jednotlivým funkcím ukládat pomocné a doplňkové informace — např. barvu, jíž má být ta která funkce vykreslena. Proto přidáme ještě trojici metod, jež umožní pro jakoukoli funkci uložit do modelu libovolný objekt, identifikovaný jménem:

 ...
 -(void)setObject:object name:(NSString*)name functionIndex:(int)n;
 -objectWithName:(NSString*)name functionIndex:(int)n;
 -(void)removeObjectWithName:(NSString*)name functionIndex:(int)n;
 @end

Nadefinujeme také jméno notifikace

 extern NSString * const ModelChangedNotification;

a postaráme se o to, aby ji model odeslal kdykoli v něm dojde ke změně. To je obecně dobrý programátorský styl a správné využití notifikací: umožňuje to libovolnému množství dalších modulů sledovat změny v modelu, aniž by na něj musely být přímo vázány.

Konkrétní implementace modelu není z hlediska AppKitu vůbec zajímavá, protože samozřejmě využívá pouze služeb Foundation Kitu, který již známe; je ostatně zcela triviální (funkce i ostatní objekty jsou uloženy v objektech NSMutableDictionary, a ty leží uvnitř NSMutableArray; všechny metody jen tato data zpřístupňují, případně zapisují/načítají do/z objektu třídy NSData).

View

Na druhém místě sestavíme view; nejprve je však vhodné se u tohoto pojmu chvilku zdržet, abychom zamezili nejasnostem: v kontextu paradigmatu MVC je "view" kompletní zobrazení dokumentu — v našem případě definované obsahem NIBu "MyDocument.nib". Tak se na něj také budeme v tomto odstavci dívat. V AppKitu ovšem existuje třída NSView, jež representuje "zobrazitelný objekt", a jejíhož dědice budeme později implementovat pro vykreslení grafů funkcí. To je tedy "view v kontextu AppKitu", a uvnitř jediného "view v kontextu MVC" jich obvykle bývá řada.

Nyní připravíme "view v kontextu MVC" — je to triviální, prostě otevřeme "MyDocument.nib" v InterfaceBuilderu, vložíme do něj z palety tabulku, a "File's Owner" — což je samozřejmě právě náš controller MyDocument — k ní připojíme jako "delegáta" a "data source". Nastavíme samozřejmě i outlet functions.

Controller

Základní kostru zdrojového kódu controlleru nám připravil ProjectBuilder ve zdrojových souborech "MyDocument.h" a "MyDocument.m". Rozhraní je zatím prázdné; my do něj přidáme odkaz na model a na tabulku, která bude sloužit pro zobrazení a úpravy funkcí:

 @interface MyDocument:NSDocument {
     Model *model;
     IBOutlet NSTableView *functions;
 }
 @end

Jistě, chybí nám zde nějaké NSView pro zobrazení grafů funkcí; to ale, jak jsme si slíbili, doplníme až nakonec. Zatím zde nejsou ani žádné "akce"; jak uvidíme, pro základní funkčnost aplikace je skutečně nepotřebujeme — o předávání požadavků se korektně a automaticky postarají knihovny Cocoa.

Musíme ovšem zajistit základní funkčnost controlleru prostřednictvím některých jeho standardních služeb; řadu z nich pro nás ProjectBuilder již připravil a my jen doplníme implementaci (některé z připravených metod si dokonce můžeme dovolit smazat: náš controller je např. tak jednoduchý, že nepotřebuje žádnou inicializaci; smažeme tedy připravenou kostru metody init).

Tuto implementaci si ovšem ukážeme, a podrobně popíšeme. Mohla by vypadat asi takto:

 @implementation MyDocument
 -(NSString*)windowNibName {return @"MyDocument";}
 ...

Standardní metoda (připravená automaticky ProjectBuilderem), pouze určuje jméno NIBu, který obsahuje view. O načtení NIBu se už starat nemusíme — zajistí jej automaticky knihovny Cocoa právě s využitím informace z této metody.

Následující dvojice metod stačí pro kompletní podporu práce se soubory:

 ...
 -(NSData*)dataRepresentationOfType:(NSString*)aType {
     return [model contents];
 }
 -(BOOL)loadDataRepresentation:(NSData*)data ofType:(NSString*)aType {
     [model autorelease];
     return (model=[[Model modelWithData:data] retain])!=nil;
 }
 ...

První z nich Cocoa využije pro uložení dokumentu do souboru; pomocí druhé naopak načte ze souboru obsah uložených dat. O výběr souboru pomocí panelů ani o vlastní zápis/čtení souboru se starat nemusíme; to vše zajistí standardní knihovny. Po implementaci těchto dvou triviálních metod ihned korektně funguje celá skupina příkazů "Nový dokument" / "Uložit" / "Uložit jako" / "Otevřít" / "Otevřít předchozí" (seznam naposledy otevřených dokumentů samozřejmě Mac OS X udržuje pro každou aplikaci zcela automaticky) i "Návrat k naposledy uložené versi".

Následující metoda windowControllerDidLoadNib: se volá automaticky ihned po zavedení view dokumentu z odpovídajícího NIBu. My ji využijeme k tomu, aby po jakékoli změně modelu byla ihned automaticky překreslena tabulka functions — samozřejmě, využijeme k tomu notifikaci, již model odesílá po každé změně:

 ...
 -(void)windowControllerDidLoadNib:(NSWindowController*)wc {
     [[NSNotificationCenter defaultCenter] addObserver:functions selector:@selector(reloadData) name:ModelChangedNotification object:nil];
 }
 ...

Jak je vidět, je to velmi jednoduché: jen si vyžádáme od notifikačního centra (pamatujete si jej ještě z popisu služeb Foundation Kitu?), aby po jakékoli změně modelu byla automaticky odeslána tabulce functions zpráva reloadData. To je vše.

Následující dvojice metod je zcela standardní "data source" pro tabulku, jak jsme se s ním seznámili v předminulém dílu; není proto třeba je podrobně popisovat. Za samostatnou zmínku snad stojí jen jednoduchá finta, kterou jsme se zbavili potřeby samostatných tlačítek pro přidání a odstranění funkce: tabulka vždy zobrazuje na konci jeden volný řádek, do kterého můžeme vepsat novou funkci:

 ...
 -(int)numberOfRowsInTableView:(NSTableView*)tv {
     return [model numberOfFunctions]+1;
 }
 -(id)tableView:(NSTableView*)tv objectValueForTableColumn:(NSTableColumn*)col row:(int)row {
     if (row>=[model numberOfFunctions]) return @"";
     return [model functionAtIndex:row];
 }
 ...

Poslední metoda také patří do "data source" a je automaticky volána kdykoli se obsah tabulky změní (předminule jsme ji neimplementovali — třída NSTableView to pozná, a v takovém případě slouží jako "read only" tabulka).

Metoda je poměrně složitá, ale žádné záhady v ní nejsou: nejprve ověříme, zda vůbec existuje model, a pokud ne, vytvoříme prázdný. Pak zjistíme, zda změněná data — object — náhodou nejsou prázdný text: to souvisí s "fintou", o které jsme se zmínili před chvilkou. Jestliže připsáním nové funkce do prázdného posledního řádku chceme funkci přidat, je docela logické, abychom zadáním prázdného textu již existující funkci naopak zrušili.

Jestliže zadaný text prázdný není, musíme se ještě podívat, zda byl vepsán právě do toho posledního extra prázdného řádku (pak přidáme novou funkci službou modelu insertFunction:atIndex:), nebo do některého z řádků již existujících (pak použijeme službu setFunction:object:). Pokud byl zadaný text prázdný (a zároveň na některém z již existujících řádků), funkci zrušíme službou removeFunctionAtIndex:.

Nyní by měl být kód metody naprosto zřejmý (s výjimkou řádků pro "undo", jimž se věnujeme za chvilku):

 ...
 -(void)tableView:(NSTableView*)tv setObjectValue:object forTableColumn:(NSTableColumn*)col row:(int)row {
     if (!model) model=[[Model modelWithData:nil] retain];
     if ([(NSString*)object length]) // non-empty function
         if (row==[model numberOfFunctions]) {
             [[[self undoManager] prepareWithInvocationTarget:model] removeFunctionAtIndex:row];
             [model insertFunction:object atIndex:row];
         } else {
             [[[self undoManager] prepareWithInvocationTarget:model] setFunction:[model functionAtIndex:row] atIndex:row];
             [model setFunction:object atIndex:row];
         }
     else // empty function
         if (row<[model numberOfFunctions]) {
             [[[self undoManager] prepareWithInvocationTarget:model] insertFunction:[model functionAtIndex:row] atIndex:row];
             [model removeFunctionAtIndex:row];
         }
 }
 ...

Podpora služby undo je nesmírně jednoduchá a efektivní díky objektovému jádru jazyka Objective C, které nám dovoluje se zasílanými zprávami všelijak kouzlit. Výraz [self undoManager] prostě vrátí tzv, undo manager — objekt, který udržuje informace o undo v rámci tohoto dokumentu. Objektová magie začíná až se zprávou prepareWithInvocationTarget:. Ta totiž řekne undo manageru "příští zprávu, kterou dostaneš — ať je jakákoli — nezpracovávej; jen si ji zapamatuj přesně jak je, včetně všech argumentů. Teprve v případě, že uživatel vyvolá službu 'Undo', tuto zprávu beze změny pošli objektu, který byl argumentem zprávy prepareWithInvocationTarget:".

Jestliže tedy uživatel vyvolá "Undo" po přidání nové funkce, undo manager automaticky pošle modelu zprávu removeFunctionAtIndex: s patřičným číslem řádku — jak jsme si to vyžádali na prvém z řádků, věnovaných službě undo. Podobně je tomu v obou zbývajících případech.

Mimochodem, přidáním těchto tří řádků jsme nejen implementovali korektní službu "Undo", ale zároveň jsme zařídili to, že aplikace sleduje stav dokumentu, a nedovolí jej zavřít pokud obsahuje neuložené změny:

Tuto informaci samozřejmě lze získat z undo manageru, takže Cocoa se o to samozřejmě zcela automaticky a korektně stará.

Tím jsme vlastně hotovi; zbývá už jen triviální metoda dealloc, která zruší model a odstraní automatické odesílání zprávy reloadData při změnách modelu (vše ostatní — speciálně tedy grafické objekty view — uvolní Cocoa automaticky a opět se o to nemusíme starat).

 ...
 -(void)dealloc {
     [model release];
     [[NSNotificationCenter defaultCenter] removeObserver:functions];
     [super dealloc];
 }
 @end

To je celé: jako editor funkcí už naše aplikace bez problémů funguje, a korektně podporuje práci se soubory, všechny standardní příkazy z nabídky "File", undo, díky využití standardních tříd i třeba copy/cut/paste či textový drag&drop nebo Services a dlouhou řadu dalších služeb. Napsali jsme přitom méně, než sto řádků zdrojového textu (včetně kompletní implementace modelu, již zde neuvádíme): inu, to je Cocoa.

Ještě tedy zbývá slíbená grafika.

Nejprve pomocné údaje

Pro zobrazení každé funkce potřebujeme několik pomocných údajů: číselný rozsah od-do, určující interval, ve kterém chceme graf funkce vidět, a barvu a tloušťku čáry, jíž bude funkce kreslena. Můžeme na to použít třeba NSColorWell, NSForm (pro čísla od-do) a NSSlider (pro tloušťku čáry):

 @interface MyDocument:NSDocument {
     Model *model;
     IBOutlet NSTableView *functions;
     IBOutlet NSColorWell *colour;
     IBOutlet NSCell *from,*to;
     IBOutlet NSSlider *line;
 }
 -(IBAction)changeColour:sender;
 -(IBAction)changeFrom:sender;
 -(IBAction)changeTo:sender;
 -(IBAction)changeLine:sender;
 @end

Přidáme v InterfaceBuilderu objekty do okna, nastavíme vhodně jejich atributy, a vše "nadrátujeme". Pak už stačí implementovat v controlleru dokumentu (ve třídě MyDocument) výše deklarované "akce", např.

 -(IBAction)changeColour:sender {
     int row=[functions selectedRow];
     if (row>=0 && row<[model numberOfFunctions]) {
         [[[self undoManager] prepareWithInvocationTarget:model] setObject:[model objectWithName:@"colour" functionIndex:row] name:@"colour" functionIndex:row];
         [model setObject:[colour color] name:@"colour" functionIndex:row];
     }
 }

Zde je vše už, doufám, jasné. Přidáme ještě jednu trochu složitější metodu — zprávu tableViewSelectionDidChange: posílá standardně tabulka svému delegátu kdykoli se mění vybraný řádek. My jí využijeme pro zobrazení údajů, týkajících se právě zvoleného řádku:

 -(void)tableViewSelectionDidChange:(NSNotification*)nn {
     int row=[functions selectedRow];
     if (row>=0 && row<[model numberOfFunctions]) {
         id o;
         if ((o=[model objectWithName:@"colour" functionIndex:row])) [colour setColor:o];
         else [colour setColor:[NSColor blackColor]];
         if ((o=[model objectWithName:@"from" functionIndex:row])) [from setObjectValue:o];
         else [from setIntValue:0];
         if ((o=[model objectWithName:@"to" functionIndex:row])) [to setObjectValue:o];
         else [to setIntValue:1];
         if ((o=[model objectWithName:@"line" functionIndex:row])) [line setObjectValue:o];
         else [line setIntValue:0];
     }
 }

I tento kód je snad zcela zřejmý, a je to vše, co je zapotřebí, aby editor kompletně fungoval (ach ano — ještě využijeme notifikací k tomu, aby se metoda tableViewSelectionDidChange: zavolala automaticky také po každé změně modelu):

Nyní už nám zbývá opravdu jen vlastní graf.

"View v kontextu AppKitu"

Jak už jsme si vysvětlili výše, AppKit používá pro zobrazení objekty třídy NSView; proto se jim říká "views", ačkoli se to trochu plete s "view" ve smyslu MVC — to samozřejmě obvykle obsahuje mnoho objektů třídy NSView.

My si nyní ukážeme, jak se v Cocoa implementuje nový dědic třídy NSView, takový, který zajišťuje nějaké speciální zobrazení: v našem případě půjde o zobrazení funkcí z modelu. Je to poměrně jednoduché: prostě vytvoříme novou třídu jako dědice třídy NSView, a implementujeme v ní metodu drawRect:. Pak jen hlavičkový soubor s interface třídy vhodíme do okna InterfaceBuilderu (aby o ní InterfaceBuilder věděl), doplníme do NIBu objekt "CustomView" a v inspektoru určíme, že půjde o objekt právě naší nové třídy:

Do controlleru přidáme odpovídající outlet qview a natáhneme dráty. Upravíme kód controlleru tak, aby při změně modelu o tom informoval i view, asi takto:

 -(BOOL)loadDataRepresentation:(NSData*)data ofType:(NSString*)aType {
     [model autorelease];
     [gview setModel:model=[[Model modelWithData:data] retain]];
     return model!=nil;
 }

Nyní už zbývá jen implementace nového view; nejprve si ukážeme (a vysvětlíme) tu její část, jež využívá služeb AppKitu:

 @implementation GraphView
 -(void)setModel:(Model*)mdl {
     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(setNeedsDisplay:) name:ModelChangedNotification object:model=mdl];
 }
 -(void)dealloc {
     [[NSNotificationCenter defaultCenter] removeObserver:self];
     [super dealloc];
 }
 ...

První dvojice metod je triviální: jen zajistí, že při jakékoli změně modelu bude view ihned překresleno (protože se mu automaticky pošle zpráva setNeedsDisplay:); dealloc pak tento požadavek zruší ve chvíli, kdy view přestává existovat.

Následující metoda je kompletně napsána "ve Foundationu", a prozatím si pro zjednodušení její obsah neukážeme — nemá totiž s AppKitem a s obecnou implementací view tříd zhola nic společného:

 ...
 -(NSEnumerator*)xyForFunction:(NSString*)fnc from:(double)from to:(double)to step:(double)step {
 ...
 }
 ...

Metoda interpretuje zadanou funkci, a vrátí (prostřednictvím enumerátoru, který je flexibilnější než pole) řadu souřadnic x,y,x,y,..., určujících klíčové body dané funkce na intervalu <from,to>; s krokem step.

Následující metoda toho využije pro sestavení cesty, která representuje vlastní graf funkce: cestu v AppKitu representuje standardní Bezierova křivka, sestrojená takto:

 ...
 -(NSBezierPath*)pathForFunction:(NSString*)fnc from:(double)from to:(double)to step:(double)step {
     NSBezierPath *pp=[NSBezierPath bezierPath];
     NSEnumerator *en=[self xyForFunction:fnc from:from to:to step:step];
     [pp moveToPoint:NSMakePoint([[en nextObject] doubleValue],[[en nextObject] doubleValue])];
     while (fnc=[en nextObject]) if ([fnc length])
         [pp lineToPoint:NSMakePoint([fnc doubleValue],[[en nextObject] doubleValue])];
     return pp;
 }
 ...

Nejsložitější je vlastní metoda drawRect:. V ní postupně procházíme všechny funkce, pro každou z modelu "vytáhneme" pomocné údaje, a vykreslíme ji:

 ...
 -(void)drawRect:(NSRect)rect {
     ...
     NSRect br=[self bounds];
     int i,n=[model numberOfFunctions];
     for (i=0;i<n;i++) {
         double from=0,to=1,step=.1,line=0;
         NSColor *colour=[NSColor blackColor];
         id o;
         if ((o=[model objectWithName:@"colour" functionIndex:i])) colour=o;
         if ((o=[model objectWithName:@"from" functionIndex:i])) from=[o doubleValue];
         if ((o=[model objectWithName:@"to" functionIndex:i])) to=[o doubleValue];
         if ((o=[model objectWithName:@"line" functionIndex:i])) line=[o doubleValue];
         step=(to-from)/(NSWidth(br)/5); // a point each 5 pixels
         [colour set];
         NSBezierPath *path=[self pathForFunction:[model functionAtIndex:i] from:from to:to step:step];
         [path setLineWidth:line];
         NSRect pr=[path bounds];
         NSAffineTransform *tr=[NSAffineTransform transform];
         [tr scaleXBy:NSWidth(br)/NSWidth(pr) yBy:NSHeight(br)/NSHeight(pr)];
         [tr translateXBy:NSMinX(br)-NSMinX(pr) yBy:NSMinY(br)-NSMinY(pr)];
         [path transformUsingAffineTransform:tr];
         [path stroke];
     }
     ...
 }
 @end

Jak je vidět, požadovanou barvu určíme (pro jakoukoli následující kreslicí operaci) prostě tak, že objektu třídy NSColor pošleme zprávu set (analogicky bychom mimochodem mohli určit font, kdybychom vykreslovali text). Nastavení tloušťky čáry je zřejmé.

Za pozornost ovšem stojí využití třídy NSAffineTransform. Ta reprezentuje jakoukoli afinní (maticovou) transformaci, a my ji využíváme pro pohodlné "roztažení" grafu funkce do celého prostoru našeho view. Uvědomíme-li si, že v proměnné br je uložen obdélník, určující rozměry view, zatímco co proměnné pr jsme uložili obdélník, vymezující cestu (tj. graf funkce) — obojí zajistila standardní zpráva bounds — je už snad použití zpráv scaleXBy:yBy: a translateXBy:yBy: zřejmé. Cestu pak transformujeme zprávou transformUsingAffineTransform:, a vykreslíme zprávou stroke.

A to už je opravdu všechno: aplikace je hotová, a funkční:

Veškerý zdrojový kód včetně automaticky generovaného zabírá cca 360 zdrojových řádků; z toho jsme sami napsali stěží dvě třetiny. Čistého času bylo na aplikaci a její ladění (při psaní jsem zapomněl asi na dvě drobnosti, jež bylo třeba najít v debuggeru) zapotřebí něco málo přes hodinu (psaní tohoto článku ovšem zabralo déle). To je zkrátka programování v Cocoa.

A to je už všechno

Dnešním dílem jsme dokončili kurs programování v Cocoa: samozřejmě, že jsme ani zdaleka nepopsali všechny služby a možnosti; ukázali jsme si však základy a principy, na kterých už každý může pohodlně dál stavět s využitím standardní dokumentace.

Nakonec ještě jednou Foundation Kit

Ačkoli v těchto dílech většinou "Foundationové" úseky vynecháváme, vyplatí se ukázat implementaci metody xyForFunction:from:to:step:; volání vnějších programů (v našem případě kalkulátoru bc, který za nás interpretuje zadané funkce a počítá jejich hodnoty) s přesměrováním všech standardních vstupů a výstupů není zcela triviální, a přitom se velice často hodí. Bez dalších komentářů si proto ukážeme odpovídající kód; popis podrobností případný zájemce snadno najde ve standardní dokumentaci. Proměnné accumulatedOutput a accumulatedError jsou properties, pro bližší informace o syntaxi a sémantice výrazů bc stačí v Terminalu napsat "man bc":

 -(void)gotData:(NSNotification*)nn {
     NSFileHandle *fh=[nn object];
     NSString *s=[[NSString alloc] initWithData:[fh availableData] encoding:NSASCIIStringEncoding];
     [accumulatedOutput appendString:[s autorelease]];
     [fh waitForDataInBackgroundAndNotify];
 }
 -(void)gotError:(NSNotification*)nn {
     NSFileHandle *fh=[nn object];
     NSString *s=[[NSString alloc] initWithData:[fh availableData] encoding:NSASCIIStringEncoding];
     [accumulatedError appendString:[s autorelease]];
     [fh waitForDataInBackgroundAndNotify];
 }
 -(NSEnumerator*)xyForFunction:(NSString*)fnc from:(double)from to:(double)to step:(double)step {
     if (step==0 || from<to && step<0 || from>to && step>0) [NSException raise:@"Function" format:@"Invalid from (%f) to (%f) step (%f)",from,to,step];
     NSTask *task=[[[NSTask alloc] init] autorelease];
     NSPipe *ipipe=[NSPipe pipe],*opipe=[NSPipe pipe],*epipe=[NSPipe pipe];
     NSFileHandle *input=[ipipe fileHandleForWriting],*output=[opipe fileHandleForReading],*error=[epipe fileHandleForReading];
     NSNotificationCenter *nc=[NSNotificationCenter defaultCenter];
     [task setLaunchPath:@"/usr/bin/bc"];
     [task setArguments:[NSArray arrayWithObject:@"-lq"]];
     [task setStandardInput:ipipe];
     [task setStandardOutput:opipe]; [task setStandardError:epipe];
     [nc addObserver:self selector:@selector(gotData:) name:NSFileHandleDataAvailableNotification object:output];
     [nc addObserver:self selector:@selector(gotError:) name:NSFileHandleDataAvailableNotification object:error];
     [output waitForDataInBackgroundAndNotify]; [error waitForDataInBackgroundAndNotify];
     accumulatedError=[NSMutableString string]; accumulatedOutput=[NSMutableString string];
     [task launch];
     [input writeData:[[NSString stringWithFormat:@"for (x=%f;x<=%f;x+=%f) {x;%@}\nquit\n",from,to,step,fnc] dataUsingEncoding:NSASCIIStringEncoding]];
     [input closeFile];
     [task waitUntilExit];
     [nc removeObserver:self name:NSFileHandleDataAvailableNotification object:error];
     [nc removeObserver:self name:NSFileHandleDataAvailableNotification object:output];
     if ([accumulatedError length]) [NSException raise:@"Function" format:@"Error in function:\n%@\n%@",fnc,accumulatedError];
     return [[accumulatedOutput componentsSeparatedByString:@"\n"] objectEnumerator];
 }


Zpět Obsah

Copyright © Chip, O. Čada 2000-2003