Zpět Obsah Další

Jednoduchá ukázka...


Ukažme si jen tak "na ochutnání" jak se vlastně v systému Cocoa programuje. Připravíme jednoduchoučký program, jaký patrně zná většina správců WWW stránek: převod českého textu ve stránce na univerzální kódování UNICODE (jiný jednoduchý příklad — textový editor — si můžete prohlédnout zde). Ukážeme si vývoj programu v několika krocích:

V těchto příkladech nebudeme nijak podrobně popisovat použité služby; uvedeme jen stručný komentář nebo základní vysvětlení. Podrobnější seznámení s tou kterou službou (z Foundation nebo AppKitu) se samozřejmě dočkáte v dalších dílech našeho seriálu! Pokud by nebyly zřejmé některé prvky syntaxe Objective C, naleznete jeho základní popis v oddíle věnovaném objektům.

Obyčejný řádkový program

Nejprve si ukážeme to nejjednodušší, co vůbec může být: obyčejný řádkový program, jemuž jako argument dáme jméno vstupního souboru a jméno výstupního souboru, a program korektně převede jeden na druhý (a samozřejmě, "po cestě" zajistí požadované úpravy českých znaků).

Spustíme ProjectBuilder, vyžádáme si vytvoření nového projektu, z nabídky typů projektu zvolíme "Tool" (to je právě triviální řádkový program), a určíme jméno projektu a složku (pro uživatele DOSu nebo windows adresář), do které se má projekt uložit. ProjectBuilder automaticky vytvoří prázdnou kostru programu i s funkcí main. Doplníme triviálně kód pro dekódování argumentů a volání vlastní převodní služby (z třídy HTMLUniConversion, již naprogramujeme za chviličku); výsledek bude v ProjectBuilderu vypadat nějak takto:

P01

Vyžádáme si vytvoření třídy HTMLUniConversion, a ProjectBuilder automaticky připraví oba zdrojové soubory (hlavičkový i soubor pro implementaci), uloží do nich standardní deklarace a definice, a ihned implementaci otevře v editoru, abychom ji mohli doplnit. Kompletní kód bude prozatím triviální, a bude vypadat takto:

@implementation HTMLUniConversion
+(NSString*)convertString:(NSString*)input
{
    NSMutableString *out=[NSMutableString string];
    int i;

    for (i=0;i<[input length];i++) {
        unichar cc=[input characterAtIndex:i];

        if ((cc&0xff80)==0) [out appendFormat:@"%c",cc];
        else [out appendFormat:@"&#%d;",cc];
    }
    return out;
}
+(void)convertFile:(NSString*)infile toFile:(NSString*)outfile
{
    NSString *inp=[NSString stringWithContentsOfFile:infile];

    [[self convertString:inp] writeToFile:outfile atomically:YES];
}
@end

To je celé, a program hned napoprvé funguje korektně, žádné ladění nás nečeká. Smysl a funkce všech služeb by měly být víceméně jasné z kontextu: stringWithContentsOfFile: načte obsah souboru, writeToFile:atomically: naopak obsah textového řetězce do souboru zapíše (volba atomically:YES zajistí zápis do dočasného souboru, a teprve je-li zápis úspěšný, smazání původního a přejmenování dočasného).

Prostřednictvím služby characterAtIndex: můžeme přistupovat k jednotlivým znakům textového řetězce; v Cocoa se na každý řetězec — ať obsahuje cokoli — můžeme dívat jako na UNICODE text. Pak je zřejmý i následující test, má-li UNICODE znak bity 0xff80 nulové, je to docela obyčejný plain ASCII znak. Konečně služba appendFormat: prostě přidá na konec řetězce text, zkonstruovaný podle pravidel printf-formátu (s několika rozšířeními, jež jsou zde nepodstatná).

Rozdělení na dvě služby (vlastní převod "ze stringu do stringu" metodou convertString:, a nad ní postavený převod "ze souboru do souboru" metodou convertFile:toFile:) je klasická ukázka korektního programování: vždy se vyplatí implementovat výkonné metody co nejjednodušší, a pak je doplňovat službami v dalších metodách, postavených kolem nich. Vyplatí se to dvakrát: předně, jednodušší výkonné metody se snáze ladí; za druhé, program je mnohem flexibilnější (nebyl by pro nás např. žádný problém implementovat převod "ze vstupu na výstup", aniž bychom cokoli ukládali do souboru).

