Die ersten Schritte in Qt

Willkommen in der Welt von Qt – dem Werkzeug für plattformunabhängige GUI-Entwicklung (GUI = graphical user interface, also eine graphische Benutzerschnittstelle). In dieser Einführung erklären wir die Grundlagen von Qt, am Beispiel eines einfachen Notepad-Programmes, welches wir Schritt für Schritt entwickeln werden. Nach der Lektüre dieses Artikels, werden Sie in der Lage sein, tiefer in die Übersichten und API-Dokumentationen einzutauchen und die Informationen zu finden, die Sie bei der Entwicklung Ihrer Anwendungen benötigen.

Hallo Notepad

In unserem ersten Beispiel soll einfach ein Eingabefeld in einem Fenster angezeigt werden – das wohl einfachste Qt Programm mit einer GUI.

Ein Texteingabefenster

Und hier ist der zugehörige Quellcode:

  1. #include <QApplication>
  2. #include <QTextEdit>
  3.  
  4. int main(int argv, char **args)
  5. {
  6.      QApplication app(argv, args);
  7.      QTextEdit textEdit;
  8.      textEdit.show();
  9.          
  10.      return app.exec();
  11. }

Gehen wir nun den Code Schritt für Schritt durch. In den ersten beiden Zeilen werden die Header-Dateien für QApplication [doc.trolltech.com] und QTextEdit [doc.trolltech.com] eingebunden, welches die beiden Klassen sind, die wir für dieses Beispiel benötigen. Für jede Klasse in Qt existiert eine nach ihr benannte Header-Datei.

In Zeile 6 wird das QApplication-Objekt angelegt. Dieses Objekt verwaltet die Anwendungs-Ressourcen und wird für jedes Qt-Programm benötigt, welches eine GUI besitzt. Es werden außerdem die Variablen argv und args übergeben, da Qt auch einige Kommandozeilenparameter verarbeitet.

In Zeile 7 wird das QTextEdit-Objekt erstellt. Ein Texteingabefeld ist ein graphisches Element in der GUI. In Qt, werden solche Elemente Widgets genannt. Beispiele für andere Widgets sind Schieberegler (scroll bars), Beschriftungen (labels), oder eine Options-Auswahl (radio buttons). Ein Widget kann außerdem als Behälter für andere Widgets dienen; ein Dialog oder das Anwendungsfenster sind beispielsweise solche Behälter.

Danach wird unser Texteingabefeld in Zeile 9 in seinem eigenen Fensterrahmen schließlich sichtbar gemacht. Da Widgets auch die Funktion von Containern erfüllen (zum Beispiel ein QMainWindow, welches Werkzeugleisten, Menüs, eine Statusbar und einige andere Widgets enthalten könnte), ist es möglich ein einzelnes Widget in seinem eigenen Fenster anzeigen zu lassen. Widgets sind standardmäßig nicht sichtbar, sie können aber mit der Funktion show() sichtbar gemacht werden.

Zeile 10 schließlich startet die Ereignisschleife von QApplication. Sobald eine Qt Anwendung läuft, werden Ereignisse erzeugt und an die Widgets der Anwendung geschickt. Solche Ereignisse werden beispielsweise beim Drücken von Maus- und Tastaturtasten generiert. Sobald man einen Text in das Eingabefeld eingibt, empfängt dieses die “Taste gedrückt”-Ereignisse und reagiert darauf in
dem es den eingegebenen Text darstellt.

Um die Anwendung zu erstellen und auszuführen, öffne die Kommandozeile und wechsele in das Verzeichnis, in dem sich die .cpp Datei des Programms befindet. Tippe folgendes ein, um das Programm zu erstellen:

  1. qmake -project
  2. qmake
  3. make

Nun existiert eine ausführbare Datei im part1-Verzeichnis (Unter Windows kann es sein, dass anstatt make der Befehl nmake verwendet werden muss. Außerdem wird die Anwendung im Verzeichnis part1/debug, oder part1/release erstellt). qmake ist Qt’s Werkzeug zum Erstellen und benutzt dazu eine Konfigurationsdatei. qmake generiert diese Datei für uns, wenn wir es mit dem Argurment “-project” aufrufen. Es verarbeitet die Konfigurationsdatei (mit der Dateiendung .pro) und erzeugt eine Makefile, welche für die Erstellung des Programmes benötigt wird. Wir werden das Schreiben eigener .pro-Dateien später noch behandeln.

Weitere Informationen

Hinzufügen eines Beenden-Knopfes

