Amikor az ember programot ír, gyakran fontos, hogy a programnak szép, könnyen kezelhető felülete legyen. Ennek megvalósítása azonban nem túl egyszerű feladat. Sokan elkészítik ugyan a grafikus felületet, de az hosszú, bonyolult lesz és ha újra szép programot kell írni akkor megint jön a szenvedés. 

Jó lenne, ha lenne valami olyan segítség, ami megkönnyíti számunkra a felhasználói felület megírását. Ilyen lehet pl: A Pascalhoz adott TVision (OOP rovatunk foglalkozik vele) de az sajnos szöveges felület ami mostanság nem annyira korszerű (noha mint azt az OOP későbbi számainkban látni fogjuk, lehet rajta szépíteni). Itt vannak még a Windows különböző verziói melyek a sokéves fejesztésnek köszönhetően mára igen sok lehetőséget bocsájtanak a programozók rendelkezésére is. Néha azonban mégis megesik, hogy a Windows sem igazán járható megoldás, mert nekünk egészen egyedi igényeink vannak. Ilyenkor kerülünk olyan helyzetbe, hogy saját felhasználói felületet kell írnunk. Az elkövetkezendőkben ezzel a problémával szeretnék foglalkozni.

Alapelvek a felület kialakításakor


Ha kicsit zavaros az elkövetkezendő néhány bekezdés, akkor bocsánat érte, később igyekszem mindent megmagyarázni.
 

  • Akármilyen felületet is akarunk írni, meg kell valósítanunk az eseményorientált programvezérlést. Ez annyit tesz, hogy a programot egységes szerkezetbe rendezett események vezérlik, azaz a program egy ciklusban kering, ami figyeli, hogy megmozdították-e az egeret, lenyomtak-e egy billentyűt, mit művel a CD stb. majd ha valahol történik valami, akkor meghívja a megfelelő programrészt. Pl: kattintottak az egérrel mire a program megnézi, hogy mi felett történt a kattintás és annak megfelelő programrészt hív meg.

  • A képernyőt megtöltő dolgokat egységes adatszerkezetbe kell rendezni vagyis valahol adatként el kell tárolni a képernyőn szereplő dolgok adatait és azt, hogy az eseményekre hogyan kell reagálniuk. (a gyakorlati megvalósítást lásd lentebb)

  • Lehetővé kell tenni, hogy a különböző képernyőelemek egymással is kommunikálhassanak. pl: Ha "lenyomunk" egy gombot a képernyőn, akkor meg kell hívódnia egy programrésznek. (ha a gomb egy ablakban van, akkor a gombnak egy üzenetet kell küldenie az ablaknak, hogy "őt megnyomták" mire az ablak végrehajtja a szükséges teendőket.)

Ha betartjuk a fent leírt alapelveket akkor a későbbiekben egyszer megírt képernyőelemekből könnyedén írhatunk programokat.

Kicsit gyakorlatiasabban

A képernyőelemek tárolása egyszíntű felületnél

Először talán kezdjük egy egyszerűbb felülettel. Első lépésben a felület legyen csak egy színtű azaz a képernyőelemek között nincs hierarchia (nem tudunk olyat csinálni, hogy a desktop-ra felrakott ablakban lévő csoportobjektumra felrakunk egy gombot) - itt az összes képernyőelem a desktop-on foglal helyet. 

A képernyőelemek adatait láncba rendezve érdemes tárolni mert így egy esemény beérkezésekor könnyedén el lehet dönteni, hogy melyik képernyőelemnek kell adni a vezérlést.

A láncot oda-vissza mutatózzuk, hogy visszafelé is könnyű legyen a lépegetés. 

Ha a láncot rekordok alkotják, akkor a rekordoknak tartalmazniuk kell procedure vagy function típusú mezőket is (ezekre ugorna a program amikor a képernyőelem eseményt kap), melyeket a rekord első használata előtt be kell állítani. Ez is járható út de igen körülményes. 

A Pascalban viszont lehetőségünk van objektumok használatára is melyekkel sokkal egyszerűbben megoldhatjuk a problémát ráadásul a program áttekinthetőbb és rugalmasabb is lesz. 

Az objektumok deklarálása

Elsőként deklarálunk egy ősobjektumot, mely azokat a mezőket és metódusokat tartalmazza, melyek az összes képernyőelemre jellemzőek. Ilyen pl: a kirajzolás, inicializálás, lebontás, eseményfeldolgozás. 

Először is lássuk az ősobjektumot:

Type   POldObject=^TOldObject;   TOldObject=Object     X, Y, W, H: Integer;     Options   : Word;     EventMask : Word;     Next      : POldObject;     Prev      : POldObject;     Constructor Init(ax, ay, aw, ah: Integer);     Destructor  Done;                          Virtual;     Procedure  HandleEvent(Event: TEvent);    Virtual;     Procedure   Draw;                          Virtual;     Procedure  DrawIt;                        Virtual;   End;
POldObject az alapobjektumra mutató pointertípus 
Mezők: X, Y a képernyőelem bal felső sarkának koordinátái 
W, H a képernyőelem szélessége és magassága 
Options a képernyőelem tulajdonságai 
EventMask a képernyőelem által kért eseménytípusok listája 
Next a következő képernyőelemre mutató alapobjektum típusú pointer. 
Prev az előző képernyőelemre mutató alapobjektum típusú pointer. 
Metódusok: Init Ez az objektum Inicializálását végzi. Ez állítja be az objektum/képernyőelem alapbeállításait 
Done Ez kiveszi az objektumot a láncból, és törli magát memóriából. 
HandleEvent Ez az objektum/képernyőelem eseményértelmezője. A HandleEvent akkor hívódik meg, ha olyan esemény érkezik, ami az objektumnak szól és az EventMaskban szerepel. (bővebben lásd később) A TEvent az eseményt tartalmazza ami miatt meghívódott a metódus. 
Draw Ez végzi a képernyőelem kirajzolását. 
DrawIt előkészíti a kirajzolást. 

Az eseménykezelés

Amikor a program elindul, a képernyőn szereplő objektumok inicializálódnak, majd meghívódik az eseményosztó procedúra. Ez a rutin dönti el, hogy a beérkező eseményt melyik objektumnak kell megkapni. Ha van olyan objektum amely jogosult az eseményre akkor meghívja az eseménykezelő metódusát (HandleEvent). 

Az esemény-adatszerkezet

Amint fentebb is utaltam rá, az eseményeket egységes szerkezetbe kell rendezni. Ezt a legegyszerűbben egy alternatív rekorddal tehetjük meg:

Const Az események neveihez konstans számokat rendelünk.
  EvNothing    = 1; Nincs esemény
  EvMouseMove  = 2; Egér mozgatása
  EvMouseDown  = 4; Egér gombnyomás
  EvMouseUp    = 8; Egér gomb felengedés
  EvKeyDown    = 16; Billentyű leütés
  EvMessage    = 32; Üzenet (másik objektumtól)
  EvCommand    = 64; Parancs (másik objektumtól)
Type
  TEvent = record Az eseményeket leíró alternatív rekord
    What: Word; Az esemény típusát leíró mező
    case Word of Ha a típus ...
      EvNothing: (); EvNothing akkor nincs további mező
      EvCommand: ( EvCommand akkor van egy
        Command: Word); Command mező, ami leírja, hogy milyen parancs érkezett.
      EvMouseUp,
      EvMouseDown,
      EvMouseMove: (
EvMouseUp, EvMouseDown, EvMouseMove akkor van
        Buttons: Byte; Buttons mező - az egér gombjainak állapotát írja le
        WhereX : Integer; WhereX és WhereY mező ami az
        WhereY : Integer); egér helyzetét mutatja
      evKeyDown: ( EvKeyDown akkor
        Shift  : Byte; A Shift mező a Shift gombok állapotát mutatja
       case Integer of A lenyomott billentyűt ki lehetolvasni
          0: (KeyCode: Word); KeyCode formában
          1: (CharCode: Char; vagy Karakterképben és 
              ScanCode: Byte)); Scankódban (csak ha a karakterkép #0 egyébként mindíg 0 értéke van)
      evMessage: ( EvMessage akkor
        InfoPtr: Pointer); az InfoPtr a küldő objektum címét tartalmazza.
  end;

Természetesen az esemény-adatszerkezetnek nem feltétlenül kell pontosan ilyennek lennie. A magyarázat csak azért ilyen részletes, mert a példaprogram is ilyen eseményszerkezettel dolgozik. 

Az eseményfigyelő procedúra
 

Var Az egér figyeléséhez
  OldX,OldY: Integer; szükséges globális
  OldButtons: Byte; változók
Procedure GetEvent(Var E: TEvent); Az eseményfigyelő rutin
 Function Shiftpress:Byte; Assembler;
{ 1: RShift  2:LShift  3:Both }
A Shiftpress funkció
 Asm megadja, hogy a Shift
   XOR  AX, AX gombok közül melyek
   MOV  AH, 02h vannak lenyomva.
   INT  16h
   AND  AX, 3
 End;
Var Belső változók
  EX,EY: Word;
  b1: byte;
  ch: Char;
  cb: Byte;
Begin
  E.What:=EvNothing; Alapesetben nincs esemény
  if Keypressed then A gombnyomás ellenőrzése
  Begin
    E.What:=EvKeyDown; Ha van, az eseménytípus beállítása
    E.Shift:=ShiftPress; A Shift status beállítása
    ch:=Readkey; A billentyő lekérdezése
    if ch=#0 then E.KeyCode:=Ord(Readkey)*256 else Ha 0 a billentyűkód akkor a következő billentyűkódot is lekérdezi.
    Begin
      E.KeyCode:=ord(ch); különben a KeyCode az ASCII kód,
      E.CharCode:=ch; a CharCode ua. csak Char típusú változóban
    End;
  End;
  EX:=HiMouseX; Egérhelyzet lekérdezése
  EY:=HiMouseY;
  E.Buttons:=Buttons; Gombok lekérdezése
  b1:=buttons;
  if ((OldX<>EX) or (OldY<>EY)) then Ha az egér helyzete változott
  Begin akkor
    E.What:=EvMouseMove; Eseménybeállítás
    E.WhereX:=EX; A helyzet betöltése az
    E.WhereY:=EY; eseményleíróba
  End;
  if (((b1 and 1)<>0) and ((OldButtons and 1)=0)) or Ha lenyomták valamelyik
     (((b1 and 2)<>0) and ((OldButtons and 2)=0)) then egérgombot
  Begin akkor
    E.What:=EvMouseDown; Eseménybeállítás
    E.WhereX:=EX; A helyzet betöltése az
    E.WhereY:=EY; eseményleíróba
  End;
  if (((b1 and 1)=0) and ((OldButtons and 1)<>0)) or Ha felengedték valamelyik
     (((b1 and 2)=0) and ((OldButtons and 2)<>0)) then egérgombot
  Begin akkor
    E.What:=EvMouseUp; Eseménybeállítás
    E.WhereX:=EX; A helyzet betöltése az
    E.WhereY:=EY; eseményleíróba
  End;
  OldX:=EX; Az egér helyzetének frissítése
  OldY:=EY; Az egér helyzetének frissítése
  OldButtons:=b1; Az egér helyzetének frissítése
End;
Az esemény osztó rutin

Ez az a programrész, mely a program futás közbeni vezérlését végzi. Ez meghívja az eseményfigyel? procedúrát és eldönti, hogy melyik objektumnak kell adni a vezérlést.

Procedure System_Run; 
Var
  E: TEvent;
  P1,P2: POldObject;
Begin
  Repeat
    GetEvent(E); Az eseményfigyelő meghívása
    Case E.What of
    EvKeyDown: Begin Ha megnyomtak egy gombot 
                if Focused<>Nil then és a Focused nem NIL
                Begin akkor a program a 
                  if Focused^.EventMask and EvKeyDown<>0 then Focused^.HandleEvent(E); Focused-nek adja a vezérlést ha jogosult rá.
                 End;
               End;
    EvMouseDown, Ha az egérrel történik valami
    EvMouseUp, akkor
    EvMouseMove: Begin
                   P1:=PFirstObject;
                  While P1<>Nil do a program elkezdi keresni az
                  Begin objektumokat tartalmazó
                     P2:=P1^.Next; láncban azt az objektumot, 
                    if IsInArea(E.WhereX, E.WhereY, P1^.X, P1^.Y, P1^.W, P1^.H) and amelyik felett áll az egérkurzor.
                        (P1^.EventMask and E.What<>0) then
                     Begin Ha megtalálta az objetumot és az jogosult az eseményre
                       SetFocus(P1); akkor azt beállítja Focused-nek és meghívja
                       P1^.HandleEvent(E); a HandleEvent rutinját.
                     End;
                     P1:=P2; A következő képernyőelemre lép. Ez azért nem P1:=P1^.Next mert ha közben a P1-et törölték a memóriából akkor kiakadna. (a P1 pointer nem létez? objektumra muatatna)
                   End;
                 End;
    End;
  Until SystemExit; A ciklus addig ismértlődik, amíg a SystemExit globális változó értéke False.
End;

A lemezen található példaprogram ugyanezzel a rendszerrel dolgozik. A programban egyelőre csak gomb képernyőelem szerepel, de remélem sikerült felkeltenem az érdeklődéseteket a téma iránt. A következő részben kicsit továbbfejlesztjük a felületet és néhány újabb képernyőelemet készítünk a most tárgyalt felületi alapokhoz