Dnes si vytvoříme základ hledání min. Ukážeme si základy práce s třídami (class) a práci s dynamickými poli a jejich použití.
Hledání min je jednoduchá hra, ve které má hráč k dispozici políčka, na kterých je rozmístěn určitý počet min a která postupně odkrývá. Pokud narazí na pole s minou, prohrál. V opačném případě se na políčku zobrazí číslo (0-8), které udává, kolik min se v okolí nachází. Políčka, o kterých si myslí, že skrývají minu, si může označit kliknutím pravým tlačítkem myši. Cílem hry je správně označit všechna políčka s minami.
Struktura programu
Na první pohled je zřejmé, že pro miny a praporky si nevystačíme s komponentou jako label, která umí zobrazovat pouze text. Dalo by se sice vystačit i s nimi, ale výsledek by byl nepřehledný, matoucí a málo efektivní. Proto použijeme komponentu image (obrázek). Ta ale zabírá v paměti výrazně více místa, a proto si nemůžeme dovolit jen tak vytvořit pole o velikosti 20 × 30 obrázků. Místo toho si vystačíme pouze se dvěma. Do prvního si uložíme obrázek, na kterém budou všechny možné varianty políček, která budeme rozkopírovávat do druhého obrázku. I přesto se ale nevyhneme použití pole. V něm ale namísto celých komponent uložíme pouze několik důležitých informací. Jelikož také budeme chtít mít možnost nechat uživatele vybrat si rozměry hracího pole, musíme s tím počítat už nyní (věnovat se tomu ale budeme až někdy příště). Proto si nadeklarujeme konstanty sirka, vyska, velikost a pocetmin. Tyto konstanty budou globální, a proto je deklarujeme ještě před částí type. Pro začátek jim můžeme nechat třeba tyto hodnoty:
const sirka = 30;
vyska = 20;
velikost = 16; //označuje velikost jednoho políčka
pocetmin = 99;
type
...
Pro ukládání informací o minách si vytvoříme vlastní strukturovaný datový typ, který pojmenujeme TPolicko. Mohli bychom použít záznam (record), ale ten nemůže obsahovat vlastní procedury a funkce. To umožňuje třída (class), která obsahuje objekty, kterými mohou být i procedury a funkce. Procedury a funkce třídy se obecně nazývají metody.
TPolicko = class
x,y:integer; //souřadnice políčka (x,y)
mina, //zda-li políčko obsahuje minu
kliknuto, //zda-li už je políčko odkryto
oznaceno:boolean; //zda-li je označeno praporkem jako mina
end;
Samozřejmě využijeme možnost vytvořit si vlastní metodu – vytvoříme funkci MinOkolo, která bude vracet počet min v okolí a v případě, že se na políčku nachází mina, bude vracet hodnotu 9. Zatím ji ale vytvářet nebudeme, protože pro ni ještě nemáme vše připravené (pokud ji i přesto deklarujeme, tak ji zatím necháme jako poznámku).
Proměnné typu třída se nazývají instance a na rozdíl od běžných proměnných se musí inicializovat (pro jejich použití nestačí jen deklarace). K inicializaci se používají speciální metody, které se nazývají konstruktory a které se starají o vytvoření všech objektů třídy (případně nastavení počáteční hodnoty). Naopak pro uvolnění z paměti se používají metody zvané destruktory. Každá třída zdědila po základní třídě TObject konstruktor Create a destruktor Destroy (lepší je volat jej prostřednictvím metody Free, kterou také automaticky obsahují všechny třídy). Často si ale s těmito základními metodami nevystačíme, a proto si vytvoříme vlastní. Deklarují se stejně jako obyčejné procedury, pouze se změní klíčové slovo procedure na constructor. Náš konstruktor nastaví hodnoty x a y podle zadaných parametrů a ostatní hodnoty na false.
//deklarace
TPolicko = class
x,y:integer;
mina,kliknuto,oznaceno:boolean;
constructor Init(x,y:byte);
//function MinOkolo:byte; //zatím pouze poznámka!
end;
...
//definice (v části implementation)
constructor TPolicko.Init(x,y:byte);
begin
self.x := x;
self.y := y;
self.Mina := false;
self.Kliknuto := false;
self.Oznaceno := false;
end;
Abychom bezpečně rozlišili objekty x a y od parametrů se stejným jménem (které je překryjí), použili jsme proměnnou self. Definici jsme umístili do stejného místa jako u metod formuláře – v implementation. Všimněte si, že místo obvyklého TForm1 jsme použili TPolicko.Init, protože tato metoda patří do třídy TPolicko.
Komponenty
Na formulář přidáme 2 komponenty Image (záložka Additional). Do vlastnosti Picture u Image1 nastavíme pomocí Object Inspectoru zdrojový obrázek. Můžete použít třeba tento – pokud si chcete vytvořit svůj, tak dodržte rozložení políček na obrázku. Jelikož Delphi dokáže některé metody (grafická práce s obrázkem), které použijeme, použít pouze na bitmapy, musí být náš obrázek také ve formátu BMP.
Dále přidáme ještě jeden label, který umístíme do levého horního rohu a který bude zobrazovat počet doposud neoznačených min.
Dynamické pole
Nyní si vytvoříme dvourozměrné dynamické pole TPolicek. Od „obyčejných“ statických polí se liší tím, že při deklaraci není určena jejich velikost. Tu lze určit a měnit v průběhu programu. Do části type napíšeme:
type
TPolicko = class
...
end;
TPole = array of array of TPolicko; //pole je dvourozměrné
A do části var programu (pod Form1:TForm1)
Pole:TPole;
Musíme se také postarat o nastavení délky pole. To se děje použitím metody SetLength(DynamickePole,velikost). U dvojrozměrných polí může mít každé „podpole“ různou velikost, poté se musí nastavovat pro každé zvlášť (SetLength(DynamickePole[i],velikost);), nebo mohou mít všechny stejnou a nastavit se najednou (SetLength(DynamickePole,velikost,velikost_podpoli);). Indexy dynamických polí začínají vždy nulou a končí velikostí −1. Velikost pole lze zjistit funkcí Length(Pole).
Jelikož zatím uživatel nebude mít možnost měnit rozsah pole během běhu programu, nastavíme ji pouze jednou při spuštění – v metodě onCreate formuláře. Pro nastavení ostatních věcí na začátku hry si ale vytvoříme proceduru NovaHra.
procedure TForm1.FormCreate(Sender: TObject);
var x,y:byte;
begin
randomize; //spuštění generátoru náhodných čísel
SetLength(Pole,sirka,vyska); //nastaveni velikosti pole
for x := 0 to sirka - 1 do
for y := 0 to vyska - 1 do
Pole[x,y] := TPolicko.Init(x,y); //inicializace prvků v poli
NovaHra; //připravení nové hry
end;
Na začátku nové hry musíme samozřejmě vymazat staré hodnoty – nastavit znovu hodnoty objektů na false. Dále se musíme postarat o přizpůsobení velikosti Image2 a připravit na něj čisté hrací pole. Komponenty Image (a některé další) mají svoje plátno (tzv. Canvas), jež se skládá z grafických bodů, které mají svoji barvu (barva je typu TColor, který je definován v jednotce Graphics, a je tvořena výčtem jednotlivých barev). Tu lze několika metodami měnit – buď přímo (Image1.Canvas.Pixels[x,y] := clBlue;), nebo pomocí několika složitějších procedur.
- vytvořit čáru z aktuálního bodu (LineTo(x,y);) – pokud chcete pouze změnit výchozí pozici a nevytvářet čáru, použijte proceduru MoveTo(x,y)
- okopírováním části jiného canvasu –
CopyRect(CilovyRECT,vychoziCanvas,vychoziRECT); – vytvořit geometrický útvar (např. obdélník)
A ještě budeme potřebovat globální proměnné zbyvamin (která udává počet zbývajících min) a konechry (aby při ukončení hry nemohl hráč dále hrát) a samozřejmě nesmíme zapomenout na rozložení min po hracím poli.
type
...
TForm1 = class(TForm)
...
public
zbyvamin:integer;
konechry:boolean;
end;
...
procedure TForm1.NovaHra;
var x,y,oznaceno_min:byte; //řídící proměnná cyklu
begin
for x := 0 to sirka - 1 do //vynulování starých hodnot
for y := 0 to vyska - 1 do
begin
Pole[x,y].kliknuto := false;
Pole[x,y].mina := false;
Pole[x,y].oznaceno := false;
end;
Label1.Caption := 'Zbývá min: '+inttostr(pocetmin); //nastavení informačního labelu
Image2.Width := sirka*velikost-1; //nastavení rozměrů obrázku v závislosti na velikosti pole
Image2.Height := vyska*velikost-1;
for x := 0 to sirka - 1 do
for y := 0 to vyska - 1 do
Image2.Canvas.CopyRect(Rect(velikost*x-1,velikost*y-1,velikost*(x+1)-1,velikost*(y+1)-1),Image1.Canvas,Rect(12*velikost,0,13*velikost,velikost));
//rozkopírování políček z obrázku Image1 na Image2
for oznaceno_min := 1 to pocetmin do
begin
repeat //'náhodné' rozložení min v poli
x := random(sirka);
y := random(vyska);
until not (Pole[x,y].mina); //kontrola, aby se miny neopakovaly
Pole[x,y].mina := true;
end;
zbyvamin:=pocetmin;
konechry := false; //výchozí nastavení proměnných
end;
Když už políčka inicializujeme, měli bychom se postarat i o jejich uvolnění z paměti.
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
var x,y:byte;
begin
for x := 0 to sirka - 1 do
for y := 0 to vyska - 1 do
pole[x,y].Free;
end;
Když už máme připravené pole, můžeme naprogramovat i již výše zmíněnou funkci MinOkolo.
function TPolicko.MinOkolo:byte;
var min_okolo:byte;
begin
if Pole[x,y].mina then min_okolo := 9 else //pokud je zde mina, výsledkem je 9
begin
min_okolo := 0; //začínáme nulou
if (self.x > 0) and (Pole[self.x-1,self.y].Mina) then inc(min_okolo);
if (self.y > 0) and (Pole[self.x,self.y-1].Mina) then inc(min_okolo);
if (self.x < sirka-1) and (Pole[self.x+1,self.y].Mina) then inc(min_okolo);
if (self.y < vyska-1) and (Pole[self.x,self.y+1].Mina) then inc(min_okolo);
if (self.x > 0) and (self.y > 0) and (Pole[self.x-1,self.y-1].Mina) then inc(min_okolo);
if (self.x > 0) and (self.y < vyska-1) and (Pole[self.x-1,self.y+1].Mina) then inc(min_okolo);
if (self.x < sirka-1) and (self.y > 0) and (Pole[self.x+1,self.y-1].Mina) then inc(min_okolo);
if (self.x < sirka-1) and (self.y < vyska-1) and (Pole[self.x+1,self.y+1].Mina) then inc(min_okolo);
end; //a za každou minu okolo přičteme 1 (inc(x) zvyšuje hodnotu x o 1)
Result := min_okolo; //vrácení výsledku
end;
Tady si musíme dát pozor na rozměr pole. Pokud bychom se snažili překročit rozsah pole, skončí náš program chybou. Mohlo by se zdát, že při vyhodnocování podmínek by mohla nastat chyba, jenže nenastane. Je to dáno způsobem vyhodnocování podmínek – pokud jsou 2 výrazy spojeny pomocí spojky and a první z nich má hodnotu false, je jisté, že celý výraz bude také false, a proto se nemusí vyhodnocovat 2. část výrazu.
Pro vyhodnocování klikání uživatele použijeme metodu OnMouseDown Image2. Musíme totiž rozlišit, které tlačítko myši bylo zmáčknuto. Pokud pravé, bude se měnit označení praporkem na políčku, pokud levé, musí se zobrazit příslušný obrázek a pokud je na něm mina, ukončit hru, nebo pokud je na něm 0, tak zobrazit i sousední políčka.
procedure TForm1.Image2MouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var sx,sy,minkolem:byte;
begin
sx:= x div velikost;
sy:= y div velikost; //zjištění souřadnic zmáčknutého políčka
if (not Pole[sx,sy].kliknuto) and (not konechry) then
//musíme zkontrolovat, zda-li již není políčko označené a zda-li není konec hry
begin
if (Button = mbRight) then //rozlišení tlačítka
begin
if not (Pole[sx,sy].Oznaceno) then
begin
Image2.Canvas.CopyRect(Rect(sx*velikost-1,sy*velikost-1,(sx+1)*velikost-1,(sy+1)*velikost-1),Image1.Canvas,Rect(11*velikost,0,12*velikost,velikost));
zbyvamin := zbyvamin-1;
//označení praporkem
end
else
begin
Image2.Canvas.CopyRect(Rect(sx*velikost-1,sy*velikost-1,(sx+1)*velikost-1,(sy+1)*velikost-1),Image1.Canvas,Rect(12*velikost,0,13*velikost,velikost));
inc(zbyvamin); //odstranění praporku
end;
Pole[sx,sy].Oznaceno := not Pole[sx,sy].Oznaceno; //změna příslušné hodnoty
Label1.Caption := 'Zbývá min: '+inttostr(zbyvamin); //úprava informace o zbývajících minách
if zbyvamin = 0 then //kontrola vítězství
KontrolaMin;
end
else
if (Button = mbLeft) and (not Pole[sx,sy].oznaceno) then //nelze klikat na políčka s praporkem
begin
minkolem := Pole[sx,sy].MinOkolo; //využití fce na zjištění min v okolí
Image2.Canvas.CopyRect(Rect(sx*velikost-1,sy*velikost-1,(sx+1)*velikost-1,(sy+1)*velikost-1),Image1.Canvas,Rect(minkolem*velikost,0,(minkolem+1)*velikost,velikost));
(*jelikož máme příslušné obrázky seřazeny za sebou podle rostoucí hodnoty, kterou vrací funkce MinOkolo,
stačí správně posunout vyřezávaný obrázek *)
Pole[sx,sy].kliknuto := true; //označení, že se zde už hrálo
case minkolem of
0: begin //zmáčknutí sousedních tlačítek
if (sx > 0) then Image2.OnMouseDown(Image2,mbleft,[],(sx-1)*velikost,sy*velikost);
if (sy > 0) then Image2.OnMouseDown(Image2,mbleft,[],sx*velikost,(sy-1)*velikost);
if (sx < sirka-1) then Image2.OnMouseDown(Image2,mbleft,[],(sx+1)*velikost,sy*velikost);
if (sy < vyska-1) then Image2.OnMouseDown(Image2,mbleft,[],sx*velikost,(sy+1)*velikost);
if (sx > 0) and (sy > 0) then Image2.OnMouseDown(Image2,mbleft,[],(sx-1)*velikost,(sy-1)*velikost);
if (sx > 0) and (sy < vyska-1) then Image2.OnMouseDown(Image2,mbleft,[],(sx-1)*velikost,(sy+1)*velikost);
if (sx < sirka-1) and (sy > 0) then Image2.OnMouseDown(Image2,mbleft,[],(sx+1)*velikost,(sy-1)*velikost);
if (sx < sirka-1) and (sy < vyska-1) then Image2.OnMouseDown(Image2,mbleft,[],(1+sx)*velikost,(sy+1)*velikost);
end;
9: begin //konec hry a zobrazení pozic min
konechry := true;
ukazminy;
end;
end;
end;
end;
end;
V programu jsme ale použili 2 procedury (kontrolamin a ukazminy), které musíme samozřejmě také naprogramovat. Miny je totiž nutno zkontrolovat, zda-li jsou umístěny na správných pozicích.
procedure TForm1.UkazMiny;
var x,y:byte;
begin
for x := 0 to sirka - 1 do
for y := 0 to vyska - 1 do
begin //zajímají nás 2 situace - políčko je označené a nemělo by být, nebo není označené a mělo by být
if (Pole[x,y].oznaceno) and (not Pole[x,y].mina) then //1. možnost, zobrazení červeného křížku
Image2.Canvas.CopyRect(Rect(x*velikost-1,y*velikost-1,(x+1)*velikost-1,(y+1)*velikost-1),Image1.Canvas,Rect(10*velikost,0,11*velikost,velikost))
else
if (not Pole[x,y].oznaceno) and (Pole[x,y].mina) then //2. možnost, zobrazení miny
Image2.Canvas.CopyRect(Rect(x*velikost-1,y*velikost-1,(x+1)*velikost-1,(y+1)*velikost-1),Image1.Canvas,Rect(9*velikost,0,10*velikost,velikost))
end;
end;
procedure TForm1.KontrolaMin;
var
x,y:byte;
ukoncit:boolean;
begin
ukoncit := true; //výchozí hodnota
for x := 0 to sirka - 1 do
for y := 0 to vyska - 1 do
begin
if pole[x,y].Mina and (not pole[x,y].oznaceno)
then
ukoncit := false; //pokud nějaká mina není v pořádku, změníme na false
end;
if ukoncit then // pokud je ukoncit stále true, jsou všecny miny označeny správně
begin
for x := 0 to sirka - 1 do //zobrazení nezobrazených políček
for y := 0 to vyska - 1 do
if not pole[x,y].mina then Image2.OnMouseDown(Image2,mbleft,[],x*velikost,y*velikost);
konechry := true; //konechry
//dotaz na novou hru
if messagedlg('Blahopřeji, vyhráli jste,'+#13+'chcete spustit novou hru?',mtCustom,[mbYes,mbNo],0) = mryes then Novahra;
end;
end;
Na konci procedury jsme použili funkci MessageDlg vyvolávající zprávu, která má ale oproti jednoduché zprávě Showmessage daleko více možností. Má tyto parametry:
- řetězec zobrazené zprávy
- typ zprávy (ikonka) – mtError, mtInformation, mtWarning, mtCustom
- tlačítka – množina, která může obsahovat např. mbYes, mbNo, mbCancel, mbIgnore
- číslo nápovědy (nemáme nápovědu → 0)
- funkce vrací hodnotu stisknutého tlačítka (jako jména tlačítek, jen místo mb je mr → mrYes, mrNo, …), tato vrácená hodnota je typu word
Menu
Většina aplikací ve Windows s oblibou využívá menu (nabídku). Delphi podporuje 2 druhy menu – hlavní menu (main menu) zobrazené na vrchu formuláře a popup menu, které se zobrazuje po kliknutí pravým tlačítkem myši.
Nám se více hodí hlavní menu. Jeho tvorba je velmi jednoduchá. Na formulář přidáme komponentu MainMenu (ze záložky Standard) a dvojklikem na ni otevřeme okno pro editaci menu, kde je již připravený objekt typu TMenuItem. Nastavíme jeho caption na „Hra“ a vidíme, že se pod ním vytvořila prázdná položka a zároveň se připravil další prázdný objekt, obojí typu TMenuItem. Vnořenému objektu přiřadíme caption „Nová hra“. Zároveň s tím se připraví další prázdná položka. Stejným způsobem tedy vytvoříme ještě položku „Konec“ a mezi ně vložíme oddělovací čáru (nastavením captionu položky na „–“).
Všimněte si také, jak jsou tyto objekty hiearchicky řazeny v ObjectTreeView.
Po zavření editačního okna vidíme, že se příslušná nabídka připojila k formuláři (nebo můžete sami přiřadit menu formuláři – vlastnost formuláře Menu). Kliknutím na položku Hra se nabídka rozevře a zobrazí nabídku. Kliknutím na jednu z položek se vytvoří procedura obsluhující kliknutí na ni. Jejich kód je celkem jednoduchý a není na něm nic zajímavého.
procedure TForm1.Novhra1Click(Sender: TObject);
var tlacitko:word;
begin
if not konechry
then
tlacitko := messagedlg('Chcete začít novou hru?',mtconfirmation,[mbyes,mbno],0);
//dotaz na novou hru
if (tlacitko = mryes) or konechry
then
novahra;
end;
procedure TForm1.Konec1Click(Sender: TObject);
begin
close; //konec programu
end;
Závěrem
Na závěr je ještě zapořebí pohrát si s detaily – změnit ikony, Caption formluláře, pohrát si s umístěním komponent na formuláři a s jejich velikostmi (kromě Image2, o který se staráme během programu), nastavit vlastnost Image1.Visible na false, aby obrázek na formuláři „nepřekážel“ a podobné úpravy.