Az előző szám vége valami oyasmivel kezdődött, hogy Most mát tényleg foglalkozhatunk az objektumok elkészítésével. Akkor nagyvonalakban leírtam a gomb objektumot, hogy valamin lehessen tesztelni a rendszer működését. A mostani számban igyekszem valamilyen emberi nyelven kifejteni az objektumok (képernyőelemek) elkészítésének általánosan érvényes alapelveit. (Huhh, ez a néhány sor csekély 3 órámba került - igaz, közben megnéztem két filmet)

Hogy is működik a rendszer? 

Az eddig tárgyalt rendszer az initáláskor létrehozza az objektumokat és a háttérrel együtt kirajzolja őket. Ezután a vezérlés az eseményosztó procedúrára kerül (TXSystem.Run) amely meghívja az eseményszerző procedúrát (GetEvent), mely visszaadja egy TEvent szerkezetben a legutolsó eseményt (egér vagy billentyű). Az esemény kiértékelésekor az eseményosztó kiválasztja azt az objektumot, amelyiknek kapnia kell az eseményt (ha egér akkor az az objektum, amelyik felett áll az egér; ha billentyű akkor a Focused objektum). Ha a képernyőelem jogosult az esemény megszerzésére (az esemény típusa szerepel az EventMask mezőben), akkor az objektum fókuszálódik és a vezérlés az objektum HandleEvent metódusára kerül azzal az eseménnyel együtt, amelyik kiváltotta a HandleEvent meghívását. A HandleEventben a képernyőelem egyedi módon eldönti, hogy mit kell csinálni a beérkezett eseménnyel és ettől függően hív meg procedúrákat. Ha az objektumok feletti vezérlésnek információt akar átadni, akkor az objektum Ownerének Process metódusát hívja meg, melynek átadhat egy TEvent típusú rekordot. A HandleEvent lefutása után a vezérlés visszakerül az eseményosztó procedúrára és a kör bezárult - ismét meghívódik az esemény szerző rutin. A rendszerből való kilépés a TXSystem SystemExit változójának True-ra állításával érhető el. A kilépés után (normális programban) meghívódik a TXSystem Done metódusa, mely egyenként eltávolít a memóriából minden objektumot. (A képernyőelemek Done metódusait hívja meg egymás után)

Erre a gyors áttekintésre azért volt szükség, hogy átlássuk, valójában mikor mit is csinál a program.

Az INIT Constructor 

Ezzel tulajdonképpen nincs sok gond, fontos megjegyezni, hogy az Init konstruktor paramétereit célszerű úgy megválasztani, hogy az objektum rugalmas legyen, de ne kezelhatetlen. Az, hogy az általunk megírt objektumok milyen paramétereket kérjenek tulajdonképpen egyéni ízlés kérdése.

Az Init konstruktorban szokás beállítani a változók kezdeti értékét is. Célszerű használni az Inherited Init-et mivel enek paraméterei az objektum méretei - amit beállít - és kinullázza a TOldObject-ben is létező mezőket.

Figyelem! Az INIT konstruktorban még nem szabad hivatkozni az objektum azon mezőire, melyek a képernyőelem rendszerbe illesztését szolgálják (Owner, Next, Prev) hiszen ilyenkor az objektum még nincs rendszerben (majd az AddObject-tel kerül bele) következésképp ezek a mezők NIL-re mutatnak)

Hogy célszerű megírni a HandleEvent metódust?  

A HandleEvent metódusokat egy Case szerkezetként érdemes felépíteni, mivel ebben a formában még bonyolult procedúráknál is könnyen átlátható marad. Erre példa a következő forráskód, mely egy szövegmező HandleEvent procedúráját mutatja:
Procedure   TTextBox.HandleEvent;
Var
  TX: String[1];
Begin
  Case Event.What Of
Látható, hogy az első Case szerkezetbe tettük a beérkező esemény típusát (célszerű akkor is létrehozni ezt a szerkezetet, ha csak egy féle eseményt szokott kapni az objektum)
  EvKeyDown: Begin
               Case Event.KeyCode Of
