Dnes navážeme na náš poslední program Hledání min. Vytvoříme program s více formuláři a ukážeme si práci s *.ini soubory.
Minule jsme vytvářeli hledání min, které je ovšem velmi omezeno. Hráč nemůže měnit velikost hracího pole ani počet min. Tento program ještě vylepšíme, budete tedy potřebovat zdrojový kód.
Z programu jsem ale vynechal poznámky, budete-li tedy mít problém s nějakou částí, podívejte se do minulého dílu.
Formulář „Nastavení“
Začneme přidáním dalšího formuláře, na který umístíme prvky pro zadávání nových rozměrů. V nabídce File → New
vyberte možnost Form
. Do projektu se přidá další formulář pojmenovaný Form2. Když se nyní pokusíte projekt uložit (samozřejmě pomocí Save All
), zjistíte, že v projektu přibyl další soubor. Implicitní pojmenování nabízené Delphi je Unit2.pas, my jej ale pojmenujeme Nastaveni_f.pas. Tento soubor představuje další jednotku (unitu), obsahuje vlastní formulář a také vlastní část zdrojového kódu (jednotlivé formuláře zobrazíte v nabídce View → Forms
).
Tento formulář se sice při spuštění programu vytvoří, ale nezobrazí. Zobrazí se pouze hlavní formulář (u nás Form1). Nastavení, který formulář je hlavní, lze změnit v nabídce Project → Options
na záložce Forms
změnou položky Main form.
Tento formulář budeme během běhu programu potřebovat zobrazit. V Delphi jsou dva způsoby zobrazení formuláře – modální a nemodální. Rozdíl mezi nimi spočívá v tom, že modální zobrazení neumožňuje práci s ostatními formuláři (uživatel může pracovat pouze s tímto formulářem). U nemodálního zobrazení může uživatel mezi formuláři přepínat.
//kód pro zobrazení formuláře
JmenoFormulare.show; //nemodální zobrazení
JmenoFormulare.showmodal; //modální zobrazení
Zavření formuláře se programově uskuteční klasicky pomocí JmenoFormulare.Close. Při zavření hlavního formuláře se ukončí program.
My použijeme modální zobrazení, k otevírání budeme používat naše menu. Přidáme do něj položku „Nastavení“ a do procedury obsluhující jeho stisk (TForm1.Nasaven1Click) napíšeme
Form2.showmodal;
Při pokusu o kompilaci ale zjistíme, že se program nezkompiluje. V jednotce Miny_f (původně Unit1) totiž není žádná proměnná Form2. Delphi ale pozná, že chceme použít Form2 z jednotky Nastaveni_f a nabídne nám přidání reference do Miny_f. Pokud si chcete přidání zařídit sami, připište na začátek bloku implementation uses a jméno unity.
...
implementation
uses Nastaveni_f; //nová část kódu
{$R *.dfm}
...
Toto nám umožní používat v jednotce Miny_f i proměnné, procedury, funkce atd. unity Nastaveni_f (stále ale nemůžeme v jednotce Nastaveni_f používat věci z Miny_f, lze to vyřešit přidáním uses Miny_f; do Nastaveni_f). Když nyní spustíme program, můžeme si stisknutím položky v menu zobrazit druhý formulář. Po jeho zavření můžeme opět pracovat s Form1.
Nastavení vlastností Form2
Na Form2 rozmístíme 4 přepínací tlačítka (RadioButton) s Caption nastaveným na 'Začátečník', 'Pokročilý', 'Expert' a 'Vlastní', 2 tlačítka s Caption nastaveným na 'OK' a 'Cancel' a GroupBox. To je komponenta, která může obsahovat další komponenty a popisek (Caption), který nastavte na 'Vlastnosti'. Do GroupBoxu umístíme 3 Edity s hodnotou text '0' a 3 Labely s Caption 'Šířka', 'Výška' a 'Počet min'. Upravte také velikost a Caption formuláře a nastavte mu styl okraje (BorderStyle) na bsDialog – to způsobí, že u tohoto formuláře nemůže uživatel měnit velikost a také nebude mít všechny systémové ikony. Zůstane pouze křížek pro ukončení formuláře, chcete-li se zbavit i jej, nastavte u vlastnosti BorderIcons její vlastnost biSystemMenu na false. Vše bude nyní vypadat přibližně jako na obrázku.
Přepínací tlačítka budou umožňovat 3 předdefinovaná nastavení a navíc bude mít uživatel možnost si zvolit vlastní, samozřejmě ale s určitými omezeními. Do Unit2 přidáme několik konstant s následujícími hodnotami.
const Hard_sirka = 30;
Hard_vyska = 20;
Hard_pocetmin = 99;
Medium_sirka = 16;
Medium_vyska = 16;
Medium_pocetmin = 40;
Easy_sirka = 9;
Easy_vyska = 9;
Easy_pocetmin = 10;
Max_sirka = 30;
Min_sirka = 9;
Max_vyska = 25;
Min_vyska = 9;
Min_pocetmin = 10;
//Max_pocetmin - podle rozmeru, polovina policek (sirka * vyska div 2)
Nejjednodušší metoda bude metoda onClick tlačítka Button2 (tlačítko Cancel).
procedure TForm2.Button2Click(Sender: TObject);
begin
close;
end;
Nyní se postaráme o chování editů. Nejdříve si ale ujasníme, co od nich budeme očekávat. Poté vytvoříme proceduru, která bude obsluhovat metodu onKeyPress všech našich editů.
Po stisknutí Enteru se předá zaměření (focus). K tomu využijeme funkci SelectNext, která přebírá 3 parametry. Prvním je výchozí zaměřená komponenta typu TWinControl. Předávaným parametrem bude komponenta, jejíž metoda je obsluhována, tedy Sender, přetypovaná na TWinControl. Další dva parametry jsou typu boolean. První z nich určuje, zda-li se bude postupovat dopředu (true) nebo dozadu (false). Poslední parametr udává, zda-li se bude při předávání zaměření přihlížet k vlastnosti objektů TabStop. Předávání focusu se uskutečňuje ve stanoveném pořadí, které je určeno vlastností objektů TabOrder a které se vytváří při umísťování komponent na formulář. Pokud vám nevyhovuje, můžete jej samozřejmě upravit změnou hodnot této vlastnosti u jednotlivých komponent.
Při stisknutí Escapu se v editu obnoví hodnota, kterou obsahoval před poslední změnou, a zaměření se předá tlačítku Button2 (tlačítko Cancel). Tentokrát ovšem nepoužijeme funkci SelectNext, ale zavoláme metodu tlačítka SetFocus. Abychom docílili obnovení hodnoty v editu, musíme si ji nejdříve uložit. To učiníme se zaměřením editu. Při této příležitosti se vyvolá metoda onEnter. K uložení použijeme zatím nevyužitou vlastnost Tag. Vytvoříme tedy novou proceduru EditEnter, kterou poté asociujeme s událostí onEnter všech našich editů (v Object Inspectoru na záložce Events se po vybrání události a stisknutí rozbalovací šipky nabídnou všechny dostupné procedury s vyhovujícími parametry). Aby to bylo možné, musí se parametry naší procedury shodovat s parametry této události.
procedure TForm2.EditEnter(Sender: TObject);
begin
(Sender as TEdit).Tag := strtoint((Sender as TEdit).Text);
end;
Aby uživatel nezadával nesmysly, zakážeme reakci na nechtěné klávesy. Pokud bude stisknutá klávesa jiná než číslice (0-9), Enter, Backspace nebo Escape, nastavíme parametr Key na #0 (prázdný znak). Reakci na Escape a Enter jsem popsal výše, obsloužení Backspacu nepotřebuje nijak upravovat. Toto „vynulování“ zamezí reakci na ostatní klávesy. To se ale nevztahuje na tzv. řídící klávesy (např. šipky, Delete), které mají speciální zacházení. Nyní následuje kód procedury EditKeyPress.
procedure TForm2.EditKeyPress(Sender: TObject; var Key: Char);
//tuto proceduru asociujeme s událostí onKeyPress všech našich editů
const backspace = #8;
enter = #13;
esc = #27;
//pro názornost jsem přidal tyto konstanty
begin
if not (key in ['0'..'9',backspace,enter,esc]) then key := #0
//ostatní tlačítka se "vynulují" - nezapíší se
else
if key = enter then SelectNext((Sender as TWinControl),true,true);
//při enteru se předá zaměření
if key = esc then
begin
(Sender as TEdit).Text := inttostr((Sender as TEdit).Tag);
//obnovení hodnoty uložené při OnEnter
Button2.SetFocus;
//předání zaměření buttonu2
end
else
RadioButton4.Checked := true;
//zatrhnutí RadioButtonu4
end;
Ještě přidáme kontrolu hodnot při ztrátě focusu (v metodě editů onExit). Tentokrát to ale uděláme pro každý edit zvlášť, protože hodnoty v každém z nich musí splňovat jiné parametry.
procedure TForm2.Edit1Exit(Sender: TObject);
begin
if Edit1.Text = '' then Edit1.Text := inttostr(Edit1.Tag) else //prázdný edit -> obnovení hodnoty z Tagu
if strtoint(Edit1.Text) > Max_sirka then Edit1.Text := inttostr(Max_sirka) else //příliš velké
if strtoint(Edit1.Text) < Min_sirka then Edit1.Text := inttostr(Min_sirka); //příliš malé
if strtoint(Edit3.Text) > strtoint(Edit1.Text)*strtoint(Edit2.Text) div 2 then Edit3.Text := inttostr(strtoint(Edit1.Text)*strtoint(Edit2.Text) div 2);
//se změnou rozměrů se změní kritérium počtu min
end;
//podobně u následujících editů
procedure TForm2.Edit2Exit(Sender: TObject);
begin
if Edit2.Text = '' then Edit2.Text := inttostr(Edit2.Tag) else
if strtoint(Edit2.Text) > Max_vyska then Edit2.Text := inttostr(Max_vyska) else
if strtoint(Edit2.Text) < Min_vyska then Edit2.Text := inttostr(Min_vyska);
if strtoint(Edit3.Text) > strtoint(Edit1.Text)*strtoint(Edit2.Text) div 2 then Edit3.Text := inttostr(strtoint(Edit1.Text)*strtoint(Edit2.Text) div 2);
end;
procedure TForm2.Edit3Exit(Sender: TObject);
begin
if Edit3.Text = '' then Edit3.Text := inttostr(Edit3.Tag) else
if strtoint(Edit3.Text) > strtoint(Edit1.Text)*strtoint(Edit2.Text) div 2 then Edit3.Text := inttostr(strtoint(Edit1.Text)*strtoint(Edit2.Text) div 2) else
if strtoint(Edit3.Text) < Min_pocetmin then Edit3.Text := inttostr(Min_pocetmin);
end;
Abychom zabránili problémům s přetečením (tj. zadání větší hodnoty než je rozsah proměnné), omezíme maximální počet znaků v editech nastavením jejich vlastnosti MaxLength. Mně se jako potenciálnímu uživateli pracovalo nejlépe s maximem tří znaků, vy si však můžete (v rozumném rozmezí) vybrat vlastní nastavení.
Nyní jsou na řadě RadioButtony. Myslím, že následující část kódu nepotřebuje žádný komentář.
procedure TForm2.RadioButton1Click(Sender: TObject);
begin
Edit1.Text := inttostr(Easy_sirka);
Edit2.Text := inttostr(Easy_vyska);
Edit3.Text := inttostr(Easy_pocetmin);
end;
procedure TForm2.RadioButton2Click(Sender: TObject);
begin
Edit1.Text := inttostr(Medium_sirka);
Edit2.Text := inttostr(Medium_vyska);
Edit3.Text := inttostr(Medium_pocetmin);
end;
procedure TForm2.RadioButton3Click(Sender: TObject);
begin
Edit1.Text := inttostr(Hard_sirka);
Edit2.Text := inttostr(Hard_vyska);
Edit3.Text := inttostr(Hard_pocetmin);
end;
Další procedura, kterou vytvoříme, se bude jmenovat prepnuti a bude mít parametry sirka, vyska a pocetmin, všechny typu integer. Nebude obsluhovat žádnou událost žádné komponenty, ale, jak název napovídá, bude zajišťovat přepnutí. Před zobrazením Formu2 totiž provede ještě několik jednoduchých příkazů a nakonec sama zobrazí Form2. Po vytvoření této procedury nahradíme Form2.showmodal za Form2.prepnuti (sirka, vyska, pocetmin). Nyní se již nemusíme starat o výchozí nastavení komponent při zobrazení Formu2.
procedure TForm2.prepnuti (sirka,vyska,pocetmin:integer);
begin
Edit1.Text := inttostr(sirka);
Edit2.Text := inttostr(vyska);
Edit3.Text := inttostr(pocetmin);
if (sirka = Easy_sirka) and (vyska = Easy_vyska) and (pocetmin = Easy_pocetmin) then Form2.RadioButton1.Checked := true
else // parametry jsou easy
if (sirka = Medium_sirka) and (vyska = Medium_vyska) and (pocetmin = Medium_pocetmin) then Form2.RadioButton2.Checked := true
else // parametry jsou medium
if (sirka = Hard_sirka) and (vyska = Hard_vyska) and (pocetmin = Hard_pocetmin) then Form2.RadioButton3.Checked := true
else // parametry jsou hard
Form2.RadioButton4.Checked := true;
Form2.ShowModal; //zobrazení Form2
end;
Poslední procedurou z této jednotky bude procedura obsluhující metodu onClick tlačítka OK. K tomu ovšem zatím nemáme připravený zbytek programu, proto se nyní vrátíme k jednotce Miny_f a tuto proceduru doplníme později.
Měnitelné rozměry hracího pole
Naše práce bude usnadněna tím, že jsme s touto možností počítali již od začátku a použili jsme konstanty sirka, vyska a pocetmin. Proto nám stačí pouze nahradit je proměnnými a zajistit patřičné změny rozměrů našeho hracího pole v návaznosti na změně těchto proměnných. V takovém případě bude samozřejmě potřeba začít novou hru, proto příslušnou část kódu přidáme do procedury NovaHra.
Nejdříve ale odstraníme konstanty sirka, vyska a pocetmin a nahradíme je proměnnými. Tyto proměnné nemohou být součástí třídy Form1, protože tak bychom vytvořili proměnné Form1.sirka, Forma1.vyska a Form1.pocetmin. To by způsobilo problémy u částí kódu, které nejsou součástí třídy Form1 (třeba metody třídy TPolicko). Zbývá nám tedy možnost použít globální proměnné a přidat tyto tři k proměnným Form1 a Pole.
var
Form1: TForm1;
Pole:TPole;
vyska,sirka,pocetmin:integer;
Vlastní změna rozměrů bude spočívat ve zrušení starého pole a vytvoření nového podle zadaných rozměrů. To svěříme nové proceduře, která bude přebírat parametry nových rozměrů pole. Také nesmíme zapomenout upravit velikost formuláře podle nové velikosti pole.
procedure TForm1.ZmenaRozmeru(novasirka,novavyska,novypocetmin:integer);
var x,y:integer;
begin
for x := 0 to sirka - 1 do
for y := 0 to vyska - 1 do
Pole[x,y].Free; //uvolnění jednotlivých prvků
Pole := nil; //a samotného pole
//z paměti
sirka := novasirka;
vyska := novavyska;
pocetmin := novypocetmin; //nové parametry
SetLength(Pole,sirka,vyska); //vytvoření nového pole
for x := 0 to sirka - 1 do
for y := 0 to vyska - 1 do
Pole[x,y] := TPolicko.Init(x,y); //inicializace prvků
// úprava velikosti formuláře:
Form1.ClientWidth := Image2.Left*2 + sirka*velikost;
Form1.ClientHeight := Image2.Top + Image2.Left + vyska*velikost;
//tuto část přidáme později i na začátek programu do procedury FormCreate
end;
Toto bude potřeba vykonat při změně možností, přesněji při potvrzení nových hodnot. Vrátíme se tedy do unity Nastaveni_f a naprogramujeme proceduru Button1Click, kterou jsme vynechali. Nebude ani nijak složitá, stačí pouze zavolat proceduru ZmenaRozmeru, vytvořit novou hru a zavřít formulář s nastavením.
procedure TForm2.Button1Click(Sender: TObject);
begin
Form1.ZmenaRozmeru(strtoint(Edit1.Text),strtoint(Edit2.Text),strtoint(Edit3.Text));
Form1.NovaHra;
close;
end;
Soubory INI
Dále se zaměříme na uložení nastavení a jeho opětovné načtení při dalším spuštění aplikace. Použijeme k tomu inicializační (INI) soubor. Pro práci s nimi je v Delphi speciální třída TIniFile. Pro jejich použití je nezbytné přidat do části uses na začátku unity knihovnu IniFiles.
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ExtCtrls, StdCtrls, Menus, IniFiles;
Do procedury FormCreate přidáme lokální proměnnou ini typu TIniFile. Jelikož se jedná o třídu, je potřeba ji před použitím inicializovat. Jako parametr předáme konstruktoru jméno INI souboru, se kterým budeme pracovat. V našem případě to bude soubor miny.ini v místě umístění programu.
ini := TIniFile.Create(ExtractFileDir(Application.ExeName)+'\miny.ini');
//Application.ExeName - jméno programu včetně adresy
//ExtractFileDir - funkce, která vrací adresu z úplného jména souboru
Nejdříve se ale podíváme na strukturu dat v tomto souboru.
[Sekce1]
udaj1=hodnota
udaj2=hodnota
[Sekce2]
x=10
y=50
povoleni=false
text=Nejaky text
Data jsou uspořádaná do jednotlivých sekcí i s názvem údaje, ke kterému se vztahují. Pro vlastní získání nebo zapsání dat slouží několik funkcí podle typu dat. My budeme pracovat jen s čísly, tak si vystačíme s WriteInteger a ReadInteger (práce s jiným typem dat je ovšem velmi podobná – např. s řetězci se pracuje pomocí ReadString a WriteString atp.). Obě přejímají 3 parametry. První je řetězec určující název sekce, druhý je řetězec určující název údaje. Třetí parametr je zapisovaná hodnota, případně výchozí hodnota, kterou funkce vrátí, selže-li načtení hodnoty. Po skončení práce opět uvolníme instanci z paměti.
procedure TForm1.FormCreate(Sender: TObject);
var x,y:byte;
ini:TIniFile;
begin
randomize; //mělo by být vždy na začátku programu
ini := TIniFile.Create(extractFileDir(application.ExeName)+'\miny.ini');
sirka := ini.ReadInteger('Pole','sirka',30);
vyska := ini.ReadInteger('Pole','vyska',20);
pocetmin := ini.ReadInteger('Pole','min',99);
if (sirka > 30) or (sirka < 9) then sirka := 30;
if (vyska > 25) or (vyska < 9) then vyska := 20;
if (vyska*sirka div 2 < pocetmin) or (pocetmin < 10) then pocetmin := vyska*sirka div 8;
ini.Free;
//úprava rozměrů formuláře a obrázku
Form1.ClientWidth := Image2.Left*2 + sirka*velikost;
Form1.ClientHeight := Image2.Left + Image2.Top + vyska*velikost;
Image2.Width := sirka*velikost-1;
Image2.Height := vyska*velikost-1;
...
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
var ini:TIniFile;
x,y:byte;
begin
ini := TIniFile.Create(extractFileDir(application.ExeName)+'\miny.ini');
ini.WriteInteger('Pole', 'sirka', sirka);
ini.WriteInteger('Pole', 'vyska', vyska);
ini.WriteInteger('Pole', 'min', pocetmin);
ini.Free;
for x := 0 to sirka - 1 do
for y := 0 to vyska - 1 do
begin
Pole[x,y].Free;
end;
Pole := nil;
end;
Protože přepsat data v uloženém INI souboru nevyžaduje žádné odborné znalosti, přidal jsem kontrolu načtených dat podle dříve zmiňovaných kritérií. Také bychom neměli zapomenout přizpůsobit nově získaným hodnotám velikost formuláře a Image2.
Aby bylo možné po sobě uklidit, přidáme ještě do menu položku „Vymazat uložená nastavení“, která se postará o odstranění našeho INI souboru, a položku „Uložit nastavení po ukončení“, která bude mít charakter zaškrtávacího tlačítka. Toho docílíme nastavením vlastnosti AutoCheck této položky na true. Pro funkčnost je pak potřeba zařadit vytvoření INI souboru do podmíněného příkazu.
procedure TForm1.Vymazatuloennastaven1Click(Sender: TObject);
begin
if FileExists(extractFileDir(application.ExeName)+'\miny.ini') then
DeleteFile(extractFileDir(application.ExeName)+'\miny.ini');
end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
var ini:TIniFile; x,y:byte;
begin
if Uloitnastavenpoukonen1.Checked then
begin //pouze zařadíme vytvoření ini souboru do podmíněného příkazu
ini := TIniFile.Create(extractFileDir(application.ExeName)+'\miny.ini');
ini.WriteInteger('Pole', 'sirka', sirka);
ini.WriteInteger('Pole', 'vyska', vyska);
ini.WriteInteger('Pole', 'min', pocetmin);
ini.Free;
end;
...
Poslední problém
A jsme skoro u konce. Na závěr jsem si však ještě nechal poslední problém. Jak jste si možná všimli, nastne problém, pokud zvětšíme pole oproti stavu na začátku programu. Velikost pole se nezmění. Ve skutečnosti to není pravda. Pole se vytvoří a vše funguje, výsledek se pouze nevykreslí na Image2. Je to dáno tím, že Canvas obrázku se inicializuje s těmito rozměry a při kreslení mimo něj se nic nezobrazí. Jelikož Canvas je read-only vlastností (pouze pro čtení), nemůžeme jej uvolnit z paměti a znovu inicializovat. Můžeme to ovšem udělat s celým obrázkem. Do procedury ZmenaRozmeru přidáme následující část kódu.
Image2.Free; //uvolnění z paměti
//vytvoření nové instance
Image2 := TImage.Create(Form1);
Image2.Parent := form1;
Image2.Top := 20;
Image2.Left := 1;
Image2.OnMouseDown := Form1.Image2MouseDown;
Image2.Width := sirka*velikost-1;
Image2.Height := vyska*velikost-1;
Tím jsou naše miny hotové, příště se k nim již vracet nebudeme.