In einer realen Anwendung wird normalerweise mehr benötigt, als ein einzelnes Widget. Daher werden wir nun einen QPushButton [doc.trolltech.com] unter dem Eingabefeld anlegen, der die Anwendung schließt, sobald er gedrückt wird (z.B. wenn mit der Maus darauf geklickt wird).

Ein Knopf zum Beenden der Anwendung

Werfen wir zunächst einen Blick auf den Code:

  1. #include <QtGui>
  2.  
  3. int main(int argv, char **args)
  4. {
  5.      QApplication app(argv, args);
  6.          
  7.      QTextEdit textEdit;
  8.      QPushButton quitButton("Quit");
  9.          
  10.      QObject::connect(&quitButton, SIGNAL(clicked()), qApp, SLOT(quit()));
  11.          
  12.      QVBoxLayout layout;
  13.      layout.addWidget(&textEdit);
  14.      layout.addWidget(&quitButton);
  15.          
  16.      QWidget window;
  17.      window.setLayout(&layout);
  18.          
  19.      window.show();
  20.          
  21.      return app.exec();
  22. }

In Zeile 1 wird der Header QtGui eingebunden, der alle GUI Klassen in Qt enthält.

Interessant ist Zeile 10 in der wir Qt’s Slot und Signal Mechanismus verwenden, um die Applikation zu beenden, sobald der Beenden-Button gedrückt wurde. Ein Slot ist eine Funktion, die zur Laufzeit über ihren Namen(eine Zeichenkette bestehend aus Buchstaben) aufgerufen werden kann. Ein Signal hingegen ist eine Funktion die, wenn sie aufgerufen wird, alle Slots, die mit dem Signal verbunden wurden, aufruft. Wir sagen dazu auch: Der Slot wird mit dem Signal verbunden (connect) und das Signal wird emittiert (emit), also gesendet.

quit() ist ein Slot von QApplication, der die Anwendung beendet. clicked() ist ein Signal, welches von QPushButton emittiert wird, nachdem der Knopf gedrückt wurde. Die statische Funktion QObject::connect() übernimmt das Verbinden von Slot und Signal. SIGNAL und SLOT sind zwei Makros, welche die Funktionssignaturen des Signals und des Slots, die verbunden werden sollen, übernehmen. Außerdem müssen wir die Zeiger zu den Objekten, welche das Signal senden und empfangen sollen, übergeben.

In Zeile 12 wird ein QVBoxLayout [doc.trolltech.com] erstellt. Wie schon erwähnt wurde, können Widgets als Behälter für andere Widgets dienen. Es ist zwar möglich, die Größe und Position von Kind-Widgets direkt zu setzen, oft ist es aber einfacher dafür ein Layout zu verwenden. Ein Layout verwaltet die Abmessungen und Lage der Kinder in einem Widget. QVBoxLayout beispielsweise, ordnet seine Kind-Widgets vertikal in einer Spalte an.

Das Texteingabe-Feld und der Knopf werden dem Layout in Zeile 13 und 14 hinzugefügt. In Zeile 17 wird schließlich das Layout einem Widget zugewiesen.

Weitere Informationen

Von QWidget ableiten

Wenn der Nutzer die Anwendung schließt, möchte man vielleicht, dass ein Dialog geöffnet wird, in dem sie oder er gefragt wird, ob die Anwendung wirklich geschlossen werden soll. In diesem Beispiel werden wir eine eigene, von QWidget abgeleitete, Klasse erstellen, und einen Slot hinzufügen, den wir dann mit dem Beenden-Knopf verbinden.

Von QWidget ableiten

Schauen wir uns den Code an

  1. class Notepad : public QWidget
  2. {
  3.      Q_OBJECT
  4.  
  5.      public:
  6.           Notepad();
  7.        
  8.      private slots:
  9.           void quit();
  10.        
  11.      private:
  12.           QTextEdit *textEdit;
  13.           QPushButton *quitButton;
  14.  };

Das Q_OBJECT Makro muss als erstes in unsere Klassendefinition, und deklariert die Klasse als QObject (Natürlich muss sie auch von QObject abgeleitet sein). QObject fügt einer normalen C++-Klasse verschiedene Fähigkeiten hinzu. Beispielsweise können zur Laufzeit der Klassenname und Slotnamen ermittelt werden. Es ist auch möglich die Parametertypen eines Slots abzufragen und ihn auszuführen.

In Zeile 9 wird der Slot quit() deklariert, was mit dem slots-Makro recht einfach ist. Der quit()-Slot kann nun zu Signalen mit passender Signatur verbunden werden (in diesem Fall zu Signalen, die keine Parameter haben).

