Qt-Programmierung


[ Seminar Linux, WWW, Java und Internet ] ... [ Thema KDE ] ... [ KDE-Programmierung ]

Übersicht


Allgemeines zu Qt

Qt (gesprochen 'cute') ist eine C++ Klassenbibliothek und ein GUI-Toolkit für Unix- und X11-Systeme.

GUI-Toolkits sind in der Windows-Welt eher unbekannt. Unter Unix-Systemen jedoch weit verbeitet. Dies liegt daran, dass die Windows-API bereits viele Funktionen wie Knöpfe, Rollbalken und Menüs anbietet. Dies ist unter Unix nicht der Fall. Das X Window System, die vorherrschende graphische Benutzeroberfläche, ist zwar extrem flexibel, bietet dem Softwareentwickler aber nicht allzu viele Hilfsmittel. Es stehen nur eher primitive Operationen wie Linien und Rechtecke zeichnen oder Vorder- und Hintergrundfarbe setzen zur Verfügung. Ausserdem meldet das System Benutzeraktionen an das Programm. Der Vorteil dieses Minimalismus ist, dass all dies netzwerktransparent passieren kann.

Natürlich möchte niemand auf diese Weise eine ganze Applikation entwickeln. Deshalb sind mit der Zeit diverse Toolkits entstanden, die 'High-Level'-Funktionen anbieten. Eines der Bekanntesten ist Motif, mit dem unter anderem auch der CDE (Common Desktop Environment) sowie die Unixversionen von Netscape entwickelt worden sind. Weitere Toolkits sind gtk, wxWindows und eben Qt.
Eine der bemerkenswertesten Eigenschaften von Qt ist seine Portabilität. Wenn man sich bei der Programmierung geschickt anstellt, dann kann ein unter Linux mit Qt entwickeltes Programm nach einer einfachen Neukompilierung genauso unter Windows lauffähig sein.

Dabei können sich die GUI-Elemente von Qt der jeweiligen Oberfläche anpassen, so dass es unter Unix wie ein Motif-Programm und unter Windows wie mit der MFC entwickelt erscheint. Dies wird dadurch erreicht, dass Qt alle GUI-Elemente selbst zeichnet und daher von der jeweiligen Plattform nur die einfachen Zeichenroutinen verwendet. Andere plattformübergreifende Toolkits wählen einen anderen Ansatz. So werden beim Java-AWT (zumindest in den Versionen vor Swing) alle Java-API-Aufrufe an die zugrundeliegende System-API weitergeleitet. Diesen Ansatz bezeichnet man auch als 'API Layering'. Eine andere Methode besteht darin die API-Funktionen eines bekannten Systems nachzubilden und auf ein fremdes System umzuleiten. Dieses Verfahren, das zum Beispiel beim WINE-Projekt gewählt wurde, nennt man 'API Emulation'.

Ein Punkt, der im Zusammenhang mit Qt immer wieder zu heftigen Disputen führt (wer Spaß an solchen Flamewars hat kann ja mal auf Slashdot nach den Stichworten 'qt' und 'kde' suchen), ist die Lizenz unter der Qt vertrieben wird. Solange man mit Qt nur Freie Software entwickelt, brauchen keine Lizenzgebühren an die Herstellerfirma Troll Tech entrichtet werden. Programmierer kommerzieller Anwendungen müssen dies allerdings tun. Desweiteren war es bisher nicht möglich, Fehler in der Bibiliothek einfach zu korrigieren und dieser Umstand ist es, der vielen Leuten in der 'Szene' sauer aufstösst. Die neueste Version von Qt (2.0.2) steht jedoch unter einer neuen Lizenz (der QPL), die selbst vom Gründer der FSF (Free Software Foundation) Richard M. Stallman als 'Freie Software' anerkannt wurde. Bleibt nur zu hoffen, dass die Streitereien damit in Zukunft ein Ende haben.

Hello World

Nun ist es an der Zeit, sich die Finger dreckig zu machen und etwas Code zu schreiben. Als Beispiel muss wie üblich das traditionelle 'Hello world'-Programm herhalten.

