Program Fotbal

Vytvoříme program, který bude zpracovávat tabulku sportovní soutěže (fotbal.dpr). Soutěž, kterou takto zpracujeme, má specielní pravidla. Hraje se jednokolově, každý s každým. Pokud je v soutěži 12 týmů, znamená to, že se hraje 11 kol a v každém kole se hraje 6 zápasů. Za výhru získává tým 2 body, za remízu bod, za prohru žádný. Zápas může být kontumován, týmu mohou být odečteny další body. Program by měl umět vytvořit rozpis soutěže, umožnit zadávání výsledků zápasů a sestavovat tabulky. Přidáme také textový výstup na tiskárnu.

Uvedená pravidla neodpovídají třeba fotbalové lize, program by šel však pro jiné soutěže upravovat. Podle těchto pravidel se hraje pražská Hanspaulská liga malého fotbalu.

obrázek 1: Program Fotbal

Hlavními ovládacími prvky programu jsou hlavní menu a skupina labelů v levé části formuláře. Menu slouží k ovládání projektu, který budeme zpracovávat, klikání na labely vyvolává jednotlivé části zpracování projektu. Funkce jednotlivých částí je popsána v nápovědě.

Opět nebudeme rozebírat realizaci celé aplikace, ale probereme části, které se objevují nově a projdeme zajímavá místa projektu. Zdrojový kód je opět podrobně okomentován.

Další unita

Tentokrát je kladen důraz na vzhled celé aplikace i jejich výstupů. Výstupy jsou textové (do mema nebo na tiskárnu) a tyto texty jsou formátovány. Používám neproporcionální font, aby šířky znaků byly stejné a text se tak bude dobře formátovat do tabulek. Velmi často se sestavují textové řetězce z několika hodnot, které se ještě upravují. V Pascalu můžu pro úpravu textových řetězců využívat například funkce Pos a Copy. Pos vrací pozici podřetězce v řetězci, Copy vrací podřetězec z řetězce podle určení pozice a délky. Podrobný popis těchto funkcí a dalších funkcí pro práci s řetězci naleznete v nápovědě.

Nabídka funkcí pro práci s řetězci je v Pascalu dost omezena. Vytvořil jsem proto 3 další funkce pro práci s textovými řetězci:

function AtStr(s1,s2:string;c:integer):integer;
function LeftStr(s:string;x:integer):string;
function RightStr(s:string;x:integer):string;

Funkce AtStr vrací pozici c-tého výskytu řetězce s1 v řetězci s2. Pokud se řetězec s1 v řetězci s2 c-krát nevyskytuje, funkce vrací nulu. Funkce LeftStr vrací x znaků zleva z řetězce s. Pokud je řetězec s kratší, než x znaků, je vrácen celý řetězec s. Podobně funguje funkce RightStr, která vrací x znaků zprava.

Tyto funkce jsou celkem užitečné a mohl bych je využívat i v jiném projektu. Všechny funkce pracují výhradně s parametry, nejsou závislé na globálních proměnných ani na formuláři. Tyto funkce tedy zapíšu do jiné unity a v unitě, kam zapisuji kód celého projektu využiji druhou unitu.

Z menu File vyberu volbu New a kliknu na ikonu Unit. V okně editoru se vytvoří nová unita, ve které je už zapsána základní struktura. V části interface zapíšu hlavičky nových podprogramů, v části implementation pak celé funkce. Unitu uložím pod jménem OpStr.pas (v menu File volba Save).

Pokud chci využívat funkce z unity OpStr, Musím v unitě projektu na tuto skutečnost upozornit. Pokud bych tak neučinil, volání nových funkcí končí chybou. Unity, které chci v projektu využít, zapisuji za klíčové slovo uses na začátku části interface. Za tímto klíčovým slovem je už seznam unit, které se připojují automaticky. Připojím tedy jméno OpStr a dále také unitu Printers, kterou využiji pro výstup na tiskárnu.

obrázek 2: Připojení dalších unit

V projektu teď můžu využít všechny funkce, které jsem v unitě OpStr připravil. Pokud bych chtěl unitu OpStr použít i v jiném projektu, provedu to stejným způsobem. Pro aplikaci není důležité, jak je zapsáno tělo podprogramů zapsaných v dalších unitách, důležité je, co podprogramy dělají, jak je volat a jak jim předat parametry. Pokud bych tedy změnil těla funkcí, ale zůstaly by stejné jejich hlavičky i funkce, aplikace to vůbec nepozná.

