Az SDL multimédiás könyvtár

Czirkos Zoltán, Dobos-Kovács Mihály, Lant Gábor · 2021.08.24.

Grafikus programozás az SDL multimédiás könyvtárral.

Az SDL egy platformfüggetlen multimédiás függvénykönyvtár. A programozók számára egy egységes felületet biztosít a grafikus megjelenítéshez, hangok megszólaltatásához, billentyűk, egér és botkormányok kezeléséhez, miközben az egyes géptípusok, operációs rendszerek különbségeit elfedi. Így az SDL-lel megírt program működik különféle Windows verziókon, de Linuxokon, Mac OS X-en, és még néhány okostelefonon is.

Az alap SDL-ben nincsenek vonal, kör, és egyéb primitívek kirajzolásához függvények. Ahhoz további könyvtárakat kell telepíteni (pl. SDL_gfx). Ezen függvénykönyvtárak tudása viszont már elég nagy. Az SDL_ttf segítségével bármilyen betűtípust használva rajzolhatunk, az SDL_mixer több hang és zene megszólaltatását teszi lehetővé, az SDL_net pedig a hálózatprogramozás ügyes-bajos dolgait rejti egy platformfüggetlen réteg mögé. Az SDL_image nevű kiegészítő sokféle képformátumot (PNG, JPG) ismer; ilyen fájlokat lehet vele betölteni, és a programban kirajzolni.

Ez az írás tartalmaz néhány olyan információt (pl. a többmodulos programokkal kapcsolatban), amelyek csak egy későbbi előadás után lesznek teljesen érthetőek. De addig is használhatóak az instrukciókat pontosan követve. Az SDL telepítéséről egy külön írásban olvashattok.

1. Az első program

SDL: grafikus primitívek

Alább látható az első program. Ez kirajzol néhány kört a képernyőre, utána pedig addig vár, amíg a felhasználó be nem zárja az ablakot a nagy piros X-szel.

Az első lépés az SDL könyvtár inicializálása, ezt az SDL_Init() nevű függvénnyel tehetjük meg. Az SDL alrendszerekből áll (grafika, hang, időzítés stb.), legegyszerűbb azt mondani, hogy inicializáljuk mindegyiket (SDL_INIT_EVERYTHING).

Ezután létrehozunk egy 440×360 képpont méretű ablakot az SDL_CreateWindow() hívással. Ennek meg kell adni az ablak pozícióját és méretét; az utolsó paraméterében pedig a tulajdonságait (átméretezhető-e stb.), ez most egyszerűen 0. A visszatérési értéket eltároljuk az SDL_Window *window változóba, így tudunk majd az ablakra hivatkozni – amúgy lehetne több is.

A harmadik lépés egy megjelenítő (renderer) létrehozása az ablakhoz. Ez végzi majd a rajzolást az ablakba. Azért kell ezt külön lépésként kezelni, mert a megjelenítést végezheti a program is (szoftveres), vagy a grafikus kártya adta lehetőségeket közvetlenül is kihasználhatnánk (hardveres megjelenítés). Erről itt részletesen nem lesz szó, hanem csak a 4. féléves Számítógépes grafika tárgyban. A Prog1 példaprogramjai mindenhol szoftveres megjelenítést (SDL_RENDERER_SOFTWARE) használnak majd, mert azt könnyebb kezelni.

Ez a függvény is adhat NULL pointert, ami azt jelenti, hogy valami probléma történt. Ha nem, akkor viszont indulhat a rajzolás!

#include <SDL.h>
#include <SDL2_gfxPrimitives.h>
#include <math.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    /* SDL inicializálása és ablak megnyitása */
    if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
        SDL_Log("Nem indithato az SDL: %s", SDL_GetError());
        exit(1);
    }
    SDL_Window *window = SDL_CreateWindow("SDL peldaprogram", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 440, 360, 0);
    if (window == NULL) {
        SDL_Log("Nem hozhato letre az ablak: %s", SDL_GetError());
        exit(1);
    }
    SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE);
    if (renderer == NULL) {
        SDL_Log("Nem hozhato letre a megjelenito: %s", SDL_GetError());
        exit(1);
    }
    SDL_RenderClear(renderer);

    /* rajzok */
    int x, y, r;
    r = 50;

    /* karika */
    x = 100;
    y = 100;
    circleRGBA(renderer, x, y, r, 255, 0, 0, 255);
    circleRGBA(renderer, x + r, y, r, 0, 255, 0, 255);
    circleRGBA(renderer, x + r * cos(3.1415 / 3), y - r * sin(3.1415 / 3), r, 0, 0, 255, 255);

    /* antialias karika */
    x = 280;
    y = 100;
    aacircleRGBA(renderer, x, y, r, 255, 0, 0, 255);
    aacircleRGBA(renderer, x + r, y, r, 0, 255, 0, 255);
    aacircleRGBA(renderer, x + r * cos(3.1415 / 3), y - r * sin(3.1415 / 3), r, 0, 0, 255, 255);

    /* kitoltott kor */
    x = 100;
    y = 280;
    filledCircleRGBA(renderer, x, y, r, 255, 0, 0, 255);
    filledCircleRGBA(renderer, x + r, y, r, 0, 255, 0, 255);
    filledCircleRGBA(renderer, x + r * cos(3.1415 / 3), y - r * sin(3.1415 / 3), r, 0, 0, 255, 255);

    /* attetszo kor */
    x = 280;
    y = 280;
    filledCircleRGBA(renderer, x, y, r, 255, 0, 0, 96);
    filledCircleRGBA(renderer, x + r, y, r, 0, 255, 0, 96);
    filledCircleRGBA(renderer, x + r * cos(3.1415 / 3), y - r * sin(3.1415 / 3), r, 0, 0, 255, 96);

    /* szoveg */
    stringRGBA(renderer, 110, 350, "Kilepeshez: piros x az ablakon", 255, 255, 255, 255);

    /* az elvegzett rajzolasok a kepernyore */
    SDL_RenderPresent(renderer);

    /* varunk a kilepesre */
    SDL_Event ev;
    while (SDL_WaitEvent(&ev) && ev.type != SDL_QUIT) {
        /* SDL_RenderPresent(renderer); - MacOS Mojave esetén */
    }

    /* ablak bezarasa */
    SDL_Quit();

    return 0;
}

A legalsó ciklusban található egy megjegyzés – az egy SDL hiba miatt van, ami macOS Mojave esetén jöhet elő. Lásd lentebb.

A program köröket rajzol, négyféleképpen. Az első három körnél egyszerűen kiszínezi azokat a képpontokat (pixel), amelyek a körívre esnek. A második háromnál ennél okosabb. Ahol a körív nem pont a képpontra esik, ott a szomszédos képpontok között színátmenetet képez. Ezt az eljárást úgy nevezik, hogy antialiasing. Így a rajz szebb, a körív nem annyira recegős.

A pozíció és a méret megadása után következik mindegyik függvénynél a szín megadása. Ezek három komponensből állnak: vörös, zöld és kék, mindegyik 0-tól 255-ig. A 255, 0, 0 jelenti a vöröset, 255, 255, 255 pedig a teljesen fehéret. A legutolsó paraméter az átlátszatlanságot adja meg, amely ugyancsak egy 0 és 255 közötti érték. 0 jelenti a teljesen átlátszót, 255 pedig a teljesen átlátszatlant. Ez látszik a jobb alsó sarokban, ahol a köröknél az érték 255 helyett csak 96. Így azok színei keverednek.

