Grafikus Qt programok írása segédeszközök nélkül Grafikus felületű Qt programokat ahogy láttuk, készíthetünk egy egyszerű szövegszerkesztővel is, bár a Qt jó támogatást ad a grafikus felület grafikus tervezésére a QtCreator és a QtDesigner segítségével. Ezzel azért foglalkozunk, hogy megismerjük a Qt grafikus elemek jellegzetességeit. A grafika egy ún. Grafikus Felhasználói Felületen (Graphical User Interface GUI) jelenik meg. Ilyen van a Windows-ban a Linuxban, a Mac-eken és a mobil eszközökön. Az eseményhurok (event loop) A QApplication::exec() az esemény hurok, vagy ciklus. Minden Qt-s grafikus program tartalmaz egy ilyet. Ezt többnyire egy külön fájlba (pl. main.cpp) helyezzük el A legegyszerűbb esetben ez a fájl így néz ki: #include "saját_include_fájlunk.h" #include <QApplication> int main(int argc, char *argv[]) QApplication a(argc, argv); AlapWidgetünk w; // pl. a QDialog-ból leszármaztatott widget, de lehet // akár egy QLabel is w.show(); // kirajzolja az AlapWidget-et, de az még nem jelenik meg // csak amikor feldolgozzuk a kirajzolási üzeneteket return a.exec(); // itt az eseményhurokban Az AlapWidgetünk declarációja a saját_include_fájlunk.h fájlban van. Az eseményhurok feladata az operációs rendszerből származó felhasználó, vagy programok által generált események (pl. gombnyomás, egérműveletek,érintés, kirajzolás) kezelése. A Qt események olyan objektumok, amelyek vagy az alkalmazáson belüli történéseket, vagy olyan külső történéseket (pl gombnyomás, egérmutató mozgatás) reprezentálnak, amelyekről az alkalmazásnak tudnia kell. Amikor egy esemény történik a Qt rendszer létrehoz egy azt reprezentáló objektumot és elküldi azt a programunk valamelyik objektumának. Háromféle esemény van: spontán események Az ablakkezelő rendszer generálja. A rendszer sorba állítja ezeket, ahonnan egymás után kerülnek az eseményhurokba POST-olt események Qt, vagy az alkalmazás generálja ezeket. A Qt állítja ezeket sorba és helyezi be egymás után az esemény ciklusba feldolgozásra Közvetlen (SENT) események Qt, vagy az alkalmazás generálja ezeket és közvetlenül a fogadó objektum-nak küldjük el, ez nem kerül be a sorba.
Az eseményeket leszármaztatott osztályokban magunk is lekezelhetjük. Ha pl. azt akarjuk, hogy egy címke méretezésével annak betűmérete is nőjön, akkor a QLabel resizeevent függvényét kell átdefiniáljuk. Ehhez a QLabel-ből leszármaztatunk egy másik objektumot, amelyben lekezeljük a méretváltoztatást és azt használjuk a QLabel helyett: Class QMyLabel : public QLabel Q_OBJECT void resizeevent(qresizeevent *event) QFont f = font(); f.setpointsizef( f.pointsize()f * (event->oldsize().height()/size().height() ); setfont(f);
Widget-ek Minden grafikus Qt program ún. widget-eken alapul. Ezek megjeleníthető grafikus komponenseket 1 (pl. gombok, cimkék, szövegbeviteli mezők) tartalmazó egyszerű, vagy összetett objektumok. Szerepelhetnek önállóan a képernyőn ahogy azt láttuk, de más widgetek részeként is. A legtöbb widget valamely más widgethez, a szülőjéhez (párent) tartozik, abban jelenik meg. Minden widget a QWidget-ből van leszármaztatva, ami maga viszont a QObject-ből. A QObject-nek sok hasznos tulajdonsága van, amit a widgetek-ben is használhatunk. Egy különösen hasznos tulajdonsága, hogy tetszőleges számú és nevű saját mezőt (property) adhatunk hozzá. Amikor leszármaztatunk egy widget-et egy másikból az osztály deklaráció elejére mindig be kell írni a Q_OBJECT makrót, különben nem lesz érvényes widget-ünk! Az első saját grafikus objektum, amit megjelenítünk a képernyőn rendszerint vagy a QMainWindow, vagy a QDialog widgetből leszármaztatott saját widget-ünk. Azért kell ezekből leszármaztatni saját widget-eket, hogy hozzájuk adhassuk a saját widget-jeinket. A widget-ek méretét és a befoglaló widget-hez (parent), vagy ha ilyen nincs - a képernyőhöz képesti helyzetét a geometriája (lekérdezése: QRect rect = widget.geometry(), beállítása widget.setgeometry( rect)) adja meg. A rect tartalma: x, y, szélesség, magasság, az x,y koordináták a szülő bal felső sarkához képest értendőek 2. Egyebek között minden widgethez tartozik még egy minimális és egy maximális méret (szélesség, magasság), egy betű fajta (font név, méret, stílus), valamint egy stíluslap (style sheet). Ha a minimális és maximális méretek megegyeznek a widget nem méretezhető át. Ez jól használható pl. a fő, vagy dialógus ablakoknál. Példaként készítsünk egy jegyzetfüzet programot, amibe szabadon írhatunk szövegeket! A jegyzetfüzet így fog kinézni: 1 Ha a szülő widget nem látható, akkor természetesen a benne levők sem, de a benne levő widget-eket el is rejthetjük. 2 Az x tengely vízszintesen jobb, az y tengely függőlegesen lefelé mutat.
A tervezésnél kétféleképpen járhatunk el. 1. gondosan megtervezzük kockás papíron és minden méretet manuálisan állítunk be. Ha azt szeretnénk azonban, hogy az ablak mérete változtatható legyen és ekkor a jegyzetfüzet szerkesztő része az aktuális ablakkal nőjön, vagy csökkenjen, akkor ez nem igazán jó út. Akkor is gond lehet, ha a képernyőfelbontás, vagy a képernyő betűmérete megváltozik. 2. Megvalósítjuk az elrendezést úgy, hogy a szükséges méreteket a rendszer számolja ki nekünk. Ehhez elrendezéseket (layout-okat) használhatunk. Az elrendezések nem widget-ek, mert maguk nem jelennek meg a képernyőn. Mi a 2. utat fogjuk követni. Van vízszintes (horizontal), függőleges (vertical), rács (grid) és nyomtatvány (form) elrendezés. Az utolsóban az elemeket páronként sorokba rendezi el a Qt. Minden sorba két elem (pl. szöveg és beviteli mező) kerül. Bármelyik elrendezés esetén az ablak méretének változtatásával az elemek mérete is változik. Első ránézésre a függőleges elrendezést választanánk, kezdjük tehát azzal!
Mindenekelőtt hozzunk létre egy alkönytárat a saját könyvtárunkban. Legyen a neve npad! Hozzuk létre a következő fájlokat: main.cpp, notepad.h, notepad.cpp: Main.cpp #include "notepad.h" #include <QApplication> int main(int argc, char *argv[]) QApplication a(argc, argv); Notepad w; // AlapWidgetünk most a Notepad a notepad.h-ból w.show(); return a.exec(); Notepad.h A fájl ezzel kezdődik: ifndef NOTEPAD_H #define NOTEPAD_H Ezzel a két sorral elérjük, hogy a header csak egyszer kerüljön feldolgozásra. A záró #endif a fájl legvégére kerül. Becsatoljuk az összes widget include fájlját: #include <QMainWindow> #include <QtWidgets/QPushButton> #include <QtWidgets/QTextEdit> #include <QtWidgets/QWidget> #include <QtWidgets/QVBoxLayout> A QVBoxLayout tartalmazza a függőleges elrendezést. Minden widget az Ui namespace része, ezért a mi új widgetünk is oda tartozik: namespace Ui class Notepad; Amikor egy widgetet származtatunk le egy másikból, mindig meg kell mondani, hogy az a QObject leszármazottja. Ezt a Q_OBJECT makróval tesszük meg. class Notepad : public QMainWindow Q_OBJECT A nyilvános részbe csak a konstruktor és a destruktor kerül. A widget-ek (és a QMainWindow is egy widget) konstruktorában adjuk meg azt a szülő (parent) widget et, aminek az ablakába
ez a widget megjelenik majd. Ez a mi esetünkben egy nullptr 3 lesz, mert az ablakunk a fő ablak.. public: explicit Notepad(QWidget *parent = 0); ~Notepad(); A private részbe kerülnek a widget-eink és a grafikát felépítő függvény: private: A QMainWindow-nak szüksége van egy speciális widgetre, ami tulajdonképpen az összes elem szülője lesz. Ez a centralwidget. A többi widget: QWidget *centralwidget; QBoxLayout *vertlayout; QTextEdit *edtnote; QPushButton *btnclose; Az ablakot fel kell építeni, hozzá kell adjuk az összes widget-et. Ezt a void setupui(); függvény végzi majd el. SIGNAL-ok és SLOT-ok Mikor a bezárás gombot megnyomjuk az ablaknak be kell záródnia. A gombnyomás is egy esemény, és azt szeretnénk, hogy ezt az eseményt a fő ablak kapja meg. Két widget egymással a signals and slots mechanizmuson keresztül beszélget. Az egyes signal-okat és slot-okat explicit módon a connect() függvénnyel kapcsoljuk majd össze. Jelen esetben azt akarjuk, hogy amikor a btnclose gombra kattintunk 4, akkor a fő ablaknak egy függvénye (hívjuk pl. btncloseclicked()-nek) hívódjon meg amivel az bezárja magát 5. Ehhez a btncloseclicked() függvényt a fő ablak fogadóhelyévé (slot-jává) kell tegyük. 3 A nullptr a C++11-ben bevezetett kulcsszó. Jelentése megegyezik a korábbi NULL define-éval. 4 Vagy billentyűzettel benyomták, amit ebben a példában nem valósítunk meg. 5 Tehát nem a gomb zárja be az ablakot, hanem az saját magát.
Ehhez ezt kell beírjuk: private slots: void btncloseclicked() close(); A slots kulcsszót a C++ fordító program nem látja, az egy Qt kiegészítés, amire most semmi szükség nem lenne, de szokjuk meg, hogy oda írjuk, mert a későbbiekben használni fogjuk. Zárjuk be az osztálydeklarációt és a fájlt! ; #endif // NOTEPAD_H A következő fájl a notepad.cpp Notepad.cpp Ez a header fájl beolvasásával kezdődik, amit a konstruktor követ:. include "notepad.h" Notepad::Notepad(QWidget *parent) : QMainWindow(parent) setupui(); A konstruktorban felépítjük a felhasználói felületet. Mint látni fogjuk az egyes widget-eket a new operátorral hozzuk létre, ezért azt hinnénk, hogy kilépés előtt fel is kell szabadítsuk őket, de erre nincs szükség. A szülő (parent) widgetek gondoskodnak minderről 6. A destruktor ezért most nem csinál semmit: Notepad::~Notepad() A felhasználói felület elkészítését a setupui() függvény végzi: void Notepad::setupUi() Állítsuk be az ablak (kezdő) méretét 400 x 300 pixelre: resize(400, 300); 6 Természetesen a new-val dinamikusan létrehozott saját (nem QTs) objektumainkat, illetve azokat a Qts objektumokat, amiket nem adunk hozzá más QTs objektumokhoz nekünk kell felszabadítanunk!
Az ablak pozícióját nem adjuk meg, azt az operációs rendszer fogja meghatározni. Közvetlenül egy QMainWindow-hoz (tehát a belőle leszármaztatott Notpead-hez sem) nem adhatunk hozzá widge-eket, ezért ehhez szükségünk lesz egy speciális szerű widget-re. Ezt nevezzük mondjuk centralwidget-nek, mert majd a setcentralwidget() függvénnyel adjuk hozzá a Notepad-hez. centralwidget = new QWidget(this); Beállítjuk a centralwidgettet az ablakhoz, ezzel érjük el, hogy a widget-jeink megjelenhessenek: setcentralwidget(centralwidget); Minden más widget a centralwidet-re kerül, ezért a centralwidget hez kapcsoljuk a layoutot, amelyet úgy állítunk be, hogy legyen egy 11 pixeles margója az elemek körül és az elemek egymástól 6 pixelre legyenek: vertlayout = new QVBoxLayout(centralWidget); vertlayout->setspacing(6); vertlayout->setcontentsmargins(11, 11, 11, 11); Elkezdjük hozzáadni a widget-eket. Minden widget a fő ablak centralwidget-ében jelenik meg, ezért mindegyik szülője az lesz. De a widgetet a layouthoz is hozzá kell adni, mert az fogja a méretét és a helyét meghatározni. A layout viszont nem lesz szülője a widgeteknek 7. Minden widget létrehozásakor megadjuk a szülőjét és, hozzácsatoljuk a layouthoz: edtnote = new QTextEdit(centralWidget); vertlayout->addwidget(edtnote); btnclose = new QPushButton(centralWidget); btnclose->settext("&bez\303\241r\303\241s"); // &Bezárás UTF8 kódolással vertlayout->addwidget(btnclose); Ezután megmondjuk az ablaknak, hogy a gomb benyomására zárja be magát. A connect függvénnyel összekapcsoljuk egy adott widget (itt btnclose) valamelyik signal-ját (itt clicked() ) a fogadó widget (Notepad-tehát ez az objektum) adott slot-jával (btncloseclicked()) A signal elküldését megcsinálja a gomb. Az lenne jó, ha ezt írhatnánk: connect( btnclose, clicked, this, btncloseclicked ), de ezt a C++ szabályai nem engedik. A connect függvény mutatókat vár, de sem a QPushButton::clicked() sem a btncloseclicked() mindkettő void - nem ad ilyet vissza. Még azt sem írhatnánk, hogy 7 Egyrészt a layout nem widget, másrészt egy widget-nek csak egy szülője lehet.
QPushButton::clicked, ugyanis a clicked() nem egy nem sztatikus függvénye a nyomógombnak, ezért nem lehet meghívni konkrét objektum nélkül. A függvény törzsére mutató mutató azonban megadható, hiszen a függvények minden objektumra ugyanazok 8 : connect( btnclose, &QPushButton::clicked, this, &Notepad::btnCloseClicked ); // setupui Most már csak a projekt és Makefile-t kell elkészíteni: Npad.pro és Makefile Qmake-qt5 -project Ismét bele kell javítsunk az npad.pro fájlba. Adjuk hozzá a következő két sort: QT += core gui widgets QMAKE_CXXFLAGS += -std=c++11 A többi már egyszerű: qmake-qt5 make Futtassuk a programot:./npad&. Ez nem egészen azt produkálja, amit vártunk: Az ablak ugyan méretezhető és a gomb magassága sem változik, de a gomb az egész ablak szélességét elfoglalja. Ez a függőleges elrendezés tulajdonsága. Ahhoz, hogy a gomb a jobb oldalon maradjon és a mérete se változzon egyfelől a függőleges elrendezést rácsosra kell cseréljük, másfelől használnunk kell egy láthatatlan elemet a vízszintes térkitöltőt (horizontal 8 Emlékezzünk arra, hogy minden nem sztatikus tagfüggvény vár egy, az aktuális objektumra mutató mutatót (pointert) mint rejtett paramétert. A connect-re később más lehetőségeket is megismerünk majd. Ezekről akkor fogunk beszélni, amikor a QtCreatort/QtDesigner-t használjuk.
spacer). Ez egy olyan elem, ami addig nyúlik, ameddig szükséges és ezért az ablak méretének változásával a többi elem méretét a layout nem fogja megváltoztatni 9. A változtatások: A notepad.h-ban az #include <QtWidgets/QVBoxLayout> sort cseréljük le #include <QtWidgets/QGridLayout> -ra és a QVBoxLayout-ot cseréljük le QGridLayout-ra: QGridLayout *gridlayout; a QPushButton elé pedig szúrjuk be ezt: QSpacerItem *horizontalspacer; A notepad.cpp-ben a setupui()-t cseréljük le a következőre: void Notepad::setupUi() resize(400, 300); centralwidget = new QWidget(this); gridlayout = new QGridLayout(centralWidget); gridlayout->setspacing(6); gridlayout->setcontentsmargins(11, 11, 11, 11); edtnote = new QTextEdit(centralWidget); gridlayout->addwidget(edtnote, 0, 0, 1, 2); horizontalspacer = new QSpacerItem(295, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); gridlayout->additem(horizontalspacer, 1, 0, 1, 1); btnclose = new QPushButton(centralWidget); btnclose->settext("&bez\303\241r\303\241s"); gridlayout->addwidget(btnclose, 1, 1, 1, 1); setcentralwidget(centralwidget); connect( btnclose, &QPushButton::clicked, this, &Notepad::btnCloseClicked ); 9 Természetesen van függőleges térkitöltő (vertical spacer) is. Sok esetben azonban a kívánt elrendezés még ezekkel sem érhető el, ilyenkor plusz widget-eket is kell használnunk.
Látható, hogy a rácsos elrendezésben az addwidget() hívásokban újabb szám paraméterek jelentek meg. A szintaxis a következő: QGridLayout::AddWidget(QWidget *widget, int fromrow, int fromcolumn, int rowspan, int columnspan, Qt::Alignment alignment = Qt::Alignment()) Az első két paraméter az elem kezdő oszlopa és sora a rácsban, a második kettő pedig, hogy hány sorra, ill. oszlopra terjed ki az elem. Az utolsó paramétert alapértelmezettnek hagyjuk. Már csak egy make parancs és az npad program készen van és futtattható.
A teljes program main.cpp #include "notepad.h" #include <QApplication> int main(int argc, char *argv[]) QApplication a(argc, argv); Notepad w; w.show(); return a.exec(); notepad.h #ifndef NOTEPAD_H #define NOTEPAD_H #include <QtCore/QVariant> #include <QtWidgets/QGridLayout> #include <QtWidgets/QMainWindow> #include <QtWidgets/QPushButton> #include <QtWidgets/QSpacerItem> #include <QtWidgets/QTextEdit> #include <QtWidgets/QWidget> #include <QMainWindow> namespace Ui class Notepad; class Notepad : public QMainWindow Q_OBJECT public: explicit Notepad(QWidget *parent = 0); ~Notepad(); private: QWidget *centralwidget; QGridLayout *gridlayout; QTextEdit *edtnote; QSpacerItem *horizontalspacer; QPushButton *btnclose; void setupui(); private: void btncloseclicked() close(); ; #endif // NOTEPAD_H notepad.cpp #include "notepad.h"
Notepad::Notepad(QWidget *parent) : QMainWindow(parent) setupui(); Notepad::~Notepad() void Notepad::setupUi() resize(400, 300); centralwidget = new QWidget(this); setcentralwidget(centralwidget); gridlayout = new QGridLayout(centralWidget); gridlayout->setspacing(6); gridlayout->setcontentsmargins(11, 11, 11, 11); edtnote = new QTextEdit(centralWidget); gridlayout->addwidget(edtnote, 0, 0, 1, 2); horizontalspacer = new QSpacerItem(295, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); gridlayout->additem(horizontalspacer, 1, 0, 1, 1); btnclose = new QPushButton(centralWidget); btnclose->settext("&bez\303\241r\303\241s"); gridlayout->addwidget(btnclose, 1, 1, 1, 1); //if (!connect(btnclose, SIGNAL(clicked()), this, SLOT(btnCloseClicked()))) // // edtnote->append("no valid connection to button"); // if(!connect( btnclose, &QPushButton::clicked,this, &Notepad::btnCloseClicked)) edtnote->append("no valid connection to button"); // setupui notepad.pro ###################################################################### # Automatically generated by qmake (3.0) V szept. 18 14:08:55 2016 # javításokkal ###################################################################### TEMPLATE = app TARGET = npad INCLUDEPATH +=. # Input HEADERS += notepad.h SOURCES += main.cpp notepad.cpp QT += core gui widgets