Na rovinu: v kolika jiných prostředích by to šlo takhle snadno? A to jsme ještě ani pořádně nezačali!

Práce se soubory a adresáři

Stávající podoba programu by ovšem zřetelně snesla pár vylepšení: určitě by měl být možné vynechat příponu souboru, a program by si sám měl doplnit standardní "html". Měli bychom také mít možnost určit jen jeden soubor, a konverse by se pak provedla na místě. Konečně, pro rozsáhlejší skupiny stránek by jistě bylo příjemné, kdybychom mohli určit jen složku, ve kkteré chceme zkonvertovat všechny HTML soubory, samozřejmě rekursivně, včetně složek vnořených. To vše je v Cocoa docela jednoduché — stačí přepsat obsah funkce main takto:

int main (int argc, const char *argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    // insert your code here
    NSString *infile,*outfile=nil;
    NSFileManager *fm=[NSFileManager defaultManager];
    BOOL isFolder;
    
    if (argc<2) {
        printf("HtmlUnicodeTool v1.0 (c) OCSoftware\n\n"
               "HtmlUnicodeTool <input> [<output>]\n\n"
               "translates national characters to UNICODE\n"
               "if <output> is not given, translates in place\n"
               "if <input> is a folder, translates all HTML\n"
               "files recursively\n");
        exit(0);
    }
    infile=[NSString stringWithCString:argv[1]];
    if (argc>2) outfile=[NSString stringWithCString:argv[2]];

    if (outfile==nil) outfile=infile;
OnceAgain:
    if (![fm fileExistsAtPath:infile isDirectory:&isFolder]) {
        // if no extension, try again with "html"
        if ([[infile pathExtension] length]==0) {
            infile=[infile stringByAppendingPathExtension:@"html"];
            goto OnceAgain;
        }
        printf("Input \"%s\" does not exist",[infile cString]);
        exit(1);
    }
    if (isFolder) { // recursively
        NSDirectoryEnumerator *enumerator=[fm enumeratorAtPath:infile];
        NSString *fname;

        while (fname=[enumerator nextObject])
            if ([[fname pathExtension] isEqualToString:@"html"]) {
                NSString *from=[infile stringByAppendingPathComponent:fname];
                NSString *to=[outfile stringByAppendingPathComponent:fname];

                [HTMLUniConversion convertFile:from toFile:to];
            }
    } else { // just plain file
        if ([[outfile pathExtension] length]==0)
            outfile=[outfile stringByAppendingPathExtension:@"html"];
        [HTMLUniConversion convertFile:infile toFile:outfile];
    }

    [pool release];
    exit(0);       // insure the process exit status is 0
    return 0;      // ...and make main fit the ANSI spec.
}

Tentokrát nepožadujeme zadání druhého souboru; není-li určen, použije se prostě stejné jméno jako pro soubor/složku vstupní. Pak zkontrolujeme existuje-li vůbec vstupní soubor (nebo složka — služba fileExistsAtPath:isDirectory: zároveň zjistí, o co jde); pokud ne, zkusíme to znovu s příponou "html".

Následuje druhý příkaz if, který větví program podle toho, zda byla zadána složka nebo soubor. Pro případ složky se použije služba enumeratorAtPath:, jež vrátí speciální objekt — enumerátor. Ten pak postupně na základě služby nextObject vrací všechny soubory, uložené uvnitř zadané složky a jejích podsložek. Stačí tedy ověřit příponu, a je-li rovna "html", soubor zkonvertujeme (smysl služby stringByAppendingPathComponent: je snad zřejmý).

Konečně, případ, kdy byl zadán obyčejný soubor, se od minulé varianty liší pouze tím, že ke jménu výstupního souboru je podle potřeby doplněna přípona "html".

Ani teď nebylo zapotřebí nic ladit; díky pohodlným a přehledným službám Foundation Kitu i tento program fungoval bez problémů hned napoprvé.

Aplikace s grafickým uživatelským rozhraním

