Hlavní navigace

Programování pro X Window System (7)

13. 5. 2004
Doba čtení: 9 minut

Sdílet

V předchozích článcích jsme probrali toolkit GTK+. Nyní se budeme věnovat druhému slíbenému toolkitu - Qt. Nebudeme se pouštět do takových podrobností jako u GTK+, protože Qt v mnoha ohledech funguje obdobně jako GTK+ a navíc je velmi dobře dokumentované.

Qt

V předchozích článcích jsme probrali toolkit GTK+. Nyní se budeme věnovat druhému slíbenému toolkitu – Qt. Nebudeme se pouštět do takových podrobností, jako u GTK+, protože Qt v mnoha ohledech funguje obdobně jako GTK+ a navíc je velmi dobře dokumentované. Není problém naučit se používat Qt pro psaní aplikací s pomocí dokumentace dodávané s toolkitem, která obsahuje tutoriály, vysvětlení principů, na nichž je toolkit postaven, referenční příručku a návody k použití doplňkových nástrojů (Qt Designer, qmake apod.). Dokumentace je ve formátu HTML a lze ji prohlížet buď v libovolném webovém browseru, nebo pomocí aplikace Qt Assistant, která je součástí toolkitu.

Toolkit Qt je multiplatformní, dostupný pro Unix (X11), Mac OS X, embedded Linux a MS Windows. Ještě více než GTK+ skrývá detaily okenního systému, nad nímž běží. Qt je objektově orientované a napsané v C++ rozšířeném o mechanismy definice signálů a slotů, dynamické identifikace typů a properties. Rozšíření jsou implementována pomocí maker a preprocesoru moc (Meta Object Compiler). Existují i rozhraní pro použití Qt z jiných jazyků: Perlu (PerlQt) a Pythonu (PyQt). Při přípravě tohoto článku jsem vycházel z verze toolkitu 3.1. Aktuální verze je 3.3.2, nicméně k rozdílům mezi těmito verzemi se ani nedostaneme. Pokud chceme Qt aplikaci doplnit o skriptování, je možné použít nástroj QSA (Qt Script for Applications) založený na jazyce ECMAScript.

Toolkit Qt je dostupný volitelně pod komerční licencí pro vývoj closed-source softwaru nebo pod GPL, popř. QPL (Qt Public License) pro vývoj free softwaru. Podobně duální licencování má i QSA.

Hello World

Seznamování se s toolkitem Qt začneme triviálním programem, který zobrazí okno s tlačítkem. Po zavření okna program skončí. Tlačítko lze stisknout, ale nic nedělá. Celý zdrojový kód programu je v souboru qthello.cpp.

Hlavičkové soubory Qt jsou rozdělené podle jednotlivých tříd. Někdy je skupina funkčně svázaných tříd definovaná v jednom souboru, ale obvykle má každá třída samostatný hlavičkový soubor. V našem prvním programu budeme používat třídy QApplication aQPushButton.

#include <qapplication.h>
#include <qpushbutton.h>

Každý program má právě jednu instanci třídy QApplication. Ta implementuje cyklus zpracování událostí, inicializaci, závěrečný úklid a zpracování globálních nastavení platných pro celou aplikaci. Konstruktor dostává argumenty funkce main a odebere z nich argumenty určené pro Qt, např. -style, -display nebo-name. Kompletní seznam je uveden v dokumentaci třídy QApplication.

QApplication app(argc, argv);

Po inicializaci aplikačního objektu můžeme vytvářet widgety. Náš jednoduchý program bude mít jediný widget – tlačítko.

QPushButton hello("Hello world!", 0);

Konstruktor má tři parametry: nápis na tlačítku, rodičovský widget a jméno widgetu. Zde je tlačítko top-level widget, proto nemá rodiče. Jméno widgetu se nezobrazuje, je to interní pojmenování. Příliš se nepoužívá, proto má tento parametr implicitní hodnotu 0. Dá se použít pro nalezení objektu podle jména (metody QObject::child a QObject::queryList) a pro ladění (metoda QObject::dumpOb­jectTree). V GTK+ je pro top-level okna vyhrazena speciální třída GtkWindow. V Qt může být libovolný widget top-level. Tlačítko nastavíme jako hlavní widget aplikace. Tím zajistíme, že při zavření okna s tlačítkem program skončí.

app.setMainWidget(&hello);

Každý widget je potřeba zobrazit, jinak by sice existoval jako objekt v paměti, ale nebyl by vidět. Při zobrazení widgetu se zobrazí i všichni jeho potomci.

