A Delphi programozási nyelv

Dinamikus GUI építése Delphivel

A Delphi a legtöbb esetben jól használható GUI építő eszközzel rendelkezik, ami az eseménykezelést is jelentősen könnyíti. Ez azonban főleg akkor használható, ha az építeni kívánt felület statikus, a program során nem változik sokat. Arra pedig, hogy dinamikusan változó komponensállományú felületet építsünk, vagy olyat, ahol bizonyos komponenseinket valamilyen belső struktúrában (pl. listában) szeretnénk látni, természetesen az eszköz nem alkalmas. Ezért ezt minden esetben magunknak kell megírni közvetlenül a kódba, ez a fejezet erről ad leírást, példákkal.

Komponensek dinamikus létrehozása

Ha a GUI építő eszközzel a formunkra „felhúzunk” egy komponenst, a kód az eszköz által épített részébe, a form konstruktorába bekerül egy hívás a komponens konstruktorára, valamint utasítások, amelyek beállítják a komponens propertyjeit az általunk a GUI építő „properties” ablakában megadottakra. Ezeket a dolgokat értelemszerűen a GUI építő mellőzése esetén magunknak kell a kódba írni. Ebből a szempontból érdekes property a „name”, amelyet a GUI építő arra használ, hogy a komponensre mutató globális változó neve ez lesz. Mivel most pont az a lényeg, hogy dinamikusan akarjuk változtatni a komponensállományunkat, erre a globális változók használata nem alkalmas, így a name propertyvel nem kell foglalkozni, a tároló konténeren keresztül fogjuk tudni elérni komponenseinket.

Az alábbi példában képeket helyezünk el egy panelen, amit tömbben tárolunk. Ha ezeket az adatokat megadjuk, akkor a kép biztosan ott és úgy fog megjelenni, ahogy akartuk, egy oszlopban egymás alatt, a kep1.jpg, kep2.jpg, ... fájlok tartalmát mutatva, a panel1-en.

var ... kep:array[1..8] of TImage; ... implementation ... procedure TForm1.FormCreate(Sender: TObject); var i:Integer; begin for i:=1 to 8 do begin kep[i]:=TImage.Create(self); kep[i].parent:=panel1; kep[i].top:=10+(i-1)*80; kep[i].left:=10; kep[i].width:=80; kep[i].height:=80; kep[i].picture:=loadFromFile('kep'+intToStr(i)+'.jpg'); end; end;

Egyébként a képek gyűjteményesítésére pont van beépített komponens, amit ajánlott és kézenfekvő használni a fenti megoldás helyett, de más komponensekre általában nincs.

Komponensek megkülönböztetése, altípusok megkülönböztetése

A gyűjteményben tárolásnál egyből felmerül két kérdés. Az egyik, hogy tudunk-e különböző típusú komponenseket tárolni. Mivel a Delphiben ez megengedett, amennyiben a gyűjtemény típusa egy közös őse a tárolt objektumok típusainak, ezért igen. Csak egy közös őst kell találni, ami persze lehet a TComponent, de ha ennél konkrétabbat is tudunk mondani, azzal később jól járunk.

A másik, hogy hogyan fogjuk őket megkülönböztetni, hiszen ha egy globális változóval hivatkozunk mindig a komponenseinkre, akkor, ha más referencián keresztül érjük egyszer el őket, akkor úgyis csak összehasonlításokkal tudjuk kitalálni, hogy melyik az az objektum (mi milyen néven ismerjük). De ha például egy 100 elemű tömbben vannak képeink, és mondjuk egy eseménykezelő ad egy referenciát az objektumra, nem célszerű, ha ekkor csak 100 (átlagosan 50) összehasonlítással tudjuk kitalálni, hogy az melyik eleme a tömbnek. Ekkor jutnának eszünkbe ilyen trükkök, hogy a Top és Left propertykből fejtsük vissza a sorszámot, na az ilyesmi helyett van szükségünk valami igazi megoldásra. Egyszerű esetben, ha például egy tömbben tárolunk valamilyen komponenseket, akkor használhatjuk a Tag propertyt. Ennek a propertynek ugyanis semmilyen más funkciója nincs, csak amit mi adunk neki, egy számot tudunk benne tárolni, a TComponent-ből van.