A Case szerkezetbe is lehet Case szerkezetet tenni, de ez általában csak billentyűkódok értelmezésére jó. Egér események értelmezésénél általában marad a jó öreg IF-THEN
               32..255: Begin
                          TX:=Event.CharCode;
                          If Cursor<Length(Text) Then
                             Text:=Copy(Text,1,Cursor-1)+TX+Copy(Text,Cursor,255)
                          Else Text:=Text+TX;
                          Inc(cursor);
                        End;
               8:       Begin
                          If Cursor>1 Then
                             Text:=Copy(Text,1,Cursor-2)+Copy(Text,Cursor,255);
                          Dec(cursor);
                        End;
               256*75 : Dec(cursor);
               256*77 : Inc(cursor);
               End;
               DrawIt;
             End;
  End;
  Inherited HandleEvent(Event);
End;

Eseményre várakozás HandleEvent-ben 

Amikor olyan objektumot írunk, amely több esemény egymás utácni bekévetkezésétől tesz függővé dolgokat (Pl: egy gomb amelyre ha rákattintunk benyomódik, de csak akkor küld parancsot a processznek, ha a gomb felett el is engedik az egeret) rákényszerülünk, hogy ne a rendszer eseményosztó procedúráját használjuk, hanem a HandleEvent.ben keringje a Program.

Lássunk egy példát! A következő forráskód a TGomb objektum HandleEvent metódusának részlete:

  EvMouseDown: Begin
Ez a programrész csak akkor hajtódik végre, ha a gomb felett megnyomták az egér valamelyik gombját.
                 Status:=True; OS:=False;
A gombnál ha a Status True akkor a Draw benyomódott gombot rajzol, egyébként kiengedettet.
                 Repeat
                   If Event.What=EvMouseMove Then Status:=IsInArea(Event.WhereX,Event.WhereY,X,Y,W,H);
                   If Status<>OS Then DrawIt;