No dobrá, říkáte si, to je sice pěkné, ale kdo stojí o řádkové programy?  V pořádku — tentokrát uděláme kompletní aplikaci, s menu, s okýnky a vůbec se všemi pěti P. Aplikace bude mít okno, v němž se bude zobrazovat jméno právě konvertovaného souboru, a bude v ní i grafický ukazatel průběhu práce. Soubory a/nebo adresáře bude možné nejen zvolit pomocí standardního otevíracího panelu, ale také je budeme moci vhodit ("drag&drop") na ikonu aplikace.

Začneme zase v ProjectBuilderu; nyní však vytvoříme nový projekt typu "Aplikace". ProjectBuilder nám vytvoří opět standardní kostru projektu; její součástí je i soubor "HtmlUnicodeApp.nib", který obsahuje síť objektů, representujících uživatelské rozhraní aplikace (nebo jeho část — souborů NIB můžeme mít v aplikaci podle potřeby libovolné množství). Poklepeme na něj, a ProjectBuilder automaticky otevře aplikaci, určenou pro práci s objektovými sítěmi — InterfaceBuilder.

V něm vidíme, že automaticky vygenerovaný NIB obsahuje standardní aplikační menu a jedno okno. V menu již jsou připraveny příkazy pro všechny standardní operace; zrušíme tedy ty, jež nebudeme potřebovat (např. příkaz "File/New"): v okně s menu je prostě označíme myší, a stiskneme klávesu Delete:

Menu v InterfaceBuilderu

Do okna umístíme tabulku (pro zobrazení všech souborů, jež budou konvertovány), textové pole pro určení cílové složky a tlačítko, jímž otevřeme panel pro výběr cílové složky z disku. Všechny prvky prostě do okna "naházíme" pomocí myši z palety; na obrázku vidíme umístění posledního tlačítka na místo:

P03_IB

Připravíme druhé okno (i okna, stejně jako ostatní grafické prvky, prostě vytáhneme z palety), a do něj uložíme grafické prvky pro zobrazování průběhu práce: textová pole pro jméno zpracovávaného souboru a jméno cílového souboru, a ukazatel průběhu.

Nyní připravíme objekt, který bude řídit komunikaci mezi grafickým uživatelským rozhraním a engine aplikace (engine nám bude representovat třída HTMLUniConversion, již převezmeme beze změny z minulého projektu). Většinu vlastností tohoto objektu můžeme pohodlně připravit v InterfaceBuilderu: přepneme jeho okno do režimu "Classes", zvolíme třídu, z níž chceme dědit (bude to základní třída NSObject, protože nepožadujeme žádné specifické služby, a programujeme v Objective C; kdybychom chtěli programovat v Javě, vybrali bychom ovšem java.lang.Object).

Příprava nové třídy v IB

Obrázek ukazuje, že jsme již začali definovat "outlety" a "akce". Jejich význam je jednoduchý: "outlet" je proměnná objektu, jež obsahuje odkaz na jiný objekt; "akce" je zpráva, již objekt za nějakých okolností může dostat. Je tedy zřejmé, že budeme definovat "outlety" pro všechny objekty, jež chceme mít ve zdrojovém kódu k dispozici — table pro tabulku a target pro textové pole z prvého okna (jež vidíme na předminulém obrázku). "Akce" naopak definujeme pro všechny akce, jež může uživatel tak či onak vyvolat; na obrázku vidíme akci browseTarget:, již vyvolá stisknutí tlačítka u textového pole.

Přidáme ještě "outlety" pro textová pole a pro indikátor postupu v druhém okně, a doplníme zbývající akce pro položky menu "Open" (přidá soubory do tabulky) a "Save" (provede konversi) — obě položky již v menu jsou, uložil je tam jako standardní již ProjectBuilder. Pak si vyžádáme vytvoření instance objektu, a můžeme navázat všechna potřebná spojení.

To v InterfaceBuilderu uděláme tak, že přidržíme klávesu Control, a "natáhneme drát" od objektu, jehož "outlet" chceme naplnit (nebo který může na základě uživatelova požadavku vyvolat "akci") na objekt, jenž má být prostřednictvím "outletu" přístupný (nebo jehož "akce" se má provést). Propojení z následujícího obrázku například zajistí, že stiskne-li uživatel tlačítko "vybrat...", provede se akce browseTarget: objektu Controller:

P05_IB