Die erste Programmversion wird ein kleines Fenster erzeugen, in dem der berühmte Text stehen soll. Obwohl der Quelltext relativ simpel ist, enthält er bereits eine Menge Elemente, die ein Qt-Programm ausmachen.
   helloqt.cpp
   1 #include <qapplication.h>
   2 #include <qlabel.h>
   3
   4 int main(int argc, char* argv[])
   5 {
   6   QApplication myApp(argc, argv);
   7
   8   QLabel* myLabel = new QLabel("Hello world");
   9   myLabel->resize(80, 30);
  10
  11   myApp.setMainWidget(myLabel);
  12   myLabel->show();
  13
  14   return myApp.exec();
  15 }
  
In den ersten beiden Zeilen werden Qt-Headerdateien eingebunden. Üblicherweise tragen sie den gleichen Namen wie die zugehörigen Klassen in Kleinbuchstaben und mit der Erweiterung .h statt .cpp. In einigen seltenen Fällen kommt es auch mal vor, dass mehrere Klassen in einer einzigen Headerdatei deklariert werden. So finden sich beispielsweise QListView als auch QListViewItem in der Datei qlistview.h.

In diesem Beispiel wird qapplication.h für die Klasse QApplication und qlabel.h für QLabel eingebunden.

Die nächste wichtige Anweisung steht in Zeile 6. Dort wird eine Instanz der Klasse QApplication erzeugt. Jede Qt-Applikation muss genau ein Objekt dieser Klasse besitzen. Dieses Objekt ist für die gesamte Ereignisbehandlung verantwortlich und hält auch ansonsten alles zusammen.

Wie man sieht erhält der Konstruktor als Parameter die Kommandozeilenargumente. Dies geschieht, weil QApplication einige spezielle Argumente verarbeiten und dann aus den Variablen entfernen kann. So kann man beispielsweise mit dem Paramter -style das Aussehen der GUI-Elemente festlegen.
In Zeile 8 wird ein Objekt der Klasse QLabel erzeugt. Dieses Objekt wird verwendet, um einfache Schriften oder Grafiken darzustellen.

In Qt-Programmen sollten alle Widgets auf dem Heap (das heißt also mit new) erzeugt werden, da sie dann, wenn ihr Elternobjekt gelöscht wird, automatisch mit entfernt werden.
Anschließen wird in Zeile 9 die Größe des Labels geändert, damit der Text 'hello word' gerade genug Platz hat.
Die Anweisung in Zeile 11 ist sehr wichtig. Hier wird der QApplication mitgeteilt, dass das QLabel ihr Main widget ist. Das Main widget ist dasjenige Widget, welches die Anwendung beendet, wenn es geschlossen wird. Sollte kein Main widget definiert worden sein, so kann man das Fenster zwar über dem Schliessenknopf schliessen, das Programm würde aber versteckt weiterlaufen.

Als nächstes muss das Label sichtbar gemacht werden. Geschieht dies nicht, so wäre nur ein leeres Fenster zu sehen, da alle Widgets, die nicht Kinder eines bereits sichtbaren Widgets sind, per Voreinstellung unsichtbar sind.

Die Anweisung in Zeile 14 startet die Ereignisbehandlung. Die QApplication fängt nun alle Ereignisse, die ihr von X mitgeteilt werden ab und leitet sie an die richtigen Widgets weiter.

Nun ist es an der Zeit das Programm zu kompilieren und zu linken. Dies geschieht durch Eingabe folgender Anweisung:
$> g++ -I$QTDIR/include -L$QTDIR/lib -lqt -o helloqt helloqt.cpp
Wenn Qt korrekt installiert worden ist, erscheint jetzt nach Aufruf von helloqt folgendes Fenster auf dem Bildschirm:

[Hello Qt]


Hello World mit einem Endeknopf

Im folgenden wird das obige Beispiel mit einem Knopf, der das Programm beenden soll, erweitert. In Qt werden Knöpfe durch die Klasse QPushButton, welche eine Unterklasse von QButton, welche wiederum von QWidget abgeleitet worden ist, repräsentiert.