Miután elvégeztük az összes rajzolást, meg kell hívni az SDL_RenderPresent() függvényt, hogy az elkészült rajzot megjelenítsük. A rajzolások először csak a memóriában történtek, és igazából a hívás hatására kerül ki minden az ablakba. Ez azért előnyös, mert így a felhasználó nem fogja látni, ahogy egyesével jelennek meg az elemek, hanem csak a végeredményt – animációnál ez fontos lesz. A további rajzolásokkal a meglévő képet módosítjuk; az eredmény pedig egy újabb SDL_RenderPresent() hatására jelenik meg.

Az SDL_gfx függvénykönyvtár néhány rajzeleme (grafikus primitíve):

  • pixelRGBA(kép, x, y, r, g, b, a) – képpont rajzolása.
  • lineRGBA(kép, x1, y1, x2, y2, r, g, b, a) – szakasz.
  • thickLineRGBA(kép, x1, y1, x2, y2, v, r, g, b, a) – vastag szakasz.
  • rectangleRGBA(kép, x1, y1, x2, y2, r, g, b, a) – téglalap.
  • boxRGBA(kép, x1, y1, x2, y2, r, g, b, a) – kitöltött téglalap.
  • circleRGBA(kép, x1, y1, R, r, g, b, a) – kör.
  • trigonRGBA(kép, x1, y1, x2, y2, x3, y3, r, g, b, a) – háromszög.
  • filledTrigonRGBA(kép, x1, y1, x2, y2, x3, y3, r, g, b, a) – kitöltött háromszög.
  • stringRGBA(kép, x, y, szöveg, r, g, b, a) – szöveg.

A vonalas rajzokat (szakasz, kör, háromszög stb.) készítő függvényeknek mind van aa-val kezdődő párjuk is. Ezen felül minden függvénynek van egy nem RGBA-ra, hanem Color-ra végződő nevű párja: az utóbbiak a négy, színt megadó paraméter helyett csak egyetlen egyet várnak. Ez az egyetlen Uint32 típusú paraméter 0xRRGGBBAA formában tartalmazza a színkomponenseket és az átlátszóság információt. Tehát mind a vörös, zöld, kék komponensnek, mind az átlátszóságnak egyetlen egy bájt jut. Így egy szín egyetlen egy változóban is eltárolható. A 32 biten megadott színkód bitműveletekkel állítható elő (r<<24 | g<<16 | b<<8 | a). Például az alábbi sorok teljesen ekvivalensek, mindegyik félig átlátszó lila kört rajzol:

filledCircleRGBA(screen, 320, 240, 100, 255, 0, 255, 128);
filledCircleRGBA(screen, 320, 240, 100, 0xFF, 0, 0xFF, 0x80);
filledCircleColor(screen, 320, 240, 100, 0xFF00FF80);

Dokumentáció

  • Az SDL függvényeinek dokumentációja elérhető az SDL Wiki oldalán.
  • Az SDL_gfx függvényeről pedig az SDL2_gfx oldalán lehet olvasni.

2. Események, eseményvezérelt programozás

Események kezelése: egér

Az egyszerű, konzolos programok lineárisan működnek: a printf()-fel mondhatunk valamit a felhasználónak, a scanf()-fel pedig kérdezhetünk tőle valamit. Nem gond az, hogy a scanf() megakasztja a programot, mert amíg nincs meg a bemenő adat, addig úgysem tudna továbbhaladni a program. Egy játéknál, meg általában a grafikus programoknál ez nincs így. A programnak itt egyszerre több bemenete van: a billentyűzetre és az egérre is reagálnia kell, arról nem is beszélve, hogy ha a felhasználó épp nem nyúl semelyikhez, akkor is folytatódnia kell a képernyőn látható eseményeknek. Nem akadhat meg a játék attól, hogy éppen nem nyomtuk meg egyik gombot sem!

Ezért találták ki az eseményvezérelt programozást. Az SDL a programhoz beérkező eseményeket összegyűjti (billentyűzet, egérmozdulatok, időzítések, ablak bezárása), és azokat keletkezésük sorrendjében adja nekünk. Ezt a programnak egy eseményhurokban (event loop) kell feldolgoznia, amely nagyon egyszerű:

SDL_Event event;

while (fut_a_program) {
    SDL_WaitEvent(&event);  /* várunk a következő eseményre */

    switch (event.type) {   /* esemény típusa szerinti esetszétválasztás */

        ...                 /* esemény feldolgozása */

    }
}

Az SDL_WaitEvent() függvény addig vár, amíg meg nem történik a következő esemény; amint az bekövetkezik, akkor az adatait beteszi az event nevű, SDL_Event típusú struktúrába (azért veszi át cím szerint, hogy ezt meg tudja tenni). A várakozás után az eseményt feldolgozhatjuk, annak típusa szerint:

  • case SDL_QUIT: kilépés, a felhasználó az ablak bezárása ×-re kattintott; break;
  • case SDL_MOUSEMOTION: egérmozdulat; break;
  • case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: egérgomb kattintás és elengedés; break;
  • case SDL_KEYDOWN: case SDL_KEYUP: billentyűzet események; break;

Az event struktúra az esemény típusától függően további információkat tartalmaz. Egérmozgás esetén az event.motion struktúrát tölti ki az SDL_WaitEvent() a koordinátákkal: event.motion.x a vízszintes, event.motion.y a függőleges koordináta. Kattintásnál az event.button struktúra adattagjai vesznek fel értékeket: az event.button.button adattag mutatja, hogy melyik gombról van szó SDL_BUTTON_LEFT, SDL_BUTTON_MIDDLE, SDL_BUTTON_RIGHT.

Az alábbi C programban rajzolni lehet az egérrel. A működést a kód közepén lévő eseményhurok irányítja. A bal gombbal lehet rajzolni, a jobb gombbal pedig törölni az ablak tartalmát. Az eseményvezérlés kellemes vonása, hogy a program gyakorlatilag semennyire sem terheli le a számítógépet. Amíg nincs esemény, addig ugyanúgy alszik, ahogyan azt egy scanf()-re várakozás esetén is teszi.