hello.show();

Po vytvoření a zobrazení widgetů a dokončení ostatních inicializačních akcí je čas nastartovat zpracování událostí.

return app.exec();

Zpracování událostí skončí zavřením hlavního widgetu nebo voláním QApplication::qu­it() či QApplication::exit. Metoda QApplication::exec pak vrátí návratový kód použitelný jako návratový kód programu.

Vytvoření spustitelného programu

Qt obsahuje utilitu qmake, která umí automaticky vygenerovat kompletní Makefile. Její použití je pohodlnější než psát Makefile ručně. Další možnost představují programy autoconf a automake, které používá např. projekt KDE. Před spuštěním qmake je nutné nastavit environmentové proměnnéQTDIR (adresář, kde je instalované Qt, např. /usr/X11R6) a QMAKESPEC (kombinace platformy a kompilátoru, např. freebsd-g++). Následující postup se skládá z pěti kroků:

  1. Vytvoříme adresář pojmenovaný stejně, jako se bude jmenovat program. V našem případě to bude qthello. Do adresáře uložíme veškeré zdrojové soubory programy. My máme pouze soubor qthello.cpp.
  2. Přepneme se do adresáře (cd qthello) a příkazem qmake -project vygenerujeme projektový soubor. V něm jsou informace, jež qmake používá při generování Makefile.
  3. Projektový soubor můžeme upravit textovým editorem. V dokumentaci Qt je podrobně popsáno, co všechno můžeme do tohoto souboru napsat.
  4. Vygenerujeme Makefile příkazem qmake.
  5. Spustíme make.

Signály, sloty a události

Signály a události už známe z GTK+. Slotje jen jiný název pro handler signálu. V GTK+ se události překládají na signály a dál se zpracovávají pomocí handlerů signálů. Qt používá signály a události jako dva oddělené mechanismy pro komunikaci mezi objekty v programu, resp. mezi programem a okolím.

Každá třída odvozená z QObject má definovány množiny signálů a slotů. Signály i sloty jsou speciální metody. Mohou mít parametry, ale vždy mají návratový typ void. Objekt vygeneruje signál pomocí

emit signál(argumenty);

když se s ním stane něco, co by mohlo zajímat i jiné objekty – např. uživatel vybere položku z menu. Qt následně zavolá všechny sloty, které jsou k signálu připojeny. Náš triviální program qthello můžeme upravit, aby po stisku tlačítka skončil, připojením slotu QApplication::quit k signálu QPushButton::clic­ked.

QObject::connect(&hello, SIGNAL(clicked()), &app, SLOT(quit()));

Stejně jako v GTK+ fungují signály a sloty synchronně, tj. sloty se volají okamžitě po emitování signálu a emit se vrátí, teprve až skončí poslední slot. Výhoda oproti GTK+ je v tom, že při napojování Qt kontroluje, zda signál a slot mají shodný počet parametrů i jejich typy. Připojený slot lze od signálu opět odpojit pomocí metody QObject::discon­nect.

Události jsou generovány okenním systémem (např. QMouseEvent), časovačem (QTimerEvent) nebo aktivitou na soketu (interní události použité v implementaci QSocketNotifier). Události si mohou posílat mezi sebou i objekty v rámci programu. Pro tento účel je vyhrazen typ QCustomEvent, ale lze použít i libovolný jiný typ. Když objekt dostane událost, volá se nejprve metoda event. Ta předá událost k dalšímu zpracování specifické metodě podle typu události, např. paintEvent pro událost QPaintEvent. Když widget odmítne voláním metody ignore událost pocházející z klávesnice nebo z myši (QKeyEvent,QMou­seEvent), bude se událost propagovat do rodičovského widgetu. Objekt se může pomocí QObject::insta­llEventFilter zaregistrovat jako filtr (event filter) pro nějaký jiný objekt. Filtr dostává všechny události určené pro druhý objekt dřív než on. Jestliže potřebujeme odchytávat úplně všechny události v programu, lze toho dosáhnout instalací filtru na aplikační objekt QApplication. Filtry událostí používá v Qt např. třída QAccel, která odchytává kombinace kláves definované jako akcelerátory. Události se zpracovávají asynchronně. Každá událost je uložena do fronty, odkud ji někdy později vybere cyklus zpracování událostí QApplication::exec. Do fronty se dá přidat libovolná událost voláním QApplication::pos­tEvent. Při použití pro komunikaci uvnitř programu lze s událostmi pracovat i synchronně. Metoda QApplication::sen­dEvent volá přímo handler události v cílovém widgetu.