Az igazán jó megoldás viszont természetesen a származtatás. Ha mondjuk TMyImage típusú a tömb, ahol a TMyImage a TImage-ből származik, és van még valamilyen, a külvilágban való azonosításra szolgáló adattagja (v. propertyje), akkor ezt könnyen bővíthetjük többdimenziós tömbre vagy bármilyen más struktúrára. Ennek a megoldásnak hátránya a Tag használatához képest ugyanakkor, hogy komplikálttá válik a származtatás, ha többféle típusú objektumunk is lehet a gyűjteményben.

Példa többdimenziós tömb esetén képek tárolására.

type TMyImage = class(TImage) posX: Integer; posY: Integer; end; … var ... kep:array[1..8,1..5] of TMyImage;

Akár Taget, akár származtatást használunk a Create() után, a propertyk kezdőértékének béállításánal le kell tárolni a gyűjteményben elfoglalt helynek megfelelő értékeket az új adattagokban illetve a Tagben. Példák:

kep[i].Tag := i; kep[i][j].posX := i; kep[i][j].posY := j;

Eseménykezelés dinamikusan létrehozott komponensekkel

Ha GUI készítővel hozunk létre komponenseket, akkor az IDE azt is megteszi nekünk, hogy az eseménykezelő függvényt is legenerálja a különböző eseményekhez, és a properties ablakban (az events tabon) pedig összerendelhetjük az eseménykezelő függvényeket az objektumaink eseményeivel. Ezt kell tehát magunknak csinálnunk ez esetben. Nyilván az eseménykezelő függvényeket nem lehet és nem is kell sokszorosítani, több objektumhoz (a gyűjtemény tagjaihoz) kell ugyanazt az eseménykezelő függvényt rendelni az adott eseményhez. Így bármelyik objektum adott eseménye váltódik ki, a függvény meghívódik. Azt, hogy mégis melyik objektum váltotta ki az eseményt, a minden eseménykezelő paraméterei között megtalalható sender:TObject referencián keresztül kaphatjuk meg. Ezt viszont értelemszerűen először castolni kell a megfelelő típusra. Ha pedig valamilyen okból (például a GUI mögött álló adatmodellel való kapcsolattartás érdekében) az objektum tároló konténerben elfoglalt helyére vagyunk kíváncsiak, a fent olvasható megkülönböztetési technikák egyikét kell alkalmazni.

