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
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 komponenseket1 (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őek2. 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:
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. 1
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 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 #include #include #include #include
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 nullptr3 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. QWidget *centralWidget;
A többi widget: 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 kattintunk4, akkor a fő ablaknak egy függvénye (hívjuk pl. btnCloseClicked()-nek) hívódjon meg amivel az bezárja magát5. 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. 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. 4
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ől6. 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 widgeteknek7. 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 ugyanazok8: 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áltoztatni9. A változtatások: A notepad.h-ban az #include
sort cseréljük le #include
-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 ); }
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. 9
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 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 #include #include #include #include #include #include #include
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