Adventi naptár

Czirkos Zoltán · 2021.08.24.

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 említett union és függvényre mutató pointer témakörök ismerete.

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 {
            char felirat[20];           /* a gomb szövege */
        } gomb;
        struct {
            double jelenlegi;           /* erteke; 0.0-1.0 */
        } gorditosav;
        struct {
            unsigned char r, g, b;      /* szin */
        } szinesteglalap;
        struct {
            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(renderer, 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_RenderPresent() 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.