Správa paměti

Qt používá velmi jednoduchý, nicméně dobře fungující model správy paměti. Objekty tříd odvozených z QObject jsou organizovány do stromových struktur. Při vytvoření nového objektu se zadává jeho rodič jako parametr konstruktoru. Když je 0, vytvoří se kořen nového stromu. Při zrušení objektu se Qt postará o automatické zrušení všech jeho potomků ve stromu objektů. Ve třídě QObject jsou definovány metody pro manipulaci se stromy objektů. Metoda objectTrees vrací seznam kořenů všech existujících stromů. Rodičovský a synovské objekty se dají zjistit voláním metod parent a children. Ke změně stromu slouží metody insertChild a removeChild.

Widgety, tj. objekty odvozené z třídy QWidget navíc vytvářejí stromy widgetů. Ty určují, jak budou jednotlivé widgety do sebe vnořeny na obrazovce. Kořeny stromů widgetů jsou top-level okna. Obvykle strom widgetů koresponduje se stromem objektů, takže při zrušení widgetu se automaticky zruší všechny widgety, které jsou v něm vizuálně obsaženy. Při vytvoření objektu se rodič zadaný jako parametr konstruktoru použije pro zapojení jak do stromu objektů, tak i do stromu widgetů. Změny ve stromě widgetů je možné provádět metodou QWidget::reparent. Je třeba dávat si pozor na to, že přesun widgetu v rámci stromů objektů nemění pozici widgetu ve stromě widgetů. Pokud má být zachována korespondence obou struktur, je nutné spolu s QObject::in­sertChild nebo QObject::remo­veChild volat také QWidget::reparent.

Při zrušení objektu mohou na různých místech programu zbýt ukazatele na již neexistující objekt. Jejich dereferencování typicky způsobí havárii programu. Qt proto definuje šablonu QGuardedPtr, což je ukazatel, který se automaticky nastaví na 0 při zrušení objektu, na nějž ukazuje.

Programátoři používající jazyk C znají dobře problémy s rozhodováním, kdo je vlastníkem nějaké datové struktury a měl by se postarat o její dealokaci. Např. při předávání řetězce – parametru typu char * – do funkce musíme vědět, zda si funkce udělá kopii řetězce, nebo ne. A když ne, zda funkce řetězec dealokuje, nebo zda ho má dealokovat volající. V grafické aplikaci je mnoho datových struktur s podobnými problémy: obrázky, fonty, kurzory, apod. GTK+ řeší problém s tím, kdo a kdy má objekt zrušit, pomocí počítání referencí. Tím ale není vyřešen jiný problém – často bychom chtěli předávat parametry funkcí hodnotou, aby následně funkce i volající mohli datovou strukturu měnit a změny provedené uvnitř funkce se neprojevily vně a naopak. Jenže datové struktury, jako jsou pixmapy, mohou být hodně velké a jejich kopírování je drahá operace. Pokud funkce ani volající nebudou následně hodnotu měnit, bylo kopírování zbytečné. Když se naopak parametr předává odkazem (resp. pomocí ukazatele), nekopíruje se datová struktura při volání funkce, ale musíme mít na paměti, že když ji chceme změnit, musíme si nejdřív udělat kopii.

root_podpora

Qt odstraňuje oba problémy pomocí datových struktur s implicitně sdílenými daty. Objekty QString, QBitmap, QPixmap, QCursor apod. jsou ve skutečnosti velmi malé struktury, které obsahují ukazatel na data uložená v samostatně alokovaném bloku paměti. Používá se zde počítání referencí a copy-on-write. U dat je poznamenáno, kolik objektů je používá. Když tento počet klesne na 0, paměť s daty je uvolněna. Jestliže je počet odkazů větší než jedna a některý objekt chce data změnit, nejprve si automaticky vytvoří privátní kopii a změna dat se provede v ní. V Qt je tedy možné předávat např. řetězce (typuQString) hodnotou bez obav, že bude program zpomalen zbytečným kopírováním.

V instancích šablony QMemArray a ve třídách QImage a QMovie se používá explicitní sdílení dat. To znamená, že kopie objektu bude s původním objektem sdílet data, ale nefunguje copy-on-write. Změnu provedenou prostřednictvím jednoho objektu uvidí všechny objekty používající stejná data. Objekt se může odpojit od sdílené kopie a vytvořit si privátní kopii dat voláním metody detach nebocopy.

Byl pro vás článek přínosný?