Uživatelé VisualBASICu se možná zeptají: proč ale vůbec takové propojování, když bychom mohli definovat přímo akce pro jednotlivé objekty uživatelského rozhraní? Namísto definování akce pro Controller a "drátování" na tlačítko bychom tedy akci nadefinovali a naprogramovali rovnou pro to tlačítko.

Jistě, možné by to bylo. Takové programování však je zásadně špatné, protože míchá dohromady dvě naprosto nesouvisející záležitosti: prvky uživatelského rozhraní, a jejich konkrétní význam. Toto propojení má být v objektovém systému volné, co možná nejvolnější — Cocoa právě proto využívá "dráty". Konkrétní implementace "akcí" pak je tam, kde skutečně má být — v samostatném objektu, stojícím mezi engine aplikace a jejím GUI. Díky tomu je engine nezávislé na GUI a GUI zase na engine; to je hlavní příčina obrovské flexibility prostředí Cocoa.

Můžeme se na to podívat i z ryze praktického hlediska: "visualbasicový" přístup je nesmírně nepružný, protože fixuje konkrétní akci na konkrétní prvek GUI. Ne tak Cocoa, jež umožňuje navázat podle potřeby libovolnou strukturu vazeb: dejme tomu, že uživatelé by si přáli mít možnost vyvolat panel nejen tlačítkem "vybrat...", ale také příkazem "Target" z menu. V Cocoa pro zajištění takového požadavku vůbec nemusíme programovat: prostě přidáme odpovídající položku do menu, a natáhneme jeden "drát" navíc:

P06_IB

Po "natahání všech drátů" si ještě vyžádáme, aby nám InterfaceBuilder vygeneroval kostry zdrojových souborů pro třídu Controller, a vrátíme se do ProjectBuilderu.

Zde nejprve přidáme již hotový "engine": příkaz "Add Files" otevře panel pro výběr souborů, kde zvolíme zdrojový soubor s implementací "HTMLUniConversion.m". ProjectBuilder soubor zkopíruje do tohoto projektu, a automaticky k němu přibere i soubor s rozhraním "HTMLUniConversion.h".

Nyní už zbývá jen dokončit implementaci třídy Controller, a budeme hotovi: s ničím ostatním si nemusíme lámat hlavu, korektní zavedení NIBu, vyplnění "outletů", provedení "akcí" na základě činnosti uživatele a podobně už zajistí služby Cocoa automaticky.

Krom "outletů" budeme ve třídě Controller potřebovat ještě jednu proměnnou: pole jmen souborů, jež by se měly konvertovat. Přidáme ji proto do interface; přidaný řádek je vyznačen tučně, obyčejný text ukazuje kostru, již InterfaceBuilder vygeneroval automaticky:

@interface Controller : NSObject
{
    id currName;
    id currPath;
    id currTarget;
    id progress;
    id table;
    id target;
    NSMutableArray *files;
}
- (void)addSources:(id)sender;
- (void)browseTarget:(id)sender;
- (void)convert:(id)sender;
@end

Implementace "akcí" bude sice malinko složitější, opravdu ale jen nepatrně. Ukážeme si postupně všechny tři; nejprve tu nejjednodušší, kterou je browseTarget:

- (void)browseTarget:(id)sender
{
    NSOpenPanel *op=[NSOpenPanel openPanel];

    [op setAllowsMultipleSelection:NO];
    [op setCanChooseDirectories:YES];
    if ([op runModal]==NSOKButton) [target setStringValue:[op filename]];
}

Zprávou openPanel si vyžádáme standardní objekt, representující v Cocoa panel pro otevírání souborů. Nastavíme jeho atributy (zde především to, že chceme vybírat složky a ne soubory), a spustíme jej zprávou runModal. Po návratu (a ověření návratové hodnoty, jež indikuje, zda uživatel nezavřel panel tlačítkem "Cancel") prostě zapíšeme adresář, který byl v panelu vybraán (filename) do odpovídajícího textového pole zprávou setStringValue:.

Abychom mohli pohodlně implementovat akci addSources:, připravíme si nejprve pomocnou metodu addSourcesFrom:, která do tabulky přidá HTML soubory podle svého argumentu. Ilustrujeme zde i schopnost objektového prostředí, zvanou polymorfismus: argumentem metody addSourcesFrom: totiž bude moci být stejně dobře přímo soubor či adresář, jako pole objektů. Bude-li to pole, zavolá prostě metoda sama sebe rekursivně na každý prvek v poli:

- (void)addSourcesFrom:object // array, or folder, or file
{
    if ([object isKindOfClass:[NSArray class]]) {
        NSEnumerator *en=[object objectEnumerator];

        while (object=[en nextObject]) [self addSourcesFrom:object];
    } else {
        NSFileManager *fm=[NSFileManager defaultManager];
        BOOL isFolder;

        if ([fm fileExistsAtPath:object isDirectory:&isFolder]) {
            if (!files) files=[NSMutableArray new];
            if (isFolder) {
                NSDirectoryEnumerator *en=[fm enumeratorAtPath:object];
                NSString *file;

                while (file=[en nextObject])
                    if ([[file pathExtension] isEqualToString:@"html"])
                        [files addObject:[object stringByAppendingPathComponent:file]];
            } else [files addObject:object];
            [table reloadData];
        }
    }
}

První if ověří, zda argumentem nebylo pole: ano-li, probereme jeho prvky pomocí již známého enumerátoru, a na každý se metoda zavolá rekursivně (příjemce zprávy self je prostě tentýž objekt, který danou metodu právě teď zpracovává). Jinak je implementace téměř totožná s funkcí main z druhého příkladu, takže by měla být zřejmá snad všem — jedinou novinkou je zpráva reloadData, a ta prostě informuje tabulku, že se zobrazovaná data změnila.

Nyní by již mělo být zřejmé jak implementovat akci addSources: — je to skutečně triviální:

- (void)addSources:(id)sender
{
    NSOpenPanel *op=[NSOpenPanel openPanel];

    [op setAllowsMultipleSelection:YES];
    [op setCanChooseDirectories:YES];
    if ([op runModal]==NSOKButton) [self addSourcesFrom:[op filenames]];
}

Povšimněme si, že tentokrát na rozdíl od browseTarget: dovolujeme volbu více souborů nebo adresářů najednou. Hned můžeme také na pár řádcích zajistit, aby bylo možné soubory a adresáře do aplikace vhazovat ("drag&drop"): stačí implementovat standardní metodu application:openFile:; framework Cocoa se automaticky postará o to, aby odpovídající zprávu dostal tzv. delegát aplikace — jímž je v našem případě právě objekt Controller — kdykoli do aplikace vhodíme nějaký objekt:

- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename
{
    [self addSourcesFrom:filename];
    return YES;
}

Zbývá poslední metoda convert:. Tu nám trochu komplikuje ovládání okna, ve kterém zobrazujeme postup konverze; bez něj by i tato metoda byla naprosto triviální. Pro lepší přehled proto v následujícím zdrojovém textu zobrazíme tučně ty řádky, jež zajišťují vlastní komversi; ostatní — jak si hned vysvětlíme — řídí informační okno:

- (void)convert:(id)sender
{
    NSEnumerator *en;
    NSString *file;
    NSString *tgt=[target stringValue];
    unsigned long long size=0;
    NSFileManager *fm=[NSFileManager defaultManager];

    [currName setStringValue:@"Sizing files..."];
    [currTarget setStringValue:@""];
    [progress setIndeterminate:YES];
    [progress setUsesThreadedAnimation:YES];
    [progress startAnimation:self];
    [[progress window] center];
    [[progress window] makeKeyAndOrderFront:self];
    for (en=[files objectEnumerator];file=[en nextObject];) {
        [currPath setStringValue:file];
        [[progress window] display];
        size+=[[fm fileAttributesAtPath:file traverseLink:YES] fileSize];
    }
    if ([tgt length] && [[[fm fileSystemAttributesAtPath:tgt] objectForKey:NSFileSystemFreeSize] unsignedIntValue] < size) {
        [[progress window] orderOut:self];
        NSRunAlertPanel(@"No disk space",@"Not sufficient free space at \"%@\"",nil,nil,nil,tgt);
        return;
    }

    [progress setMaxValue:size];
    [progress setDoubleValue:0];
    [progress setIndeterminate:NO];
    for (en=[files objectEnumerator];file=[en nextObject];) {
        NSString *tt=[tgt stringByAppendingPathComponent:file];

        [currName setStringValue:[file lastPathComponent]];
        [currPath setStringValue:file];
        [currTarget setStringValue:tt];
        [HTMLUniConversion convertFile:file toFile:tt];
        [progress incrementBy:[[fm fileAttributesAtPath:file traverseLink:YES] fileSize]];
        [[progress window] display];
    }
    [[progress window] orderOut:self];
}