Das Erzeugen eines Buttons unterscheidet sich nicht sehr von der Art wie das Label im letzten Beispiel hinzugefügt wurde. Viel interessanter ist, dass man an diesem Beispiel sehen kann, wie Qt Benutzeraktivitäten verarbeitet.
   helloqt2.cpp
   1 #include <qapplication.h>
   2 #include <qlabel.h>
   3 #include <qpushbutton.h>
   4
   5 int main(int argc, char* argv[])
   6 {
   7   QApplication myApp(argc, argv);
   8
   9   QWidget* myWidget = new QWidget();
  10   myWidget->setGeometry(400, 300, 120, 90);
  11
  12   QLabel* myLabel = new QLabel("Hello world", myWidget);
  13   myLabel->resize(80, 30);
  14
  15   QPushButton* myQuitButton = new QPushButton("Quit", myWidget);
  16   myQuitButton->setGeometry(10, 50, 100, 30);
  17   QObject::connect(myQuitButton, SIGNAL(clicked()), &myApp, SLOT(quit()));
  18
  19   myApp.setMainWidget(myWidget);
  20   myWidget->show();
  21
  22   return myApp.exec();
  23 }
  

[Hello Qt mit Quitbutton]

Um mehr als ein Widget darzustellen, mussten einige Dinge verändert werden. So dient jetzt ein drittes GUI-Element als Container für den Text und den Knopf. Dieses zusätzliche Element trägt den Namen myWidget und ist eine Instanz der Klasse QWidget. Das bedeutet es hat keinerlei spezielle Eigenschaften, kennt aber alle generellen Widgetfunktion wie Verschieben und Vergrößern.

In den Konstruktoren von QLabel und QPushButton wird myWidget als zweiter Parameter mit übergeben. Dies ist sehr wichtig, denn dadurch wird myWidget zum Elternwidget der beiden GUI-Elemente.

Ein weiterer Unterschied ist, dass die Größen und Positionen der Widgets jetzt explizit mit der Methode setGeometry() gesetzt werden. Die vier Parameter sind horizontale Position, vertikale Position, Breite und Höhe in Pixeln. Während die Positionsangaben für das Label und den Button relativ zum Elternwidget angegeben werden, beziehen sich diese Angaben für das Main widget auf den gesamten Bildschirm.

