V minul²ch dφlech naÜeho serißlu, v∞novanΘho programovßnφ v objektovΘm prost°edφ Cocoa, kterΘ jste mohli najφt v tiÜt∞nΘm Chipu a naposled na Chip CD, jsme se zab²vali knihovnou slu₧eb pro prßci s grafick²m u₧ivatelsk²m rozhranφm a na aplikaΦnφ ·rovni: AppKitem.
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Üφ 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, kterΘ 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, kterΘ 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) stßle stejn² 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Θ.
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 NIB: "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 odpustit.)
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, kterou vyu₧φvß a doporuΦuje sama firma Apple:
Druhß v∞c, kterou 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.
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 NIB "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 urΦitß funkce vykreslena. Proto p°idßme jeÜt∞ trojici metod, kterΘ 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 tato data jen 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 NIB "MyDocument.nib". Tak se na n∞j takΘ budeme v tomto odstavci dφvat. V AppKitu ovÜem existuje t°φda NSView, kterß 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 inicializac, 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 NIB, kter² obsahuje view. O naΦtenφ NIB 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 NIB. 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, kterou 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, 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 budeme v∞novat 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∞ tak, 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 prvnφ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.
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ßme, jasnΘ. P°idßme jeÜt∞ jednu trochu slo₧it∞jÜφ metodu ù zprßvu tableViewSelectionDidChange: posφlß standardn∞ tabulku svΘmu delegßtovi kdykoli se m∞nφ vybran² °ßdek. My ji 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 notifikace 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.
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 NIB 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, kterß 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 v∙bec 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 do 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φ jsme zapomn∞li 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 trvalo dΘle). To je zkrßtka programovßnφ v Cocoa.
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.
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];
}