Konverse se provede ve dvou blocích: v prvém jen zjistíme údaje, potřebné pro korektní zobrazení indikátoru postupu, a zároveň ověříme, je-li na cílovém disku dostatek místa (jestlipak vás to napadlo, nenechat uživatele zjistit podobnou nepříjemnost až ve dvou třetinách práce? V NeXTStepu a OS X je to naprosto samozřejmé!).

Implementace je celkem zřejmá: nejprve nastavíme vhodné hodnoty do textových polí pro jméno aktuálního souboru ("Sizing...") a cílový soubor (prázdný řetězec), a změníme typ ukazatele postupu práce na "indeterminate", tj. takový, který jen ukazuje že se něco děje, ale není jasné jak to bude trvat dlouho. Vyžádáme si také animaci indikátoru v samostatném threadu (aby indikátor běžel aniž bychom se o to museli sami starat), a zapneme ji. Pak umístíme informační okno ([progress window], doslova tedy "to okno, ve kterém je objekt progress") do středu obrazovky a "vytáhneme" jej do popředí zprávou makeKeyAndOrderFront:. Nakonec jen posčítáme velikosti všech souborů (přitom zobrazujeme jméno právě testovaného souboru v textovém poli currPath), a ověříme, je-li na cílovém disku dost místa.

Druhý blok je ještě jednodušší: přestavíme indikátor průběhu na "determinate", a určíme, že bude zobrazovat hodnoty od nuly do celkové velikosti všech souborů. Pak pro každý soubor zobrazíme jeho jméno, cestu a cílový soubor v odpovídajících textových polích informačního okna (currName, currPath, currTarget), provedeme konversi, a inkrementujeme indikátor o velikost souboru. Úplně nakonec, když je vše hotovo, informační okno zase schováme zprávou orderOut:.

Mimochodem, zprávy display musíme použít, chceme-li aby se okno překreslilo okamžitě (což je samozřejmě v tomto případě nutné). Pokud nám ale stačí překreslení okna až při čekání na další událost (myš, klávesnici apod.), nemusíme se start ani o to, a Cocoa okno překreslí sama: podívejme se znovu na implementaci metody browseTarget: — žádné display zde nepoužíváme, ale přesto se nová hodnota v textovém poli target zobrazí korektně.

Nakonec zbývá jen zajistit, aby tabulka zobrazovala správné hodnoty. Přestože jsme si to malinko zkomplikovali tím, že tabulka má dva sloupce, z nichž prvý zobrazuje jméno souboru a druhý cestu, je to opravdu jednoduché: stačí v objektu, který pro tabulku slouží jako data source — v našem případě je to opět Controller — implementovat dvě jednoduché metody:

-(int)numberOfRowsInTableView:(NSTableView*)table
{
    return [files count];
}
-(id)tableView:(NSTableView*)table objectValueForTableColumn:(NSTableColumn*)col row:(int)row
{
    if ([[col identifier] isEqualToString:@"soubor"])
        return [[files objectAtIndex:row] lastPathComponent];
    return [[files objectAtIndex:row] stringByDeletingLastPathComponent];
}

Je to jasné, ne? Prvá metoda řekne tabulce, kolik bude mít řádků. Druhá pak dostane identifikátor sloupce (povšimněte si if-u, jímž se zjistí, jde-li o sloupec se jménem souboru nebo s cestou) a číslo řádku, a vrátí prostě objekt, který se na tom místě v tabulce má zobrazit.

A to je opravdu všechno. Aplikace je hotova (přesně řečeno, bude, jakmile ji přeložíme a odladíme); díky přehlednosti a jednoduchosti systému Cocoa chodila hned napodruhé, ladit nebylo třeba skoro nic — napoprvé jsem jen zapomněl na příkazy[[progress window] display], ale to se mi připomnělo samo hned, jakmile jsem při prvém sppuštění viděl, že informační okno se nepřekresluje po každém souboru.

