Zpět | Obsah | Další |
Ve dvou minulých dílech našeho seriálu, věnovaného programování v objektovém prostředí Cocoa, jsme se začali zabývat knihovnou služeb pro práci s grafickým uživatelským rozhraním a na aplikační úrovni: AppKitem. Minule jsme se soustředili na paradigma MVC — model, view, controller; zároveň jsme si ale ukázali nejjednodušší použití některých služeb AppKitu (konkrétně práce se dvěma prvky GUI: s tabulkou a s panelem pro otevření souboru). Dnes se podobně podíváme na služby některých dalších tříd.
Samozřejmě, že ani zdaleka neprojdeme AppKit celý: jeho rozsah by to neumožnil; jen standardní příručka ve formátu PDF má zhruba 20 MB. Ukážeme si ale některé základní služby a principy, jež umožní programátorům v Cocoa začít psát jednoduché aplikace — a ostatním dají alespoň rámcovou představu o tom, jak pohodlně se v Cocoa píše.
Jednoduchý prohlížeč souborů
Z výše popsaných důvodů by ani nemělo smysl procházet třídy AppKitu postupně a o každé si říci pár slov: pokud bychom se měli zmínit o všech, mohli bychom o každé z nich říci tak málo, že by to nebylo k ničemu dobré. Nadto, to je styl referenční příručky, a my se zde spíš učíme s Kakaem zacházet. Proto použijeme jiný, daleko lepší postup: ukážeme si kompletní implementaci jednoduché aplikace, a důkladně popíšeme, jak při její tvorbě využíváme právě AppKit.
Abychom se mohli na AppKit dobře soustředit, zvolíme aplikaci, jejíž datový model (psaný samozřejmě ve FoundationKitu) je extrémně jednoduchý: ukážeme si implementaci jednoduchého prohlížeče souborů. Jeho datovým modelem je prostě systém složek a souborů v počítači; na přístup k němu nám stačí pár triviálních řádků s využitím třídy NSFileManager. Vše ostatní bude právě AppKit.
Nejprve připravíme uživatelské rozhraní
První krok při tvorbě jakékoli aplikace v Cocoa je vždy stejný: uspoříme si nudnou práci s přípravou projektu tím, že si vyžádáme vytvoření kompletního template v aplikaci ProjectBuilder: v našem případě půjde o projekt typu "Cocoa Application".
Po vytvoření projektu pak v přehledu souborů otevřeme skupinu "Resources" a poklepáním otevřeme soubor "MainMenu.nib", který obsahuje základní GUI nově vytvářené aplikace:
ProjectBuilder automaticky spustí aplikaci InterfaceBuilder, kterou už důvěrně známe z minulých dílů našeho seriálu; není proto zapotřebí podrobně popisovat detaily přípravy grafického uživatelského rozhraní nové aplikace. Prostě si řekneme, co je třeba udělat:
Controller
Vzhledem k tomu, že náš controller bude řídit zobrazení souborů v browseru, je celkem zřejmé, že musíme připravit jeden "outlet" (nevíte-li, co to je, vyhledejte si znovu díky, věnované InterfaceBuilderu!). Pojmenujeme jej "browser", a natáhneme od něj "drát" k browseru v okně aplikace.
Kromě toho potřebujeme ještě dvojici dalších "drátů":
Vygenerujeme kostry zdrojových souborů třídy Controller, a tím jsme s grafickým uživatelským rozhraním aplikace hotovi. Můžeme se pustit do vlastní práce (převážně v AppKitu): implementovat odpovídající zdrojový kód, který zajistí správnou funkci aplikace.
Zobrazení složek a souborů
Nejprve zajistíme, aby browser správně zobrazoval obsah pevného disku. Již jsme se zmínili o tom, že pro vlastní přístup ke složkám a souborům využijeme standardní objekt FoundationKitu NSFileManager; nejprve tedy do interface třídy Controller přidáme proměnnou "fm", ve které si budeme tento objekt pro pohodlný přístup udržovat:
// Controller.h
#import <Cocoa/Cocoa.h>
@interface Controller : NSObject {
IBOutlet Controller *browser;
NSFileManager *fm; // přidáno ručně
}
@end
Knihovny AppKitu se automaticky postarají o to, že kterýkoli objekt zavedený z NIBu automaticky dostane zprávu awakeFromNib, pokud samozřejmě obsahuje odpovídající metodu. My toho využijeme pro pohodlnou inicializaci proměnné "fm" (o outlet "browser" se ovšem starat nemusíme: ten je inicializován automaticky v rámci zavádění NIBu díky "drátu", jímž jsme jej spojili s browserem v okně). Do zdrojového souboru Controller.m tedy přidáme následující metodu:
//Controller.m
#import "Controller.h"
@implementation Controller
-(void)awakeFromNib {
fm=[NSFileManager defaultManager];
}
Nyní se postaráme o korektní zobrazení složek a souborů v browseru. Podobně jako tomu bylo v minulém dílu u tabulky, i browser využívá dvojici metod: prostřednictvím prvé se svého "delegáta" nejprve "zeptá" na počet objektů, jež má zobrazit, a pak volá druhou pro určení jednotlivých objektů v odpovídajících řádcích. U browseru je to ovšem malinko složitější, protože representuje hierarchii objektů: sloupec 0 bude zobrazovat kořenovou složku disku, sloupec 1 obsah toho, co je vybráno ve sloupci 0, a tak dále. První metodu, browser:numberOfRowsInColumn:, tedy implementujeme takto:
-(int)browser:(NSBrowser*)sender numberOfRowsInColumn:(int)column {
NSString *path=column?[sender pathToColumn:column]:@"/";
return [[fm directoryContentsAtPath:path] count];
}
Jde-li o sloupec ("column") nula, vyžádáme si v prvém řádku "path" rovnou "/" — jak víme, v unixovém systému souborů lomítko vždy representuje kořenovou složku. Není-li tomu tak, optáme se browseru ("sender" — objekt, který nám zprávu browser:numberOfRowsInColumn: poslal) na označenou cestu až po daný sloupec pomocí zprávy pathToColumn:. Browser na tuto zprávu standardně reaguje vytvořením textového řetězce, obsahujícího jména všech zvolených polí ve všech sloupcích až po ten určený, spojená znakem '/' — což je samozřejmě přesně to, co jsme chtěli. Pak si jen na druhém řádku od file manageru vyžádáme počet objektů v dané složce.
Koncepce druhé metody, určující obsah objektu v daném řádku, se už od tabulky, již známe z minulého dílu, trochu liší (je tomu tak z historických důvodů — browser je mnohem starší než tabulka). U tabulky jsme z controlleru prostě tabulce předali objekt, který se má zobrazit. U browseru je tomu naopak: browser předá controlleru "buňku", ve které se objekt zobrazí, a nechá jej, aby správně nastavil její atributy.
"Buňka" je nový objekt AppKitu, se kterým jsme se zatím nesetkali: objekty třídy NSCell a jejích podtříd representují "cosi, co se dá zobrazit"; podívejte se do předminulého dílu na popis rozdílů mezi třídami NSCell a NSView.
Kdykoli má browser zobrazit nový řádek v novém sloupci, pošle svému "delegátu" zprávu browser:willDisplayCell:atRow:column:. Implementace odpovídající metody ve třídě Controller bude vypadat takto:
-(void)browser:(NSBrowser*)sender willDisplayCell:(id)cell
atRow:(int)row column:(int)column {
NSString *path=column?[sender pathToColumn:column]:@"/";
NSString *component=[[fm directoryContentsAtPath:path] objectAtIndex:row];
NSDictionary *attrib=[fm fileAttributesAtPath:
[path stringByAppendingPathComponent:component] traverseLink:YES];
[cell setTitle:component];
[cell setLeaf:![[attrib objectForKey:NSFileType] isEqualToString:NSFileTypeDirectory]];
}
Nejprve zjistíme, jaká cesta požadovanému objektu předchází — přesně stejně, jako v metodě browser:numberOfRowsInColumn:. Do proměnné "component" pak uložíme jméno objektu, který se má zobrazit (získané opět z file manageru); do proměnné "attrib" pak uložíme atributy tohoto objektu. V předposledním řádku pak nastavíme obsah "buňky" na jméno daného objektu (pomocí zprávy setTitle:).
Pro pochopení posledního řádku si musíme uvědomit, že browser, jako hierarchický zobrazovač, musí mít k dispozici informaci o tom, které objekty mají další vnitřní strukturu (složky), a které ne (soubory). I tuto informaci, stejně jako jméno objektu, nese buňka: pošleme-li ji zprávu setLeaf: s pravdivým argumentem, bude interpretována jako listový objekt (soubor). V posledním řádku je vidět, jak k tomu využijeme atributy objektu.
Tím je základ aplikace hotov; můžeme ji zbuildovat a použít browser pro prohlížení systému složek a souborů. Zatím jsme si ovšem ukázali jen službu awakeFromNib, a spolupráci s browserem; budeme proto ještě chvíli pokračovat, a ukážeme si některé další služby AppKitu.
Podpora Services
Jako další příklad doplníme do aplikace plnohodnotnou podporu Services. To je dobrý příklad mj. proto, že v sobě v podstatě zahrnuje i práci se schránkou (řekneme si jak a proč), a také proto, že vinou carbonovských nesmyslů jsou dnes Services v Mac OS X neprávem opomíjeny, a je velmi žádoucí, aby na ně programátoři v Cocoa nezapomínali.
Pro pasivní využití Services nám stačí zařídit jen tři věci:
Vzhledem k tomu, že jsme zapojili náš controller do responder chainu (připojením na outlet "delegate" objektu "File's Owner"), můžeme vše implementovat přímo v něm. Pro zajištění prvého bodu prostě stačí přidat do metody awakeFromNib řádek
[NSApp registerServicesMenuSendTypes:[NSArray arrayWithObject:NSFilenamesPboardType]
returnTypes:nil];
Globální proměnná NSApp vždy obsahuje odkaz na aktuální objekt třídy NSApplication; jeho prostřednictvím máme přístup ke všem standardním službám na úrovni aplikace jako celku. Zde zaregistrujeme naši aplikaci jako potenciálního uživatele Services, jež pracují nad datovým typem NSFilenamesPboardType — tj. jméno souboru.
Na základě této registrace může AppKit zasílat zprávu validRequestorForSendType:returnType: kdykoli chce zjistit, zda právě v tomto okamžiku lze danou službu využít. Implementace je samozřejmě triviální — v naší aplikaci lze službu využít kdykoli, když je označen jakýkoli objekt. Ověříme tedy jen, zda je v browseru nějaká "buňka" označena; pokud ano, vrátíme odkaz na controller:
-(id)validRequestorForSendType:(NSString*)sendType returnType:(NSString*)returnType {
if ([browser selectedCell]) return self;
return nil;
}
Malinko složitější je jen poslední metoda, writeSelectionToPasteboard:types:, již AppKit volá v případě, že uživatel zvolil odpovídající službu, a úkolem aplikace je předat této službě (přesněji řečeno, jiné aplikaci, jež službu nabízí) označená data.
Příčinou komplikace je to, že v browseru může být označeno libovolně mnoho objektů najednou, a my samozřejmě musíme systému služeb předat všechny. Proto je zapotřebí nejprve pomocí NSEnumeratoru připravit seznam všech označených objektů, a pak teprve jej předat dál. I tak nám na ni stačí přibližně deset řádků:
-(BOOL)writeSelectionToPasteboard:(NSPasteboard*)pboard types:(NSArray*)types {
if ([types containsObject:NSFilenamesPboardType] && [browser selectedCell]) {
NSString *rootPath=[[browser path] stringByDeletingLastPathComponent];
NSMutableArray *selectedFiles=[NSMutableArray array];
NSEnumerator *en=[[browser selectedCells] objectEnumerator];
NSCell *cell;
while (cell=[en nextObject])
[selectedFiles addObject:
[rootPath stringByAppendingPathComponent:[cell title]]];
[pboard declareTypes:[NSArray arrayWithObject:NSFilenamesPboardType] owner:nil]; //*
return [pboard setPropertyList:selectedFiles forType:NSFilenamesPboardType]; //*
}
return NO;
}
Většinu řádků vyjma dvou označených hvězdičkou si nemusíme zvlášť popisovat; převážně jde o služby, jež už dávno známe z FoundationKitu. To, že browser na základě zprávy selectedCells vrátí pole všech označených buněk je snad také zřejmé.
Podívejme se však trochu blíže na služby třídy NSPasteboard: jde o standardní třídu AppKitu, jež se stará o komunikaci a předávání dat mezi různými aplikacemi. O detaily se samozřejmě nemusíme starat: naším úkolem je pouze určit typ dat, jež chceme předat — k tomu právě slouží zpráva declareTypes: z prvého označeného řádku — a o vlastní zapsání dat pomocí zprávy setPropertyList:forType: na druhém.
Zde se právě scházíme s využitím schránky: pokud bychom chtěli zajistit, aby aplikace podporovala standardní služby Cut/Copy/Paste, prostě v controlleru implementujeme odpovídající metody cut, copy a paste, a v nich obdobným způsobem využijeme další objekt třídy NSPasteboard, v tomto případě standardní schránku, již můžeme kdykoli získat pomocí výrazu [NSPasteboard generalPasteboard]. Nechceme-li do schránky ukládat strukturu ("property list"), ale textový řetězec či obecná binární data, máme k dispozici zprávy setString:forType: a setData:forType:. A čtení ze schránky? To uvidíme za chvilku, v souvislosti s nabídkou vlastních služeb.
Mimochodem, z hlediska využití existujících služeb je to vše: můžeme aplikaci zbuildovat a vyzkoušet — hned máme k dispozici např. služby pro archivaci souborů:
Nabídka vlastních služeb
Když už jsme se službami začali, ukážeme si také, jak vlastní služby implementovat: doplníme do naší aplikace službu "Select in MiniWM", která označený text interpretuje jako jméno složky nebo souboru, a danou složku či soubor zobrazí v browseru. Zároveň si na implementaci této služby samozřejmě ukážeme řadu dalších služeb a vlastností AppKitu.
Zatímco využívat Services mohla aplikace čistě programově, s nabídkou služeb to tak jednoduché není: služby aplikací musí být k dispozici i když aplikace neběží (operační systém aplikaci v takovém případě automaticky spustí kdykoli to je zapotřebí). Proto musí aplikace služby, jež nabízí, publikovat v nějakém srozumitelném a "zvenku čitelném" formátu.
AppKit tyto údaje — a dlouhou řadu dalších — udržuje ve standardním souboru jménem Info.plist, který obsahuje všechny podstatné informace o aplikaci, a který je standardně uložen uvnitř její složky (v podřízené složce Contents).
Obsah souboru Info.plist je vcelku jednoduchý: jde o slovník (tj. množinu dvojic <klíč, hodnota>); my do něj chceme přidat klíč "NSServices", jehož hodnotou je pole — každá položka tohoto pole representuje jednu službu. Údaje o službě jsou určeny (vnořeným) slovníkem, jehož klíče jsou (mj.):
V ProjectBuilderu slouží pro úpravy souboru Info.plist tzv. Target editor — editor vlastností výsledné aplikace. Konkrétní nastavení (už se specifikací naší nové služby) vidíme na následujícím obrázku:
Pomocí tlačítka "New Sibling" přidáme nový záznam, pojmenujeme jej "NSServices", změníme jeho typ na "Array", klepnutím na trojúhelníček vlevo jej otevřeme. Tlačítko se změní na "New Child" — přidáme jím položku 0, změníme její typ na "Dictionary", otevřeme jej... a tak dále. Nelekněte se, když vám editor pod rukou přehází položky (třídí je podle abecedy v nejnevhodnějších chvílích), a buďte rádi, že implementujeme jen jednu službu, protože editor bohužel nepodporuje práci se schránkou...
Podívejte se znovu na obrázek, kde je vidět kompletní záznam pro jednu službu "Select in MiniWM", která zpracovává textové řetězce (NSStringPboardType) a nic nevrací, a bude aplikaci "MiniWM" předána pomocí zprávy selectService.
To ostatní už jednoduché: nejprve řekneme systému, že všechny požadavky na služby řeší náš controller a nikdo jiný. Samozřejmě, jde opět o standardní službu třídy NSApplication — stačí do metody awakeFromNib přidat jediný řádek
[NSApp setServicesProvider:self];
O uživatelské rozhraní spojené se službami se ovšem starat nemusíme — to zajistí standardní knihovny. Ty také zabezpečí to, že pokud skutečně uživatel v rámci nějaké jiné aplikace naši službu "Select in MiniWM" zvolí, naše aplikace bude automaticky spuštěna (pokud již náhodou neběží), a controller dostane zprávu selectService: — její jméno jsme zvolili sami, jako "NSMessage" v souboru Info.plist.
V rámci zprávy, kterou systém posílá pro provedení služby, mohou být předány až tři argumenty: prvním je sdílená schránka (opět jde samozřejmě o objekt třídy NSPasteboard), ve které nalezneme data, nad kterými je třeba službu provést (a do které bychom uložili návratovou hodnotu, kdyby nějaká byla). Druhý a třetí argument zde nepoužijeme — jedná se o data, jež mohou rozlišovat různé služby v dynamických systémech, a prostor pro hlášení chyby. Aby bylo možné tyto argumenty pohodlně předat, doplní systém k našemu jménu služby (selectService) ještě ":userData:error:". Je tedy třeba v souboru Controller.m implementovat metodu
-(void)selectService:(NSPasteboard*)pboard
userData:(NSString*)data error:(NSString**)error {
if ([[pboard types] containsObject:NSStringPboardType]) {
NSString *s=[pboard stringForType:NSStringPboardType];
[[browser window] makeKeyAndOrderFront:self];
if (![browser setPath:s]) NSBeep();
}
}
V prvém řádku jen ověříme, jestli jsme opravdu dostali textová data. Pomocí standardní zprávy stringForType: je "vytáhneme" ze schránky a uložíme do proměnné "s" (přesně stejně bychom mohli získat jakákoli data ze standardní systémové schránky pro implementaci příkazu Paste).
Na předposledním řádku využijeme další dvojici služeb AppKitu: předně, kterýkoli objekt "view" samozřejmě ví, ve kterém okně leží — a vrátí odpovídající objekt třídy NSWindow na základě standardní zprávy window. Mezi mnoha službami, jež nabízí třída NSWindow, pak je i možnost přemístit okno do popředí a předat mu fokus zprávou makeKeyAndOrderFront:.
Na posledním řádku — poté, co jsme okno naší aplikace přesunuli do popředí — se pokusíme nastavit cestu v browseru na zadaný text zprávou setPath:. Pokud se to nepodaří, pípneme (stejně dobře bychom samozřejmě mohli zobrazit komplexní chybovou hlášku).
Samozřejmě, že bezprostředně po zbuildování aplikace její služby v ostatních aplikacích k dispozici nebudou — systém musí nejprve upravit svou databázi všech služeb, a naši aplikaci mezi ně zahrnout. Aby se to stalo, musíme
V OpenStepu bylo odjakživa možné si velmi nepříjemné odlogování uspořit provedením jednoduchého příkazu make_services; Apple však tento příkaz bohužel přestal podporovat, takže máme smůlu.
Co dále
Dnes jsme se naučili pracovat s browserem, ukázali jsme si některé z mnoha služeb tříd NSApplication, NSWindow, NSView, seznámili jsme se s podporou Services a nepřímo jsme si také ukázali, jak implementovat práci se schránkou. Kromě toho jsme si vysvětlili, co to je a k čemu je dobrý soubor Info.plist, a umíme jej editovat v ProjectBuilderu.
Příště dokončíme naše vyprávění o AppKitu (a zároveň celý kurs programování v Cocoa) tím, že si ukážeme podporu Cocoa pro aplikace, jež zpracovávají více dokumentů zároveň.
Zpět | Obsah | Další |
Copyright © Chip, O. Čada 2000-2003