Anstatt in der main()-Funktion die GUI zu erstellen und den Slot zu verbinden, nutzen wir nun den Konstruktor der Notepad-Klasse.

  1. Notepad::Notepad()
  2. {
  3.      textEdit = new QTextEdit;
  4.      quitButton = new QPushButton(tr("Quit"));
  5.      
  6.      connect(quitButton, SIGNAL(clicked()), this, SLOT(quit()));
  7.  
  8.      QVBoxLayout *layout = new QVBoxLayout;
  9.      layout->addWidget(textEdit);
  10.      layout->addWidget(quitButton);
  11.  
  12.      setLayout(layout);
  13.  
  14.      setWindowTitle(tr("Notepad"));
  15. }

Wie man in der Klassen-Definition gesehen hat, haben wir Zeiger auf unsere QObjects (textEdit und quitButton) verwendet. Eine Faustregel ist, QObjects immer auf dem Heap anzulegen und sie niemals zu kopieren.

Dann haben wir die tr()-Funktion auf unsere Texte angewendet. Diese Funktion ist notwendig, wenn Sie Ihre Applikation in mehreren Sprachen (z.B. in Englisch oder Chinesisch) anbieten möchten. Wir werden hier nicht weiter auf Details eingehen, aber Sie können dem Qt Linguist Link weiter unten folgen, um mehr Informationen zu erhalten.

Weitere Informationen

Eine .pro Datei erstellen

In diesem Beispiel schreiben wir unsere eigene .pro Datei, anstatt von qmake’s -project Option Gebrauch zu machen.

  1.  HEADERS =  notepad.h
  2. SOURCES =  notepad.cpp \
  3.          main.cpp

Die folgenden Befehle erstellen das Beispiel-Projekt:

  1. qmake
  2. make

Verwendung von QMainWindow

Für viele Anwendungen ist es von Vorteil, QMainWindow [doc.qt.nokia.com] zu benutzen, da es sein eigenes Layout mitbringt, zu dem man eine Menüleiste, Andockende Widgets, Werkzeugleisten und eine Statusleiste hinzufügen kann. QMainWindow [doc.qt.nokia.com] besitzt einen zentralen Bereich, der von jedem beliebigen Widget genutzt werden kann. In unserem Fall werden wir dort das Texteingabefeld platzieren.

Texteingabefeld in einem QMainWindow

Betrachten wir die neue Notepad-Klassendefinition:

  1.  #include <QtGui>
  2.  
  3. class Notepad : public QMainWindow
  4. {
  5.      Q_OBJECT
  6.  
  7.      public:
  8.           Notepad();
  9.  
  10.      private slots:
  11.           void open();
  12.           void save();
  13.           void quit();
  14.  
  15.      private:
  16.           QTextEdit *textEdit;
  17.  
  18.           QAction *openAction;
  19.           QAction *saveAction;
  20.           QAction *exitAction;
  21.  
  22.           QMenu *fileMenu;
  23. };

Um ein Dokument speichern und öffnen zu können, fügen wir zwei neue Slots hinzu, die wir im nächsten Abschnitt implementieren.

Es kommt häufig vor, dass im Hauptfenster der selbe Slot von mehreren Widgets aufgerufen werden soll. Zum Beispiel von Menüeinträgen und Knöpfen auf einer Werkzeugleiste. Um das zu vereinfachen, bietet Qt die Klasse QAction [doc.qt.nokia.com] an, welche an mehrere Widgets übergeben, und an einen Slot gebunden werden können. Zum Beispiel können sowohl QMenu [doc.qt.nokia.com], als auch QToolBar [doc.trolltech.com] Menüeinträge und Werkzeug-Knöpfe für die gleichen QActions anlegen. Wie das funktioniert sehen wir gleich.

Genau wie vorhin, nutzen wir den Konstruktor unserer Notepad-Klasse, um die GUI zu erstellen.

  1. Notepad::Notepad()
  2. {
  3.      saveAction = new QAction(tr("&Open"), this);
  4.      saveAction = new QAction(tr("&Save"), this);
  5.      exitAction = new QAction(tr("E&xit"), this);
  6.  
  7.      connect(openAction, SIGNAL(triggered()), this, SLOT(open()));
  8.      connect(saveAction, SIGNAL(triggered()), this, SLOT(save()));
  9.      connect(exitAction, SIGNAL(triggered()), qApp, SLOT(quit()));
  10.  
  11.      fileMenu = menuBar()->addMenu(tr("&File"));
  12.      fileMenu->addAction(openAction);
  13.      fileMenu->addAction(saveAction);
  14.      fileMenu->addSeparator();
  15.      fileMenu->addAction(exitAction);
  16.  
  17.      textEdit = new QTextEdit;
  18.      setCentralWidget(textEdit);
  19.  
  20.      setWindowTitle(tr("Notepad"));
  21. }