WWW aplikace

Nakonec si ukážeme využití opět stejného aplikačního engine ve webové aplikaci. Abychom mohli lépe ukázat podobnosti (a odlišnosti) webových a standardních aplikací, zvolíme obdobný model práce, jako ten, který jsme programovali v minulém případě — a to i přesto, že pro webovou aplikaci není optimální: každé zadané URL sice ihned zkonvertujeme, ale namíto okamžitého zobrazení jej zařadíme do tabulky. Z ní teprve bude mít uživatel možnost otevřít původní nebo zkonvertovanou stránku.

Začátek je vlastně stejný jako již několikrát: v ProjectBuilderu vytvoříme nový projekt, tentokrát ale zvolíme typ "WebObjects Application". Do projektu opět přidáme již hotový engine "HTMLUniConversion.m" příkazem "Add Files" (v praxi bychom velmi snadno mohli z engine udělat framework, který by všechny tři programy sdílely; v této ukázce pro to ale není důvod). Podobně, jako uživatelské rozhraní standardní aplikace representoval NIB, je uživatelské rozhraní WWW aplikace representováno WO komponentami; základní komponentu jménem "Main" nám opět ProjectBuilder připravil automaticky. Otevřeme ji poklepáním, tím se spustí WebObjectBuilder, aplikace pro tvorbu a editaci komponent.

WebObjectsBuilder obsahuje kompletní (a velmi luxusní) HTML editor: všechny statické části komponenty — nadpisy, grafické prvky a podobně — v něm připravíme standardně jako v jakémkoli jiném HTML editoru:

P07_WOB

Kromě ryze statických prvků (jako třeba nadpis) jsme použili vlastně i jeden prvek dynamický: formulář, obsahující textové pole a tlačítko sice je standardní součástí HTML, WebObjecs s jeho prvky však dokáže pracovat stejně, jako by se jednalo o dynamické objekty. Další použitý prvek — tabulka — sice je plně statický, my ji ale upravíme tak, aby dynamicky zobrazovala všechny konvertované stránky (podobně, jako tabulka v aplikaci z minulého příkladu zobrazovala seznam souborů pro konverzi).

Nyní musíme nadefinovat "outlety" a "akce" podobně, jako tomu bylo v InterfaceBuilderu. WebObjectsBuilder je o něco flexibilnější: pole allUrls (odpovídající poli files z minulé aplikace) můžeme připravit rovnou v něm. Navíc budeme potřebovat "outlet" pro textové pole, další "outlet" pro tabulku, a jedinou "akci" pro tlačítko "Přidat":

P08_WOB

Na obrázku vidíme navazování akce addUrl na tlačítko "Přidat". Navíc je vidět, že "outlet" url již byl navázán na textové pole ve formuláři. Smysl je naprosto stejný, jako v InterfaceBuilderu: jakmile uživatel — v tomto případě uživatel WWW browseru — klepne na tlačítko "Přidat", dostane komponenta zprávu addUrl. Její kód přitom bude moci rovnou použít proměnnou url, jež bude obsahovat hodnotu, kterou uživatel předtím zapsal do textového pole.

Pro zobrazení kompletního obsahu pole allUrls v tabulce budeme potřebovat nové, dynamické komponenty. První a o něco málo složitější je opakování, objekt třídy WORepetition. Ten umožňuje opakování kteréhokoli prvku komponentu, tolikrát, kolikrát jen potřebujeme. Je zřejmé, že nám se velmi dobře hodí pro opakování řádku tabulky: budeme chtít zobrazit právě tolik řádků, kolik bude v poli allUrls prvků.

WORepetition funguje trochu podobně, jako klasický cyklus for: musíme určit nejen "obor hodnot", přes který se bude cyklus provádět, ale také "řídící proměnnou", která bude při každé iteraci cyklu obsahovat právě aktuální hodnotu. Proto jsme připravili "outlet" tableItem: ten bude sloužit jako "řídící proměnná", a pro každou iteraci cyklu (tj. pro každý řádek tabulky) bude obsahovat jeden prvek pole allUrls.

