Um die Wahrheit zu erkennen,
muss man die Ursachen kennen.
— Francis Bacon
Bevor Sie mit dem eigentlichen Studium der Qt-Bibliothek beginnen und zu den nächsten Kapiteln übergehen, sollten Sie sich unbedingt mit den Werkzeugen und Mitteln vertraut machen, die für die Arbeit mit Qt erforderlich sind.
Integrierte Entwicklungsumgebung
Es gibt zahlreiche integrierte Entwicklungsumgebungen (IDE, Integrated Development Environment), die eine komfortable und effiziente Erstellung von Qt-Projekten ermöglichen. Zu den bekanntesten gehören Microsoft Visual Studio, Xcode, IBM Eclipse sowie moderne Umgebungen mit Unterstützung für künstliche Intelligenz wie Cursor AI, Visual Studio Code mit AI-Plugins und sogar Xcode mit Integration von AI-Tools. Viele dieser Umgebungen bieten CMake-Unterstützung, Syntax-Highlighting für Qt-spezifische Makros und schnelle Navigation zu Signal- und Slot-Definitionen.
Eine der komfortabelsten Umgebungen für die Qt-Entwicklung bleibt jedoch Qt Creator (Abb. 3.1). Diese IDE ist Teil der offiziellen Qt-Distribution und wurde von Anfang an für die Erstellung plattformübergreifender grafischer und Konsolenanwendungen unter Nutzung aller Framework-Möglichkeiten konzipiert. Die Beschreibung von Qt Creator ist zu Recht in einem eigenen Kapitel dieses Buches ausgegliedert. Wenn Sie sich sofort näher damit vertraut machen möchten, schlagen Sie bitte Kapitel 47 auf.

In der modernen Entwicklungspraxis wird zunehmend ein Ansatz verwendet, bei dem für die Arbeit an einem und demselben Projekt gleichzeitig mehrere IDEs eingesetzt werden. Dies ermöglicht eine optimale Nutzung der Vorteile jeder Umgebung: So kann beispielsweise das Projekt in Qt Creator geführt werden, während für die schnelle Code-Navigation oder die Arbeit mit AI-Tools Visual Studio Code oder Cursor AI geöffnet wird, und für Build und Debugging Visual Studio oder Xcode verwendet werden. Projektdateien können parallel in mehreren Umgebungen ohne Konflikte geöffnet werden, was besonders für große und multiplattform-Teams praktisch ist. Ein solcher Multi-IDE-Ansatz erhöht die Flexibilität erheblich und beschleunigt den Entwicklungsprozess.
Das Programm Qt Assistant
Die Dokumentation ist das, was ein Entwickler am häufigsten nutzt. Es gibt auch ein Werkzeug, das ihm eine schnelle Suche nach benötigten Informationen ermöglicht. Dieses Werkzeug ist das Programm Qt Assistant (Abb. 3.2), das in seiner Funktionsweise einem Webbrowser ähnelt. Qt Assistant bietet die Möglichkeit, Text in allen verfügbaren Qt-Dokumenten zu durchsuchen. Um den Pfad zum Speicherort einer bestimmten Dokumentation festzulegen, können Sie den Parameter -docPath verwenden.
Das Programm Qt Assistant ist auch in Qt Creator integriert. Dadurch kann es verwendet werden, ohne das IDE-Fenster zu verlassen, und es liefert kontextbezogene Hilfe direkt im Code-Editor-Fenster. Entwickler können mit Hilfe der Klasse QHelpEngine ebenfalls Qt Assistant in ihre eigenen Programme einbetten.