#include <SDL.h>
#include <SDL2_gfxPrimitives.h>
#include <math.h>
#include <stdbool.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    /* SDL inicializálása és ablak megnyitása */
    if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
        SDL_Log("Nem indithato az SDL: %s", SDL_GetError());
        exit(1);
    }
    SDL_Window *window = SDL_CreateWindow("SDL peldaprogram", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 440, 360, 0);
    if (window == NULL) {
        SDL_Log("Nem hozhato letre az ablak: %s", SDL_GetError());
        exit(1);
    }
    SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE);
    if (renderer == NULL) {
        SDL_Log("Nem hozhato letre a megjelenito: %s", SDL_GetError());
        exit(1);
    }
    SDL_RenderClear(renderer);
    
    /* az esemenyvezerelt hurok */
    bool quit = false;
    bool click = false;
    int elozox = 0;
    int elozoy = 0;
    while (!quit) {
        SDL_Event event;
        SDL_WaitEvent(&event);

        bool rajzoltam = false;

        switch (event.type) {
            /* eger kattintas */
            case SDL_MOUSEBUTTONDOWN:
                if (event.button.button == SDL_BUTTON_LEFT) {
                    click = true;
                    elozox = event.button.x;
                    elozoy = event.button.y;
                }
                else if (event.button.button == SDL_BUTTON_RIGHT) {
                    boxColor(renderer, 0, 0, 439, 359, 0x000000FF);
                    rajzoltam = true;
                }
                break;
            /* egergomb elengedese */
            case SDL_MOUSEBUTTONUP:
                if (event.button.button == SDL_BUTTON_LEFT) {
                    click = false;
                }
                break;
            /* eger mozdulat */
            case SDL_MOUSEMOTION:
                if (click) {
                    aalineColor(renderer, elozox, elozoy,
                                event.motion.x, event.motion.y, 0xFFFFFFFF);
                    rajzoltam = true;
                }
                /* a kovetkezo mozdulat esemenyhez */
                elozox = event.motion.x;
                elozoy = event.motion.y;
                break;
            /* ablak bezarasa */
            case SDL_QUIT:
                quit = true;
                break;
        }

        if (rajzoltam)
            SDL_RenderPresent(renderer);
    }

    SDL_Quit();

    return 0;
}

Maga az eseményhurok ennél a progamnál tulajdonképpen egy állapotgép. Na nem azért, mert switch van benne (az csak az események típusának megállapításához kell), hanem mert az egyes események jelentése eltérő attól függően, hogy mik történtek a múltban. Például az egérmozdulatnál csak akkor rajzolunk, ha előzőleg egy kattintás eseményt már feldolgoztunk. Minden mozdulatnál megjegyezzük a koordinátákat, hogy a legközelebbi ugyanilyen eseménynél tudjuk, honnan hova kell húzni a vonalat.

SDL_WaitEvent vagy SDL_PollEvent?

Eseménykezelésre az SDL-ben két függvény is van, a WaitEvent és a PollEvent. A kettő között a különbség az, hogy a WaitEvent megvárja, amíg történik valami, és addig nem tér vissza, amíg nincs feldolgozandó esemény. A PollEvent azonnal visszatér mindig, akkor is, ha épp nincs teendő. A PollEvent függvényt a legritkább esetben használjuk, ciklusban különösen nem, mert 100%-ra járatnánk a gépet. A legtöbbször a WaitEvent-re van szükség. Ha a várakozás idejét limitálni kell, akkor időzítőket érdemes használni – lásd a következő részt.

3. Az időzítők használata

Pattogó labda

Előbb arról volt szó, hogy a program futásának nem szabad megszakadnia amiatt, mert eseményre vár – és aztán jött egy program forráskódja, amely nem csinál semmit, azaz alszik az események között. Hogy fog akkor a játék tovább futni, amíg a felhasználó nem nyúl se a billentyűzethez, se az egérhez? Nagyon egyszerű: létre kell hozni egy időzítőt, amely adott időközönként generál egy eseményt. Ha létrejön az esemény, annak hatására fel fog ébredni az eseményhurok – de fel fog ébredni a billentyűzet vagy az egér hatására is.

Időzítőt létrehozni az SDL_AddTimer() függvénnyel lehet. Ennek paraméterei a következők: 1) mennyi idő múlva hívódjon meg (ezredmásodperc), 2) melyik függvény hívódjon meg, 3) egy tetszőleges mutató, amit paraméterként meg fog kapni a függvény. (Ha ez nem kell semmire, akkor lehet NULL.) A függvény visszatérési értéke egy SDL_TimerID típusú azonosító, amivel hivatkozhatunk az időzítőre (pl. az SDL_RemoveTimer()-nek paraméterként adva letilthatjuk azt.) A hívás tehát így néz ki:

id = SDL_AddTimer(20, idozit, NULL);

A paraméterként adott függvény fejléce kötött, ilyen kell legyen:

Uint32 idozit(Uint32 ms, void *param);

Vagyis az SDL időzítője által meghívott függvény megkapja paraméterként azt, hogy milyen időközökre lett beállítva, és a tetszőleges felhasználói paramétert. Visszatérési értéke pedig egy egész szám, hogy legközelebb hány ezredmásodperc múlva hívódjon meg. Legegyszerűbb, ha egy return ms; sorral fejezzük be a függvényt, amiben általában amúgy sincs más, csak egy felhasználói típusú esemény létrehozása, és beillesztése a várakozási sorba:

Uint32 idozit(Uint32 ms, void *param) {
    SDL_Event ev;
    ev.type = SDL_USEREVENT;
    SDL_PushEvent(&ev);
    return ms;   /* ujabb varakozas */
}

Az eseménykezelő hurkot tehát ki kell egészíteni az SDL_USEREVENT típusú esemény(ünk) feldolgozásával. A labdát pattogtató program így néz ki:

#include <stdlib.h>
#include <stdbool.h>
#include <SDL.h>
#include <SDL2_gfxPrimitives.h>


/* ablak megnyitasa */
void sdl_init(int szeles, int magas, SDL_Window **pwindow, SDL_Renderer **prenderer) {
    if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
        SDL_Log("Nem indithato az SDL: %s", SDL_GetError());
        exit(1);
    }
    SDL_Window *window = SDL_CreateWindow("SDL peldaprogram", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, szeles, magas, 0);
    if (window == NULL) {
        SDL_Log("Nem hozhato letre az ablak: %s", SDL_GetError());
        exit(1);
    }
    SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE);
    if (renderer == NULL) {
        SDL_Log("Nem hozhato letre a megjelenito: %s", SDL_GetError());
        exit(1);
    }
    SDL_RenderClear(renderer);

    *pwindow = window;
    *prenderer = renderer;
}


/* ez a fuggveny hivodik meg az idozito altal.
 * betesz a feldolgozando esemenyek koze (push) egy felhasznaloi esemenyt */
Uint32 idozit(Uint32 ms, void *param) {
    SDL_Event ev;
    ev.type = SDL_USEREVENT;
    SDL_PushEvent(&ev);
    return ms;   /* ujabb varakozas */
}


