InfoC adventi naptár
Eszköztárak
Írjunk SDL programot, amelyik kirajzol gombokat, csúszkákat és egy színes téglalapot! A csúszkákból legyen három darab. Ezekre kattintva lehessen beállítani a vörös, zöld, és kék színkomponenseket (RGB), amelyeknek megfelelő színű lesz a téglalap, miután az egyik gombra kattintott a felhasználó. A másik gombbal lehessen kilépni a programból.
Ennek az írásnak nem célja, hogy objektumorientált programozás bevezető legyen. Pár dolog elő fog azért kerülni, de inkább csak problémafelvetésként. A megértéshez szükséges az előadáson bemutatott
union
és függvényre mutató pointer témakörök ismerete.
Tartalom
1 Widgetek

A grafikus felhasználói felületek elemeit angolul widget-nek szokták nevezni. Ezek egyrészt különbözőek működésükben: a csúszka aktívan reagál a kattintásra (megváltozik a tárolt érték), a színes téglalap nem csinál semmit. Másrészt hasonlítanak is egymásra, abban, hogy van egy pozíciójuk és egy méretük a képernyőn. Bizonyos szempontból rokonok.
A hasonlóságaik:
- Mindegyiknek van helye (x, y) és mérete (szélesség, magasság) a képernyőn.
- Mindegyiknek van egy sötét színű kerete, és egy árnyékolt háttere.
- Mindegyikről meg kell tudnia mondani a programnak, hogy arra kattintott-e a felhasználó.
A különbségek:
- A színes téglalap megjelenít egy színt, de nem reagál a kattintásra.
- A csúszkák reagálnak a kattintásra, és mindegyik ugyanúgy működik.
- A gombokon felirat van, de a feliratok különbözőek. Ugyancsak, a kattintás hatására eltérő dolog történik.
- Gombokon kívül is lehetnek feliratok.
Az előadáson szerepelt, hogy eltérő típusú adatokat tárolni ugyanazon az adatterületen
union
segítségével lehet. Ez nagyon C-s. Valami ilyesmit csinálhatunk:
/* egy widget, az altalanos es a specialis adatokkal */ typedef struct Widget Widget; struct Widget { int x, y, szeles, magas; /* pozicio es meret */ enum WidgetTipus { /* ilyen típusú lehet */ gomb, gorditosav, szinesteglalap, felirat } tipus; union { struct GombAdat { char felirat[20]; /* a gomb szövege */ } gomb; struct GorditosavAdat { double jelenlegi; /* erteke; 0.0-1.0 */ } gorditosav; struct SzinesTeglalapAdat { unsigned char r, g, b; /* szin */ } szinesteglalap; struct FeliratAdat { char szoveg[20]; } felirat; } adat; };
A megadott típus alapján ki tudjuk választani, hogy egy bizonyos Widget
típusú struktúra milyen fajta adatait tárolja; és az alapján tudunk választani a
union
-ben lévő adatok közül a megfelelő struktúrát. Ugyancsak, ha
megírjuk a különböző függvényeket, amelyek egy gombot, vagy egy csúszkát
rajzolnak ki a képernyőre, akkor ki tudjuk választani egy widgethez a megfelelőt.
Ehhez azonban minden egyes helyen, ahol rajzolást kell csinálni, egy switch()
kellene; ezt elkerülendő, inkább minden egyes elemben tároljunk el egy pointert is,
amely az elemnek a saját kirajzoló függvényére mutat. Vagyis legyen még
egy ilyen adattag is a struktúrában:
void (*rajzolo_fv)(Widget *widget);
Bár különfélék, a union
használata miatt a Widget
struktúra
egységes C típus minden fajta elemhez. Ez azért jó, mert berakhatjuk ezeket az elemeket egy
tömbbe (a tömb ugyebár egyforma típusú elemek tárolója); ha a felhasználó kattint egyet valahova
(x, y koordináta), akkor a tömbben lévő összes elem mérete és pozíciója alapján el tudjuk
dönteni, hogy konkrétan melyikre. Mivel ennek eldöntéséhez csak azt kell tudni, hogy melyik
widget hol van az ablakban, azt nem, hogy mi az, az ezt kezelő programrész egységes lehet. Egy
egyszerű for()
ciklust kapunk! Bár a tömb számunkra eltérő típusú elemeket
tartalmaz, a ciklus közösen tudja kezelni őket, a közös tulajdonságaik alapján.
Nézzük meg a csúszkát közelebbről! Ha egy ilyenre kattint a felhasználó, akkor be tud állítani
egy színkomponenst. Ha a bal szélére kattint, akkor minimális lesz, ha a jobb szélére, akkor
maximális. Ezt egy 0 és 1 közötti double
értékkel tárolható. Minden csúszka
ugyanúgy viselkedik, és mindegyiknek a kattintás koordinátáit is kell tudnia (mert látniuk kell,
melyik részük fölött volt az egérmutató). Írni kell tehát egy függvényt, amelyik egy csúszkán
belüli kattintást dolgoz fel. Az x
és y
relatív koordináták, a csúszka bal
felső sarkához képest:
void csuszka_kattintas(Widget *csuszka, int x, int y) { csuszka->adat.csuszka.jelenlegi=(double) (x-1)/(csuszka->szeles); csuszka_rajzol(csuszka); }
A kirajzolása pedig így nézhet ki. Először meghívja a widget_alap_rajzol()
függvényt, amelyik amúgy mindegyik típusra működik; ez rajzolja a keretet az adott widget köré, és a
színátmenetet háttérnek. Ehhez azért van külön függvény, mert mindegyikre közös. Ha azt
változtatjuk, így majd az összes widget egységesen vált kinézetet. A csúszka ezután kirajzolja
a saját belsejét; ami egyszerűen egy színes csík:
void csuszka_rajzol(Widget *csuszka) { widget_alap_rajzol(csuszka); boxColor(screen, csuszka->x, csuszka->y, csuszka->x+csuszka->szeles * csuszka->adat.csuszka.jelenlegi, csuszka->y+csuszka->magas-1, csuszkaszin); }
2 A callbackek
Mi a helyzet a gombokkal? A gombok eltérő dolgot csinálnak; az egyikre kattintva a téglalap átszíneződik, a másik pedig bezárja a programot. Az viszont közös bennük, hogy a kattintás hatására történik valami, aminek amúgy nincs is köze a gomb belső lelkivilágához. Ezt egy függvényre mutató pointerrel lehet jól megoldani; az egyik gomb a téglalap átszínezéséhez tartozó függvényt kapja, a másik pedig egy olyan függvényt, amelyik befejezi a programot. Ezzel általánosíthatjuk egy gomb működését. Egy programban többféle gombot hozhatunk létre, amelyek mind mást csinálnak.
Ami nagyon fontos, hogy így az egyes tevékenységekhez tartozó függvényeket nem kell beírnunk
a grafikus programrészek (gomb rajzolása, egérkattintások kezelése stb.) közé. A dolgot tovább
általánosíthatjuk, ha nem csak a gombokhoz rendelünk hozzá ilyen ún. callback függvényt
(amelyre mutató pointert a grafikus modulnak adunk, és az kattintás esetén visszahívja
azt), hanem észrevesszük, hogy bármelyik widgethez társítható ilyen. Létrehozhatunk ennek
segítségével egy speciális, a többitől eltérő működésű csúszkát is, vagy olyan színes
téglalapot, amely képes valami módon a kattintásokra reagálni. Például az egérgombot nyomva
tartva rajzolni lehet rá. Ha nincs szükség callbackre, akkor pedig a függvénypointer
NULL
lehet, ezzel jelezzük a grafikus modulnak, hogy az a widget passzív.
widgetek[0]=uj_gomb(216, 10, 50, 32, "Kilép"); widgetek[0]->felhasznaloi_cb=kilep_gomb_cb; /* programból kilépés */ widgetek[7]=uj_gomb(10, 170, 50, 32, "Mehet"); widgetek[7]->felhasznaloi_cb=mehet_gomb_cb; /* csúszkák alapján szín beállítása */
Ennél is tovább általánosítható a dolog. Ha az alsó, Mehet feliratú gombra kattintunk, akkor a három csúszka aktuális értéke alapján állítódik be a téglalap színe. Az ezt végző függvénynek, amelyik a gomb callbackje, ismernie kell a három csúszkát és a téglalapot. Ezeket a callback paramétereként kell átvegye:
typedef struct UIAdat { Widget *r, *g, *b, *teglalap; } UIAdat;
De ennek tartalmával foglalkozni nem a gomb dolga, hanem a gomb használójáé.
Hogy ne kössünk meg semmit a grafikus modul írásakor, az extra
paraméter típusa, az előadáson bemutatott adatokhoz hasonlóan void*
lehet. Egy
void*
mutatóval bármire rámutathatunk; ha egynél több paraméter kell, akkor azokat
berakjuk egy struktúrába, és az arra mutató pointert veszi át a callback. A függvény belsejében
ezt a típus nélküli pointert a saját típusra vissza kell majd alakítani; hasonlóan ahhoz,
ahogyan egy qsort()
-hoz való összehasonlító függvényben is kell. A widgetek
általános tulajdonságaihoz ezért a lenti mezőket is hozzátesszük. (Az x és y koordináta azért
szerepel itt is, hátha olyan callbacket akarunk írni, amelyik figyelembe veszi azt is. A gomb
ebben a programban nem használja a kapott értékeket.)
Észrevéve, hogy tulajdonképp a widget saját működését is ilyen függvényen keresztül végezhetjük, végülis két függvénypointert teszünk minden widgetbe. Az egyik a belső működését adja (pl. a csúszka állítható), a másik pedig a felhasználói felületben a hozzá társított működés:
/* belso lelkivilag: ha a kattintasra kell valamit csinalni, pl. csuszka erteke */ void (*kattintas_fv)(Widget *widget, int x, int y); /* kivulrol tarsitott mukodes, a beepitett mukodesen tul */ void (*felhasznaloi_cb)(Widget *widget, int x, int y, void *param); void *felhasznaloi_cb_param; /* ezt a parametert megkapja a param valtozoban */
3 Fogjuk őket össze
Az előbb már szó esett róla, hogy a widgetek egy tömbbe kerülnek. A tömb a programban
pointereket tartalmaz; az egyes widgeteket dinamikusan lehet foglalni le. Minden típushoz
tartozik egy külön függvény; a függvény végzi a memóriafoglalást, és a paraméterek
alapján a tulajdonságok beállítását. A csúszka példája lent látható.
Az uj_widget()
függvény feladata
a memória foglalása, és a méretek beállítása; ezt mindegyik típusnál meg kell
csinálni, ezért külön függvény lett belőle. A többi paramétert egyszerűen be kell másolni.
Widget *uj_csuszka(int x, int y, int szeles, int magas, double kezdeti) { Widget *uj=uj_widget(x, y, szeles, magas); uj->tipus=csuszka; uj->rajzolo_fv=csuszka_rajzol; /* ezzel rajzolodik ki */ uj->kattintas_fv=csuszka_kattintas; /* a sajat, belso mukodese */ uj->adat.csuszka.jelenlegi=kezdeti; return uj; }
Az eseményhurok megkapja a felhasználótól érkező kattintásokat. Az SDL a kattintások adatai
mellé megadja a koordinátát. Így könnyű megkeresni azt a widgetet, amelyiknek a területén
éppen az egérmutató volt abban a pillanatban. A widget típusától függően ilyenkor elindulhat egy
beépített függvény (ez a helyzet a csúszkák esetén), és ha van, lefut egy külön megadott
callback (ez pedig a gombok esetén). Mivel ezek hatására a widgetek esetleg megváltozhattak, az
újrarajzolás miatt meghívja az SDL_Flip()
függvényt.
A main()
függvényben létrejönnek az egyes widgetek. Az átszínező gombhoz a
fentiek alapján egy struktúrába kerülnek be a kezelt widgetekre mutató pointerek. Miután minden
kész, az esemenyvezerelt_main()
függvény indul el; és onnantól kezdve a program
mindent a felhasználói input alapján csinál. A bejövő eseményeknél meghatározza, hogy melyik
widgetnek szólnak. A widgetek callbackjai, egészen pontosan az alsó gombé pedig a fent leírt
feladatot valósítja meg: hogy a beállított színkomponensek alapján a kattintás hatására a
téglalapnak új színt ad. Ennek lelke az mehet_gomb_cb()
függvény; a program többi
része a felület elemeinek leprogramozása.
A programból kilépő gomb hatására az eseményhuroknak (a while
ciklus) be kell
fejeződnie. Ez úgy is megoldható, hogy a hozzá tartozó callback egy SDL_QUIT
típusú
eseményt rak az SDL esemény várakozási sorának végére. Így pontosan ugyanaz lesz a hatása, mint
az ablak bezárásának. Persze más megoldást is el lehet képzelni erre.
4 Eszköztárak
A program forráskódja pedig letölthető innen: advent9-widget.c. Az SDL-es program fordításához az extrák menüpont alatt segítség.
Senkinek nem ajánlom, hogy maga kezdjen toolkitet, vagyis eszköztárat kódolni. Ez az írás azért született, hogy bemutassa, egy ilyen nagyjából hogy működik belülről, illetve néhány általános problémára és megoldási lehetőségre rávilágítson. Több platformfüggetlen eszközkészlet is létezik. Ha nincs megkötve, érdemes ezek közül választani, hiszen a platformfüggetlenség nagy előny bármely program számára. Néhány ismertebb:
- GTK+: Linuxból származik, C-ben íródott. Néhány ötletet ehhez a programhoz a GTK+-ból vettem. Működik Windowson, Linuxon és Macen is.
- wxWidgets: C++-os. Érdekessége, hogy minden operációs rendszeren a natív widgeteket használja – vagyis nem maga rajzolgatja ki azokat. Így minden operációs rendszeren a vele írt programok úgy néznek ki, mint a másik ottani programok. A Code::Blocks ezzel készült.
- Qt: a KDE alapja, ez is C++-ban íródott.
Viszont ez a toolkit még elő fog kerülni az adventi naptárban, egy későbbi napon.