QAction [doc.qt.nokia.com] werden mit dem Text erstellt, der auch in den Widgets erscheinen soll, zu denen wir sie hinzufügen (in userem Fall Menüeinträge). Wollten wir sie auch zu einer Werkzeugleiste hinzufügen, könnten wir für die Aktionen auch Icons festlegen.

Wenn nun einer der Menüeinträge ausgewählt wird, löst der Eintrag die Aktion aus und der daran gebundene Slot wird aufgerufen.

Weitere Informationen

Speichern und Laden

Dieses Beispiel zeigt die Implementierung der Laden- und Speichern-Slots, die wir im vorigen Beispiel hinzugefügt haben.

Der Öffen-Dialog

Beginnen wir mit dem Öffnen-Slot

  1. QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"), "", tr("Text Files (*.txt);;C++ Files (*.cpp *.h)"));
  2.      
  3. if (fileName != "") {
  4.      QFile file(fileName);
  5.      if (!file.open(QIODevice::ReadOnly)) {
  6.           QMessageBox::critical(this, tr("Error"), tr("Could not open file"));
  7.            return;
  8.      }
  9.      QString contents = file.readAll().constData();
  10.      textEdit->setPlainText(contents);
  11.      file.close();
  12. }

Im ersten Schritt wird der Anwender nach dem Namen der zu öffnenden Datei gefragt. Qt bietet dazu den QFileDialog [doc.trolltech.com], in dem der Nutzer eine Datei auswählen kann. Das Bild zeigt den Dialog unter Kubuntu. Die statische Funktion getOpenFileName() zeigt einen modalen Datei-Auswahl-Dialog der erst zur Hauptanwendung zurückkehrt, wenn der Anwender eine Datei ausgewählt, oder den Dialog geschlossen hat. Der Rückgabewert ist entweder der Pfad zur gewählten Datei, oder eine leere Zeichenkette, wenn der Nutzer den Dialog abgebrochen hat.

Nachdem wir überprüft haben, ob ein Dateipfad zurückgegeben wurde, versuchen wir die Datei mit der open()-Methode zu öffnen. Diese gibt den Wert true zurück, wenn die Datei erfolgreich geöffnet werden konnte. Wir werden an dieser Stelle nicht über Fehlerbehandlung sprechen, aber sie können den Links unter dem Eintrag “Weiterführende Informationen” folgen. Konnte die Datei nicht geöffnet werden, verwenden wir QMessageBox [doc.trolltech.com], um den Fehler in einem Dialog an zu zeigen (weitere Details können Sie in der Klassenbeschreibung von QMessageBox [doc.trolltech.com] nachlesen).

Das Lesen der Datei trivial. Wenn man die Funktion readAll() verwendet, wird der gesamte Inhalt der Datei in einem QByteArray [doc.qt.nokia.com] zurück liefert. Die Funktion constData() liefert alle Daten des Arrays als einen const char* Pointer zurück, wofür es in der QString Klasse einen Konstruktor gibt. Der Text kann dann im Textfeld angezeigt werden. Damit der Datei-Deskriptor wieder an das System zurück gegeben werden kann, schließen wir am Ende die Datei mit der close()-Methode

Lassen Sie uns jetzt mit dem Save() Slot weiter machen

  1. QString fileName = QFileDialog::getSaveFileName(this, tr("Save File"), "", tr("Text Files (*.txt);;C++ Files (*.cpp *.h)"));
  2.  
  3. if (fileName != "") {
  4.      QFile file(fileName);
  5.      if (!file.open(QIODevice::WriteOnly)) {
  6.           // error message
  7.      } else {
  8.           QTextStream stream(&file);
  9.           stream << textEdit->toPlainText();
  10.           stream.flush();
  11.           file.close();
  12.      }
  13. }

Um den Inhalt des Textfeldes in eine Datei zu schreiben, benutzen wir die QTextStream [doc.trolltech.com] Klasse, welche das QFile-Objekt umhüllt. Der Text-Stream kann QStrings direkt in die Datei schreiben; QFile [doc.trolltech.com] aktzeptiert in der write()-Funktion von QIODevice [doc.trolltech.com] hingegen nur Rohdaten (char*).

Weitere Informationen
  • Dateien und EA-Geräte: QFile [doc.qt.nokia.com], QIODevice [doc.qt.nokia.com]

Categories: