Egyszerű
grafikus alkalmazások implementálása Linux/KDevelop/Qt/C++ környezetben
Előszó
Az itt olvasható leírás az ELTE Informatikai Karán oktatott (Elemi) Alkalmazások Fejlesztése c. tárgy 3. félévi tananyagához készült segédanyag, ebből kifolyólag feltételezi az azt megelőző félévekben leadott anyagok, valamint az ablakvezérelt alkalmazások implementálásának ismeretét.
A Qt grafikus fejlesztői környezetről az internet végtelenjében rengeteg forrásból olvashatunk, de ennek az írásnak nem célja azok kiváltása, így aki részletes leírásra vágyik a rendszerrel kapcsolatban, az keresse meg az őt érdeklő információt valamelyik webkereső motor segítségével. Nagy vonalakban arról van szó, hogy a Qt egy programozási segédeszköz, ami arra hivatott, hogy bizonyos általános implementációs feladatokat elvégezzen helyettünk. A Qt Linux alkalmazás, de már Windows operációs rendszer alatt is használható, bár erre a célra adott a Microsoft divatos fejlesztői környezete, a Visual Studio .NET. Elvben mind egyforma, például ha létrehozok egy új ablakot, ráhelyezek az egérrel a toolbox –ról egy gombot (QButton) és a tulajdonságai között a feliratát megváltoztatom, akkor a környezet generálja helyettem az általam grafikusan megtervezett programkódot. Természetesen ezek mind egy megfelelő keretrendszerre telepítve használhatóak, mivel előre megadott osztályokat használnak.
A Qt használata a tapasztalatszerzésen alapul, a feltelepített Qt alkalmazás elindításához elegendő a parancssorba begépelni, hogy designer, majd entert ütni és a használatához minden szükséges információ a rendelkezésünkre áll, ha a fenti módon elindítjuk a dokumentációját, az assistant paranccsal (megj.: ez a dokumentáció online formában is megtalálható, lásd a lecke végén). Mivel a lépések (minimális angoltudás mellett) triviálisnak mondhatóak, ebben a leírásban csupán nagy vonalakban ismertetjük a design létrehozásának folyamatát.
Specifikáció
A bemutató egy gyakorlati példán keresztül kívánja megközelíteni a témát. A program amit implementálunk, grafikus megjelenítését tekintve menüvezérelt négyzetrajzoló alkalmazás.
Első lépés
Hozzuk létre az alkalmazásunkat és a megjelenítő ablak design –ját Qt segítségével!
A Qt –t elindítva válasszuk ki a C++ project opciót és határozzuk meg a kívánt elérési utat! Példánkban a projectre MY néven hivatkozunk.
A projecthez a File->New gombra kattintva adjunk hozzá egy QWidget objektumot, ez lesz az alkalmazásunk főablaka! Rögtön mentsük is el! Példánkban az objektumra myWidget néven hivatkozunk.
A projecthez a fenti módon adjunk hozzá egy C++ main függvényt tartalmazó fájlt (alapszintű C++ ismerettel rendelkezők tudják, hogy az operációs rendszer a program végrehajtását a main függvény végrehajtásával indítja el, így arra mindig szükség van, hiszen nélküle nem lehet tudni, hogy kezdetben hova kerüljön a vezérlés) és válasszuk ki, hogy melyik ablakunkhoz kívánjuk azt hozzárendelni! Ezzel a hozzárendeléssel az alkalmazás indításakor a kiválasztott ablak főablakká válik és megjelenítésre kerül. Az ezt megvalósító kódot a Qt generálja.
Mentsünk el mindent, majd fordítsunk és futtassunk! Ezt a következő utasítássorozat végzi el a project könyvtárában (a projectfájl neve my.pro):
qmake –project
qmake my.pro
make
./my
Ha működik, akkor egy üres ablakot látunk, ami nem csinál semmit, ha fordítási hiba történt, akkor sajnos a semmiben is sikerült hibát vétenünk…
Most az ablakunkra dobáljuk a menü elemeit, a kész design (ui) letölthető a lépés végén!
Letöltések: my.pro, main.cpp, mywidget.ui, mywidget.ui.h
Második lépés
Implementálunk egy osztályt, amivel a megjelenítést végezzük.
A most következő osztályszármaztatáshoz a Qt nem nyújt segítséget, ezért kénytelenek vagyunk az első lépésben ismertetett módon üres C++ fej- és törzsállományokat hozzáadni a projectünkhöz és önállóan implementálni azt. Tegyük is meg!
Mentsük el, a példánkban myPanel néven hivatkozunk rá!
Az osztályt amit létrehozunk, egy QFrame nevű ősosztályból származtatjuk és a myWidget főablakunkból példányosítjuk majd.
Mivel ez az osztály fogja végrehajtani a rajzolást, itt szólok pár szót a rajzolás menetéről. A QFrame egy QWidget –ből származtatott ősosztály és mint ilyen, igaz rá, hogy a grafikus megjelenítését egy paintEvent nevű szignálhoz kapcsolódó slot metódus végzi. Ez a szignál jelet küld minden esetben, amikor az indokolt (például kikerül a QWidget a takarásból és az eddig fedésben levő rész miatt újra kell azt rajzolni), de előidézhetjük programutasítással is. Ekkor az alapértelmezett, az objektum látványát megvalósító rajzoló metódusokon kívül egy virtuális metódust is meghív a vezérlés, ez a paintEvent(QPaintEvent*). Ez a metódus újradefiniálható és ennek a törzsében elvégezhetjük a kívánt speciális rajzolásunkat. A fent leírtak alapján természetesen a (pl. fedésből kikerült) objektum újrarajzolása mindig együtt jár a saját rajzoló utasításokat tartalmazó metódusunk meghívásával.
Ennek fényében az általános esethez szükségünk van egy paintEvent metódusra, ami az őstől örökölt virtuális metódus felüldefiniálása (reimplement), és egy konstruktorra. A példánkban a rajzolást előkészítő és az azt megvalósító utasításokat két külön metódusba csoportosítottam, így szükségem volt még egy függvényre. A specifikáció a következő:
A megvalósításban egyelőre hagyjuk a rajzoló metódust üresen, viszont készítsük azt elő! A QPainter osztályt használjuk a rajzolás elvégzéséhez, ezért azt létre kell hoznunk, be kell állítanunk és át kell adnunk paraméterként rajzolásra. Két beállítást végzünk el, a viewPort és window beállításokat, az első az objektum rajzolandó felületét határozza meg relatív téglalap formájában, a második pedig általános indexelést ad. Az első egyértelmű, a másodikhoz fűzök egy kis kommentárt. A setWindow(x,y,a,b) utasítással beállíthatjuk a legkisebb kirajzolandó pont koordinátáját (x,y) -ra, ez a „bal alsó sarok”, a legnagyobbat pedig (x+a,y+b) -ra, ami a „jobb alsó sarok”. Ez azt eredményezi, hogy csupán az itt meghatározott konkrét keretek között kell meghatározni a rajzolandó alakzatokat, a megjelenítő objektumok átméretezései nem befolyásolják majd azt, az így szükségessé vált transzformációkat elvégzi helyettünk a QPainter.
Letöltések: my.pro, mypanel.h, mypanel.cpp
Harmadik lépés
Kössük össze a myPanel osztályunkat a myWidget főablakunkkal!
A Qt designer –ben válasszuk ki a myWidget főablakunkat, kattintsunk az Object Explorer ablakra és ott a members fülre. Itt deklarálhatjuk a slotokat, függvényeket, inklúdokat és osztályváltozókat. Mivel Qt –ben hoztuk létre a myPanel osztályunkat definiáló fájlokat, a designer automatikusan hozzáadta őket a projectünkhöz, így azzal nincs különösebb gondunk. Példányosításhoz azonban deklarálnunk kell egy myPanel típusú változót (lehet private) és létrehoznunk azt. Ezt normális esetben konstruktor segítségével végeznénk, de a QWidget –ünk konstruktora fenntartott eljárás, ahonnan a Widget elemeit definiálja a program, így marad egy „álkonstruktor”: hozzunk létre egy új függvényt (lehet private) és adjuk neki az init() speciális függvénynevet! Ezt a függvényt meg fogja hívni a program, ha elvégezte a konstruktor utasításait. Itt példányosítsuk a myPanel osztályunkat az Object Explorer –ben definiált változónkkal, valamint jelenítsük azt meg! Hogy lássuk, hogy hol van, én ideiglenesen átállítottam a háttérszínét feketére.
Írjunk a myWidget főablakunkhoz még egy eljárást, ami elhelyezi és megfelelően méretezi a myPanel QFrame objektumunkat a főablakunk méretei alapján!
Letöltések: mywidget.ui, mywidget.ui.h
Negyedik lépés
Létrehozunk egy időzítőt, ami vált a megjelenítendő képek között.
Deklaráljunk a megismert módon egy QTimer típusú változót. A QTimer ahogy a neve is mutatja egy időzítő, beállítható, hogy hány milliszekundumról számoljon vissza és ennek lejártával egy timeout() szignált küld. Kapcsoljuk össze ezt a szignált egy - a függvénydeklaráláshoz hasonlóan létrehozandó - slot –tal és itt kényszerítsük ki a myPanel QFrame –ünk frissítését a repaint() szignálküldő metódussal! Ezzel elértük, hogy szabályos időközönként frissüljön a megjelenítendő kép. Ezen kívül drótozzuk be a design menüjének felső két elemét, ami a QTimer –ünk indításáért, leállításáért és a visszaszámlálás időtartamának beállításáért felelős!
Letöltések: mywidget.ui, mywiget.ui.h
Ötödik lépés
Fejezzük be a félbehagyott myPanel osztályunkat, írjuk meg az általános négyzetrajzoló metódust!
Mivel a programunk vezérlését a myWidget főablakunkból végezzük, célszerű a myPanel rajzoláshoz szükséges beállításait onnan elvégezni. Jelen példában egy szokatlan és nem túl elegáns módszert dolgoztam ki, hogy gyors, egyszerű és áttekinthető legyen a programkód, de most és itt megjegyzem, hogy ezt senki se próbálja meg otthon, mert veszélyes!
A módszer lényege, hogy a myPanel pointereket kap a rajzoláshoz szükséges adatokat tartalmazó memóriaterületekhez, így a főablakból módosított tulajdonságok azonnal elérhetőek lesznek a myPanel –ben is.
Előnyök: gyors és egyszerű.
Hátrányok: ellenkezik a modularizációs elvekkel (nem önálló a myPanel modul), a pointerek definiálatlansága fagyáshoz vezethez, stb…
Fejlesztési javaslat: elegánsan úgy nézne ki a rendszer, hogy lenne egy külön beállítások osztály, amit használna a myPanel, ez saját memóriaterülettel rendelkezne és a főablakunk csak függvények segítségével érhetné el azokat (pl getPen(), setPen(QPen))…
Szóval a nem szép, de egyszerű megoldásunkhoz deklarálnunk kell pointereket, amik a kirajzolás paraméterezéséhez szükségesek. A rajzoláshoz meg kell határoznunk azt, hogy az alakzatot mivel rajzoljuk meg (QPen), milyen kitöltést alkalmazunk (QBrush), hogy definiáltunk –e transzformációs mátrixot (bool), a transzformációs mátrixot (QWMatrix), a négyzet oldalhosszát (int) és a négyzet bal felső sarkának koordinátáját (QPoint). Megjegyzés: az utolsó kettő helyett átadhatnánk egy négyzetet definiáló paramétert is (QRect).
Miután deklaráltuk a fejállományban a pointereket (természetesen private), létre kell hoznunk egy függvényt, amivel beállíthatjuk azok értékeit a főablakban létrehozott megfelelő memóriaterületek címeire. Eddig triviális.
Most következhet a rajzoló metódusunk, a myDraw(QPainter*)! A frissen deklarált pointereink alapján már adott, hogy mit kell csinálni: beállítjuk a „tollat” (setPen), beállítjuk a kitöltést (setBrush), beállítjuk a transzformációt, ha szükséges (setWorldMatrix) és végül elvégezzük a kirajzolást (drawRect).
Ezzel a myPanel általános négyzetrajzoló osztály elkészült, többet nem nyúlunk hozzá!
Letöltések: mypanel.h, mypanel.cpp
Hatodik lépés
Létrehozzuk a myPanel paraméterezését megvalósító memóriaterületeket.
A korábban elsajátított módon deklarálunk minden myPanel –ben létrehozott pointerhez egy-egy változót a myWidget főablakunkban (Object Explorer -> Members -> class variables gyk.), és az „álkonstruktorunkban” (init) létrehozzuk azok memóriaterületeit (amennyiben pointerként deklaráltuk azokat), valamint meghívjuk a myPanel beállító függvényét ezen változók által reprezentált memóriaterületek memóriacímeivel. Miután ez megtörtént, ha a főablakunkban módosítjuk a most deklarált változónk valamelyike által reprezentált értéket, akkor azzal a myPanel megfelelő pointerváltozója által mutatott értéket változtatjuk!
Letöltések: mywidget.ui, mywidget.ui.h
Hetedik lépés
Valósítsuk meg a háttérszínt beállító comboBox implementációját!
A comboBox új érték kiválasztásakor több szignált is lead. Ezek közül mi az activated(int) szignált használjuk, ehhez hozunk létre egy slotot, amihez kapcsolódhat. Ezt a műveletet teljes egészében elvégzi helyettünk a Qt, ha kijelöljük a megfelelő comboBox –ot és a Property Editor / Signal Handlers ablak Signal Handlers fülét kiválasztva duplán kattintunk a megfelelő szignálra, majd megadjuk a slot kívánt nevét. Tegyük is meg és a myWidget header fájljában meg is jelenik az új, üres törzsű slotunk!
Ez a slot hajtódik végre, ha új elemet választunk ki a legördülő menüből. A paramétere az új, kiválasztott elem indexe. Mivel mi tudjuk, hogy melyik indexhez melyik szín tartozik, ezért egy egyszerű, többágú elágazással (case) és a myPanel QFrame setBackgroundColor függvényével végrehajtható a feladat.
Ha ezt a slotot manuálisan meghívjuk a konstruktorból, azzal elvégeztük a myPanel háttérszínének alapértelmezett beállítását.
Letöltések: mywidget.ui, mywidget.ui.h
Nyolcadik lépés
Valósítsuk meg a menü többi elemének implementációját!
A legtöbb elem implementációja az előző lépésben tárgyaltak alapján nem kíván hosszabb kifejtést. Megjegyzem, hogy a Slider valueChanged eseményéhez érdemes slotot írni, melynek paramétere az új beállított érték, csakúgy, mint a spinBox esetében.
Az oldalhosszhoz, pozicionáláshoz és forgatáshoz tartozó checkBox –ok stateChanged szignáljához kapcsoljunk slotokat és a ki nem jelöltség jelenti majd azt, hogy az adatot random generáljuk, ezzel egyelőre nem foglalkozunk, ha pedig ki van jelölve, akkor el kell végezni a hozzá tartozó elem alapján a megfelelő myPanel pointerhez tartozó adatok módosítását. A hozzá tartozó elem értéke kijelölt esetben legyen módosítható, egyébként ne (enabled)!
A transzformációhoz kapcsolódó checkBox kijelöltsége jelentse azt, hogy egyáltalán történjen –e transzformáció és kapcsolódjon a myPanel boolean pointeréhez (ért. szer.)!
Még ejtek pár szót a transzformációs mátrixról. Minden grafikus transzformáció felfogható egy a tér bázisain elvégzett mátrixszorzásként, és minden megfelelő dimenziójú mátrix megfelel egy-egy transzformációnak. Ennek részletesebb ismertetése más tantárgyak feladata. A Qt –nek hála, az alapvető transzformációk elvégzéséhez nincs szükségünk ezekre az ismeretekre: a transzformációs mátrixunk (alapértelmezésben egységmátrix) végrehajtja a kívánt műveletet, ha transzformáljuk azt a translate, rotate, vagy scale függvények segítségével. Példánkban ezek közül csupán a rotate –re van szükség. Fontos tudni, hogy a rotate azt az alapmátrixot transzformálja, amire azt meghívjuk, így egy elforgatáshoz szükséges transzformációs mátrix meghatározásához mindig egységmátrixból kell kiindulni, amit megkapunk a reset() függvény meghívásával!
Letöltések: mywidget.ui, mywidget.ui.h
Kilencedik lépés
Végezzük el a megjelenítést vezérlő alapbeállításokat!
Egyszerűen hívjuk meg a menüelemek értékmódosítását kezelő slotokat (előző lépésben implementáltuk őket) az aktuális értékeikkel, így kapunk egy könnyen módosítható, dinamikus alapbeállítást!
Letöltések: mywidget.ui, mywidget.ui.h
Tizedik lépés
Valósítsuk meg a meg nem határozott beállítások véletlenérték-generálását!
A véletlenérték-generálás visszavezethető a véletlenszám-generálásra, amit a standard C++ srand() és rand() függvényének segítségével végezhetünk el. A srand() függvény paramétere egy generáló szám, ami alapján a véletlenszám-sorozatot generálja, ezt érdemes a rendszeridő aktuális értékével paraméterezni, hogy megfelelő véletlenszám szimulációt kapjunk.
A megoldás módszere az, hogy a szükséges beállításokhoz véletlenszámokat generálunk, méghozzá úgy, hogy azok benne legyenek a felvehető értékek intervallumában, majd a megfelelő menüelem kiválasztott értékét módosítjuk és meghívjuk az értékváltozást kezelő slotot. Figyeljünk arra, hogy a véletlenérték-generáláskor olyan értékeket határozzunk meg, amelyek beleférnek a megjelenítő objektumunkba (lásd második lépés, setWindow és oldalhossz)!
A véletlenérték-generálást természetesen minden megjelenítés előtt el kell végeznünk, tehát a QTimer objektumunk timeout() szignáljához tartozó újrarajzoló slot törzsébe kell kerülnie!
Letöltések: mywidget.ui, mywidget.ui.h
Végszó
Ezzel elkészült az alkalmazás! Akinek kedve van, az útmutatás nélkül javíthatja a leírás közben említett szépséghibákat, akinek nincs kedve, az pedig fordítsa, futtassa és gyönyörködjön a színes négyzetekben, amikért ennyit dolgoztunk!
Kapcsolódó linkek
Gregorics Tanár Úr honlapja, Szabóné Nacsa Rozália Tanárnő honlapja, Zimmer Attila honlapja, Qt hivatalos honlapja