Vytvoříte-li tedy podprogramy, které mají širší využití než v jednom projektu, zapsáním do zvláštní unity umožníte jednoduše jejich další používání. Je to lepší, než zdrojový kód kopírovat. Není však možné použít práci s globálními proměnnými v hlavní unitě aplikace a nejde ani pracovat s vlastnostmi a metodami komponent.

Unity mohou obsahovat nejen podprogramy, ale také definice konstant a typů, které pak můžeme využívat v projektu, kam unitu připojíme. Výhodou zvláštních unit je také to, že není nutné přenášet zdrojový kód, stačí připojit unitu v přeloženém tvaru, tedy s příponou .dcu. Pokud byste chtěli poskytnout svojí unitu někomu jinému, nemusíte ukazovat, jak je unita naprogramovaná, stačí uvést, co unita nabízí a jak se to používá. Tyto informace najdete v části interface, opis této části můžete např. dodat s přeloženou unitou.

Více o nápovědě

V této aplikaci je použita složitější verze standardní nápovědy. Nápověda obsahuje více témat, má vytvořený obsah a rejstřík a je možné v ní vyhledávat podle klíčových slov.

Text nápovědy se opět zapisuje v textovém souboru, který se uloží ve formátu RTF. Jednotlivá témata se oddělují zalomením stránky. Témata je opět nutné pojmenovat, což se provádí vložením poznámky pod čarou uvozenou symbolem #. Pro vytvoření rejstříku je třeba u jednotlivých témat uvést klíčová slova. Klíčová slova se uvádí opět v poznámce pod čarou, která je uvozena písmenem K. V poznámce pod čarou uvozenou symbolem $ se uvádějí názvy témat, na které je odkazováno při vyhledávání klíčových slov. Typicky se tedy u každého tématu (jedna stránka nápovědy) nacházejí 3 poznámky pod čarou.