int main(int argc, char *argv[]) {
    enum { ABLAK=360 };
    enum { GOLYO_R=10 };
    typedef struct Golyo {
        int x, y;
        int vx, vy;
    } Golyo;
    
    
    SDL_Window *window;
    SDL_Renderer *renderer;
    sdl_init(ABLAK, ABLAK, &window, &renderer);

    /* idozito hozzaadasa: 20 ms; 1000 ms / 20 ms -> 50 fps */
    SDL_TimerID id = SDL_AddTimer(20, idozit, NULL);

    /* animaciohoz */
    Golyo g;
    g.x = ABLAK/2;
    g.y = ABLAK/3;
    g.vx = 3;
    g.vy = 2;

    /* szokasos esemenyhurok */
    bool quit = false;
    while (!quit) {
        SDL_Event event;
        SDL_WaitEvent(&event);

        switch (event.type) {
            /* felhasznaloi esemeny: ilyeneket general az idozito fuggveny */
            case SDL_USEREVENT:
                /* kitoroljuk az elozo poziciojabol (nagyjabol) */
                filledCircleRGBA(renderer, g.x, g.y, GOLYO_R, 0x20, 0x20, 0x40, 0xFF);
                /* kiszamitjuk az uj helyet */
                g.x += g.vx;
                g.y += g.vy;
                /* visszapattanás */
                if (g.x < GOLYO_R || g.x > ABLAK-GOLYO_R)
                    g.vx *= -1;
                if (g.y < GOLYO_R || g.y > ABLAK-GOLYO_R)
                    g.vy *= -1;
                /* ujra kirajzolas, es mehet a kepernyore */
                filledCircleRGBA(renderer, g.x, g.y, GOLYO_R, 0x80, 0x80, 0xFF, 0xFF);
                SDL_RenderPresent(renderer);
                break;

            case SDL_QUIT:
                quit = true;
                break;
        }
    }
    /* idozito torlese */
    SDL_RemoveTimer(id);

    SDL_Quit();

    return 0;
}

Itt nagyon fontos, hogy csak a kép teljes megrajzolása után hívjuk meg az SDL_RenderPresent()-et. Ha a törlés után is meghívnánk, akkor az animáció villódzna (flicker), így viszont szép, folytonos a megjelenítés. Törölni viszont kell, hiszen mindig az előzőleg megrajzolt képet módosítjuk.

Nem csak egy, hanem akár egyszerre több időzítőt is létrehozhatunk. Hogy ezeket meg lehessen különböztetni, az általuk generált eseményeknek érdemes külön azonosítót adni. Az események típusa, az event.type adattag nem felsorolt típus, hanem egy egyszerű egész szám. Az SDL dokumentációja pedig azt mondja, hogy az SDL_USEREVENT konstanstól (ez egy felsorolt típusú érték) fölfelé bármilyen saját eseményt definiálhatunk. Ezért ezeket használhatjuk akár úgy is, hogy az egyik időzítőnk SDL_USEREVENT+1, a másik SDL_USEREVENT+2 stb. típusú eseményeket generál.

4. Képfájlok beolvasása

Sakktábla

Ez nagyon egyszerű feladat: az SDL_image nevű függvénykönyvtárnak van egy IMG_LoadTexture() nevű függvénye. Ennek paramétere a megjelenítő mellett a betöltendő kép, ami elég sokféle formátumú lehet (az SDL_image dokumentációja szerint BMP, GIF, JPEG, LBM, PCX, PNG, PNM, TGA, TIFF, WEBP, XCF, XPM és XV). A függvény visszatérési értéke egy SDL_Texture*, vagyis egy mutató a betöltött képre. Ezzel tudunk később hivatkozni rá, mert bent maradt a gép memóriájában. Ha már nincs rá szükség, fel kell azt szabadítani, az SDL_DestroyTexture() függvénnyel. Ha ezt nem tesszük meg, a betöltött képek miatt a programunk egyre több memóriát foglal. Úgyhogy ez fontos!

A betöltött képpel sincsen nehéz dolgunk: az SDL_RenderCopy() függvény tud képet megjeleníteni. Ennek a paraméterei: megjelenítő, kép, forrás téglalap, cél téglalap. Vagyis nem csak a teljes képet tudja másolni, hanem annak csak egy részletét is, a cél kép tetszőleges pozíciójára. A pozíciókat és a méreteket SDL_Rect típusú struktúrákkal kell megadni; ezekre a függvény pointereket vesz át:

SDL_Rect forrasterulet = { forras_x, forras_y, forras_szelesseg, forras_magassag };
SDL_Rect celterulet    = { cel_x, cel_y, cel_szelesseg, cel_magassag };

SDL_BlitSurface(megjelenito, kep, &forrasterulet, &celterulet);

Ha a teljes forrás képet szeretnénk másolni, akkor a forrás téglalapra mutató pointer lehet NULL, de a cél méreteit ilyenkor is meg kell adni. Ha le szeretnénk kérdezni egy betöltött kép méretét, azt a SDL_QueryTexture(source, NULL, NULL, &w, &h); hívással tehetjük meg.

Sakk figurák
pieces.png (jobb klikk a letöltéshez)

Az alábbi programban kihasználjuk azt, hogy a kép egy részét is lehet másolni. A program tartalmaz egy felsorolt típust, amely a fenti képen látható figurák sorrendjében nevezi meg azokat. A babu_rajzol() függvényen belül kiszámítódnak a fenti képen belüli koordináták (melyik figuráról van szó). Ez a kép egyébként átlátszó képpontokat is tartalmaz, az SDL ezt is támogatja. A fájlt le kell tölteni, és a futtatható (.exe) mellé tenni pieces.png néven.

Néhány fontos dolog:

  • A betöltött képet, amíg nem dobtuk el az SDL_DestroyTexture() függvénnyel, akárhányszor használhatjuk.
  • A képeket ide-oda adogathatjuk a programban: az SDL_Texture* típusú képhivatkozás akár függvény paramétere vagy visszatérési értéke is lehet.
  • Az SDL_RenderCopyEx() függvény forgatni is tud.
  • Az SDL_image teljes dokumentációja itt érhető el: SDL_image.
#include <SDL.h>
#include <SDL_image.h>
#include <SDL2_gfxPrimitives.h>
#include <stdlib.h>


/* mezon allo figura. ugyanolyan sorrendben vannak, mint a kepen,
 * igy a kapott egesz szamok megegyeznek a png-beli indexekkel */
typedef enum Babu {
    VKiraly, VVezer, VBastya, VFuto, VHuszar, VGyalog,
    SKiraly, SVezer, SSastya, SFuto, SHuszar, SGyalog
} Babu;

/* a pieces.png fajlban levo figurak merete */
enum { MERET = 52 };

/* kirajzol egy babut; a forras a betoltott png, a cel nevu kepre rajzol.
 * melyik babut, milyen koordinatakra: melyik, x, y. */
void babu_rajzol(SDL_Renderer *renderer, SDL_Texture *babuk, Babu melyik, int x, int y) {
    /* a forras kepbol ezekrol a koordinatakrol, ilyen meretu reszletet masolunk. */
    SDL_Rect src = { (melyik % 6) * 62 + 10, (melyik / 6) * 60 + 10, MERET, MERET };
    /* a cel kepre, ezekre a koordinatakra masoljuk */
    SDL_Rect dest = { x, y, MERET, MERET };
    /* kepreszlet masolasa */
    SDL_RenderCopy(renderer, babuk, &src, &dest);
}


/* ablak megnyitasa */
void sdl_init(int szeles, int magas, SDL_Window **pwindow, SDL_Renderer **prenderer) {
    if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
        SDL_Log("Nem indithato az SDL: %s", SDL_GetError());
        exit(1);
    }
    SDL_Window *window = SDL_CreateWindow("SDL peldaprogram", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, szeles, magas, 0);
    if (window == NULL) {
        SDL_Log("Nem hozhato letre az ablak: %s", SDL_GetError());
        exit(1);
    }
    SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE);
    if (renderer == NULL) {
        SDL_Log("Nem hozhato letre a megjelenito: %s", SDL_GetError());
        exit(1);
    }
    SDL_RenderClear(renderer);

    *pwindow = window;
    *prenderer = renderer;
}