Az eseménykezelő függvényeket futási időben szabadon hozzárendelhetjük, illetve elvehetjük objektumok eseményeitől, de ellentétben más technológiákkal (pl. MS C#) több eseménykezelő nem rendelhető egy objektum egy eseményéhez, a hozzárendelt eseménykezelő egy procedure típusú propertyben van tárolva. Természetesen ez nem igazi korlátozás, hiszen készíthetünk egy esernyő procedúrát, amely sorba meghívja az eseménykezelőket, továbbpasszolva a paramétereket, és az esernyő procedúrát rendelhetjük az eseményhez.

var ... kep:array[1..8] of TImage; ... implementation ... procedure TForm1.PicturesDragOver(Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean); begin ... accept:=false; if (<we want to accept the dragging>) then Accept:=true; end; procedure TForm1.FormCreate(Sender: TObject); var i:Integer; begin for i:=1 to 8 do begin kep[i]:=TImage.Create(self); ... kep[i].onDragOver:=PicturesDragOver; end; end;

Amint az látható, a függvény szignatúráját nem kell megadni. Ez azért van így, mert a szignatúra kötelezően csak egyféle lehet, nem lehet több különböző szignatúrájú függvényt ugyanahhoz az eseményhez kötni.

Komponensek dinamikus megszüntetése

Az objektumok gyűjteményben tárolásának egyik értelme a számok futási időben való változtatása. A létrehozás és a megszüntetés is elég egyszerű, mivel a komponensek kirajzolását a VCL megoldja, nekünk csak a komponens memóriából való kitörléséről illetve oda való létrehozásáról kell gondoskodnunk.

kep[i].Free; ... // later kep[i]:=TImage.Create(self);

A Free procedúra gondoskodik arról, hogy csak akkor törlődjön a komponens (objektum), ha más referencia nem mutat rá, így az biztonságos.

Egyszerű grafikus programok készítése VCL komponensekkel

Bár a VCL komponensek (így a TImage sem) nem arra vannak tervezve, hogy játékot készítsenek velük, ez mégis lehetséges, és egyszerű játékok esetében gyakran kifizetődőbb, mint valamilyen grafikus library (akár beépített) használata. Komoly korlátozás viszont, hogy a komponensek kirajzolási ideje és költsége elég sok, így olyan programok, ahol sűrűn (másodpercenként sokszor) változtatnak helyet a komponensek, nem igazán készíthetőek hatékonyan így.

Tipikusan olyan programok készíthetőek így, ahol nem idő, hanem user beavatkozás hatására történik valami, és nem az a lényeg, hogy ez a user beavatkozás minél gyakrabban történjen. A következőkben megnézünk egy példát.

Grafikus játék készítése VCL komponensekkel

Pipe játék

A program a régről „pipe” vagy hasonló néven ismert játék Delphi VCL implementációja, elindul a golyó, és építeni kell előtte a csővezetéket, hogy el tudjon jutni a célba. A felső panelről az alsóra be kell húzni (drag and drop) a csődarabok egyikét, a golyó jobbra indul és a piros csővéghez szeretne eljutni. Ha ez sikerül, akkor nehezítés (gyorsabb golyó) következik.

A programban a játék állapotának reprezentációja az Image objektumok Tag propertyjével történik. A felső panelen 3 Image objektum van, a képük és a Tagjük annak megfelelően változik, hogy melyik csődarabot reprezentálják. Az alsó panelen négyzetrácsban Image-ek vannak, kezdetben kép nélkül és Tag=0-val. A játék állapota kétféleképpen változik: egyrészt a golyó folyamatosan (TTimer tickjeire) halad előre, amerre a cső vezet, másrészt a user drag and drop módszerrel a felső panel egyik image-ét az alsó panel egy olyan image-ére húzza, amelynek a Tagje jelenleg 0, vagyis a mezőn még nincs csődarab. Elvenni csődarabot nem lehet, így a játék végessége (mind időben, mind lépésekben) is garantálva van.

A programban annak érdekében, hogy a képek az exe fájllal legyenek csomagolva, nem futási időben olvasom be őket, hanem egy-egy statikus, csak a képek tárolására szolgáló, megjelenítésre nem kerülő Image objektumban tartom őket. Innen másolom át őket más Image objektumokba futási időben.

Az objektumok megkülönböztetése néven említett probléma itt nem jelentkezik, mivel egy esemény (drag and drop) kiváltódásánál a reakció egyáltalán nem függ attól, hogy a négyzetrács melyik elemére és melyik eleméről történt a drag and drop, csak attól, hogy az adott elemeknek milyen az állapota (Tag – és vizuálisan a kép). De ha lenne szükség erre az információra is, azt a fent említett származtatásos technikával, „posX” és „posY” adattagokkal tudnánk megoldani.

A drag and drop példakód:

procedure Tform1.MyDragDrop(Sender,Source: TObject; X,Y: Integer); begin if (Sender as TImage).tag>0 then exit; (Sender as TImage).picture:=(Source as TImage).picture; (Sender as TImage).tag:=(Source as TImage).tag; (Source as TImage).tag:=random(7)+1; if (source as TImage).tag=1 then (source as TImage).picture:=image1.Picture; ... end;

Amint látszik, nem derül ki és nincs is szükség, hogy kiderüljön az elem a négyzetrácsban elfoglalt helye, de ha például szükség lenne annak megállapítására, hogy az újonnan felrakott elem illeszkedik-e az eddigi vezetékhez, akkor a következőképpen nézne ki a kód:

procedure Tform1.MyDragDrop(Sender,Source: TObject; X,Y: Integer); var coordX, coordY: Integer; begin ... coordX := (Source as TmyImage).posX; coordY := (Source as TmyImage).posY; if (kep[coordX][coordY].tag=1 ... ... end;

Komplex input űrlapok építése dinamikusan létrehozott VCL komponensekkel

A komponensek ilyen módon való menedzselése arra is lehetőséget teremt, hogy felhasználóbarát, dinamikusan változó beviteli mezőkkel lássunk el egy űrlapot. Így elkerülhető a feleslegesen sok vagy nem odaillő input mezők megjelenítése, valamint lehetővé válik végtelen nagyságú adatszerkezetek bevitele, korszerű körülmények között. Erre nézünk néhány példát.

Végtelen engedélyezett hosszúságú lista bevitele

Feltételezzük, hogy valamilyen komplex adatszerkezetünk van, amiből egy „listányit” akarunk egyszerre, kulturált körülmények között bevinni. Tehát az adatszerkezetünkhöz tudunk csinálni űrlapot, csak éppen azt kéne sokszorosítani. Értelemszerűen az lenne jó, ha nem kerülne megjelenítésre felesleges, üres mező-együttes. Kezelés szempontjából kétféle megoldás szokott alkalmazásra kerülni. Mindkettőben egymás felett (esetleg mellett) vannak egy-egy objektum (komplex) bevitelére szolgáló form elemek, ezeket tudjuk szerkeszteni. A különbség abban van, hogy hogyan növeljük a lista méretét. Az egyik megoldás egy gomb, amire a legalsó alatt új mező-együttes jelenik meg. Ennek hátránya, hogy több új elemet is hozzáadhatunk anélkül, hogy kitöltöttük volna, vagy ha nem, akkor a gomb néha letiltásra kerül, ami nehézkessé teszi a kezelést. A másik megoldás a mezők kitöltöttségének figyelése, és annak függvényében mezők megszüntetése és létrehozása. Nyilván ez utóbbi a komplexebb, de felhasználóbarát és a VCL komponensekkel aránylag könnyen megvalósítható.

A példában egy háromelemű struktúránk van, egy szöveges, egy logikai és egy választó mezőnk van egy sorban. Először nézzük, amikor gombnyomásra jön létre új sor.

Vegtelen lista input

A vezérlőket listában vagy tömbben tároljuk, kezdetben egyet sem hozva létre belőlük, nyilvántartva, hogy hányat hoztunk már létre, és gombnyomásra létrehozva egy újat, default értékekkel („”, false, First).

type TFormPanel = class(TPanel) edit: TEdit; box: TCheckBox; combo: TComboBox; id: Integer; procedure Create(…); // calling the constuctors of fields // and setting initial values up end;

A könnyebb kezelésért az egy objektumhoz tartozó beviteli mezőkre a fenti komponenst vezetjük be. Így a gomb „Click” eseménykezelője a következőképp fog kinézni:

procedure TForm1.Button1Click(Sender: TObject); var a: TFormPanel; begin a := TFormPanel.Create(self); // setting up initial values based on list.Size; list.Add(a); end;

Ide tehát a gombnyomáson kívül nem kellett eseménykezelés, nézzük a másik esetet. Ekkor az eseménykezelőkből többre lesz szükségünk, de előbb írjuk le részletesebben a kívánt működést. Akkor akarjuk, hogy egy új sor jöjjön létre, ha az előző sorban a szöveges mező kitöltésre került. Akkor akarjuk, hogy a legalsó sor eltűnjön, ha a legalsó előtti sorban, a szöveges mezőben üres string van.

Vegtelen lista

A képen látható a „stabil” eset, az egyensúlyi állapot, hiszen pontosan megfelelő a sorok száma. Innen elmozdulás kétféleképpen lehet: ha beírunk valamit a legalsó szöveges mezőbe, vagy ha kitöröljük a szöveget a 4. sorból. A változás minden esetben a mező tartalmának változásakor jelentkezik, és látható, hogy soha nem érinti az aktuálisan szerkesztés alatt álló (focus) mezőt.

Az eseménykezelő tehát két esetben kell, hogy reagáljon valamit a tartalom változására.

procedure TForm1.MyEditChange(Sender: TObject); begin if ((sender as TEdit).Parent as TFormPanel).id = rowCount then begin ... end; // is edit filled? if ((sender as TEdit).Parent as TFormPanel).id = rowCount-2 then begin ... end; // is edit empty? end;

Ebben az esetben az összes Edit Change eventjéhez hozzárendeljük ezt az eseménykezelőt, az újonnan létrejövőkhöz is. Ennél viszont elegánsabb megoldás, ha két eseménykezelőt definiálunk, és a hozzárendeléseket dinamikusan változtatjuk. Az első eseménykezelő mindig az utolsó sor editjéhez kell hogy rendelve legyen, a második pedig az utolsó előttihez, amennyiben van. Természetesen kezdetben kell hogy legyen egy sorunk, de kettő nem feltétlenül.

procedure TForm1.FirstEditChange(Sender: TObject); begin if (sender as TEdit).Text <> '' then begin ... end; // add new row end;

procedure TForm1.SecondEditChange(Sender: TObject); begin if (sender as TEdit).Text = '' then begin ... end; // delete last row end;

A Change event akkor váltódik ki, ha a mező szerkesztés alatt van, megváltozik a tartalma és befejeződik a szerkesztése, vagyis elveszíti a fókuszt, vagy valami más miatt megszakad a szerkesztése.

Arra nincs közvetlen lehetőség, hogy szerkesztés alatt álló mező értékét figyeljük, de természetesen ez is lehetséges, csak bonyolultabb. A megoldás a KeyUp, KeyDown stb. eventek kezelésében rejlik. Ezek akkor hívódnak meg, ha lenyomunk egy billentyűt úgy, hogy a fókusz a komponensen volt. Ettől nem biztos, hogy megváltozik a tartalma (hiszen például a maximális hossz elérése miatt lehet, hogy nem kerül be az új karakter, vagy például shift volt a leütött billentyű), de ezeken az eventeken keresztül van lehetőségünk reagálni minden olyan esetben is, amikor valóban megváltozik a tartalom.

procedure EditKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState );

Összetettebb struktúrájú beviteli űrlapok

A fentiekhez hasonló eseménykezeléssel összetett struktúrájú adatok bevitelére is van lehetőség, csak meg kell jól tervezni a felületet és a működést. Az alapelv mindig az kell, hogy legyen, hogy a lehető legkevesebb olyan mező legyen a felületen, amire a user nem tart igényt. A legegyszerűbb eset, amikor egy mező ki/bekapcsol egy másik mező hatására, erre nezünk egy példát.

Valtozo input form Valtozo input form

A példában a checkbox bejelölésének illetve kijelölésének hatására változik a mellette lévő Edit láthatósága. Ehhez egyetlen nagyon egyszerű eseménykezelővel kell reagálni a checkbox Change eventjére.

procedure TForm1.CheckBoxChange(Sender: TObject); begin NationalityEdit.Visible = not (sender as TCheckBox).Selected; end;

Összetettebb struktúrák esetén érdemes paneleket használni, és egymásba ágyazni, mivel így a láthatóság könnyebben befolyásolható, valamint több panel egymás alternatívájaként jelenhet meg ugyanazon a helyen, ami panel használata nélkül káoszt okozna.

Általános komponensek bemutatása

Ebben a részben szeretném bemutatni az általános Delphi komponenseket, illetve azok jellemzőit és eseményeit.

TLabel (címke)

TEdit (szerkesztődoboz)

TMemo (többsoros szerkesztődoboz)

TBtton (gomb), TBitBtn (gomb képpel)

TCheckBox (jelölőnyégyzet)

TRadioGroup (választógomb-csoport)

TListBox (listaboboz)

TComboBox (kombinált lista)