Arbeiten mit Build-Systemen
Kein Programmierer wird bei jeder Kompilierung seines Programms die Linker-Parameter manuell eingeben und Bibliothekspfade von Hand übergeben. Viel bequemer ist es, eine Build-Konfigurationsdatei zu erstellen, die die gesamte Arbeit zur Konfiguration von Compiler und Linker übernimmt.
Die eigenständige Erstellung solcher Dateien erfordert vom Autor Erfahrung und Verständnis der Anwendungs-Linkprozesse, wobei das Format dieser Dateien je nach Plattform unterschiedlich sein wird. Früher war die Technik der Erstellung solcher Dateien ein integraler Bestandteil der Programmierung, aber jetzt hat sich vieles geändert. Und das liegt keineswegs daran, dass die Struktur der Konfigurationsdateien einfacher geworden wäre – eher im Gegenteil, sie ist komplexer geworden. Es sind einfach spezielle Utilities erschienen – Generatoren, die diese Arbeit für Sie erledigen.
Arbeiten mit CMake
In Qt6 wurde CMake (Cross-platform Make) zum primären Build-System – ein plattformübergreifendes System zur Automatisierung der Software-Erstellung, das zur Verwaltung des Software-Kompilierungsprozesses mit einfach zu verwendenden Konfigurationsdateien eingesetzt wird.
Um ein Projekt auf Basis von CMake zu erstellen, muss eine Datei CMakeLists.txt erstellt werden, die die notwendigen Anweisungen für den Projekt-Build enthält. Ein Beispiel für eine grundlegende CMakeLists.txt-Datei für ein Qt6-Projekt ist in Listing 3.1 dargestellt.
Listing 3.1. Grundlegende Datei für den Build eines Qt-Projekts mit CMake (Datei CMakeLists.txt)
cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets)
set(PROJECT_SOURCES
main.cpp
mainwindow.cpp
mainwindow.h
)
qt_add_executable(MyProject
${PROJECT_SOURCES}
)
target_link_libraries(MyProject PRIVATE
Qt6::Core
Qt6::Gui
Qt6::Widgets
)
Um die Build-Dateien aus CMake zu generieren, müssen folgende Befehle ausgeführt werden:
mkdir build
cd build
cmake ..
Danach kann zum Kompilieren des Projekts folgender Befehl verwendet werden:
cmake --build .
Oder Sie rufen direkt das Build-System Ihrer Plattform auf (z.B. make für Linux/macOS oder msbuild für Windows).
Arbeiten mit qmake
Obwohl in Qt6 CMake das bevorzugte Build-System ist, wird das Utility qmake zur Rückwärtskompatibilität weiterhin unterstützt. Es wurde ab Version 3.0 in die Qt-Distribution aufgenommen. Bemerkenswert ist, dass dieses Utility ebenso portabel ist wie Qt selbst. Bei der Erstellung von Make-Dateien interpretiert qmake Projektdateien, die die Erweiterung pro haben und verschiedene Parameter enthalten. Das Erstaunlichste ist, dass das Utility qmake nicht nur Make-Dateien, sondern auch die pro-Dateien selbst erstellen kann.
Angenommen, Sie haben angegeben, dass sich C++-Quelldateien im Verzeichnis befinden, indem Sie folgenden Befehl ausgeführt haben:
qmake –project
Als Ergebnis wird ein Versuch zur automatischen Erstellung einer pro-Datei unternommen. Das ist praktisch, da Sie sich anfangs nicht in alle Feinheiten der Erstellung von pro-Dateien vertiefen müssen. Dies kann auch nützlich sein, wenn Sie eine große Anzahl von Dateien haben, für die eine pro-Datei erstellt werden muss – dann entfällt die Notwendigkeit, ihre Namen manuell einzutragen. Eine Make-Datei aus einer pro-Datei zu erstellen, ist ganz einfach, dafür muss nur folgender Befehl ausgeführt werden:
qmake file.pro –o Makefile
| Erstellen von Dateien mit qmake auf macOS
Auf macOS gibt es zwei Hauptmöglichkeiten zur Erstellung von Dateien mit qmake: Die erste ist die Erstellung einer Make-Datei für GNU C++: Die zweite ist die Erstellung von Projektdateien für Xcode: |
Wie leicht zu erraten ist, ist file.pro der Name der pro-Datei und Makefile der Name für die zu erstellende plattformabhängige Make-Datei.
Die erste Option ist die Standardoption.
Hätten wir den Befehl ohne Parameter ausgeführt, hätte das Utility qmake versucht, im aktuellen Verzeichnis eine pro-Datei zu finden und im Erfolgsfall automatisch eine Make-Datei erstellt. Somit kann man, wenn nur C++-Quelldateien zur Verfügung stehen, ein ausführbares Programm erstellen, indem man nur drei Befehle ausführt:
qmake –project
qmake
make
Natürlich werden wir für ernsthaftere Arbeit den Inhalt der pro-Dateien ändern müssen, was eine feinere Konfiguration unserer Projekte ermöglicht. Tabelle 3.1 führt einige Projektvariablen für die pro-Datei auf, deren vollständige Liste in der offiziellen Qt-Dokumentation zu finden ist, die zusammen mit der Bibliothek selbst geliefert wird (dazu können Sie einfach das Programm Qt Assistant starten).
Tabelle 3.1. Einige Variablen für die Projektdatei
| Variable | Zweck |
|---|---|
| HEADERS | Liste der erstellten Header-Dateien |
| SOURCES | Liste der erstellten Implementierungsdateien (mit Erweiterung cpp) |
| FORMS | Liste der Dateien mit Erweiterung ui. Diese Dateien werden vom Programm Qt Designer erstellt und enthalten die Beschreibung der Benutzeroberfläche im XML-Format (siehe Kapitel 40 und 44) |
| TARGET | Name der Anwendung. Wenn dieses Feld nicht ausgefüllt ist, entspricht der Programmname dem Namen der Projektdatei |
| LIBS | Gibt die Liste der Bibliotheken an, die zum Erstellen des ausführbaren Moduls eingebunden werden müssen |
| CONFIG | Gibt Variablen an, die der Compiler verwenden soll |
| DESTDIR | Gibt den Pfad an, in den das fertige ausführbare Modul platziert wird |
| DEFINES | Hier können Variablen für den Compiler übergeben werden. Dies kann beispielsweise die Option zum Platzieren von Debug-Informationen für den Debugger im ausführbaren Modul sein |
| INCLUDEPATH | Pfad zum Verzeichnis, in dem Header-Dateien enthalten sind. Diese Variable kann verwendet werden, wenn bereits fertige Header-Dateien vorhanden sind und Sie diese im aktuellen Projekt verwenden (einbinden) möchten |
| DEPENDPATH | In diesem Abschnitt werden die für die Kompilierung erforderlichen Abhängigkeiten angegeben |
| SUBDIRS | Gibt Namen von Unterverzeichnissen an, die pro-Dateien enthalten |
| TEMPLATE | Gibt den Projekttyp an. Zum Beispiel: app – Anwendung, lib – Bibliothek, subdirs – Unterverzeichnisse |
| TRANSLATIONS | Gibt Übersetzungsdateien an, die im Projekt verwendet werden (siehe Kapitel 31) |
| QT | Liste der verwendeten Qt-Module. Zum Beispiel: core, gui, widgets, network |
Betrachten wir nun genauer die „Anatomie” von Projektdateien. Eine pro-Datei kann also folgendermaßen aussehen:
QT += core gui widgets
CONFIG += c++17
TEMPLATE = app
HEADERS += file1.h \
file2.h
SOURCES += main.cpp \
file1.cpp \
file2.cpp
TARGET = file
In der ersten Zeile werden die im Projekt verwendeten Qt-Module angegeben. Ab Qt6 muss für die Erstellung einer grafischen Benutzeroberfläche das Modul widgets explizit angegeben werden. Die zweite Zeile definiert die Version des C++-Standards, den wir verwenden möchten. In der dritten Zeile wird der Programmtyp festgelegt. In unserem Fall ist es eine Anwendung, daher TEMPLATE = app (wenn wir eine Bibliothek erstellen müssten, sollte TEMPLATE der Wert lib zugewiesen werden). Weiter werden in der Variable HEADERS alle zum Projekt gehörenden Header-Dateien angegeben, und in der Variable SOURCES – alle Implementierungsdateien des Projekts. Die Zeile TARGET definiert den Namen des Programms.
Wie aus diesem Beispiel ersichtlich ist, benötigt das Programm qmake nicht viele Informationen, da es sich auf die lokale Konfigurationsdatei stützt, die durch die Systemkonfiguration definiert ist. Eine solche Datei ist auch deshalb sehr wichtig, weil ein und derselbe Aufruf des Utilities qmake zur Erstellung unterschiedlicher Make-Dateien führt, je nachdem, auf welcher Plattform es aufgerufen wurde. Dies ist ein sehr wichtiger Schritt in Richtung Plattformunabhängigkeit der Projektdateien selbst.
Eine Projektdatei kann für die Kompilierung von Projekten verwendet werden, die sich in verschiedenen Verzeichnissen befinden. Ein anschauliches Beispiel ist die .pro-Datei Examples.pro des Beispielverzeichnisses aus dem Dateiarchiv, das unter folgendem Link verfügbar ist: https://qt-book.com/examples/Max_Schlee-Qt_6_9_Book-Examples.zip (siehe Anhang 4). Diese Datei sieht in etwa so aus:
TEMPLATE = subdirs
SUBDIRS = Example1 Example2 .... ExampleN
| Löschen von Objektdateien
Zum Löschen der Objektdateien eines Projekts dient die Option |
Migration von qmake zu CMake in Qt6
Die Migration von qmake zu CMake wurde dank der CMake-Unterstützung als primäres Build-System in Qt6 vereinfacht. Hier ist die Vorgehensweise für den Umstieg:
- Erstellen Sie über Qt Creator ein neues Beispielprojekt mit CMake. Dies hilft, sich mit der grundlegenden Struktur der für Qt6 empfohlenen
CMakeLists.txt-Datei vertraut zu machen. - Untersuchen Sie die Struktur der von Qt Creator für ein neues Projekt erstellten
CMakeLists.txt-Datei und vergleichen Sie sie mit Ihrer alten.pro-Datei. - Verwenden Sie die Entsprechungstabelle (Tab. 3.2) zwischen
qmake– undCMake-Variablen.
Tabelle 3.2. Entsprechungen zwischen qmake und CMake
| qmake | CMake |
|---|---|
| TEMPLATE = app | qt_add_executable() |
| TEMPLATE = lib | qt_add_library() |
| TARGET | Erster Parameter in qt_add_executable() oder qt_add_library() |
| HEADERS, SOURCES | Werden als Argumente für qt_add_executable() oder qt_add_library() angegeben |
| QT += core gui | find_package(Qt6 COMPONENTS Core Gui ...) + target_link_libraries() |
| DEFINES | target_compile_definitions() |
| INCLUDEPATH | target_include_directories() |
Zur Beschleunigung der Migration können automatische Migrationswerkzeuge verwendet werden, zum Beispiel das Tool qmake2cmake, das über Python mit folgendem Befehl installiert werden kann:
pip install qmake2cmake
Der Migrationsprozess des gesamten Projektbaums kann dann mit einem einzigen Befehl gestartet werden:
qmake2cmake_all <MyProjects>/qt_project
Nutzen Sie zur Automatisierung von Routineaufgaben moderne KI-Assistenten wie ChatGPT (chat.com) oder Claude (claude.ai). Sie können zum Beispiel fragen: „Konvertiere die folgende .pro-Datei in eine äquivalente CMakeLists.txt für Qt6“ (fügen Sie anschließend den Inhalt Ihrer .pro-Datei ein).
Ein Beispiel für die Entsprechung zwischen qmake (Qt5) und CMake (Qt6) ist in Tab. 3.3 dargestellt.
Tabelle 3.3. Entsprechungen zwischen qmake (Qt5) und CMake
| qmake | CMake |
|---|---|
| QT += widgets | find_package(Qt6 REQUIRED COMPONENTS Widgets) + target_link_libraries(app PRIVATE Qt6::Widgets) |
| SOURCES += main.cpp | Wird als Argument in add_executable(app main.cpp) angegeben |
| HEADERS += header.h | In der Regel nicht explizit erforderlich, wenn CMAKE_AUTOMOC verwendet wird |
| CONFIG += c++17 | set(CMAKE_CXX_STANDARD 17) |
| RESOURCES += res.qrc | qt_add_resources(app res.qrc) |
Empfehlungen für Qt-Projekte
Bei der Implementierung ist es am besten, Klassendateien in zwei separate Teile aufzuteilen. Der Definitionsteil der Klasse wird in eine Header-Datei mit der Erweiterung .h platziert, und die Implementierung der Klasse in eine Datei mit der Erweiterung .cpp. Wichtig ist, dass die Header-Datei mit der Klassendefinition die Präprozessor-Direktive #ifndef enthalten sollte. Der Sinn dieser Direktive besteht darin, Konflikte zu vermeiden, wenn dieselbe Header-Datei mehrmals in Quelldateien eingebunden wird:
#ifndef _MyClass_h_
#define _MyClass_h_
class MyClass {
...
};
#endif //_MyClass_h_
Diese Konstruktion kann auch durch eine äquivalente mit #pragma once ersetzt werden, wodurch der Header-Datei-Code kompakter wird:
#pragma once
class MyClass {
...
};
Traditionell trägt die Header-Datei in der Regel den Namen der darin enthaltenen Klasse. In Header-Dateien werden für Zeiger auf Datentypen Vorwärtsdeklarationen anstelle direkter Einbindung über die #include-Direktive verwendet, um eine schnellere Kompilierung zu erzielen. Am Anfang der Klassendefinition steht das Makro Q_OBJECT für MOC — dies ist notwendig, wenn Ihre Klasse Signale und Slots verwendet; in anderen Fällen, wenn Sie keine Metainformationen benötigen, kann dieses Makro weggelassen werden. Es ist jedoch zu beachten, dass ohne Metainformationen der Typcast qobject_cast<T>(obj) nicht verwendet werden kann:
class MyClass : public QObject {
Q_OBJECT
public:
MyClass();
...
};
Das Hauptprogramm sollte in einer separaten Datei implementiert werden, die der „Startpunkt” der Anwendung ist. Dieser Datei wird üblicherweise der Name main.cpp gegeben. Dies ist auch deshalb praktisch, weil ein Projekt aus Hunderten von Dateien bestehen kann, und wenn man diese Konvention befolgt, ist der Einstiegspunkt des gesamten Projekts leicht zu finden.
Die Einhaltung der hier beschriebenen Empfehlungen kann sich als sehr nützlich erweisen. Projekte neigen dazu, mit der Zeit zu wachsen, daher ist es sinnvoll, von Anfang an eine bestimmte Struktur beizubehalten, um sich in eigenen und fremden Projekten, die der in Qt üblichen Struktur folgen, stets zurechtzufinden.
Der Meta-Objekt-Compiler MOC
Der Meta-Objekt-Compiler (MOC, Meta Object Compiler) ist im Grunde kein Compiler, sondern ein Präprozessor, der während der Kompilierung der Anwendung ausgeführt wird und gemäß der Klassendefinition zusätzlichen C++-Code generiert. Dies ist notwendig, weil die Definition von Signalen und Slots im Quellcode des Programms allein für die Kompilierung nicht ausreicht. Der Signal-Slot-Code muss in Code umgewandelt werden, den der C++-Compiler versteht. Der Code wird in einer Datei mit dem Präfix-Namen moc_<dateiname>.cpp gespeichert.
Wenn Sie mit Projektdateien arbeiten, müssen Sie sich um die Existenz von MOC keine Gedanken machen, da die MOC-Verwaltung in diesem Fall automatisiert ist. Um eine moc-Datei manuell zu erstellen, kann folgender Befehl verwendet werden:
moc –o proc.moc proc.h
Nach dessen Ausführung erstellt MOC die zusätzliche Datei proc.moc.
Für jede Klasse, die von QObject erbt, stellt MOC ein Objekt der von QMetaObject abgeleiteten Klasse bereit. Dieses Objekt enthält Informationen über die Objektstruktur — zum Beispiel Signal-Slot-Verbindungen, Klassenname und Vererbungsstruktur.
| Binden Sie keine moc-Dateien am Ende der Hauptdatei ein!
Die generierten moc-Dateien sollten nicht über die Präprozessor-Direktive
Es ist besser, wenn sie separat kompiliert und vom Linker mit dem Hauptprogramm verbunden werden. Allerdings kann bei der Erstellung von Demo-Programmen auf diese Regel verzichtet werden, um den gesamten Code in einer einzigen |
Der Ressourcen-Compiler RCC
Fast jedes Programm greift in irgendeiner Form auf externe Ressourcen zu — Rastergrafiken, Übersetzungsdateien usw. Solche Zugriffe sind nicht besonders zuverlässig und effizient, da externe Ressourcen gelöscht werden oder aus anderen Gründen nicht verfügbar sein können, was sich zweifellos auf die korrekte Funktionsfähigkeit des Programms, sein Erscheinungsbild und seine Einsatzfähigkeit auswirken kann. Der Ressourcen-Compiler bietet die Möglichkeit, solche Dateien in ausführbare Module einzubetten, damit die Anwendung während ihrer Ausführung auf die benötigten Ressourcen zugreifen kann. Es gibt spezielle Namenskonventionen, mit denen eindeutig auf solche Ressourcen zugegriffen werden kann. Alle benötigten Dateien (Ressourcen) müssen zusammen mit ihren Pfaden in einer speziellen Datei mit der Erweiterung .qrc (Qt Resource Collection) beschrieben werden. Diese Beschreibung erfolgt in XML-Notation. Zum Beispiel:
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file>images/open.png</file>
<file>images/quit.png</file>
</qresource>
</RCC>
Unsere Beispieldatei wird vom Ressourcen-Compiler (dem Hilfsprogramm rcc) analysiert, um aus den Dateien open.png und quit.png eine einzige C++-Quelldatei zu erstellen, die alle ihre Daten enthält. Diese werden zusammen mit den übrigen Projektdateien kompiliert und gelinkt. Alle Ressourcendaten werden in der C++-Datei als ein großes Array gespeichert.
Dieser Ansatz stellt sicher, dass die benötigten Ressourcen stets verfügbar sind und hilft, Probleme durch fehlerhafte Installation der für das ausführbare Programm erforderlichen Dateien zu vermeiden. Die .qrc-Datei selbst muss in der .pro-Datei in der Variable RESOURCES angegeben werden, damit qmake die Informationen aus der Ressourcendatei berücksichtigt. Zum Beispiel:
RESOURCES = images.qrc
Bei Verwendung von CMake werden Ressourcen mit der Funktion qt_add_resources() hinzugefügt:
qt_add_resources(MyProject "resources"
PREFIX "/"
FILES
images/open.png
images/quit.png
)
Um die Datei open.png, genauer gesagt das darin enthaltene Rasterbild, zu verwenden, kann man folgendermaßen vorgehen:
plbl->setPixmap(QPixmap(":/images/open.png"));
In unserem Fall sind alle Rasterbilder im Verzeichnis images abgelegt, was der Idealfall ist. Es ist jedoch nicht immer möglich, Ressourcendateien dort zu platzieren, wo es am bequemsten wäre. Die Pfade für den Zugriff auf diese Dateien sind nicht immer kurz, was das Schreiben langer Pfade für den Zugriff auf Ressourcendateien erschwert. Dieses Problem wird durch die Verwendung von Aliasnamen gelöst, die in der XML-Datei im Tag <file> mithilfe der Option alias angegeben werden. Zum Beispiel:
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file alias="open.png">../../../very/long/path/images/open.png</file>
<file alias="quit.png">../../../very/long/path/images/quit.png</file>
</qresource>
</RCC>
Jetzt können wir in unserem Programm folgendermaßen auf unsere Ressourcendateien zugreifen:
plbl->setPixmap(QPixmap(":/open.png"));
Ohne Aliasnamen würde der Zugriff auf die Ressourcendatei so aussehen:
plbl->setPixmap(QPixmap(":/very/long/path/images/open.png"));
Zugegeben, die Variante mit Aliasnamen ist wesentlich übersichtlicher.
| Klasse QResource zum Lesen einer Datei aus einer Ressource
Das Modul QtCore stellt die Klasse QResource zum direkten Lesen einer Datei aus einer Ressource bereit, ohne zusätzliche Zwischenoperationen. Dazu muss ein Objekt erstellt und dem Konstruktor der Klasse QResource der Dateiname übergeben werden. Mit der Methode |
Struktur eines Qt-Projekts
Wir haben die Hilfsprogramme für ein Qt-Projekt einzeln betrachtet. Lassen Sie uns nun alle zusammenführen, um zu verstehen, wie sie zusammenarbeiten. Die Struktur eines Qt-Projekts ist sehr einfach — neben den C++-Quelldateien enthält sie in der Regel eine Build-Konfigurationsdatei (CMakeLists.txt für CMake oder .pro für qmake). Daraus werden Dateien für plattformunabhängige Build-Systeme generiert. Diese Dateien enthalten alle notwendigen Anweisungen zur Erstellung des fertigen ausführbaren Moduls (Abb. 3.3).