Projekt nápovědy tvoříme opět v nástroji Microsoft Help Workshop. Při mapování témat (tlačítko Map) uvedeme seznam všech témat (symbolické názvy podle poznámky uvozené symbolem #) a každému tématu přiřadíme jiné číslo. V tomto příkladu je upraveno také okno, ve kterém se bude nápověda zobrazovat. Po stisku tlačítka Windows je na kartě General zapsán nadpis okna a na kartě Position určena pozice a velikost okna. Projekt uložíme jako fotbal.hpj.

Zbývá vytvořit obsah nápovědy. Obsah nápovědy se tvoří ve zvláštním souboru, Help Contents. Tento soubor vytvoříme opět v Help Workshopu. Z menu File vybereme volbu New a v nabídce klikneme na Help Contents. V horní části okna zadáme jméno souboru (může být stejné jako jméno help projectu) a nadpis obsahu. K přidávání položek obsahu slouží tlačítka Add Above (přidej nad) a Add Below (přidej pod). Po kliknutí na tato tlačítka se zobrazí okno Edit Contents Tab Entry. Nejdříve přidáme nadpis, v okně vybereme položku Heading a zadáme název (Title). Pak můžeme přidávat jednotlivá témata. Vybíráme položku Topic, jako Title zadáme název tématu a jako Topic ID zapíšeme symbolické jméno tématu, které jsme zapsali do textu nápovědy v poznámce pod čarou.

Struktura obsahu nápovědy je hierarchická, můžeme vytvářet různé úrovně v obsahu přidáváním dalších nadpisů. Umístění jednotlivých témat v obsahu můžeme upravovat klikáním na tlačítka Move Right a Move Left.

obrázek 3: Tvorba obsahu nápovědy

Když jsme s tvorbou struktury obsahu hotovi, soubor uložíme. Vytvoří se soubor se zadaným jménem a příponou .cnt, v našem případě fotbal.cnt. Tento soubor je nutné připojit k vytvořenému help projectu. V Help Workshopu opět otevřeme fotbal.hpj a po kliknutí na tlačítko Options na kartě na kartě Files zadáme jméno souboru se strukturou nápovědy (Contents File).

Zbývá už jen nápovědu zkompilovat a uložit. Pokud je všechno v pořádku, stane se tak po kliknutí na tlačítko Save and Compile. Vznikne tak soubor fotbal.hlp.

Tak vytvoříme nápovědu, která má standardní vzhled a využívá obsah, rejstřík i vyhledávání. Zápis více témat v nápovědě využijeme také v aplikaci. Na různých místech můžeme vyvolávat různá témata nápovědy. Spolu s aplikací je pro správnou funkčnost nápovědy nutné přenášet i soubor s nápovědou (fotbal.hlp) a soubor s obsahem nápovědy (fotbal.cnt).

Kontextové menu

Pro jednotlivé funkce aplikace máme připravenu nápovědu. Nápověda jde vyvolat z menu volbou Obsah. Volba O programu pouze zobrazuje okno se základními informacemi. Pokud zobrazíme obsah nápovědy, máme pak možnost procházet jednotlivá témata. Aplikace ovšem nabízí možnost přímého vyvolání nápovědy k jednotlivým tématům.

Dílčí funkce programu jsou vyvolávány klikáním na labely v levé části formuláře. Pro každou funkci aplikace je možné přímo vyvolat téma nápovědy prostřednictvím kontextového menu jednotlivých labelů. Některé komponenty mají standardní kontextové menu. Např. u mema jsou nabízeny operace s bloky textu. Delphi dávají možnost definovat kontextové menu i pro další komponenty.

Kontextové menu vytvoříme umístěním komponenty PopupMenu na formulář. Její zástupce je na liště Standard druhý zleva. Návrh položek kontextového menu provedeme stejně jako u menu hlavního. V kontextovém menu může být však pouze jeden sloupec s položkami, který se automaticky zobrazuje po kliknutí pravým tlačítkem na komponentu, která má kontextové menu definováno. Po vytvoření kontextového menu nastavíme u komponent, u kterých se má kontextové menu použít, hodnotu vlastnosti PopupMenu na jméno vytvořeného kontextového menu. Po stisku pravého tlačítka myši na takové komponentě je menu automaticky zobrazeno.

Aplikace může obsahovat více různých kontextových menu a nastavení vlastnosti PopupMenu u komponent pak určuje, které menu bude použito. Pro jednotlivé položky kontextového menu pak můžeme zapsat reakce na události OnClick stejně jako u menu hlavního.

V našem příkladu definujeme jedno kontextové menu PopupMenu1, které bude mít jedinou položku Nápověda. Po výběru této položky zobrazíme příslušné téma nápovědy. K tomu opět využijeme metodu Application.HelpContext, která jako parametr dostane číslo tématu tak, jak jsme ho přiřadili při návrhu nápovědy (tlačítko Map). U všech sedmi labelů použijeme stejné kontextové menu. Abychom mohli rozlišit, které téma nápovědy se má zobrazit, využijeme vlastnost labelů Tag. Jako hodnotu této vlastnosti nastavíme příslušné číslo tématu.

Reagujeme na událost MouseDown u jednotlivých labelů. Reakci na tuto událost mají všechny labely opět společnou. Pokud je stisknuto pravé tlačítko myši, tak podle vlastnosti Tag parametru Sender, který určuje label, na kterém bylo stisknuto tlačítko myši, nastavíme vlastnost Tag kontextového menu. V reakci na událost OnClick jediné položky kontextového menu pak využijeme hodnotu vlastnosti PopupMenu1.Tag jako parametr metody HelpContext. Tím zajistíme zobrazení odpovídajícího tématu nápovědy.

Výhoda tohoto na první pohled komplikovaného řešení spočívá v tom, že používáme pouze jediné kontextové menu. Stejně tak procedura reagující na událost OnMouseDown u jednotlivých labelů je pouze jedna. Ve chvíli, kdy kliknu pravým tlačítkem na label, se nastaví hodnota vlastnosti Tag kontextového menu a ta se využije později, pokud vyberu položku menu Nápověda.

Tisk z aplikace

Poslední z nových možností Delphi, která je v tomto programu použita, je výstup na tiskárnu. K tomu, abych mohl z aplikace tisknout, je třeba připojit unitu Printers. Provedu tak zapsáním jména této unity za klíčové slovo uses v části interface.

Delphi nabízejí dvě možnosti tisku. Pro tisk textu můžu využít přímý řádkový výstup na tiskárnu. S tiskárnou se pak pracuje podobně jako s textovým souborem. Druhá možnost je využití vlastnosti Canvas tiskárny. Tiskárna vystupuje v projektu jako objekt Printer, jehož vlastnosti a metody můžeme využívat. Na Canvas tiskárny můžeme kreslit nebo psát stejně jako na Canvas formuláře.

V příkladu jsou použity oba postupy. Pracujeme sice výhradně s řádkovým výstupem, ale je prakticky nemožné určit, kolik řádek se vejde na jednu stránku. Protože výstup rozpisu jednotlivých zápasů je typicky vícestránkový, je pro tento výstup použit Canvas tiskárny, kde počet řádek určíme snadno.

Při použití řádkového výstupu deklarujeme proměnnou typu textfile. Zavoláme pak proceduru AssignPrn s deklarovanou proměnnou jako parametrem. Získáme tak přístup k tiskárně. Zavolání procedury Rewrite s přístupovou proměnnou jako parametrem pak zpřístupní používání tiskárny. Můžeme potom využívat proceduru Writeln stejným způsobem jako při práci s textovým souborem. Jako první parametr uvedeme přístupovou proměnnou typu textfile a dále pak textový řetězec, který chceme tisknout. Ukončení tisku zajistíme zavoláním metody System.CloseFile, kde jako parametr uvedeme opět přístupovou proměnnou typu textfile. V této chvíli je tisková úloha odeslána k vytištění.

Pokud chceme použít Canvas tiskárny, zpřístupníme ho zavoláním metody Printer.BeginDoc. Na Canvas tiskárny pak můžeme libovolně kreslit nebo vypisovat texty. Připravujeme tím k tisku jednu stránku na tiskárně. Chceme-li přejít na další stránku, zavoláme metodu Printer.NewPage. Dostaneme k dispozici Canvas nové stránky, který můžeme opět upravovat. Po skončení úprav zavoláme metodu Printer.EndDoc. V té chvíli se posílá tisková úloha na tiskárnu. Je sestavena z Canvasu jednotlivých stránek.

Pro formátování tisku můžeme využít vlastnosti a metody objektu Printer. Vlastnost Title určuje název tiskové úlohy. Vlastnost Orientation, která má hodnoty poPortrait a poLandscape, určuje způsob tisku na stránku (na výšku, na šířku). Vlastnosti PageWidth (šířka stránky) a PageHeight (výška stránky) určují rozměry Canvasu tiskárny v bodech. Můžeme využít metody Printer.Canvas.Font.TextWidth (šířka textového řetězce) a Printer.Canvas.Font.TextHeight (výška textového řetězce), abychom zjistili počty řádek a sloupců písma, které se vejdou na stránku tiskárny. Měnit můžeme také nastavení vlastností Canvasu tiskárny. Můžeme tak např. vybírat písmo (Printer.Canvas.Font) nebo určovat jeho velikost (Printer.Canvas.Font.Size).

Tisk tabulky a seznamu týmů je realizován pomocí řádkového výstupu, předpokládá se, že se tisk vejde na jednu stránku. Tisk křížové tabulky používá opět řádkový výstup, ovšem tiskne se na šířku papíru. I když může být tabulka větší než jedna stránka, počet sloupců, které se vejdou na stránku, zjistit jde a řádkový výpis se může použít. Rozpis jednotlivých zápasů se tiskne po stránkách na Canvas tiskárny, zajistí se tak vhodné rozvržení textu na jednotlivých stranách. Použití metod Canvasu tiskárny je stejné jako u Canvasu formuláře.

Po výběru volby Tisk z menu se vyvolává Printer Dialog. V tomto příkladu slouží pouze k potvrzení tisku, nepoužívají se žádné jeho vlastnosti. Podle toho, který výpis je zrovna zobrazen v memu, se nastavuje vlastnost Tag položky menu Tisk1. Tato hodnota se využije při reakci na její událost OnClick. Podle požadovaného druhu výpisu volíme způsob výstupu na tiskárnu.

Podívejme se na některé zajímavé části programu. Maximální počet týmů je nastaven na 24. Všude se však využívá konstanta Pocet, počet týmů jde tedy snadno rozšířit. Název týmu je omezen na 30 znaků, využívám typ TTym, což je textový řetězec s omezenou délkou. Pro uložení výsledků zápasů používám dvojrozměrné pole, ve kterém jsou uchovány vstřelené a obdržené branky. Počet týmů, jejich názvy a výsledky zápasů jsou ukládány v binárním souboru. Protože jsou to hodnoty různých typů, pro typ souboru je definován záznam, který obsahuje tyto hodnoty jako svoje položky. Typ položek v binárním souboru je tedy jednotný (záznam typu TSoubor). V souboru je vždy uložena jedna hodnota tohoto typu. Soubor tedy odpovídá jednomu “fotbalovému projektu”, jedné tabulce.

Pro uložení rozpisu je použito trojrozměrné pole. Řádky odpovídají kolům, sloupce zápasům a vrstvy číslům týmů. V první vrstvě je uveden domácí tým, ve druhé hostující. Postup sestavení rozpisu je vždy stejný, rozpis není tedy třeba ukládat v souboru.

Třídění tabulky je realizováno rekurzivním algoritmem quicksort. Jeho zápis najdete v proceduře Trideni. Tato procedura obsahuje lokální funkci Vyhodnot, která porovnává pořadí dvojice týmů. Protože je funkce Vyhodnot definována uvnitř procedury Trideni, můžu ji využít v těle procedury Trideni, ale nikde jinde. Podobně uvnitř procedury SestavTabulku jsou definovány lokální procedury Minitabulka a HledejChybu. Tyto dvě procedury používají princip tzv. vzájemné rekurze. Volají se navzájem. Protože nemůžu volat proceduru, která ještě nebyla definována, musí být před zápisem procedury HledejChybu uvedena hlavička procedury Minitabulka doplněná klíčovým slovem forward. Tím informuji překladač, že zápis procedury Minitabulka bude následovat později. Bez tohoto zápisu by aplikace nešla přeložit.

U komponent ComboBox a ListBox často pracuji s jednotlivými položkami. Stejně jako vlastnost Lines u mema, je vlastnost Items těchto komponent typu TStrings. K jednotlivým položkám můžu přistupovat jako k prvkům pole, kdy jako index využívám číslo řádku. S položkami pak pracuji jako s textovými řetězci. Metoda Delete vlastnosti typu TStrings vypouští řádek s číslem, které zadám jako parametr.

Velmi často převádím textové řetězce na celá čísla. K tomu používám vlastní proceduru MojeStrToInt, kde hlídám výskyt výjimky EConvertError, která může nastat při konverzi funkcí StrToInt. Zadanou hodnotu případně dále upravuji. Podobné úpravy provádím v proceduře MojeStrToInt, kterou často používám místo funkce IntToStr.

Na několika místech používám funkci MessageDlg. Tato funkce zobrazuje okno s textem. Jako parametr uvádím text okna, zobrazenou ikonu a použitá tlačítka. Funkce pak vrací, které tlačítko bylo stisknuto. V textu používaný znak Chr(13) způsobuje ukončení řádku. Návratovou hodnotu této funkce nemusím využit.

V reakci na pokus o ukončení aplikace (událost OnClose formuláře) ověřuji, zda aplikaci ukončit, pokud byl projekt změněn. Nabízí se možnost uložení změn. Podle výsledku aplikaci ukončím (parametr Action procedury FormClose nastavím na caFree) nebo nechám běžet dál (Action nastavím na caNone).

Program Fotbal je poměrně rozsáhlý, všechny nové věci jsou však vysvětleny a zdrojový kód podrobně okomentován. Proto by pro vás měl být čitelný a pochopitelný. Tato aplikace nabízí mnoho možností k rozšíření. Mohl by se třeba přidat dvoukolový systém zápasů, zápasy by se také mohly jinak bodovat. Věřím, že toto je aplikace, která může najít praktické využití.

Chtěl bych ještě přidat poděkování hlavnímu testerovi programu Fotbal, Rosťovi “KK Booaa Smrt” Kloudovi. Aktuální verzi této aplikace najdete na http://vanous.postak.com.