int main(int argc, char *argv[]) {
    SDL_Window *window;
    SDL_Renderer *renderer;
    sdl_init(320, 200, &window, &renderer);

    /* kep betoltese */
    SDL_Texture *babuk = IMG_LoadTexture(renderer, "pieces.png");
    if (babuk == NULL) {
        SDL_Log("Nem nyithato meg a kepfajl: %s", IMG_GetError());
        exit(1);
    }

    /* rajz keszitese */
    boxRGBA(renderer, 0, 0, 319, 199, 0x90, 0xE0, 0x90, 0xFF);
    babu_rajzol(renderer, babuk, VKiraly, 82, 74);
    babu_rajzol(renderer, babuk, SGyalog, 82+MERET, 74);
    babu_rajzol(renderer, babuk, VHuszar, 82+2*MERET, 74);
    SDL_RenderPresent(renderer);

    /* nincs mar ra szukseg: felszabaditjuk a memoriat */
    SDL_DestroyTexture(babuk);

    /* szokasos varakozas */
    SDL_Event event;
    while (SDL_WaitEvent(&event) && event.type != SDL_QUIT) {
    }

    SDL_Quit();
    return 0;
}

5. Szövegek megjelenítése

Az SDL_gfx stringRGBA() függvénye meg tud jeleníteni szövegeket (lásd az első példaprogramot), de sajnos a használt betűk nagyon kicsik, és nem ismeri a magyar ékezetes betűket sem (árvíztűrő tükörfúrógép). SDL_TTF függvénykönyvtár megoldja mindkét problémát. (A dokumentációja itt érhető el.) Ez tetszőleges True Type betűtípust be tud olvasni (Arial, Trebuchet stb.), és helyesen tudja kezelni az ékezetes betűket is.

Bár az angol ábécé betűit kódoló ASCII szabvány gyakorlatilag mára egyeduralkodóvá vált a világon, az ékezetes betűket és egyéb karaktereket kódoló szabványokról ez sajnos nem mondható el. Több kódtáblát, azaz betű→szám táblázatot is használnak elterjedten a világon; az egységes Unicode kódolás még nem szorította ki a többit. (Ezekről bővebben a Rémtörténet a karakterkódolásokról írásban olvashatsz.) A többféle kódtábla miatt az SDL_TTF-ben minden szövegrajzoló függvénynek három változata van: 1) a Latin-1 kódolású szöveget, 2) a Unicode kódolású szöveget, és 3) az UTF-8 kódolású szöveget váró függvény.

A használat menete a következő. A használandó betűtípus fájlt először meg kell nyitni az TTF_OpenFont() függvényhívással. Ilyenkor meg kell adni a betűk méretét is. A függvény visszatérési értéke egy TTF_Font típusú mutató, amellyel hivatkozni lehet a betűtípusra (ilyenből több is lehet), és amelyet a TTF_CloseFont() függvénynek a program végén oda kell adni, hogy felszabadítsa a memóriaterületet.

LiberationSerif-Regular.ttf (klikk a letöltéshez)

Minden alkalommal, amikor egy szöveget meg kell rajzolni, azt valamelyik TTF_Render…() függvénnyel kell tenni, függően a rajzolás kívánt minőségétől és a karakterkódolástól. A függvények visszatérnek egy SDL_Surface* típusú mutatóval, mivel a rajzolások kimenete egy kép, amelyben meg van rajzolva a felirat. Ez már átadható a megjelenítőnek, SDL_Texture objektummá alakítható a SDL_CreateTextureFromSurface() függvénnyel. Ha már nincs rá szükség, akkor pedig fel kell szabadítani a hozzá tartozó memóriaterületet az SDL_FreeSurface() hívással. (Természetesen ha sok, különféle felirat van, akkor ehhez a műveletsorhoz érdemes saját függvényeket írni. A programozásban nem copy-pastelünk!)

A rajzolások módja a következő lehet, bár a kép mindent elmond:

  • TTF_Render..._Solid: gyors, de a betűk széle recegős.
  • TTF_Render..._Shaded: nem recegős. A háttér egy megadott szín.
  • TTF_Render..._Blended: nem recegős. A háttér átlátszó.

Betűtípusokat a Windows C:\Windows\Fonts mappájában, vagy a Linux /usr/share/fonts/truetype mappájában lehet találni. Meg a neten egy csomó helyen, csak sajnos az ingyenes betűtípusokból hiányozni szokott a hosszú ő és az ű betű. A lenti program a Liberation Serif nevű betűtípust használja. A linkre kattintva letölthető fájlt a Code::Blocks projekt mappájába kell tenni.

#include <SDL.h>
#include <SDL_ttf.h>
#include <SDL2_gfxPrimitives.h>
#include <stdlib.h>


void sdl_init(int szeles, int magas, SDL_Window **pwindow, SDL_Renderer **prenderer) {
    if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
        SDL_Log("Nem indithato az SDL: %s", SDL_GetError());
        exit(1);
    }
    SDL_Window *window = SDL_CreateWindow("SDL peldaprogram", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, szeles, magas, 0);
    if (window == NULL) {
        SDL_Log("Nem hozhato letre az ablak: %s", SDL_GetError());
        exit(1);
    }
    SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE);
    if (renderer == NULL) {
        SDL_Log("Nem hozhato letre a megjelenito: %s", SDL_GetError());
        exit(1);
    }

    *pwindow = window;
    *prenderer = renderer;
}


int main(int argc, char *argv[]) {
    SDL_Window *window;
    SDL_Renderer *renderer;
    sdl_init(480, 200, &window, &renderer);

    SDL_Color feher = {255, 255, 255}, piros = {255, 0, 0};

    /* hatter */
    for (int i = 0; i < 500; ++i)
        filledCircleRGBA(renderer, rand() % 480, rand() % 200,
                         10 + rand() % 5, rand() % 256, rand() % 256, rand() % 256, 64);

    /* betutipus betoltese, 32 pont magassaggal */
    TTF_Init();
    TTF_Font *font = TTF_OpenFont("LiberationSerif-Regular.ttf", 32);
    if (!font) {
        SDL_Log("Nem sikerult megnyitni a fontot! %s\n", TTF_GetError());
        exit(1);
    }

    /* felirat megrajzolasa, kulonfele verziokban */
    SDL_Surface *felirat;
    SDL_Texture *felirat_t;
    SDL_Rect hova = { 0, 0, 0, 0 };

    /* ha sajat kodban hasznalod, csinalj belole fuggvenyt! */
    felirat = TTF_RenderUTF8_Solid(font, "TTF_RenderUTF8_Solid()", feher);
    felirat_t = SDL_CreateTextureFromSurface(renderer, felirat);
    hova.x = (480 - felirat->w) / 2;
    hova.y = 20;
    hova.w = felirat->w;
    hova.h = felirat->h;
    SDL_RenderCopy(renderer, felirat_t, NULL, &hova);
    SDL_FreeSurface(felirat);
    SDL_DestroyTexture(felirat_t);

    /* ha sajat kodban hasznalod, csinalj belole fuggvenyt! */
    felirat = TTF_RenderUTF8_Shaded(font, "TTF_RenderUTF8_Shaded()", feher, piros);
    felirat_t = SDL_CreateTextureFromSurface(renderer, felirat);
    hova.x = (480 - felirat->w) / 2;
    hova.y = 60;
    hova.w = felirat->w;
    hova.h = felirat->h;
    SDL_RenderCopy(renderer, felirat_t, NULL, &hova);
    SDL_FreeSurface(felirat);
    SDL_DestroyTexture(felirat_t);

    /* ha sajat kodban hasznalod, csinalj belole fuggvenyt! */
    felirat = TTF_RenderUTF8_Blended(font, "TTF_RenderUTF8_Blended()", feher);
    felirat_t = SDL_CreateTextureFromSurface(renderer, felirat);
    hova.x = (480 - felirat->w) / 2;
    hova.y = 100;
    hova.w = felirat->w;
    hova.h = felirat->h;
    SDL_RenderCopy(renderer, felirat_t, NULL, &hova);
    SDL_FreeSurface(felirat);
    SDL_DestroyTexture(felirat_t);

    /* tudja az ekezeteket */
    /* ez az utf8 szoveg azert nez ki ilyen rosszul, mert szinte csak ekezetes betu van benne.
     * amugy a code::blocks-ban utf8 forrasfajlt kell beallitani, es akkor irhato kozvetlenul
     * ekezetes betu ide. */
    felirat = TTF_RenderUTF8_Blended(font, "\xC3\xA1rv\xC3\xADzt\xC5\xB1r\xC5\x91 "
                                           "t\xC3\xBCk\xC3\xB6rf\xC3\xBAr\xC3\xB3g\xC3\xA9p "
                                           "\xE2\x98\xBA \xE2\x82\xAC", feher);
    felirat_t = SDL_CreateTextureFromSurface(renderer, felirat);
    hova.x = (480 - felirat->w) / 2;
    hova.y = 140;
    hova.w = felirat->w;
    hova.h = felirat->h;
    SDL_RenderCopy(renderer, felirat_t, NULL, &hova);
    SDL_FreeSurface(felirat);
    SDL_DestroyTexture(felirat_t);

    SDL_RenderPresent(renderer);

    /* nem kell tobbe */
    TTF_CloseFont(font);

    SDL_Event event;
    while (SDL_WaitEvent(&event) && event.type != SDL_QUIT) {
    }

    SDL_Quit();

    return 0;
}
Fontos!

A TTF_Init() függvényhívást elég egyszer megtenni a program legelején. A TTF_OpenFont() által beolvasott betűtípus pedig akárhányszor használható – annyi szöveget írhatunk ki vele, amennyit csak szeretnénk. Akár többféle betűtípus is lehet betöltve egyszerre. Ha valamelyikre nincs már szükségünk, csak akkor kell felszabadítani a hozzá tartozó memóriaterületet egy TTF_CloseFont() hívással. Ha többször is szeretnénk használni a betűtípust, nem szabad mindig törölni és újra betölteni azt, mivel nagyon időigényes az a művelet! A betöltött betűtípusokat hivatkozó TTF_Font* típusú mutatók a képekhez hasonlóan függvényeknek is átadhatjuk.

6. A billentyűzet kezelése

A billentyűzet kezelése SDL-ben nem nagy ördöngősség: SDL_KEYDOWN eseményt kapunk egy billentyű megnyomásánál, SDL_KEYUP eseményt az elengedésénél. Az esemény adatait tároló strktúrában az alábbi adattagok érhetőek el:

  • event.key.keysym.sym: a lenyomott billentyű azonosítója, ebből a táblázatból.
  • event.key.keysym.mod: módosító billentyűk (shift, ctrl stb.) ebből a táblázatból. Mivel egyszerre több módosító is le lehet nyomva, egy bitenkénti ÉS & művelettel kell megvizsgálni azt, amelyik érdekes. A módosítók lenyomásakor külön esemény is érkezik.

Játékokban, ahol arra vagyunk kíváncsiak, hogy nyomva van-e tartva egy billentyű, nekünk kell külön megjegyezni azt. Ez egyszerűen megoldható egy logikai típusú változóval, amelynek értékét SDL_KEYDOWN esemény esetén igazra, SDL_KEYUP esemény esetén pedig hamisra állítjuk. Az alábbi példaprogramban pontosan ez történik. A balra és jobbra nyilakat nyomva tartva lehet változtatni a háromszögek színét. Az Esc gomb pedig az ablak bezárásához hasonlóan kilép a programból.

billentyűk állapotát mutató program
#include <SDL.h>
#include <SDL2_gfxPrimitives.h>
#include <stdbool.h>
#include <stdlib.h>
 
 
/* ablak megnyitasa */
void sdl_init(int szeles, int magas, SDL_Window **pwindow, SDL_Renderer **prenderer) {
    if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
        SDL_Log("Nem indithato az SDL: %s", SDL_GetError());
        exit(1);
    }
    SDL_Window *window = SDL_CreateWindow("SDL peldaprogram", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, szeles, magas, 0);
    if (window == NULL) {
        SDL_Log("Nem hozhato letre az ablak: %s", SDL_GetError());
        exit(1);
    }
    SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE);
    if (renderer == NULL) {
        SDL_Log("Nem hozhato letre a megjelenito: %s", SDL_GetError());
        exit(1);
    }
    SDL_RenderClear(renderer);
    
    *pwindow = window;
    *prenderer = renderer;
}
 
 
int main(int argc, char *argv[]) {
    SDL_Window *window;
    SDL_Renderer *renderer;
    sdl_init(350, 200, &window, &renderer);
 
    bool quit = false;
    bool left = false;
    bool right = false;
    bool rajz = true;
    while (!quit) {
        if (rajz) {
            if (left)
                filledTrigonRGBA(renderer, 50, 100, 150, 50, 150, 150, 0x00, 0xC0, 0x00, 0xFF);
            else
                filledTrigonRGBA(renderer, 50, 100, 150, 50, 150, 150, 0xFF, 0x00, 0x00, 0xFF);
            if (right)
                filledTrigonRGBA(renderer, 300, 100, 200, 50, 200, 150, 0x00, 0xC0, 0x00, 0xFF);
            else
                filledTrigonRGBA(renderer, 300, 100, 200, 50, 200, 150, 0xFF, 0x00, 0x00, 0xFF);
            SDL_RenderPresent(renderer);
            rajz = false;
        }

        SDL_Event event;
        SDL_WaitEvent(&event);
 
        switch (event.type) {
            /* felhasznaloi esemeny: ilyeneket general az idozito fuggveny */
            case SDL_KEYUP:
                switch (event.key.keysym.sym) {
                    case SDLK_LEFT: left = false; rajz = true; break;
                    case SDLK_RIGHT: right = false; rajz = true; break;
                    case SDLK_ESCAPE: quit = true; break;
                }
                break;
                
            case SDL_KEYDOWN:
                switch (event.key.keysym.sym) {
                    case SDLK_LEFT: left = true; rajz = true; break;
                    case SDLK_RIGHT: right = true; rajz = true; break;
                }
                break;
 
            case SDL_QUIT:
                quit = true;
                break;
        }
    }
     
    SDL_Quit();
    return 0;
}