Die Build-Konfiguration enthält den Aufruf von MOC zur Generierung von zusätzlichem C++-Code und den benötigten Header-Dateien. Enthält das Projekt eine .qrc-Datei, wird ebenfalls eine C++-Datei mit den Ressourcendaten erstellt. Anschließend werden alle Quelldateien vom C++-Compiler in Objektcode-Dateien kompiliert, die vom Linker zu einem fertigen ausführbaren Modul zusammengefügt werden.
Debugging-Methoden
Es gibt keine Programme ohne Fehler und Defekte (Bugs). Und im Entwicklungsprozess entstehen häufig Probleme bei deren Auffindung. Entwickler müssen einen beträchtlichen Teil ihrer Arbeitszeit damit verbringen, vorhandene Bugs zu finden und zu beseitigen. Zu den Mitteln, die dabei helfen, deren Anzahl zu reduzieren, gehören:
- das Vorlegen des Quellcodes zur Überprüfung durch andere Entwickler (Code-Review);
- die Erstellung von Klassen für automatisierte Tests, die ausführlich in Kapitel 45 beschrieben werden.
Fehler lassen sich minimieren, aber in jedem Fall nicht vollständig vermeiden. Wenn sich ein heimtückischer Bug in Ihr Programm eingeschlichen hat, ist der Debugger das erste und wichtigste Werkzeug bei der mühsamen Suche danach. Die Aufgabe des Debuggers besteht darin, eine Umgebung bereitzustellen, in der Datenänderungen während der Programmausführung verfolgt werden können, wodurch man herausfinden kann, warum das Programm nicht wie vorgesehen funktioniert. Dank der Plattformunabhängigkeit von Qt kann der Entwickler jeden bevorzugten Debugger verwenden — zum Beispiel GDB oder den in Microsoft Visual Studio integrierten Debugger.
Darüber hinaus unterstützt Qt Creator die Integration mit den beliebtesten Debuggern (GDB, LLDB, CDB u. a.) direkt „von Haus aus” — Sie können Code starten, anhalten und schrittweise ausführen, Variablenwerte und den Aufrufstack direkt über die Qt Creator-Oberfläche einsehen, was den Debugging-Prozess komfortabel und übersichtlich macht, unabhängig vom gewählten Werkzeug. Darüber werden wir in Kapitel 47 ausführlicher sprechen.
Weitere Debugging-Methoden
Eine der Standardmethoden beim Debugging ist das Einfügen von Ausgabeanweisungen in den Quellcode, mit denen Variablenwerte angezeigt und mit erwarteten Werten verglichen werden können. Diese Methode wird von Entwicklern häufig verwendet, da es problemlos möglich ist, solche Anweisungen einzufügen oder sie als separate Dump-Methode zu formulieren. In Qt ist ein Beispiel für diesen Ansatz die Methode QObject::dumpObjectInfo(), die Metainformationen eines Objekts auf dem Bildschirm ausgibt.
Qt stellt Makros und Funktionen für das Debugging bereit, mit denen verschiedene Überprüfungen und die Ausgabe von Testnachrichten direkt in das Programm eingebettet werden können.
Die Header-Datei QtGlobal enthält die Definitionen zweier Makros: Q_ASSERT() und Q_CHECK_PTR():
Q_ASSERT()nimmt einen booleschen Wert als Argument und gibt eine Warnmeldung aus, wenn dieser Wert nichttrueist;Q_CHECK_PTR()nimmt einen Zeiger und gibt eine Warnmeldung aus, wenn der übergebene Zeiger0ist, was bedeutet, dass entweder der Zeiger nicht initialisiert wurde oder die Speicherzuweisung fehlgeschlagen ist.
Qt stellt die globalen Funktionen qDebug(), qWarning() und qFatal() bereit, die ebenfalls in der Header-Datei QtGlobal definiert sind. Ihre Verwendung ähnelt der Funktion printf(). Wie bei printf() werden diesen Funktionen ein Formatstring und verschiedene Parameter übergeben. In Microsoft Visual Studio erfolgt die Ausgabe dieser Funktionen in das Debugger-Fenster, unter Linux in den Standard-Fehlerausgabestream.
| Besonderheit der Funktion qFatal()
Der Aufruf von |
Wenn der Ausgabestream einer Meldung umgeleitet werden muss, ist eine eigene Verwaltungsfunktion zu erstellen und zu installieren. Die Installation erfolgt mit der Funktion qInstallMessageHandler(). Dieser Funktion wird als Argument die Adresse der Nachrichtenverwaltungsfunktion übergeben, die folgenden Prototyp hat:
void fct(QtMsgType type, const QMessageLogContext& context, const QString& msg)
An der Stelle fct steht der Funktionsname. Das erste Argument ist der Nachrichtentyp, der einen der Werte der Aufzählung QtMsgType annimmt: QtDebugMsg, QtWarningMsg, QtInfoMsg, QtCriticalMsg oder QtFatalMsg. Das zweite Argument liefert zusätzliche Informationen über die Nachricht, und das dritte ist die Nachricht selbst. Illustrieren wir das Gesagte anhand eines Code-Fragments (Listing 3.2).
Listing 3.2. Umleitung des Nachrichtenausgabestreams
void messageToFile(QtMsgType type,
const QMessageLogContext& context,
const QString& msg
)
{
QFile file("protocol.log");
if(!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append))
return;
QString strDateTime =
QDateTime::currentDateTime().toString("dd.MM.yy-hh:mm");
QTextStream out(&file);
switch (type) {
case QtDebugMsg:
out << strDateTime << "Debug: " << msg
<< ", " << context.file << endl;
break;
case QtWarningMsg:
out << strDateTime << "Warning: " << msg
<< ", " << context.file << endl;
break;
case QtCriticalMsg:
out << strDateTime << "Critical: " << msg
<< ", " << context.file << endl;
break;
case QtFatalMsg:
out << strDateTime << "Fatal: " << msg
<< ", " << context.file << endl;
abort();
case QtInfoMsg:
out << strDateTime << "Info: " << msg
<< ", " << context.file << endl;
break;
}
}
int main(int argc, char** argv)
{
QApplication app(argc, argv);
qInstallMessageHandler(messageToFile);
...
}
Jetzt werden alle Meldungen von qDebug(), qWarning() und qFatal() in die Datei protocol.log geschrieben statt auf der Konsole ausgegeben. Dies ist sehr praktisch, um Fehler und unerwartetes Programmverhalten zu untersuchen, das auf der Seite von Testern und Anwendern aufgetreten ist. So können Sie jederzeit darum bitten, die Datei protocol.log zur eingehenderen Untersuchung zu schicken.
| Rückkehr zur Konsolenausgabe
Wenn im Programm wieder Meldungen auf der Konsole ausgegeben werden sollen, muss lediglich die globale Funktion |
Zur Vereinfachung des Debugging-Prozesses wird empfohlen, allen Objekten Namen zuzuweisen. Diese Objekte können dann jederzeit durch Aufruf der Methode QObject::objectName() gefunden werden. Dies ermöglicht es, während der Programmausführung die Methode QObject::dumpObjectInfo() zu nutzen, die interne Informationen des Objekts anzeigt.
Beim Debugging kann auch die Installation eines Ereignisfilters für ein Objekt der Klasse QCoreApplication verwendet werden — in diesem Fall ist ein solcher Filter das erste Objekt, das Ereignisse aller Anwendungsobjekte empfängt und verarbeitet (siehe Kapitel 15).
Die einfachste Ausgabemethode in Qt ist die Verwendung eines Objekts der Klasse QDebug. Dieses Objekt ähnelt dem Standard-C++-Ausgabestreamobjekt cout. Die Ausgabe einer Nachricht im Debugger oder auf der Konsole mit der Funktion qDebug() erfolgt beispielsweise so:
qDebug() << "Test";
Diese Funktion erstellt ein Objekt der Streamklasse QDebug und übergibt dessen Konstruktor das Argument QtDebugMsg. Man könnte natürlich auch so vorgehen:
QDebug(QtDebugMsg) << "Test";
Aber wie Sie sehen, ist die vorherige Zeile kompakter, daher empfehle ich, diese zu verwenden.
In Qt6 wurden auch die Funktionen qInfo() — zur Ausgabe informativer Meldungen — und qCInfo() — zur Ausgabe informativer Meldungen mit Kategorieangabe — hinzugefügt:
qInfo() << "This is an informational message";
qCInfo("network") << "Network operation completed successfully";
Es ist wichtig zu verstehen, dass die Ausgabe über qDebug() sowohl in Debug- als auch in Release-Builds erfolgt. Wenn die Ausgabe in der Release-Version nicht erwünscht ist und alle Meldungen nur in der Debug-Version angezeigt werden sollen, kann eine leere Ausgabeverwaltungsfunktion dummyOutput() implementiert und in qInstallMessageHandler() installiert werden, wie in Listing 3.3 gezeigt.
Listing 3.3. Unterdrückung der Ausgabe der Funktionen qDebug(), qWarning() und qFatal()
void dummyOutput(QtMsgType, const QMessageLogContext&, const QString&)
{
}
int main(int argc, char** argv)
{
QApplication app(argc, argv);
#ifndef QT_DEBUG
qInstallMessageHandler(dummyOutput);
#endif
}
Dabei sollte anstelle der Funktion qDebug() folgende Schreibweise verwendet werden:
qDebug() << "Test1" << 123 << "Test2" << 456;
Wenn Sie nun eine Release-Version Ihres Programms erstellen, können Sie sicher sein, dass alle Debug-Informationen, die von qDebug(), qWarning() und qFatal() ausgegeben werden, in der Release-Version ausgeblendet sind.
Bei der Debug-Ausgabe können auch Manipulatoren verwendet werden — wie zum Beispiel hex und uppercasedigits. Der erste sorgt dafür, dass alle ganzzahligen Werte hexadezimal ausgegeben werden, der zweite wandelt alle Kleinbuchstaben in Großbuchstaben um. Als Beispiel implementieren wir ein kleines Programm zur Ausgabe des Werts der Variablen n in hexadezimaler Schreibweise und in Großbuchstaben:
int n = 7777;
qDebug() << "DEC:" << n << "= HEX:" << hex << uppercasedigits << n;
Folgendes ist auf dem Bildschirm zu sehen:
DEC: 7777 =HEX: 1E61
Nach jedem Ausgabeabschnitt von qDebug() wird ein Leerzeichen eingefügt. Zum Beispiel:
qDebug() << 1 << 2 << 3 << 4;
Die Ausgabe sieht so aus:
1 2 3 4
Mit der Methode nospace() der Klasse QDebug kann dies verhindert werden:
qDebug().nospace() << 1 << 2 << 3 << 4;
Jetzt sieht die Ausgabe so aus:
1234
Globale Qt-Definitionen
Qt enthält in der Header-Datei QtGlobal einige Makros und Funktionen, die beim Schreiben von Programmen sehr nützlich sein können.
Die Template-Funktionen qMax(a, b) und qMin(a, b) werden verwendet, um den maximalen bzw. minimalen von zwei übergebenen Werten zu bestimmen:
int n = qMax<int>(3, 5); // n = 5
int n = qMin<int>(3, 5); // n = 3
Die Funktion qAbs(a) gibt den Absolutwert zurück:
int n = qAbs(-5); // n = 5
Die Funktion qRound() rundet die übergebene Zahl auf eine ganze Zahl:
int n = qRound(5.2); // n = 5
int n = qRound(-5.2); // n = -5
Die Funktion qBound() gibt den Wert zurück, der zwischen Minimum und Maximum liegt:
int n = qBound(2, 12, 7); // n = 7
Und hier ist noch eine interessante Funktion. Der Vergleich zweier Gleitkommazahlen auf exakte Gleichheit ist einer der häufigen Programmierfehler. Die Funktion qFuzzyCompare() übernimmt in diesem Fall die gesamte Verantwortung für den korrekten Vergleich. Sie nimmt zwei Werte vom Typ double oder float entgegen und gibt den booleschen Wert true zurück, wenn die Variablen als gleich betrachtet werden, andernfalls gibt sie false zurück. Der Vergleich selbst erfolgt auf relative Weise, wobei die Vergleichsgenauigkeit mit abnehmenden Zahlenwerten der zu vergleichenden Größen zunimmt. Daher ist der einzige problematische Wert für diese Funktion der Nullwert. Aber auch dafür gibt es eine Lösung: Es genügt, die zu vergleichenden Werte entweder gleich oder größer als 1,0 zu machen. Zum Beispiel:
double dValue1 = 0.0;
double dValue2 = myFunction();
if (qFuzzyCompare(1 + dValue1, 1 + dValue2)) {
// Werte sind gleich
}
| Funktion qFuzzyCompare() und Unit-Tests
Diese Funktion kann auch sehr nützlich für das Schreiben von Unit-Tests sein, die in Kapitel 45 beschrieben werden. |
In Tab. 3.4 ist eine Liste der Qt-Typen aufgeführt, die bei der Programmierung verwendet werden können.
Tabelle 3.4. Qt-Ganzzahltypen
| Qt-Typ | C++-Äquivalent | Größe |
|---|---|---|
| qint8 | signed char | 8 Bit |
| quint8 | unsigned char | 8 Bit |
| qint16 | short | 16 Bit |
| quint16 | unsigned short | 16 Bit |
| qint32 | int | 32 Bit |
| quint32 | unsigned int | 32 Bit |
| qint64 | __int64 oder long long |
64 Bit |
| quint64 | unsigned __int64 oder unsigned long long |
64 Bit |
| qlonglong | Entspricht qint64 |
64 Bit |
| qulonglong | Entspricht quint64 |
64 Bit |
Wie aus Tab. 3.4 ersichtlich, sind die umstrittensten Typen qint64 und quint64. Überprüfen wir die in der Tabelle angegebene Bitanzahl sowie ihre Minimal- und Maximalwerte:
qDebug() << "Number of bits for qint64 =" << (sizeof(qint64) * 8);
qDebug() << "Minimum of qint64 = -" << ~(~quint64(0) >> 1);
qDebug() << "Maximum of qint64 =" << (~quint64(0) >> 1);
qDebug() << "Number of bits for quint64 =" << (sizeof(quint64) * 8);
qDebug() << "Minimum of quint64 =" << 0;
qDebug() << "Maximum of quint64 =" << ~quint64(0);
Ausgabe:
Number of bits for qint64 = 64
Minimum of qint64 = -9223372036854775808
Maximum of qint64 = 9223372036854775807
Number of bits for quint64 = 64
Minimum of quint64 = 0
Maximum of quint64 = 18446744073709551615
Informationen über die Qt-Bibliothek
Es kann sehr nützlich sein, Informationen über die auf Ihrem Computer aktuell verwendete Bibliothek zu erhalten. Zum Beispiel möchten Sie wissen, in welchem Verzeichnis Qt seine Plug-in-Dateien speichert, oder Sie müssen die aktuelle Qt-Version ermitteln usw. Für die Bereitstellung solcher Informationen ist die Klasse QLibraryInfo zuständig, die dafür eine Reihe statischer Methoden anbietet. Veranschaulichen wir ihre Verwendung anhand eines kleinen Beispiels (Listing 3.4).
Listing 3.4. Verwendung der statischen Funktionen der Klasse QLibraryInfo
#include <QDebug>
#include <QLibraryInfo>
int main()
{
qDebug() << "Is Debug Build:"
<< QLibraryInfo::isDebugBuild();
qDebug() << "Locations";
qDebug() << " Headers:"
<< QLibraryInfo::path(QLibraryInfo::HeadersPath);
qDebug() << " Libraries:"
<< QLibraryInfo::path(QLibraryInfo::LibrariesPath);
qDebug() << " Binaries:"
<< QLibraryInfo::path(QLibraryInfo::BinariesPath);
qDebug() << " Prefix"
<< QLibraryInfo::path(QLibraryInfo::PrefixPath);
qDebug() << " Documentation: "
<< QLibraryInfo::path(QLibraryInfo::DocumentationPath);
qDebug() << " Plugins:"
<< QLibraryInfo::path(QLibraryInfo::PluginsPath);
qDebug() << " Data:"
<< QLibraryInfo::path(QLibraryInfo::DataPath);
qDebug() << " Settings:"
<< QLibraryInfo::path(QLibraryInfo::SettingsPath);
qDebug() << " Examples:"
<< QLibraryInfo::path(QLibraryInfo::ExamplesPath);
qDebug() << "Version:" << QLibraryInfo::version().toString();
}
Hier ist die Ausgabe dieses Programms für Qt 6.9.0, installiert auf einem Windows-Computer:
Is Debug Build: false
Locations
Headers: "C:\Qt\6.9.0\mingw_64\include"
Libraries: "C:\Qt\6.9.0\mingw_64\lib"
Binaries: "C:\Qt\6.9.0\mingw_64\bin"
Prefix "C:\Qt\6.9.0\mingw_64"
Documentation: "C:\Qt\6.9.0\mingw_64\doc"
Plugins: "C:\Qt\6.9.0\mingw_64\plugins"
Data: "C:\Qt\6.9.0\mingw_64"
Settings: " "
Examples: "C:\Qt\6.9.0\mingw_64\examples"
Version: "6.9.0"
Zusammenfassung
In diesem Kapitel haben wir erfahren, wie ein typisches Projekt mit Qt aussieht und welche Prozesse „hinter den Kulissen” bei der Erstellung eines fertigen ausführbaren Moduls ablaufen. Wir haben die in Qt6 verwendeten Build-Systeme kennengelernt: CMake und qmake. Der Wechsel zu CMake eröffnet nicht nur den Zugang zu modernen Build-Werkzeugen, sondern vereinfacht auch die plattformübergreifende Entwicklung und die Integration von Qt mit anderen Bibliotheken erheblich. Der Einsatz automatischer Konvertierungswerkzeuge sowie von KI-Assistenten wie ChatGPT oder Claude ermöglicht es, Zeit zu sparen, Fehler zu minimieren und die Migration so schnell und reibungslos wie möglich zu gestalten.
Wir haben den MOC-Präprozessor näher kennengelernt, der beim Starten zusätzlichen Code zur Unterstützung von Signalen und Slots generiert, und haben einen kurzen Einblick in die globalen Qt-Definitionen gegeben. Wir haben außerdem die Möglichkeiten und Methoden des Debuggings von Qt-Programmen sowie die speziell dafür vorgesehenen Makros betrachtet.
Nach der Beherrschung des Stoffs dieses Kapitels sind Sie nun bereit, eigene Qt6-Anwendungen zu erstellen und zu debuggen — sowohl mit dem traditionellen qmake-Ansatz als auch mit dem empfohlenen CMake-Build-System.
Entfalten Sie das volle Potenzial dieses Kapitels! Nutzen Sie die zu diesem Kapitel bereitgestellten Zusatzmaterialien, stellen Sie eine Frage, teilen Sie Ihre Erfahrungen oder nehmen Sie an der Diskussion teil unter: https://qt-book.com/03-69-de/