Jak bude vypadat obsah pole allUrls vlastně zatím nevíme — jeho vytvoření budeme programovat až za chvilinku. Uděláme to ale tak, aby jeho složky byly "balíčky" dvou hodnot: původního URL, a nového URL pro zkonvertovanou stránku. Původní URL bude v "balíčku" uloženo pod jménem "original", a druhé pod jménem "converted".

Toho využijeme při vyplňování polí tabulky: uložíme do nich dynamický odkaz, WOHyperlink. Jeho obsah — tj. URL, na něž má odkazovat — navážeme na hodnotu tableItem.original v prvém poli tabulky, tableItem.converted ve druhém. Více toho dělat nemusíme; systém WebObjects dokáže sám korektně interpretovat tuto tečku, a vybere z "balíčku" v proměnné tableItem vždy tu správnou hodnotu.

Pro naprogramování toho zbývá již jen málo, vlastně nejméně ze všech příkladů (to je pochopitelné, protože engine máme již hotový; navíc WebObjectsBuilder je novější než InterfaceBuilder, takže se dá více věcí "nadrátovat" v něm, a můžeme méně programovat: povšimněme si, že v minulé aplikaci jsme museli pro tabulku implementovat dvě metody, jež jí dávaly zobrazovaná data; tentokrát na to stačilo šikovně "nadrátovat" WORepetition a použít vhodně tečkovou notaci ve vazbách WOHyperlinku.

Zbývá tedy jenom napsat metodu addUrl, a ta bude velmi jednoduchá:

- addUrl {
    NSString *inp=[NSString stringWithContentsOfURL:[NSURL URLWithString:url]];
    NSString *out=[HTMLUniConversion convertString:inp];
    NSString *shared=@"/tmp"; // in real configuration, this would be some shared folder
    NSString *urlShared=@"file://tmp"; // in real, something like "http://www.ocs.cz/shared"

    // put converted file to shared folder
    [out writeToFile:[shared stringByAppendingPathComponent:[url lastPathComponent]] atomically:YES];
    // append new data to the allUrls
    [allUrls addObject:[NSDictionary dictionaryWithObjectsAndKeys:
        url,@"original",
        [urlShared stringByAppendingPathComponent:[url lastPathComponent]],@"converted",
        nil]];
    return nil;
}

Nejprve si "vytáhneme" do textového řetězce obsah zadaného URL: služba URLWithString: jen převede textovou podobu na obecný URL objekt, a službou stringWithContentsOfURL: pak načteme jeho obsah do řetězce inp. Ten standardně, pomocí již dávno hotového engine, převedeme na výsledný řetězec do proměnné out.

Otázka zní, kam uložit výsledek a jak jej zpřístupnit. Pokud používáme klasický HTTP server, je nejjednodušší výsledek uložit do některé sdílené složky, kam má server snadno přístup: obecná cesta k takovéto složce je v proměnné shared. Adresa, již HTTP server použije pro přístup k této složce, samozřejmě může vypadat také lecjaks, a záleží na konkrétní konfiguraci; zde je uložena v proměnné urlShared. Následující příkaz pak zkonvertovaný string uloží do složky shared pomocí již známé zprávy writeToFile:atomically:.

Zbývá poslední věc, zapsat do pole allUrls údaje o nové dvojici <původní, zkonvertované> URL. To dělá příští příkaz — význam zprávy addObject: je zřejmý, zpráva dictionaryWithObjectsAndKeys: vytvoří právě ten "balíček", o kterém jsme před chvilkou mluvili: objekt, který obsahuje staré a nové URL, a umožňuje k nim přístup podle jmen "original" a "converted".

To je vše! Nyní stačí aplikaci zbuildovat a spustit: přístup k ní máme samozřejmě tentokrát prostřednictvím WWW browseru (a to z libovolného počítače, ze kterého je přístupný server, na kterém aplikace a HTTP server běží — obecně tedy třeba z celého světa!). Aplikace zase znovu, opět díky jednoduchosti a přehldnosti API, chodí bez jakéhokoli ladění:

P09_WWW

Poslední obrázek ukazuje stránky s původním a zkonvertovaným textem: povšimněte si zobrazení ž a š, jež dokládají, že aplikace skutečně pracuje korektně a převádí "nečitelná" osmibitová kódování na korektní UNICODE:

P10_WWW


Zpět Obsah Další

Copyright © Chip, O. Čada 2000-2003