Einstieg in die Programmierung mit Qt
Einstieg in die Programmierung mit Qt
Willkommen in der Welt von Qt — dem Cross-Plattform-Werkzeug für graphische Benutzeroberflächen (Graphical User Interface = GUI). In dieser Einführung vermitteln wir grundlegende Kenntnisse zu Qt und entwickeln dazu eine einfache Notizzettel-Anwendung. Nach dem Studium dieser Anleitung sollten Sie in der Lage sein, sich eingehend mit unseren Übersichten und der Dokumentation unserer Programmierschnittstellen (APIs) zu beschäftigen, um die benötigten Informationen für die Entwicklung Ihrer Anwendung zu finden.
Den englischen Originaltext zu dieser Übersetzung finden Sie unter der URL http://doc.qt.io/qt-4.8/gettingstartedqt.html bzw. http://doc.qt.io/gettingstartedqt.html.
Diese Anleitung setzt voraus, dass Sie schon ein bisschen mit der Programmiersprache C++ vertraut sind. Für Leser ohne diese Vorkenntnisse gibt es alternativ diese Anleitung: http://stefanfrings.de/qt_lernen/index.html
Für einen aktuellen Zugang zu C++17 und Qt6 empfehlen sich folgende Seiten:
- Für Qt: https://doc.qt.io/qt-6/qtwidgets-tutorials-notepad-example.html (englisch)
- Für C++: https://de.wikipedia.org/wiki/C%2B%2B
Hallo Notizzettel
In diesem ersten Beispiel werden wir einen einfachen Notizzettel (Notepad) erzeugen und als Fenster auf dem Schreibtisch anzeigen. Das stellt das einfachste Qt-Programm mit grafischer Oberfläche dar.
Der Quelltext dazu:
#include <QApplication>
#include <QTextEdit>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QTextEdit textEdit;
textEdit.show();
return app.exec();
}
Wir werden den Code Zeile für Zeile analysieren. In den ersten zwei Zeilen binden wir die Header-Dateien für QApplication und QTextEdit ein, die zwei für dieses Beispiel benötigten Klassen. Für alle Qt-Klassen gibt es Header-Dateien, die genauso benannt sind wie die Klasse selbst.
Zeile 6 erzeugt ein QApplication-Objekt. Es verwaltet die Ressourcen für die gesamte Anwendung und wird immer für eine Qt-Anwendung mit GUI benötigt. Das Objekt benötigt argc und argv, weil Qt-Anwendungen einige Kommandozeilen-Optionen auswerten können.
Zeile 8 erzeugt ein QTextEdit-Objekt. Ein Texteditor ist ein sichtbares Element der GUI. In Qt werden diese Elemente "Widgets" genannt (ein Kunstwort aus Window (Fenster) und Gadget (Vorrichtung)). Beispiele für weitere Widgets sind "scroll bars" (Rollbalken), "Labels" (Beschriftungen/Markierungen) und "radio buttons" (eine Auswahl von Optionen, von denen nur eine ausgewählt werden kann). Ein Widget kann als Container für andere Widgets benutzt werden, beispielsweise ein Dialog oder das Hauptfenster einer Anwendung.
Zeile 9 zeigt den Texteditor als eigenständiges Fenster auf dem Bildschirm an. Da Widgets auch als Container fungieren können (z.B. ein QMainWindow, das Werkzeugleisten, Menüs, eine Statuszeile und einige andere Widgets enthält), kann ein einzelnes Widget auch als einzelnes Fenster auf dem Bildschirm dargestellt werden. Ein Widget ist von Haus aus nicht sichtbar, erst die Funktion show() macht es sichtbar.
In Zeile 11 startet QApplication seine Ereignis-Schleife ("event loop"). Wenn eine Qt-Anwendung läuft, werden verschiedene Ereignisse ("events") erzeugt und an die Widgets der Anwendung weitergereicht. Dabei handelt es sich z.B. um das Drücken einer Maustaste oder einen Tastaturanschlag. Wenn Sie in einem Texteditor Text eingeben, erhält er verschiedene "Taste gedrückt"-Ereignisse und reagiert darauf, indem er den eingegebenen Text auf dem Bildschirm darstellt.
Um die Anwendung auszuführen, öffnen Sie eine Kommandozeilen-Umgebung (shell) und wechseln Sie in das Verzeichnis, in dem sich die .cpp-Datei ihres Programms befindet. Folgende Befehle erzeugen das Programm:
qmake -project
qmake
make
Damit wird eine ausführbare Datei im Verzeichnis teil1 erzeugt (bitte beachten Sie: unter Windows müssen Sie ggf. nmake anstelle von make verwenden. Außerdem wird die ausführbare Datei in teil1\debug oder teil1\release erzeugt). qmake ist das Erstellungs-Werkzeug von Qt und benötigt eine Konfigurationsdatei. qmake erzeugt diese Datei für uns, wenn wir es mit dem Argument -project aufrufen. Zusammen mit der Konfigurationsdatei (mit der Endung .pro) generiert qmake ein Makefile, mit dem das Programm erstellt werden kann. Wir sehen uns später an, wie man .pro-Dateien selbst schreibt.
Weitere Informationen
|. Über… |. Hier… | |Widgets und Fenster-Geometrie | Window and Dialog Widget | |Ereignisse und ihre Behandlung | The Event System |
Einen Knopf zum Beenden hinzufügen
Eine richtige Anwendung benötigt üblicherweise mehr als ein Widget. Wir werden jetzt einen QPushButton unterhalb des Notizzettels einfügen. Die Anwendung wird beendet, wenn der Knopf gedrückt wird, d.h. mit der Maus auf ihn geklickt wird.
- /qt-4.8/images/gs2.png
Sehen wir uns den Code dazu an:
#include <QApplication>
#include <QLayout>
#include <QTextEdit>
#include <QPushButton>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QTextEdit *textEdit = new QTextEdit;
QPushButton *quitButton = new QPushButton("&Quit");
QObject::connect(quitButton, SIGNAL (clicked()), qApp, SLOT (quit()));
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(textEdit);
layout->addWidget(quitButton);
QWidget window;
window.setLayout(layout);
window.show();
return app.exec();
}
In Zeile 13 verwenden wir den Signal-Slot-Mechanismus von Qt, um die Anwendung zu beenden sobald der Beenden-Knopf gedrückt wurde. Ein "Slot" ist eine spezielle Funktion, die zur Laufzeit über ihren Namen (als reine Zeichenkette) aufgerufen werden kann. Ein "Signal" ist ebenfalls eine spezielle Funktion. Wird sie aufgerufen, werden wiederum die bei ihr registrierten Slots ausgeführt. Wir nennen das "den Slot mit dem Signal verbinden" ("connect") bzw. das Signal aussenden ("emit").
quit() ist ein Slot in QApplication und beendet die Anwendung. clicked() ist ein Signal, das von QPushButton ausgesendet wird, wenn er gedrückt wird. Die statische Funktion QObject::connect() dient dazu, den Slot mit dem Signal zu verbinden. SIGNAL () und SLOT() sind zwei Makros, die die Funktions-Signaturen für die zu verbindendenden Signals und Slots erhalten. Außerdem müssen Zeiger auf die Objekte angegeben werden, die das Signal aussenden und empfangen.
In Zeile 15 wird ein QVBoxLayout erzeugt. Wie bereits erwähnt, können Widgets andere Widgets enthalten. Es ist möglich, die Geometrie (Größe und Position) eines Unter-Widgets (Kind-Widget) direkt zu setzen. Üblicherweise ist es aber einfacher, eine Layout-Klasse zu verwenden, die die Geometrie der Kinder eines Widgets verwaltet. Ein QVBoxLayout beispielsweise ordnet die Kinder in einer Zeile nebeneinander an.
In Zeile 16 und 17 wird der Editor und der Knopf in das Layout eingefügt. In Zeile 20 weisen wir das Layout einem Widget zu.
Weitere Informationen
|. Über… |. Hier… | |Signale und Slots | Signals & Slots |Layouts | Layout Management, Widgets and Layouts, Layout Examples | |Die von Qt mitgelieferten Widgets | Qt Widget Gallery, Widget Examples |
Von QWidget abgeleitete Klassen
Wenn der Benutzer die Anwendung beenden möchte, wollen Sie ihn möglicherweise vorher fragen, ob er/sie das wirklich will. In diesem Beispiel erstellen wir eine von QWidget abgeleitete Klasse und fügen einen Slot hinzu, der mit dem Beenden-Knopf verbunden wird.
- /qt-4.8/images/gs3.png
Sehen wir uns den Quelltext an:
#include <QWidget>
#include <QTextEdit>
#include <QPushButton>
class Notepad : public QWidget
{
Q_OBJECT
public:
Notepad();
private slots:
void quit();
private:
QTextEdit *textEdit;
QPushButton *quitButton;
};
Das Makro Q_OBJECT muss als erstes in der Definition der Klasse auftauchen. Es markiert unsere Klasse als QObject- sie muss natürlich auch von QObject (oder einer davon abgeleiteten Klasse) erben. QObject fügt einer normalen C++-Klasse noch einige weitere Fähigkeiten hinzu. Insbesondere können die Namen der Klasse und der Slots zur Laufzeit abgefragt werden. Außerdem ist es möglich, die Parameter eines Slots abzufragen und die dazugehörige Funktion aufzurufen.
In Zeile 13 wird der Slot quit() deklariert. Das geht mit dem Makro slots ganz einfach. Der Slot quit() kann nun mit einem Signal mit passender Signatur (jedes Signal, das keinen Parameter hat) verbunden werden.
Die Benutzeroberfläche erzeugen wir nicht mehr in der Funktion main(), sondern im Konstruktor von Notepad. Dort verbinden wir auch den Slot.
#include "notepad.h"
#include <QApplication>
Notepad::Notepad()
{
textEdit = new QTextEdit;
quitButton = new QPushButton(tr("Quit"));
connect(quitButton, SIGNAL (clicked()), this, SLOT (quit()));
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(textEdit);
layout->addWidget(quitButton);
setLayout(layout);
setWindowTitle(tr("Notepad"));
}
void Notepad::quit()
{
qApp->quit();
}
Wie sie in der Definition der Klasse gesehen haben, benutzen wir Zeiger auf unsere QObject (textEdit und quitButton). Grundsätzlich gilt, dass Sie Instanzen von QObject (und davon abgeleiteten Klassen) auf dem Heap erzeugen und sie niemals kopieren sollten.
Von jetzt an kapseln wir die für den Benutzer sichtbaren Zeichenketten in der Funktion tr(). Das ist notwendig, wenn Sie die Anwendung in mehreren Sprachen (z.B. Deutsch und Englisch) zur Verfügung stellen möchten. Auf Einzelheiten gehen wir an dieser Stelle nicht ein. Mehr erfahren Sie über den Link zu Qt Linguist in der Tabelle mit weiteren Informationen.
Weitere Informationen
|. Über… |. Hier… | |tr() und "Internationalisierung" |Qt Linguist Manual, Writing Source Code for Translation, Hello tr() Example, Internationalization with Qt | | und das Objekt-Model von Qt |[hhttp://doc.qt.io/qt-5//object.html Object Model] | |qmake und das Erstellungs-System von Qt|qmake Manual |
Eine .pro-Datei erzeugen
Für dieses Beispiel werden wir eine eigene .pro-Datei erstellen, und nicht die Option -project von qmake.
HEADERS = notepad.h
SOURCES = notepad.cpp main.cpp
Folgende Befehle erzeugen das Programm
qmake
make
Ein QMainWindow verwenden
Viele Anwendungen werden vom Einsatz eines QMainWindow (Hauptfenster) profitieren. Es hat sein eigenes Layout, dem Sie eine Menüzeile, Dock Widgets (es gibt keine vernünftige deutsche Übersetzung hierfür), Werkzeugleisten und eine Status-Zeile hinzufügen können. QMainWindow hat einen zentralen Bereich, der von jeder Art von Widget ausgefüllt werden kann. In unserem Beispiel werden wir hier den Texteditor platzieren.
- /qt-4.8/images/gs4.png
Sehen wir uns die neue Definition der Klasse Notepad an.
#include <QMainWindow>
#include <QTextEdit>
#include <QAction>
#include <QMenu>
class Notepad : public QMainWindow
{
Q_OBJECT
public:
Notepad();
private slots:
void open();
void save();
void quit();
private:
QTextEdit *textEdit;
QAction *openAction;
QAction *saveAction;
QAction *exitAction;
QMenu *fileMenu;
};
Wir fügen zwei weitere Slots hinzu, mit denen ein Dokument gelesen und gespeichert werden kann. Wir werden sie im folgenden Abschnitt implementieren.
In einem Hauptfenster muss derselbe Slot oftmals von mehreren verschiedenen Widgets aus aufgerufen werden. Dazu gehören beispielsweise Menüpunkte und Knöpfe in einer Werkzeugleiste. Um diese Aufgabe zu erleichtern, gibt es in Qt die Klasse QAction, die mit mehreren Widgets verknüpft und mit einem Slot verbunden werden kann. So erzeugen beispielsweise sowohl QMenu als auch QToolBar Menüpunkte und Werkzeug-Knöpfe aus denselben QActions. Wie das funktioniert werden wir in Kürze sehen.
Wie vorhin bauen wir die Benutzeroberfläche im Konstruktor von Notepad zusammen.
#include "notepad.h"
#include <QMenuBar>
Notepad::Notepad()
{
openAction = new QAction(tr("&Open"), this);
saveAction = new QAction(tr("&Save"), this);
exitAction = new QAction(tr("E&xit"), this);
connect(openAction, SIGNAL (triggered()), this, SLOT (open()));
connect(saveAction, SIGNAL (triggered()), this, SLOT (save()));
connect(exitAction, SIGNAL (triggered()), this, SLOT (quit()));
fileMenu = menuBar()->addMenu(tr("&File"));
fileMenu->addAction(openAction);
fileMenu->addAction(saveAction);
fileMenu->addSeparator();
fileMenu->addAction(exitAction);
textEdit = new QTextEdit;
setCentralWidget(textEdit);
setWindowTitle(tr("Notepad"));
}
QActions werden mit einem Text erzeugt, der auf/mit den Widgets dargestellt werden soll, denen wir sie zuordnen (in unserem Fall Menüpunkte). Wenn wir sie auch in einer Werkzeugleiste verwenden wollen, könnten wir ihnen noch Icons (kleine Bilder) zuweisen.
Wenn jetzt ein Menüpunkt aufgerufen wird, so wird die dazugehörige Aktion getriggert und der mit ihr verbundene Slot wird aufgerufen.
Weitere Informationen
|. Über… |. Hier… | | Hauptfenster und die zugehörigen Klassen | Application Main Window, Main Window Examples | | MDI-Anwendungen | QMdiArea, MDI Example |
Speichern und einlesen
In diesem Beispiel implementieren wir die beiden Slots open() und save(), die wir zuvor ergänzt haben.
- /qt-4.8/images/gs5.png
Wir kümmern uns zuerst um den Slot open() zum Öffnen eines Dokuments:
#include <QFileDialog>
#include <QMessageBox>
void Notepad::open()
{
QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"), "",
tr("Text Files (*.txt);;C++ Files (*.cpp *.h)"));
if (fileName != "") {
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly)) {
QMessageBox::critical(this, tr("Error"),
tr("Could not open file"));
return;
}
QString contents = file.readAll().constData();
textEdit->setPlainText(contents);
file.close();
}
}
Als erstes müssen wir den Benutzer nach der zu öffnenden Datei fragen. Qt stellt einen QFileDialog zur Verfügung, mit dem ein Benutzer eine Datei auswählen kann. Die Abbildung oben zeigt den Dialog wie er unter Kubuntu aussieht. Die statische Funktion getOpenFileName() zeigt einen modalen Dialog zur Dateiauswahl und kehrt erst zurück, wenn der Benutzer eine Datei ausgewählt oder die Aktion abgebrochen hat. Sie liefert den Pfad zur ausgwählten Datei zurück, oder eine leere Zeichenkette, falls der Benutzer die Auswahl abgebrochen hat.
Haben wir einen Dateinamen erhalten, versuchen wir die Datei mit open() zu öffnen. Die Funktion liefert true zurück, wenn das erfolgreich war. Die Fehlerbehandlung wollen wir an dieser Stelle nicht weiter vertiefen, mehr erfahren Sie in den Links im Kasten "Weitere Informationen". Falls die Datei nicht geöffnet werden kann, benutzen wir eine QMessageBox um einen Dialog mit Fehlermeldung anzuzeigen (Weitere Details finden Sie in der Klassenbeschreibung von QMessageBox).
Das eigentliche Einlesen der Daten ist einfach, wir benutzen die Funktion readAll(), die den gesamten Inhalt der Datei in einem QByteArray zurückliefert. Die Funktion constData() liefert die Daten des Arrays als const char zurück, was wiederum zur Initialisierung im Konstruktor eines QString verwendet werden kann. Der Inhalt kann damit im Texteditor dargestellt werden. Wir schließen die Datei mit close(), um die belegten Ressourcen (z.B. den File-Descriptor) an das Betriebssystem zurückzugeben.
Sehen wir uns nun den Slot save() zum abspeichern des Textes an:
#include <QFileDialog>
#include <QTextStream>
void Notepad::save()
{
QString fileName = QFileDialog::getSaveFileName(this, tr("Save File"), "",
tr("Text Files (*.txt);;C++ Files (*.cpp *.h)"));
if (fileName != "") {
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly)) {
// error message
} else {
QTextStream stream(&file);
stream << textEdit->toPlainText();
stream.flush();
file.close();
}
}
}
Zum Schreiben des Textes in eine Datei benutzen wir die Klasse QTextStream, ein Aufsatz (Wrapper) für QFile. Der QTextStream kann QStrings direkt in eine Datei schreiben; QFile hingegen kann nur einfache Zeichenketten (char) über die Funktion write() von QIODevice schreiben.
#include <QApplication>
#include <QMessageBox>
void Notepad::quit()
{
QMessageBox messageBox;
messageBox.setWindowTitle(tr("Notepad"));
messageBox.setText(tr("Do you really want to quit?"));
messageBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
messageBox.setDefaultButton(QMessageBox::No);
if (messageBox.exec() == QMessageBox::Yes)
qApp->quit();
}
main.cpp
#include "notepad.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
Notepad notepad;
notepad.show();
return app.exec();
}
Weitere Informationen
|. Über… |. Hier… | |Dateien und Ein-Ausgabe-Geräte |QFile, QIODevice |