7. Szöveg bevitele

A szövegbevitel kezelése nem olyan egyszerű feladat, mint amilyennek hangzik. A szöveg szerkesztését is kezelni kell (pl. Backspace gomb), de előfordulhat az is, hogy a szimpla billentyűzetnél bonyolultabb szövegbeviteli eszközt használ a felhasználó. Bár a magyar nyelvre ez nem jellemző, távolkeleti nyelveknél a billentyűzet használata mellett felugró ablakból kellhet kiválasztani az egyes karaktereket.

Az SDL szerencsére ezeket az eszközöket is kezeli, és a végeredményként előálló szövegdarabokat SDL_TEXTINPUT típusú események paramétereiként megkapjuk. A törlést és a bevitelt nyugtázó Entert persze ettől függetlenül nekünk kell kezelni.

Az alábbi program egy egyszerű szövegbeviteli mező megvalósítását mutatja. Ha valaki nem szeretne elmélyülni ebben, nem gond, az input_text() függvényt bátran fel lehet használni saját programokban vagy nagy házi feladatban. A paraméterei:

  • char *dest: ide írja a beolvasott szöveget, UTF-8 karakterkódolással.
  • size_t hossz: maximum ennyi bájt hosszú szöveget olvas be.
  • SDL_Rect teglalap: a szövegbeviteli mező helyét és méretét megadó téglalap.
  • SDL_Color hatter: a mező színe.
  • SDL_Color szoveg: a szöveg színe.
  • TTF_Font *font: a betűtípus, amellyel rajzol.
  • SDL_Renderer *renderer: az ablak.

Visszatérési értéke azt mondja meg, sikeres volt-e a beolvasás. A példaprogram ezt a betűtípust használja: LiberationSerif-Regular.ttf (klikk a letöltéshez). A függvény csak szoftveres megjelenítés használata esetén működik helyesen (lásd fent).

#include <SDL.h>
#include <SDL_ttf.h>
#include <SDL2_gfxPrimitives.h>
#include <stdbool.h>
#include <string.h>
#include <stdlib.h>

void sdl_init(int szeles, int magas, const char* tipus, SDL_Window **pwindow, SDL_Renderer **prenderer, TTF_Font **pfont) {
    if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
        SDL_Log("Nem indithato az SDL: %s", SDL_GetError());
        exit(1);
    }
    SDL_Window *window = SDL_CreateWindow("SDL peldaprogram", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, szeles, magas, 0);
    if (window == NULL) {
        SDL_Log("Nem hozhato letre az ablak: %s", SDL_GetError());
        exit(1);
    }
    SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE);
    if (renderer == NULL) {
        SDL_Log("Nem hozhato letre a megjelenito: %s", SDL_GetError());
        exit(1);
    }

    TTF_Init();
    TTF_Font *font = TTF_OpenFont(tipus, 32);
    if (font == NULL) {
        SDL_Log("Nem sikerult megnyitni a fontot! %s\n", TTF_GetError());
        exit(1);
    }

    *pwindow = window;
    *prenderer = renderer;
    *pfont = font;
}

void sdl_close(SDL_Window **pwindow, SDL_Renderer **prenderer, TTF_Font **pfont) {
    SDL_DestroyRenderer(*prenderer);
    *prenderer = NULL;

    SDL_DestroyWindow(*pwindow);
    *pwindow = NULL;

    TTF_CloseFont(*pfont);
    *pfont = NULL;

    SDL_Quit();
}


/* Beolvas egy szoveget a billentyuzetrol.
 * A rajzolashoz hasznalt font es a megjelenito az utolso parameterek.
 * Az elso a tomb, ahova a beolvasott szoveg kerul.
 * A masodik a maximális hossz, ami beolvasható.
 * A visszateresi erteke logikai igaz, ha sikerult a beolvasas. */
bool input_text(char *dest, size_t hossz, SDL_Rect teglalap, SDL_Color hatter, SDL_Color szoveg, TTF_Font *font, SDL_Renderer *renderer) {
    /* Ez tartalmazza az aktualis szerkesztest */
    char composition[SDL_TEXTEDITINGEVENT_TEXT_SIZE];
    composition[0] = '\0';
    /* Ezt a kirajzolas kozben hasznaljuk */
    char textandcomposition[hossz + SDL_TEXTEDITINGEVENT_TEXT_SIZE + 1];
    /* Max hasznalhato szelesseg */
    int maxw = teglalap.w - 2;
    int maxh = teglalap.h - 2;

    dest[0] = '\0';

    bool enter = false;
    bool kilep = false;

    SDL_StartTextInput();
    while (!kilep && !enter) {
        /* doboz kirajzolasa */
        boxRGBA(renderer, teglalap.x, teglalap.y, teglalap.x + teglalap.w - 1, teglalap.y + teglalap.h - 1, hatter.r, hatter.g, hatter.b, 255);
        rectangleRGBA(renderer, teglalap.x, teglalap.y, teglalap.x + teglalap.w - 1, teglalap.y + teglalap.h - 1, szoveg.r, szoveg.g, szoveg.b, 255);
        /* szoveg kirajzolasa */
        int w;
        strcpy(textandcomposition, dest);
        strcat(textandcomposition, composition);
        if (textandcomposition[0] != '\0') {
            SDL_Surface *felirat = TTF_RenderUTF8_Blended(font, textandcomposition, szoveg);
            SDL_Texture *felirat_t = SDL_CreateTextureFromSurface(renderer, felirat);
            SDL_Rect cel = { teglalap.x, teglalap.y, felirat->w < maxw ? felirat->w : maxw, felirat->h < maxh ? felirat->h : maxh };
            SDL_RenderCopy(renderer, felirat_t, NULL, &cel);
            SDL_FreeSurface(felirat);
            SDL_DestroyTexture(felirat_t);
            w = cel.w;
        } else {
            w = 0;
        }
        /* kurzor kirajzolasa */
        if (w < maxw) {
            vlineRGBA(renderer, teglalap.x + w + 2, teglalap.y + 2, teglalap.y + teglalap.h - 3, szoveg.r, szoveg.g, szoveg.b, 192);
        }
        /* megjeleniti a képernyon az eddig rajzoltakat */
        SDL_RenderPresent(renderer);

        SDL_Event event;
        SDL_WaitEvent(&event);
        switch (event.type) {
            /* Kulonleges karakter */
            case SDL_KEYDOWN:
                if (event.key.keysym.sym == SDLK_BACKSPACE) {
                    int textlen = strlen(dest);
                    do {
                        if (textlen == 0) {
                            break;
                        }
                        if ((dest[textlen-1] & 0x80) == 0x00)   {
                            /* Egy bajt */
                            dest[textlen-1] = 0x00;
                            break;
                        }
                        if ((dest[textlen-1] & 0xC0) == 0x80) {
                            /* Bajt, egy tobb-bajtos szekvenciabol */
                            dest[textlen-1] = 0x00;
                            textlen--;
                        }
                        if ((dest[textlen-1] & 0xC0) == 0xC0) {
                            /* Egy tobb-bajtos szekvencia elso bajtja */
                            dest[textlen-1] = 0x00;
                            break;
                        }
                    } while(true);
                }
                if (event.key.keysym.sym == SDLK_RETURN) {
                    enter = true;
                }
                break;

            /* A feldolgozott szoveg bemenete */
            case SDL_TEXTINPUT:
                if (strlen(dest) + strlen(event.text.text) < hossz) {
                    strcat(dest, event.text.text);
                }

                /* Az eddigi szerkesztes torolheto */
                composition[0] = '\0';
                break;

            /* Szoveg szerkesztese */
            case SDL_TEXTEDITING:
                strcpy(composition, event.edit.text);
                break;

            case SDL_QUIT:
                /* visszatesszuk a sorba ezt az eventet, mert
                 * sok mindent nem tudunk vele kezdeni */
                SDL_PushEvent(&event);
                kilep = true;
                break;
        }
    }

    /* igaz jelzi a helyes beolvasast; = ha enter miatt allt meg a ciklus */
    SDL_StopTextInput();
    return enter;
}

#define SZELES 480
#define MAGAS 240
#define FONT "LiberationSerif-Regular.ttf"
#define MAXHOSSZ 100

int main(int argc, char *argv[]) {
    SDL_Window *window;
    SDL_Renderer *renderer;
    TTF_Font *font;
    sdl_init(SZELES, MAGAS, FONT, &window, &renderer, &font);

    SDL_Color feher = {255, 255, 255}, fekete = { 0, 0, 0 };
    SDL_Rect hely;

    char szoveg[100];

    SDL_Event event;
    SDL_Surface *screen = SDL_GetWindowSurface(window);
    SDL_Surface *background = SDL_CreateRGBSurface(0, SZELES, MAGAS, 32, 0, 0, 0, 0);

    /* hatter kirajzolasa, mentese */
    for (int i = 0; i < 500; ++i)
        filledCircleRGBA(renderer, rand() % SZELES, rand() % MAGAS,
                         20 + rand() % 10, rand() % 256, rand() % 256, rand() % 256, 64);
    SDL_BlitSurface(screen, NULL, background, NULL);
    SDL_RenderPresent(renderer);

    /* szoveg bekerese */
    SDL_Rect r = { 40, 80, 400, 40 };
    input_text(szoveg, 100, r, fekete, feher, font, renderer);

    /* szoveg kirajzolasa */
    if (szoveg[0] != 0x0000) {
        SDL_BlitSurface(background, NULL, screen, NULL);

        SDL_Surface *felirat = TTF_RenderUTF8_Blended(font, szoveg, feher);
        hely.x = (screen->w - felirat->w) / 2 + 2;
        hely.y = (screen->h - felirat->h) / 2 + 2;
        SDL_BlitSurface(felirat, NULL, screen, &hely);
        SDL_FreeSurface(felirat);
        SDL_RenderPresent(renderer);
        while (SDL_WaitEvent(&event) && event.type != SDL_QUIT);
    }

    SDL_FreeSurface(background);
    sdl_close(&window, &renderer, &font);
    return 0;
}

8. Többmodulos projektek és az SDL-es programok futtatása

Van pár apróság, amit tudni kell az SDL-es projektek fordításáról és futtatásáról.

Fontos!

Az egyik a többmodulos projektekkel kapcsolatos. A Code::Blocks beépített SDL projekt varázslója egy olyan projektet hoz létre, amelyben a fő forrásfájl neve main.cpp, ami a kiterjesztése miatt alapértelmezetten nem C, hanem C++ fordítóval fordul. Ha újabb modult adunk hozzá, annak pedig .c a kiterjesztése, azt a C fordító fogja kapni. Ugyan megoldható, hogy a kettővel együtt dolgozzunk, de ennek technikai részletei túlmutatnak a Prog1 tárgyon. Használjuk inkább az SDL telepítős írásból letölthető InfoC SDL projekt típust!

A másik az SDL-es programok futtatása. Az SDL könyvtár lefordított függvényei nem kerülnek be a mi programunk futtatható, .exe fájljába, hanem külön fájlokban vannak. A végleges linkelést nem a fordító végzi, hanem az elindított program „húzza be” az indításakor a szükséges programrészeket. Ezt dinamikus linkelésnek (dynamic linking) nevezzük. Ezek a függvénykönyvtárak a *.dll fájlokban (dynamic link library) vannak. Ha az SDL-es programot el szeretnénk indítani a Code::Blockson kívülről, akkor vele egy mappába kell tenni az SDL *.dll fájljait is. Ezek a fent letölthető ZIP fájl bin mappájában vannak. A nagyobb programok telepítőinek (installer) egyébként éppen ez a feladata, hogy a program futásához szükséges dinamikusan betöltött könyvtárakat is a megfelelő helyre másolják.

Fel szokott merülni az a kérdés is, hogyan kell olyan SDL-es programot csinálni, amelynek nem nyílik külön konzol ablaka az indításkor. A Code::Blocks eltérő fordítási beállítások mellett tud .exe fájlt készíteni. Ha fent, a menüsor alatt a „Release” mód van kiválasztva a „Debug” helyett, akkor olyan .exe fájlt készít, amely nem nyit magának konzol ablakot. Az elkészült .exe a projekt mappájában, a \bin\Release almappa alatt található.

Windowson alapvetően a grafikus programoknál nem szokás az, hogy írjanak a szabványos kimenetükre. Ezért ott az SDL bezárja a szabványos kimenetet, és megnyit helyette egy stdout.txt nevű fájlt, amibe ezután a printf()-elt szövegek kerülnek. (Ugyanez történik a szabványos hibakimenettel is.) Ezért hiába printf()-elünk az SDL programokban, nem fog az a konzol ablakban megjelenni, még ha van is ilyen ablak. Ezt az SDL FAQ-ban is megemlítik, itt. Ennek elkerülésére azt javasolják, hogy újra kell nyitni a konzol ablakot. Ezt a main() függvény elejére, de az SDL_Init() hívás után az alábbi módon lehet megtenni:

#ifdef __WIN32__
    freopen("CON", "w", stdout);
    freopen("CON", "w", stderr);
#endif

Ezután már lehet printf()-elni. A __WIN32__ makrót egyébként az SDL definiálja. Mivel az csak a windowsos fordításoknál létezik, a fenti négy sort nyugodtan betehetjük a programba (#ifdef-fel együtt!), platformfüggetlen marad.

Az SDL igényli azt is, hogy a main() függvénynek meglegyenek a paraméterei: int argc és char *argv[]. Enélkül a program lefordítása nem fog mindenhol sikerülni.

A parancssori argumentumokról és a többmodulos programokról egy külön előadáson beszélünk majd.

9. MacOS Mojave problémák

Ha valaki a macOS Mojave legújabb verzióját használja, akkor egy SDL bug miatt nem renderelődik automatikusan a képernyőre, amit rajzolni szeretnénk. A probléma megoldódik, ha megmozdítjuk az SDL által készített ablakot ÉS az események kezelésébe beleteszünk egy SDL_RenderPresent(renderer) sort.

Az első SDL példaprogram esetében ez így nézne ki:

/* varunk a kilepesre */
SDL_Event ev;
while (SDL_WaitEvent(&ev) && ev.type != SDL_QUIT) {
    SDL_RenderPresent(renderer);
}