Az egér megmozdításának figyelése. Ha az egeret elviszik a gomb felől, akkor gomb visszaugrik alapállapotba, ha az egér ismét a gomb fölé kerül a gomb ismét benyomódik. Fontos megjegyezni, hogy más módon nem lehetne megoldani a gomb ijen viselkedését (gondojuk bele: tegyük fel, hogy a gombot feljogosítjuk arra, hogy fogadja az EvMouseMove eseményt is. Ekkor a HandleEvent mindig meghívódik, amikor a gomb felett megmozdul az egér. DE mi van akkor, ha levisszük az egeret a gombról? Akkor már nem fog meghívódni, vagyis a gomb "bennragad".
                   OS:=Status;
                   GetEvent(Event);
Az eseményszerző rutin meghívása. Ez azért jó, mert így ugyanolyan formában kapjuk meg az eseményeket, mintha azokhoz a gomb normális módon juott volna.
                 Until Event.What=EvMouseUp;
A HandleEvent a fenti ciklusban várakozik az egérgomb felengedése esemény beérkezésére.
                 Status:=False; DrawIt;
A gomb eredeti paramétereinek visszaállítása és kirajzolása.
                 If OS Then
                 Begin
Ha gomb be volt nyomva, amikor felengedték az egeret, akkor átadódik egy parancs a processznek.
                   Event.What:=EvCommand;
                   Event.Command:=Command;
                   Owner^.Process(Event);
                 End;
               End;

A DRAW metódusok megírása 

Lássuk csak! Először is: Mi a különbség a Draw és a DrawIt metódusok között? Lássuk először a DrawIt metódust:
Procedure TOldObject.DrawIt;
Begin
  Mouse_Hide;
  Draw;
  Mouse_Show;
End;
Jól látható, hogy a DrawIt először kikapcsolja az egeret, majd meghívja a Draw metódust, majd visszakapcsolja az egeret. Ezzel az egyszerű metódussal elértük, hogy a Draw metódusban nem kell foglalkoznunk azzal, hogy kapcsolgassuk az egeret. A mostani DrawIt itt még nagyon egyszerű, de egy többszíntű felületnél sokkal nagyobb tartalmat fog kapni.

Fontos: Mindig a DrawIt metódust hívjuk meg, ha ki akarunk rajzoltatni egy objektumot!

Kikötések a Draw metódussal szemben

A Draw metódusok azok, amelyeket a felhasználó szinte legtöbbször fog látni, így törekedni kell arra, hogy a Draw mindig csak annyit rajzoljon, amennyi feltétlenül szükséges, lehetőleg ne rajzoljon kétszer ugyanarra a pontra, tartsa be az objektumok logikai határait azaz ne rajzoljon az objektum X, Y, W, H mezőiben megjelölt területen kívűlre (1. ábra) és a lehetőségekhez képest gyors legyen.
1. ábra - Az objektumoknak csak a vonalkázott részre kell rajzolni

Első pillantásra elég bonyolultnak tűnik ez a sok kikötés, de valójában az objektumok képei nem szoktak olyan bonyolultak lenni, hogy különösebb fejtörést okozzanak a Draw rutinok írójának. Annyit azért célszerű szemelőtt tartani, hogy mivel sokféle objektum létezhet, mindig állítsunk be újra minden rajzolási paramétert (színek, szöveg típusok, kitöltés típusa stb.) különben kellemetlen meglepetések érhetnek bennünket. (Tulajdonképpen ezeket a rajzolási paramétereket állító rutinokat is beleírhatnánk a DrawIt metódusba így ezzel sem kellene foglalkoznunk).

Most már tudjuk, hogy hogyan rajzoljunk, de még nem tudjuk, hogy pontosan mit is szeretnénk. Azt, hogy egy objektumon belül a képernyőelem pontosan melyik megjelenési formáját szeretnénk megrajzolni (a benyomott vagy a kiengedett gombot), általában objektum-mezőkön keresztül célszerű átadni a Draw-nak.

Fent említettem, hogy a Draw lehetőleg csak annyit rajzoljon, amennyi feltétlenül szükséges. Ennek a kikötésnek a betartása azonban néha furcsa eredményhez vezet: képzeljük el, hogy egy objektum valamilyen okból újrarajzoltatja az egész képernyőt. Ha az általunk írt képernyőelem csak a fontos dolgokat rajzolja ki, akkor az újrarajzolás után bizonyos részek hiányozni fognak. Ennek a problémának az áthidalására célszerű a rendszerobjektumban bevezetni egy új mezőt amely Boolean típusú és ha az értéke True, akkor az objektumok Draw metódusainak a teljes objektumot ki kell rajzolnia. A mezőt hívják mondjuk DrawAll-nak. A DrawAll-t a rendszer Init konstruktora False-ra állítja, de a DrawScreen (amely minden objektumot kirajzol) ezt True-ra állítja majd lefutása után visszaállítja False-ra. Ezzel a módszerrel elértük, hogy az objektumok "tudják", hogy mikor kell teljes képet és mikjor kell csak részleteket rajzolni.

A Destruktor 

A destruktorral normális objektumoknál nincs sok gond, legtöbbször hozzá sem kell nyúlni, jó az úgy, ahogy eredetileg van. Piszkálni jóformán csak akkor kell, ha az objektum valami olyat csinál, amit külön meg kell szüntetni (dinamikus memóriafoglalás vagy temp fájl a wincsin). Fontos, hogy miután az objektum elintézte a saját külön Done-olni valóit, meg kell hívni az eredeti Done metódust, különben a képernyőelem bennmarad a rendszerben.

Hibakeresés a képernyőelemek megírásakor  

Mivel az Xvision programok az eseményosztó procedúrában keringenek, ne is próbáljuk a programot lépésről lépésre követni az indítástól úgysem fog sikerülni. Az Debuggolásra az a legjobb módszer, ha a vizsgálni kívánt képernyőelem metódusaira töréspontokat helyezünk el és amikor a program megállt a törésponton, akkor kezdjük lépésenként vizsgálni azt, hogy mit is csinál. A rendszer debuggolását szintén csak töréspontokkal tudjuk elvégezni, de általában itt csak a műveletek fejben (vagy papíron) való végiggondolása a megoldás.

Mikor adjon üzenetet az objektum a processz metódusnak?  

A legjobb, ha a képernyőelemek mindig és minden történésről tájékoztatják a processzt, mert így lehet a legkevesebb esetben szükség arra, hogy új objektumot kelljen származtatni plusz két sor miatt.

Az objektumok (a gombokat kivéve) általában EvMessage típusú üzeneteket küldenek a processznek, melynek command mez?jében küldik az esemény típusát és az InfoPtr mez? tartalmazza a küldő objektum címét. Az eseményeket úgy célszerű definiálni, hogy az esemény típusából kiderüljön, hogy az milyen képernyőelem küldte. Ez sok objektumnál sokféle eseménytípust jelent, de egyszerűvé teszi a vezérlést.

Tulajdonképpen kb. ennyi volt az, amit a képernyőelemek írásáról első körben érdemesnek láttam elmondani. És különben is már baromira elálmosodtam.
 

Jóéjszakát Gyerekek!

Gerebenics Andor
eMail:PC-XUser@IDG.HU, Subject: "Graphika rovat"