Zeile 17 scheint auf den ersten Blick ziemlich verwirrend. Sie wird im nächsten Abschnitt 'Signals und Slots' genauer besprochen. Erstmal nur soviel: Immer wenn ein Knopf gedrückt wird, sendet er ein Signal aus (in diesem Fall ein clicked()-Signal. Dieses Signal bedeutet in etwa soviel wie 'An alle die es interessiert: Jemand hat mich angeklickt. Mach was immer Du willst mit dieser Information'. Andere Programmteile können sich nun zu diesem Widget connecten und werden dann über jeden Klick informiert. Um sich mit einem Signal verbinden zu können, muss ein Slot zur Verfügung gestellt werden, welcher dann mit dem Signal verbunden wird. In der Slotmethode kann alles mögliche gemacht werden, um das Ereignis zu verarbeiten. In diesem Fall wird der Slot quit() der QApplication aufgerufen und das Programm dadurch beendet.

Signale und Slots

Das zugrundeliegende Fenstersystem meldet Benutzeraktionen (Ereignisse oder Events) auf relativ primitive Art und Weise, indem beispielsweise verkündet wird, dass der Anwender die linke Maustaste gedrückt hat während sich der Mauspfeil an Position 123, 456 befand. Natürlich kann man jetzt anhand der Positionsangabe das Widget, das angeklickt wurde, bestimmen und dann entsprechend reagieren, aber diese Aufgabe wäre sehr mühsam, fehleranfällig und auch langweilig. Eine Aufgabe die unser Toolkit übernehmen sollte.

Wichtig für den Entwickler ist jetzt wie es programmiertechnisch umgesetzt wird, dass der richtige Code als Reaktion auf den Mausclick ausgeführt wird. Die gebräuchlichen Toolkits unterscheiden sich zum Teil fundamental in diesem Punkt. Motif verwendet Callback-Funktionen, wxWindows und das Java-AWT virtuelle Methoden und bei der MFC-Progammierung benutzt man spezielle Makros, um Ereignisse an Methoden zu binden.

Qt verwendet einen neuen Ansatz, dessen Grundidee es ist, dass jedes Widget ein Signal aussenden (emittieren) kann, dass etwas passiert ist ohne zu wissen wer diese Information verwertet. Auf der anderen Seite sitzt der Code, der ausgeführt werden soll, wann immer ein spezielles Ereignis auftritt. Die Methode kann mit jedem Signal verbunden werden. Es muss einzig darauf geachtet werden, dass sie vom richtigen Typ sind. Um nun Signal und Code zusammenzubringen, müssen beide verbunden (connect) werden. Dies ist nur dann möglich, wenn die entsprechende Methode vorher als Slot deklariert worden ist.

Qt definiert einige neue Schlüsselwörter, die vom Preprozessor in korrekten C++-Code übersetzt werden. Desweiteren wird noch ein zusätzlicher Preprozessor benötigt, der aus der Klassendefinition die Informationen über Signale und Slots extrahiert und eine Art 'Verbindungscode' erzeugt. Dieser 'Meta Object Compiler' (kurz moc) ist ebenfalls im Qt-Paket enthalten.

Signale und Slots können nur als Teil einer C++-Klasse auftreten. Eine alleinstehende Funktion kann niemals ein Signal oder ein Slot werden. Desweiteren muss jede Klasse, die Signale oder Slots definieren soll, von der Klasse QObject abgeleitet worden sein. Dies macht in der Praxis auch kaum Probleme, da die meisten verwendeten Klassen in irgendeiner Form mit QWidget verwandt sind, welches wiederum von QObject geerbt hat. Als letztes muss die Klasse noch das Makro Q_OBJECT irgendwo aufrufen.

Im folgenden ist ein 'Rohbau' einer Klasse dargestellt, die Signale und Slots verwendet:
    class MyClass : public QWidget
    {
      Q_OBJECT
       // ...
      signals:
       void machWas();
       // ...
      public slots:
       void slotMachWas();
       // ...
      private slots:
       void slotMachWasAnderes();
       // ...
    }
 

Signale werden mit dem Schlüsselwort signals: deklariert. Allerdings werden sie nur deklariert, aber nicht implementiert. Darum kümmert sich Qt.

Wenn eine Komponente ein Signal aussenden möchte, macht sie dies über das Kommando emit. Im Code sieht dies beispielsweise so aus:
emit highlighted(5);
Slots werden wie jede andere C++-Methode deklariert und implementiert. Sie können auch ganz normal wie jede andere Funktion aufgerufen werden. Sie müssen einzig bei der Deklaration mit slots gekennzeichnet worden sein. Slotmethoden können prinzipiell jede Art von Parametern haben. Wenn sie aber mit einem bestimmten Signal verbunden werden sollen, so müssen sich die Parameter von Signal- und Slotmethode entsprechen.

Um ein Signal mit einem Slot zu verbinden, wird die Methode QObject::connect verwendet. Diese Funktion muss vier Dinge wissen, welches Objekt das Signal aussendet, welches Signal es aussendet, welches Objekt das Signal aufnimmt und welcher Slot mit dem Signal verbunden werden soll. Das Signal und der Slot werden einfach mit ihren Parametertypen direkt angeben und mit SIGNAL() beziehungsweise SLOT gekennzeichnet. Ein typischer Aufruf sind folgendermassen aus:
    QObject::connect(mymenu, SIGNAL(activated(int)), 
                     mycodeobject, SLOT( slotDoMenuFunction(int)));
 

Es können beliebig viele Slots mit einem Signal und beliebig viele Signale mit einem Slot verbunden werden. Die Reihenfolge, in der die Slots bei einem eintretenden Ereignis aufgerufen werden, ist allerdings in keinster Weise festgeschrieben.

Weitere Informationen zu Qt

Weitere Informationen zu Qt und dessen Programmierung findet man in der wiklich sehr guten und umfangreichen Dokumentation, die jeder Qt-Version beiliegen sollte.


[ Seminar Linux, WWW, Java und Internet ] ... [ Thema KDE ] ... [ Qt-Programmierung ] ... [ KDE-Programmierung ]