Auf dieser Seite

Qt Quick Controls - Chat-Tutorial

Diese Anleitung zeigt, wie man mit „ Qt Quick Controls “ eine einfache Chat-Anwendung erstellt. Außerdem wird erklärt, wie man eine SQL-Datenbank in eine Qt-Anwendung integriert.

Kapitel 1: Einrichtung

Beim Einrichten eines neuen Projekts ist es am einfachsten, Qt Creator. Für dieses Projekt haben wir die Anwendungsvorlage „Qt Quick “ gewählt, die eine einfache „Hello World“-Anwendung mit den folgenden Dateien erstellt:

  • CMakeLists.txt - Weist CMake an, wie unser Projekt erstellt werden soll
  • Main.qml - Stellt eine Standard-Benutzeroberfläche mit einem leeren Fenster bereit
  • main.cpp - Lädt main.qml
  • qtquickcontrols2.conf - Teilt der Anwendung mit, welchen Stil sie verwenden soll

main.cpp

Der Standardcode in „ main.cpp “ enthält zwei Include-Anweisungen:

#include <QGuiApplication>
#include <QQmlApplicationEngine>

Der erste ermöglicht uns den Zugriff auf ` QGuiApplication`. Alle Qt-Anwendungen benötigen ein Anwendungsobjekt, doch der genaue Typ hängt davon ab, was die Anwendung tut. ` QCoreApplication ` ist für nicht-grafische Anwendungen ausreichend. ` QGuiApplication ` ist für grafische Anwendungen ausreichend, die Qt Widgets, während ` QApplication ` für solche erforderlich ist, die dies tun.

Durch den zweiten Include-Befehl wird „ QQmlApplicationEngine “ verfügbar gemacht, wodurch wir unsere QML-Datei laden können.

Innerhalb von ` main()` richten wir das Anwendungsobjekt und die QML-Engine ein:

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    engine.loadFromModule("chattutorial", "Main");

    return app.exec();
}

QQmlApplicationEngine ist ein praktischer Wrapper um ` QQmlEngine` und stellt die Funktion ` loadFromModule ` bereit, mit der sich QML für eine Anwendung einfach laden lässt. Außerdem bietet er einige Erleichterungen bei der Verwendung von Dateiauswahlfeldern.

Sobald wir die Grundlagen in C++ eingerichtet haben, können wir uns der Benutzeroberfläche in QML zuwenden.

Main.qml

Passen wir den Standard-QML-Code an unsere Bedürfnisse an.

import QtQuick
import QtQuick.Controls

Sie werden feststellen, dass das Qt Quick Modul bereits importiert wurde. Dadurch haben wir Zugriff auf grafische Primitive wie „ Item “, „ Rectangle “, „ Text “ und so weiter. Eine vollständige Liste der Typen finden Sie in der Qt Quick QML Types Dokumentation.

Fügen Sie einen Import des Moduls „ Qt Quick Controls “ hinzu. Dieser bietet unter anderem Zugriff auf „ ApplicationWindow “, das den bestehenden Stammtyp „ Window “ ersetzen wird:

ApplicationWindow {
    width: 540
    height: 960
    visible: true
    ...
}

ApplicationWindow ist ein ` Window ` mit einigen zusätzlichen Annehmlichkeiten zum Erstellen eines ` header ` und eines ` footer`. Es bildet zudem die Grundlage für ` popups ` und unterstützt einige grundlegende Formatierungen, wie beispielsweise den Hintergrund ` color`.

Es gibt drei Eigenschaften, die bei der Verwendung von ApplicationWindow fast immer festgelegt werden: width, height und visible. Sobald wir diese festgelegt haben, verfügen wir über ein leeres Fenster in der richtigen Größe, das mit Inhalten gefüllt werden kann.

Hinweis: Die Eigenschaft „ title “ aus dem Standardcode wurde entfernt.

Der erste „Bildschirm“ in unserer Anwendung wird eine Liste von Kontakten sein. Es wäre schön, wenn oben auf jedem Bildschirm ein Text stehen würde, der dessen Zweck beschreibt. Die Eigenschaften „header“ und „footer“ von „ ApplicationWindow “ könnten in dieser Situation nützlich sein. Sie weisen einige Eigenschaften auf, die sie ideal für Elemente machen, die auf jedem Bildschirm einer Anwendung angezeigt werden sollen:

  • Sie sind jeweils am oberen bzw. unteren Rand des Fensters verankert.
  • Sie füllen die gesamte Breite des Fensters aus.

Wenn sich der Inhalt von Kopf- und Fußzeile jedoch je nach dem Bildschirm, den der Benutzer gerade betrachtet, ändert, ist es viel einfacher, ` Page` zu verwenden. Vorerst fügen wir nur eine Seite hinzu, aber im nächsten Kapitel zeigen wir, wie man zwischen mehreren Seiten navigiert.

    Page {
        anchors.fill: parent
        header: Label {
            padding: 10
            text: qsTr("Contacts")
            font.pixelSize: 20
            horizontalAlignment: Text.AlignHCenter
            verticalAlignment: Text.AlignVCenter
        }
    }

Zunächst fügen wir eine Seite hinzu, deren Größe mithilfe der Eigenschaft „ anchors.fill “ so angepasst wird, dass sie den gesamten Platz im Fenster einnimmt.

Anschließend weisen wir der Eigenschaft „ header “ den Wert „ Label “ zu. „Label“ erweitert das primitive Element „ Text “ aus dem Modul „ Qt Quick “, indem es Styling-Möglichkeiten und die Vererbung von „ font “ hinzufügt. Das bedeutet, dass ein „Label“ je nach verwendetem Stil unterschiedlich aussehen kann und seine Pixelgröße auch an seine untergeordneten Elemente weitergeben kann.

Wir möchten etwas Abstand zwischen dem oberen Rand des Anwendungsfensters und dem Text, daher legen wir die Eigenschaft ` padding ` fest. Dadurch wird auf jeder Seite des Labels (innerhalb seiner Grenzen) zusätzlicher Platz zugewiesen. Stattdessen können wir auch die Eigenschaften ` topPadding ` und ` bottomPadding ` explizit festlegen.

Wir legen den Text des Labels mithilfe der Funktion ` qsTr() ` fest, wodurch sichergestellt wird, dass der Text vom Übersetzungssystem von Qt übersetzt werden kann. Dies ist eine bewährte Vorgehensweise für Text, der für die Endbenutzer Ihrer Anwendung sichtbar ist.

Standardmäßig wird Text vertikal am oberen Rand seines Bereichs ausgerichtet, während die horizontale Ausrichtung von der natürlichen Leserichtung des Textes abhängt; so wird beispielsweise Text, der von links nach rechts gelesen wird, linksbündig ausgerichtet. Würden wir diese Standardeinstellungen verwenden, befände sich unser Text in der oberen linken Ecke des Fensters. Da dies für eine Kopfzeile nicht wünschenswert ist, richten wir den Text sowohl horizontal als auch vertikal an der Mitte seines Begrenzungsraums aus.

Die Projektdatei

Die Datei „ CMakeLists.txt “ enthält alle Informationen, die CMake benötigt, um unser Projekt zu einer ausführbaren Datei zu kompilieren, die wir ausführen können.

Eine ausführliche Erläuterung dieser Datei finden Sie unter „Erstellen einer QML-Anwendung“.

So sieht unsere Anwendung derzeit bei der Ausführung aus:

Leeres Anwendungsfenster mit der Kopfzeile „Kontakte“

Kapitel 2: Listen

In diesem Kapitel erklären wir, wie man mithilfe von „ ListView “ und „ ItemDelegate “ eine Liste interaktiver Elemente erstellt.

ListView stammt aus dem Modul „ Qt Quick “ und zeigt eine Liste von Elementen an, die aus einem Modell bezogen werden. „ ItemDelegate “ stammt aus dem Modul „ Qt Quick Controls “ und stellt ein Standard-View-Element zur Verwendung in Ansichten und Steuerelementen wie „ ListView “ und „ ComboBox “ bereit. Beispielsweise kann jedes „ ItemDelegate “-Element Text anzeigen, aktiviert und deaktiviert werden und auf Mausklicks reagieren.

Hier ist unser „ ListView “:

        ...

        ListView {
            id: listView
            anchors.fill: parent
            topMargin: 48
            leftMargin: 48
            bottomMargin: 48
            rightMargin: 48
            spacing: 20
            model: ["Albert Einstein", "Ernest Hemingway", "Hans Gude"]
            delegate: ItemDelegate {
                id: contactDelegate
                text: modelData
                width: listView.width - listView.leftMargin - listView.rightMargin
                height: avatar.implicitHeight
                leftPadding: avatar.implicitWidth + 32

                required property string modelData

                Image {
                    id: avatar
                    source: "images/" + contactDelegate.modelData.replace(" ", "_") + ".png"
                }
            }
        }
        ...

Größe und Positionierung

Als Erstes legen wir eine Größe für die Ansicht fest. Sie soll den verfügbaren Platz auf der Seite ausfüllen, daher verwenden wir ` anchors.fill`. Beachten Sie, dass „Page“ sicherstellt, dass für Kopf- und Fußzeile ausreichend Platz reserviert ist; daher wird die Ansicht in diesem Fall beispielsweise unterhalb der Kopfzeile platziert.

Als Nächstes legen wir für die „ ListView “ die Eigenschaft „ margins “ fest, um etwas Abstand zwischen ihr und den Rändern des Fensters zu schaffen. Die „margin“-Eigenschaften reservieren Platz innerhalb der Grenzen der Ansicht, was bedeutet, dass die leeren Bereiche vom Benutzer weiterhin „wegwischen“ können.

Die Elemente sollten innerhalb der Ansicht gleichmäßig verteilt sein, daher wird die Eigenschaft „ spacing “ auf „ 20 “ gesetzt.

Modell

Um die Ansicht schnell mit einigen Elementen zu füllen, haben wir ein JavaScript-Array als Modell verwendet. Eine der größten Stärken von QML ist die Fähigkeit, das Prototyping einer Anwendung extrem schnell zu gestalten, und dies ist ein Beispiel dafür. Es ist auch möglich, der Modelleigenschaft einfach eine Zahl zuzuweisen, um anzugeben, wie viele Elemente benötigt werden. Wenn Sie beispielsweise der Eigenschaft „ model “ den Wert „ 10 “ zuweisen, besteht der Anzeigetext jedes Elements aus einer Zahl im Bereich von „ 0 “ bis „ 9 “.

Sobald die Anwendung jedoch die Prototyp-Phase hinter sich hat, wird es schnell notwendig, echte Daten zu verwenden. Hierfür empfiehlt es sich, ein richtiges C++-Modell von subclassing QAbstractItemModel zu verwenden.

Delegat

Weiter geht es mit delegate. Wir weisen der Eigenschaft „ text “ von ItemDelegate den entsprechenden Text aus dem Modell zu. Wie genau die Daten aus dem Modell den einzelnen Delegaten zur Verfügung gestellt werden, hängt vom verwendeten Modelltyp ab. Weitere Informationen finden Sie unter „Modelle und Ansichten“ auf Qt Quick.

In unserer Anwendung sollte die Breite jedes Elements in der Ansicht der Breite der Ansicht entsprechen. Dadurch wird sichergestellt, dass der Benutzer ausreichend Platz hat, um einen Kontakt aus der Liste auszuwählen – ein wichtiger Faktor auf Geräten mit kleinen Touchscreens wie Mobiltelefonen. Die Breite der Ansicht umfasst jedoch unsere Pixelränder „ 48 “, weshalb wir dies bei der Zuweisung an die „width“-Eigenschaft berücksichtigen müssen.

Als Nächstes definieren wir einen „ Image “. Dieser zeigt ein Bild des Kontakts des Benutzers an. Das Bild ist 40 Pixel breit und 40 Pixel hoch. Wir legen die Höhe des Delegaten an der Höhe des Bildes fest, damit kein vertikaler Leerraum entsteht.

Kontaktliste mit drei Einträgen und Avataren

Kapitel 3: Navigation

In diesem Kapitel lernen Sie, wie Sie mit „ StackView “ zwischen den Seiten einer Anwendung navigieren. Hier ist die überarbeitete Datei „ main.qml “:

import QtQuick.Controls

ApplicationWindow {
    id: window
    width: 540
    height: 960
    visible: true

    StackView {
        id: stackView
        anchors.fill: parent
        initialItem: ContactPage {}
    }
}

Wie der Name schon sagt, bietet „ StackView “ eine stapelbasierte Navigation. Das zuletzt auf den Stapel „geschobene“ Element wird als Erstes wieder entfernt, und das oberste Element ist immer dasjenige, das sichtbar ist.

Genau wie bei „Page“ weisen wir „ StackView “ an, das Anwendungsfenster auszufüllen. Danach müssen wir ihm nur noch über „ initialItem “ ein Element zur Anzeige übergeben. „ StackView “ akzeptiert „ items “, „ components “ und „ URLs “.

Sie werden feststellen, dass wir den Code für die Kontaktliste in „ ContactPage.qml “ verschoben haben. Es empfiehlt sich, dies zu tun, sobald Sie eine allgemeine Vorstellung davon haben, welche Bildschirme Ihre Anwendung enthalten wird. Dadurch wird nicht nur Ihr Code lesbarer, sondern es wird auch sichergestellt, dass Elemente nur dann aus einer bestimmten Komponente instanziiert werden, wenn dies unbedingt erforderlich ist, was den Speicherverbrauch reduziert.

Hinweis: „ Qt Creator “ bietet mehrere praktische Schnellkorrekturen für QML, von denen eine es Ihnen ermöglicht, einen Code-Block in eine separate Datei zu verschieben (Alt + Enter > Move Component into Separate File).

Ein weiterer zu berücksichtigender Aspekt bei der Verwendung von „ ListView “ ist die Frage, ob man über ` id` darauf verweisen oder die angehängte Eigenschaft ` ListView.view ` nutzen soll. Der beste Ansatz hängt von verschiedenen Faktoren ab. Die Vergabe einer ID an die Ansicht führt zu kürzeren und effizienteren Bindungsausdrücken, da die angehängte Eigenschaft nur einen sehr geringen Overhead verursacht. Wenn Sie jedoch vorhaben, den Delegaten in anderen Ansichten wiederzuverwenden, ist es besser, die angehängten Eigenschaften zu verwenden, um zu vermeiden, dass der Delegat an eine bestimmte Ansicht gebunden wird. Bei Verwendung der angehängten Eigenschaften sieht die Zuweisung „ width “ in unserem Delegaten beispielsweise wie folgt aus:

width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin

In Kapitel 2 haben wir unterhalb der Kopfzeile eine „ ListView “ hinzugefügt. Wenn Sie die Anwendung für dieses Kapitel ausführen, werden Sie feststellen, dass der Inhalt der Ansicht über den oberen Rand der Kopfzeile hinaus scrollen kann:

Das sieht nicht besonders gut aus, insbesondere wenn der Text in den Delegaten so lang ist, dass er den Text in der Kopfzeile erreicht. Im Idealfall möchten wir einen durchgehenden Farbblock unter dem Kopfzeilentext, aber oberhalb der Ansicht haben. Dadurch wird sichergestellt, dass der Inhalt der ListView den Inhalt der Kopfzeile optisch nicht beeinträchtigt. Beachten Sie, dass dies auch erreicht werden kann, indem die Eigenschaft „ clip “ der Ansicht auf „ true “ gesetzt wird; dabei wird jedoch „ can affect performance “ verwendet.

ToolBar ist das richtige Werkzeug für diese Aufgabe. Es handelt sich um einen Container für anwendungsweite und kontextsensitive Aktionen und Steuerelemente wie Navigationsschaltflächen und Suchfelder. Das Beste daran ist, dass es eine Hintergrundfarbe hat, die wie üblich aus dem Anwendungsstil stammt. Hier ist es in Aktion:

    header: ToolBar {
        Label {
            text: qsTr("Contacts")
            font.pixelSize: 20
            anchors.centerIn: parent
        }
    }

Da es kein eigenes Layout hat, zentrieren wir die Beschriftung selbst darin.

Der restliche Code entspricht dem aus Kapitel 2, mit der Ausnahme, dass wir das Signal „ clicked “ genutzt haben, um die nächste Seite in die Stackview zu schieben:

            onClicked: root.StackView.view.push("ConversationPage.qml", { inConversationWith: modelData })

Wenn man ein „ Component “ oder „ url “ auf „ StackView “ schiebt, ist es oft notwendig, das (später) instanziierte Element mit einigen Variablen zu initialisieren. Die Funktion „ push()“ von „ StackView “ berücksichtigt dies, indem sie ein JavaScript-Objekt als zweites Argument entgegennimmt. Wir nutzen dies, um der nächsten Seite den Namen eines Kontakts zur Verfügung zu stellen, den diese dann zur Anzeige der entsprechenden Konversation verwendet. Beachten Sie die Syntax „ root.StackView.view.push “; diese ist aufgrund der Funktionsweise von angehängten Eigenschaften erforderlich.

Gehen wir „ ConversationPage.qml “ Schritt für Schritt durch, beginnend mit den Importen:

import QtQuick
import QtQuick.Layouts
import QtQuick.Controls

Diese sind dieselben wie zuvor, abgesehen vom zusätzlichen Import „ QtQuick.Layouts “, auf den wir gleich noch eingehen werden.

Page {
    id: root

    property string inConversationWith

    header: ToolBar {
        ToolButton {
            text: qsTr("Back")
            anchors.left: parent.left
            anchors.leftMargin: 10
            anchors.verticalCenter: parent.verticalCenter
            onClicked: root.StackView.view.pop()
        }

        Label {
            id: pageTitle
            text: root.inConversationWith
            font.pixelSize: 20
            anchors.centerIn: parent
        }
    }
    ...

Das Stammelement dieser Komponente ist eine weitere „Page“, die über eine benutzerdefinierte Eigenschaft namens „ inConversationWith “ verfügt. Vorerst bestimmt diese Eigenschaft lediglich, was in der Beschriftung der Kopfzeile angezeigt wird. Später werden wir sie in der SQL-Abfrage verwenden, die die Liste der Nachrichten in der Konversation befüllt.

Damit der Benutzer zur Seite „Contact“ zurückkehren kann, fügen wir ein „ ToolButton “ hinzu, das beim Anklicken „ pop()“ aufruft. Ein „ ToolButton “ funktioniert ähnlich wie „ Button “, bietet jedoch ein Erscheinungsbild, das besser in eine „ ToolBar “ passt.

Es gibt zwei Möglichkeiten, Elemente in QML anzuordnen: Elementpositionierer und Qt Quick Layouts. Elementpositionierer (Row, Column usw.) sind nützlich, wenn die Größe der Elemente bekannt oder feststeht und es lediglich darum geht, sie ordentlich in einer bestimmten Anordnung zu platzieren. Die Layouts in Qt Quick Layouts können Elemente sowohl positionieren als auch in der Größe anpassen, wodurch sie sich gut für anpassbare Benutzeroberflächen eignen. Im folgenden Beispiel verwenden wir ColumnLayout, um ein ListView und ein Pane vertikal anzuordnen:

    ColumnLayout {
        anchors.fill: parent

        ListView {
            id: listView
            Layout.fillWidth: true
            Layout.fillHeight: true
            ...

        }
        ...

        Pane {
            id: pane
            Layout.fillWidth: true
            ...
    }

Ein „Pane“ ist im Grunde ein Rechteck, dessen Farbe sich nach dem Stil der Anwendung richtet. Es ähnelt einem „ Frame “, mit dem einzigen Unterschied, dass es keinen Umriss um den Rand herum hat.

Elemente, die direkte untergeordnete Elemente eines Layouts sind, können verschiedene „ attached properties “ nutzen. Wir verwenden „ Layout.fillWidth “ und „ Layout.fillHeight “ auf dem „ ListView “, um sicherzustellen, dass es so viel Platz wie möglich innerhalb des „ ColumnLayout “ einnimmt. Dasselbe gilt für das „Pane“. Da „ ColumnLayout “ ein vertikales Layout ist, befinden sich links und rechts von jedem untergeordneten Element keine weiteren Elemente, sodass jedes Element die gesamte Breite des Layouts einnimmt.

Andererseits ermöglicht die Anweisung „ Layout.fillHeight “ im „ ListView “, dass dieser den verbleibenden Platz einnimmt, der nach der Unterbringung des „Pane“ übrig bleibt.

Schauen wir uns die Listview im Detail an:

        ListView {
            id: listView
            Layout.fillWidth: true
            Layout.fillHeight: true
            Layout.margins: pane.leftPadding + messageField.leftPadding
            displayMarginBeginning: 40
            displayMarginEnd: 40
            verticalLayoutDirection: ListView.BottomToTop
            spacing: 12
            model: 10
            delegate: Row {
                id: messageDelegate
                anchors.right: sentByMe ? listView.contentItem.right : undefined
                spacing: 6

                required property int index
                readonly property bool sentByMe: index % 2 == 0

                Rectangle {
                    id: avatar
                    width: height
                    height: parent.height
                    color: "grey"
                    visible: !messageDelegate.sentByMe
                }

                Rectangle {
                    width: 80
                    height: 40
                    color: messageDelegate.sentByMe ? "lightgrey" : "steelblue"

                    Label {
                        anchors.centerIn: parent
                        text: messageDelegate.index
                        color: messageDelegate.sentByMe ? "black" : "white"
                    }
                }
            }

            ScrollBar.vertical: ScrollBar {}
        }

Nachdem wir die Breite und Höhe des übergeordneten Elements angepasst haben, legen wir außerdem einige Ränder für die Ansicht fest. Dadurch erhalten wir eine schöne Ausrichtung mit dem Platzhaltertext im Feld „Nachricht verfassen“:

Die Ränder der Listenansicht sind am darunterliegenden Feld zum Verfassen von Nachrichten ausgerichtet

Als Nächstes legen wir „ displayMarginBeginning “ und „ displayMarginEnd “ fest. Diese Eigenschaften stellen sicher, dass die Delegaten außerhalb der Grenzen der Ansicht nicht verschwinden, wenn man an den Rändern der Ansicht scrollt. Am einfachsten lässt sich dies verstehen, indem man die Eigenschaften auskommentiert und beobachtet, was beim Scrollen der Ansicht passiert.

Anschließend kehren wir die vertikale Ausrichtung der Ansicht um, sodass die ersten Elemente unten stehen. Die Delegaten werden um 12 Pixel beabstandet, und zu Testzwecken wird ein „Dummy“-Modell zugewiesen, bis wir im Kapitel 4 das eigentliche Modell implementieren.

Innerhalb des Delegaten deklarieren wir ein ` Row ` als Stammelement, da der Avatar, wie in der Abbildung oben gezeigt, vom Nachrichteninhalt gefolgt werden soll.

Vom Benutzer gesendete Nachrichten sollten von denen eines Kontakts unterschieden werden. Vorläufig legen wir eine Dummy-Eigenschaft „ sentByMe “ fest, die einfach den Index des Delegaten verwendet, um zwischen verschiedenen Autoren zu wechseln. Mithilfe dieser Eigenschaft unterscheiden wir auf drei Arten zwischen verschiedenen Autoren:

  • Vom Benutzer gesendete Nachrichten werden an der rechten Bildschirmseite ausgerichtet, indem „ anchors.right “ auf „ listView.contentItem.right “ gesetzt wird.
  • Indem wir die Eigenschaft „ visible “ des Avatars (der vorerst einfach ein „Rectangle“ ist) basierend auf „ sentByMe “ setzen, zeigen wir ihn nur an, wenn die Nachricht von einem Kontakt gesendet wurde.
  • Wir ändern die Farbe des Rechtecks je nach Absender. Da wir keinen dunklen Text auf dunklem Hintergrund anzeigen möchten und umgekehrt, legen wir auch die Textfarbe je nach Absender fest. In Kapitel 5 werden wir sehen, wie das Styling solche Aufgaben für uns übernimmt.

Am unteren Bildschirmrand platzieren wir ein „ TextArea “-Element, um die Eingabe von mehrzeiligem Text zu ermöglichen, sowie eine Schaltfläche zum Senden der Nachricht. Wir verwenden „Pane“, um den Bereich unter diesen beiden Elementen abzudecken, genauso wie wir „ ToolBar “ verwenden, um zu verhindern, dass der Inhalt der Listenansicht die Seitenkopfzeile überlagert:

        Pane {
            id: pane
            Layout.fillWidth: true
            Layout.fillHeight: false

            RowLayout {
                width: parent.width

                TextArea {
                    id: messageField
                    Layout.fillWidth: true
                    placeholderText: qsTr("Compose message")
                    wrapMode: TextArea.Wrap
                }

                Button {
                    id: sendButton
                    text: qsTr("Send")
                    enabled: messageField.length > 0
                    Layout.fillWidth: false
                }
            }
        }

Das „ TextArea “-Element sollte die gesamte verfügbare Bildschirmbreite ausfüllen. Wir fügen einen Platzhaltertext hinzu, um dem Benutzer einen visuellen Anhaltspunkt zu geben, wo er mit der Eingabe beginnen soll. Der Text innerhalb des Eingabebereichs wird umgebrochen, um sicherzustellen, dass er nicht über den Bildschirmrand hinausragt.

Schließlich ist die Schaltfläche nur aktiviert, wenn tatsächlich eine Nachricht zum Senden vorhanden ist.

Kapitel 4: Modelle

In Kapitel 4 führen wir Sie durch den Prozess der Erstellung von schreibgeschützten und schreib- und lesbaren SQL-Modellen in C++ und deren Bereitstellung für QML, um Ansichten zu füllen.

QSqlQueryModel

Um das Tutorial überschaubar zu halten, haben wir uns dafür entschieden, die Liste der Benutzerkontakte nicht bearbeitbar zu machen. QSqlQueryModel ist hierfür die naheliegende Wahl, da es ein schreibgeschütztes Datenmodell für SQL-Ergebnismengen bereitstellt.

Werfen wir einen Blick auf unsere Klasse „ SqlContactModel “, die von „ QSqlQueryModel “ abgeleitet ist:

#include <QQmlEngine>
#include <QSqlQueryModel>

class SqlContactModel : public QSqlQueryModel
{
    Q_OBJECT
    QML_ELEMENT

public:
    SqlContactModel(QObject *parent = nullptr);
};

Hier gibt es nicht viel zu sehen, also fahren wir mit der Datei „ .cpp “ fort:

#include "sqlcontactmodel.h"

#include <QDebug>
#include <QSqlError>
#include <QSqlQuery>

static void createTable()
{
    if (QSqlDatabase::database().tables().contains(QStringLiteral("Contacts"))) {
        // Die Tabelle existiert bereits; wir müssen nichts tun.
        return;
    }

    QSqlQuery query;
   if (!query.exec(
        "CREATE TABLE IF NOT EXISTS 'Contacts' ("
        "   'name' TEXT NOT NULL,"
        "   PRIMARY KEY(name)"
        ")")) {
        qFatal("Failed to query database: %s", qPrintable(query.lastError().text()));
    }

    query.exec("INSERT INTO Contacts VALUES('Albert Einstein')");
    query.exec("INSERT INTO Contacts VALUES('Ernest Hemingway')");
    query.exec("INSERT INTO Contacts VALUES('Hans Gude')");
}

Wir binden die Header-Datei unserer Klasse sowie die von Qt benötigten Header-Dateien ein. Anschließend definieren wir eine statische Funktion namens „ createTable() “, mit der wir die SQL-Tabelle erstellen (falls sie noch nicht existiert) und sie dann mit einigen Dummy-Kontakten füllen.

Der Aufruf von ` database()` mag etwas verwirrend erscheinen, da wir noch keine konkrete Datenbank eingerichtet haben. Wird dieser Funktion kein Verbindungsname übergeben, gibt sie eine „Standardverbindung“ zurück, deren Erstellung wir in Kürze behandeln werden.

SqlContactModel::SqlContactModel(QObject*parent) :
    QSqlQueryModel(parent)
{
    createTable();

    QSqlQuery query;
    if (!query.exec("SELECT * FROM Contacts"))
        qFatal("Contacts SELECT query failed: %s", qPrintable(query.lastError().text()));

    setQuery(std::move(query));
    if (lastError().isValid())
        qFatal("Cannot set query on SqlContactModel: %s", qPrintable(lastError().text()));
}

Im Konstruktor rufen wir ` createTable()` auf. Anschließend erstellen wir eine Abfrage, mit der das Modell gefüllt wird. In diesem Fall interessieren wir uns lediglich für alle Zeilen der Tabelle ` Contacts `.

QSqlTableModel

SqlConversationModel ist komplexer:

#include <QQmlEngine>
#include <QSqlTableModel>

class SqlConversationModel : public QSqlTableModel
{
    Q_OBJECT
    QML_ELEMENT
    Q_PROPERTY(QString recipient READ recipient WRITE setRecipient NOTIFY recipientChanged)

public:
    SqlConversationModel(QObject *parent = nullptr);

    QString recipient() const;
    void setRecipient(const QString &recipient);

    QVariant data(const QModelIndex &index, int role) const override;
    QHash<int, QByteArray> roleNames() const override;

    Q_INVOKABLE void sendMessage(const QString &recipient, const QString &message);

signals:
    void recipientChanged();

private:
    QString m_recipient;
};

Wir verwenden sowohl die Makros „ Q_PROPERTY “ als auch „ Q_INVOKABLE “ und müssen daher moc mithilfe des Makros „ Q_OBJECT “ darüber informieren.

Die Eigenschaft „ recipient “ wird von QML gesetzt, um dem Modell mitzuteilen, für welche Konversation es Nachrichten abrufen soll.

Wir überschreiben die Funktionen ` data()` und ` roleNames()`, damit wir unsere benutzerdefinierten Rollen in QML verwenden können.

Außerdem definieren wir die Funktion ` sendMessage() `, die wir aus QML aufrufen möchten – daher das Makro ` Q_INVOKABLE `.

Werfen wir einen Blick auf die Datei „ .cpp “:

#include "sqlconversationmodel.h"

#include <QDateTime>
#include <QDebug>
#include <QSqlError>
#include <QSqlRecord>
#include <QSqlQuery>

static const char *conversationsTableName = "Conversations";

static void createTable()
{
    if (QSqlDatabase::database().tables().contains(conversationsTableName)) {
        // Die Tabelle existiert bereits; wir müssen nichts tun.
        return;
    }

    QSqlQuery query;
    if (!query.exec(
        "CREATE TABLE IF NOT EXISTS 'Conversations' ("
        "'author' TEXT NOT NULL,"
        "'recipient' TEXT NOT NULL,"
       "'timestamp' TEXT NOT NULL,"
        "'message' TEXT NOT NULL,"
        "FOREIGN KEY('author') REFERENCES Contacts ( name ),"
       "FOREIGN KEY('recipient') REFERENCES Contacts ( name )"
        ")")) {
        qFatal("Failed to query database: %s", qPrintable(query.lastError().text()));
    }

    query.exec("INSERT INTO Conversations VALUES('Ich', 'Ernest Hemingway', '2016-01-07T14:36:06', 'Hallo!')");
    query.exec("INSERT INTO Conversations VALUES('Ernest Hemingway', 'Ich', '2016-01-07T14:36:16', 'Guten Nachmittag.')");
    query.exec("INSERT INTO Conversations VALUES('Ich', 'Albert Einstein', '2016-01-01T11:24:53', 'Hallo!')");
    query.exec("INSERT INTO Conversations VALUES('Albert Einstein', 'Ich', '2016-01-07T14:36:16', 'Guten Morgen.')");
    query.exec("INSERT INTO Conversations VALUES('Hans Gude', 'Ich', '2015-11-20T06:30:02', 'Guten Morgen. Hast du mein Gemälde bekommen?')");
    query.exec("INSERT INTO Conversations VALUES('Ich', 'Hans Gude', '2015-11-20T08:21:03', 'Guten Morgen, Hans. Ja, es ist wirklich schön. Vielen Dank! "
               "Wie viele Stunden hast du dafür gebraucht?')");
}

Dies ähnelt stark dem Code „ sqlcontactmodel.cpp “, mit dem Unterschied, dass wir nun mit der Tabelle „ Conversations “ arbeiten. Außerdem definieren wir „ conversationsTableName “ als statische Konstante, da wir sie an mehreren Stellen in der Datei verwenden.

SqlConversationModel::SqlConversationModel(QObject *parent) :
    QSqlTableModel(parent)
{
    createTable();
    setTable(conversationsTableName);
    setSort(2, Qt::DescendingOrder);
    // Ensures that the model is sorted correctly after submitting a new row.
    setEditStrategy(QSqlTableModel::OnManualSubmit);
}

Wie bei SqlContactModel erstellen wir im Konstruktor als Erstes die Tabelle. Über die Funktion ` setTable()` teilen wir ` QSqlTableModel ` den Namen der Tabelle mit, die wir verwenden werden. Um sicherzustellen, dass die neuesten Nachrichten in der Konversation zuerst angezeigt werden, sortieren wir die Abfrageergebnisse nach dem Feld ` timestamp ` in absteigender Reihenfolge. Dies geht Hand in Hand mit der Einstellung der Eigenschaft ` verticalLayoutDirection ` von ` ListView` auf ` ListView.BottomToTop ` (was wir in Kapitel 3 behandelt haben).

QString SqlConversationModel::recipient() const
{
    return m_recipient;
}

void SqlConversationModel::setRecipient(const QString &recipient)
{
    if (recipient == m_recipient)
        return;

    m_recipient = recipient;

    const QString filterString = QString::fromLatin1(
        "(recipient = '%1' AND author = 'Me') OR (recipient = 'Me' AND author='%1')").arg(m_recipient);
    setFilter(filterString);
    select();

    emit recipientChanged();
}

In „ setRecipient() “ legen wir einen Filter für die von der Datenbank zurückgegebenen Ergebnisse fest.

QVariant SqlConversationModel::data(const QModelIndex &index, int role) const
{
    if (role < Qt::UserRole)
        return QSqlTableModel::data(index, role);

    const QSqlRecord sqlRecord = record(index.row());
    return sqlRecord.value(role - Qt::UserRole);
}

Die Funktion „ data() “ greift auf die Implementierung von „ QSqlTableModel “ zurück, wenn es sich bei der Rolle nicht um eine benutzerdefinierte Benutzerrolle handelt. Handelt es sich um eine Benutzerrolle, können wir „ Qt::UserRole “ davon abziehen, um den Index dieses Feldes zu ermitteln, und diesen dann verwenden, um den Wert zu finden, den wir zurückgeben müssen.

QHash<int, QByteArray> SqlConversationModel::roleNames() const
{
    QHash<int, QByteArray> names;
    names[Qt::UserRole] = "author";
    names[Qt::UserRole + 1] = "recipient";
    names[Qt::UserRole + 2] = "timestamp";
    names[Qt::UserRole + 3] = "message";
    return names;
}

In ` roleNames()` geben wir eine Zuordnung unserer benutzerdefinierten Rollenwerte zu Rollennamen zurück. Dies ermöglicht es uns, diese Rollen in QML zu verwenden. Es kann nützlich sein, eine Enumeration zu deklarieren, um alle Rollenwerte zu speichern, aber da wir im Code außerhalb dieser Funktion nicht auf einen bestimmten Wert verweisen, verzichten wir darauf.

void SqlConversationModel::sendMessage(const QString&recipient, const QString&message)
{
    const QString timestamp = QDateTime::currentDateTime().toString(Qt::ISODate);

    QSqlRecord newRecord = record();
    newRecord.setValue("author", "Ich");
    newRecord.setValue("recipient", recipient);
    newRecord.setValue("timestamp", timestamp);
    newRecord.setValue("message", message);
   if (!insertRecord(rowCount(), newRecord)) {
        qWarning() << "Failed to send message:" << lastError().text();
       return;
    }

Die Funktion „ sendMessage() “ verwendet die angegebenen Parameter „ recipient “ und „ message “, um einen neuen Datensatz in die Datenbank einzufügen. Da wir „ QSqlTableModel::OnManualSubmit “ verwenden, müssen wir „ submitAll()“ manuell aufrufen.

Verbindung zur Datenbank herstellen und Typen mit QML registrieren

Nachdem wir nun die Modellklassen definiert haben, werfen wir einen Blick auf „ main.cpp “:

#include <QtCore>
#include <QGuiApplication>
#include <QSqlDatabase>
#include <QSqlError>
#include <QtQml>

static void connectToDatabase()
{
    QSqlDatabase database = QSqlDatabase::database();
    if (!database.isValid()) {
        database = QSqlDatabase::addDatabase("QSQLITE");
        if (!database.isValid())
            qFatal("Cannot add database: %s", qPrintable(database.lastError().text()));
    }

    const QDir writeDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
    if (!writeDir.mkpath("."))
        qFatal("Failed to create writable directory at %s", qPrintable(writeDir.absolutePath()));

   // Sicherstellen, dass auf allen Geräten ein beschreibbarer Speicherort vorhanden ist.
    const QString fileName = writeDir.absolutePath() + "/chat-database.sqlite3";
   // Bei Verwendung des SQLite-Treibers erstellt „open()“ die SQLite-Datenbank, falls diese noch nicht existiert.
    database.setDatabaseName(fileName);
    if (!database.open()) {
        qFatal("Cannot open database: %s", qPrintable(database.lastError().text()));
        QFile::remove(fileName);
    }
}

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    connectToDatabase();

    QQmlApplicationEngine engine;
    engine.loadFromModule("chattutorial", "Main");
    if (engine.rootObjects().isEmpty())
        return-1;

    return app.exec();
}

connectToDatabase() stellt die Verbindung zur SQLite-Datenbank her und legt die eigentliche Datei an, falls diese noch nicht existiert.

Innerhalb von ` main()` rufen wir ` qmlRegisterType()` auf, um unsere Modelle als Typen in QML zu registrieren.

Verwendung der Modelle in QML

Da uns die Modelle nun als QML-Typen zur Verfügung stehen, sind einige kleinere Änderungen an ` ContactPage.qml` erforderlich. Um die Typen nutzen zu können, müssen wir sie zunächst über die URI importieren, die wir in ` main.cpp` festgelegt haben:

import chattutorial

Anschließend ersetzen wir das Dummy-Modell durch das richtige:

        model: SqlContactModel {}

Innerhalb des Delegaten verwenden wir eine andere Syntax für den Zugriff auf die Modelldaten:

            text: model.display

In „ ConversationPage.qml “ fügen wir denselben Import „ chattutorial “ hinzu und ersetzen das Dummy-Modell:

            model: SqlConversationModel {
                recipient: root.inConversationWith
            }

Innerhalb des Modells setzen wir die Eigenschaft „ recipient “ auf den Namen des Kontakts, für den die Seite angezeigt wird.

Das oberste Delegate-Element wird von einer Zeile (Row) in eine Spalte (Column) geändert, um den Zeitstempel unterzubringen, den wir unter jeder Nachricht anzeigen möchten:

            delegate: Column {
                id: conversationDelegate
                anchors.right: sentByMe ? listView.contentItem.right : undefined
                spacing: 6

                required property string author
                required property string recipient
                required property date timestamp
                required property string message
                readonly property bool sentByMe: recipient !== "Me"

                Row {
                    id: messageRow
                    spacing: 6
                    anchors.right: conversationDelegate.sentByMe ? parent.right : undefined

                    Image {
                        id: avatar
                        source: !conversationDelegate.sentByMe
                            ? "images/" + conversationDelegate.author.replace(" ", "_") + ".png" : ""
                    }

                    Rectangle {
                        width: Math.min(messageText.implicitWidth + 24,
                            listView.width - (!conversationDelegate.sentByMe ? avatar.width + messageRow.spacing : 0))
                        height: messageText.implicitHeight + 24
                        color: conversationDelegate.sentByMe ? "lightgrey" : "steelblue"

                        Label {
                            id: messageText
                            text: conversationDelegate.message
                            color: conversationDelegate.sentByMe ? "black" : "white"
                            anchors.fill: parent
                            anchors.margins: 12
                            wrapMode: Label.Wrap
                        }
                    }
                }

                Label {
                    id: timestampText
                    text: Qt.formatDateTime(conversationDelegate.timestamp, "d MMM hh:mm")
                    color: "lightgrey"
                    anchors.right: conversationDelegate.sentByMe ? parent.right : undefined
                }
            }

Chat-Nachrichten, bei denen Zeitstempel unterhalb des Nachrichtentextes angezeigt werden

Da wir nun über ein korrektes Modell verfügen, können wir dessen „ recipient “-Rolle im Ausdruck für die Eigenschaft „ sentByMe “ verwenden.

Das Rechteck, das für den Avatar verwendet wurde, wurde in ein Bild umgewandelt. Das Bild hat eine eigene implizite Größe, sodass wir diese nicht explizit angeben müssen. Wie zuvor zeigen wir den Avatar nur an, wenn der Autor nicht der Benutzer ist, nur dass wir diesmal die Eigenschaft „ source “ des Bildes auf eine leere URL setzen, anstatt die Eigenschaft „ visible “ zu verwenden.

Wir möchten, dass der Hintergrund jeder Nachricht etwas breiter ist (jeweils 12 Pixel pro Seite) als der Text. Ist die Nachricht jedoch zu lang, möchten wir ihre Breite auf den Rand der Listenansicht begrenzen, daher die Verwendung von ` Math.min()`. Wenn die Nachricht nicht von uns gesendet wurde, steht immer ein Avatar davor; dies berücksichtigen wir, indem wir die Breite des Avatars und den Zeilenabstand abziehen.

Im obigen Bild ist beispielsweise die implizite Breite des Nachrichtentextes der kleinere Wert. Im unteren Bild ist der Nachrichtentext jedoch recht lang, sodass der kleinere Wert (die Breite der Ansicht) gewählt wird, um sicherzustellen, dass der Text am gegenüberliegenden Bildschirmrand endet:

Lange Textpassagen werden so umgebrochen, dass sie in die Breite der Ansicht passen

Um den Zeitstempel für jede Nachricht anzuzeigen, den wir zuvor besprochen haben, verwenden wir ein Label. Datum und Uhrzeit werden mit ` Qt.formatDateTime()` unter Verwendung eines benutzerdefinierten Formats formatiert.

Die Schaltfläche „Senden“ muss nun auf einen Klick reagieren:

                Button {
                    id: sendButton
                    text: qsTr("Send")
                    enabled: messageField.length > 0
                    Layout.fillWidth: false
                    onClicked: {
                        listView.model.sendMessage(root.inConversationWith, messageField.text)
                        messageField.text = ""
                    }
                }

Zunächst rufen wir die aufrufbare Funktion ` sendMessage() ` des Modells auf, die eine neue Zeile in die Datenbanktabelle „Conversations“ einfügt. Anschließend löschen wir das Textfeld, um Platz für zukünftige Eingaben zu schaffen.

Kapitel 5: Gestaltung

Stile in „ Qt Quick Controls “ sind so konzipiert, dass sie auf jeder Plattform funktionieren. In diesem Kapitel nehmen wir einige kleinere optische Anpassungen vor, um sicherzustellen, dass unsere Anwendung mit den Stilen „Basic“, „Material“ und „Universal“ gut aussieht.

Bisher haben wir die Anwendung nur mit dem „Basic“-Stil getestet. Wenn wir sie beispielsweise mit dem „Material“-Stil ausführen, fallen sofort einige Probleme auf. Hier ist die Seite „Kontakte“:

Kontaktseite im „Material“-Design mit unzureichendem Textkontrast

Der Text in der Kopfzeile ist schwarz auf dunkelblauem Hintergrund, was die Lesbarkeit erheblich beeinträchtigt. Das Gleiche tritt auch auf der Seite „Konversationen“ auf:

Diskussionsseite im „Material“-Stil mit unzureichendem Textkontrast

Die Lösung besteht darin, der Symbolleiste mitzuteilen, dass sie das „Dark“-Design verwenden soll, damit diese Information an ihre untergeordneten Elemente weitergegeben wird und diese ihre Textfarbe auf eine hellere umstellen können. Der einfachste Weg, dies zu erreichen, ist, den Material-Stil direkt zu importieren und die angehängte Eigenschaft „Material“ zu verwenden:

import QtQuick.Controls.Material 2.12

// ...

header: ToolBar {
    Material.theme: Material.Dark

    // ...
}

Dies bringt jedoch eine feste Abhängigkeit vom Material-Stil mit sich; das Material-Stil-Plugin muss zusammen mit der Anwendung bereitgestellt werden, auch wenn das Zielgerät es nicht verwendet, da die QML-Engine den Import sonst nicht finden kann.

Stattdessen ist es besser, auf die integrierte Unterstützung von „ Qt Quick Controls “ für stilbasierte Dateiselektoren zurückzugreifen. Dazu müssen wir die Datei „ ToolBar “ in eine eigene Datei auslagern. Wir nennen sie „ ChatToolBar.qml “. Dies ist die „Standard “-Version der Datei, d. h., sie wird verwendet, wenn der Stil „Basic“ (der Stil, der verwendet wird, wenn kein Stil angegeben ist) im Einsatz ist. Hier ist die neue Datei:

import QtQuick.Controls

ToolBar {
}

Da wir in dieser Datei ausschließlich den Typ „ ToolBar “ verwenden, benötigen wir lediglich den Import „ Qt Quick Controls “. Der Code selbst hat sich gegenüber der Version in „ ContactPage.qml “ nicht geändert, was auch so sein sollte; für die Standardversion der Datei muss nichts anders sein.

Zurück in „ ContactPage.qml “ passen wir den Code an, um den neuen Typ zu verwenden:

    header: ChatToolBar {
        Label {
            text: qsTr("Contacts")
            font.pixelSize: 20
            anchors.centerIn: parent
        }
    }

Nun müssen wir die Material-Version der Symbolleiste hinzufügen. Dateiauswahlfelder erwarten, dass Varianten einer Datei in entsprechend benannten Verzeichnissen liegen, die neben der Standardversion der Datei existieren. Das bedeutet, dass wir einen Ordner namens „+Material“ in demselben Verzeichnis hinzufügen müssen, in dem sich „ChatToolBar.qml“ befindet: dem Stammverzeichnis. Das „+“ wird von QFileSelector benötigt, um sicherzustellen, dass die Auswahlfunktion nicht versehentlich ausgelöst wird.

Hier ist „ +Material/ChatToolBar.qml “:

import QtQuick.Controls
import QtQuick.Controls.Material

ToolBar {
    Material.theme: Material.Dark
}

Wir nehmen die gleichen Änderungen an ConversationPage.qml vor:

    header: ChatToolBar {
        ToolButton {
            text: qsTr("Back")
            anchors.left: parent.left
            anchors.leftMargin: 10
            anchors.verticalCenter: parent.verticalCenter
            onClicked: root.StackView.view.pop()
        }

        Label {
            id: pageTitle
            text: root.inConversationWith
            font.pixelSize: 20
            anchors.centerIn: parent
        }
    }

Jetzt sehen beide Seiten korrekt aus:

Kontaktseite im „Material“-Stil und mit korrigiertem Kontrast in der Kopfzeile

Diskussionsseite im „Material“-Stil und mit korrigiertem Kontrast in der Kopfzeile

Probieren wir den „Universal“-Stil aus:

Kontaktseite im „Universal“-Stil

Diskussionsseite im Universal-Stil

Hier gibt es keine Probleme. Bei einer relativ einfachen Anwendung wie dieser sollten beim Wechseln des Stils nur sehr wenige Anpassungen erforderlich sein.

Probieren wir nun das dunkle Design der einzelnen Stile aus. Der „Basic“-Stil verfügt über kein dunkles Design, da dies bei einem Stil, der auf möglichst hohe Leistung ausgelegt ist, einen leichten Mehraufwand bedeuten würde. Wir testen zunächst den „Material“-Stil. Fügen Sie daher unter „ qtquickcontrols2.conf “ einen Eintrag hinzu, der die Verwendung des dunklen Designs festlegt:

[Material]
Primary=Indigo
Accent=Indigo
Theme=Dark

Sobald dies erledigt ist, erstellen Sie die Anwendung und führen Sie sie aus. Folgendes sollte nun angezeigt werden:

Kontaktseite im „Material“-Stil mit dunklem Design

Diskussionsseite im Material-Design mit dunklem Design

Beide Seiten sehen gut aus. Fügen Sie nun einen Eintrag für den „Universal“-Stil hinzu:

[universal]
Theme=Dark

Nachdem Sie die Anwendung kompiliert und ausgeführt haben, sollten Sie folgende Ergebnisse sehen:

Kontaktseite im „Universal“-Stil mit dunklem Design

Diskussionsseite im Universal-Stil mit dunklem Design

Zusammenfassung

In diesem Tutorial haben wir Sie durch die folgenden Schritte zum Erstellen einer einfachen Anwendung mit „ Qt Quick Controls “ geführt:

  • Erstellen eines neuen Projekts mit „ Qt Creator “.
  • Einrichten einer einfachen „ ApplicationWindow “.
  • Definieren von Kopf- und Fußzeilen mit „Page“.
  • Anzeigen von Inhalten in einer „ ListView “.
  • Komponenten in eigene Dateien auslagern.
  • Navigieren zwischen Bildschirmen mit „ StackView “.
  • Verwendung von Layouts, damit sich die Größe der Anwendung nahtlos anpassen lässt.
  • Implementierung sowohl benutzerdefinierter schreibgeschützter als auch beschreibbarer Modelle, die eine SQL-Datenbank in die Anwendung integrieren.
  • Integration von C++ mit QML über „ Q_PROPERTY “, „ Q_INVOKABLE “ und „ qmlRegisterType()“.
  • Testen und Konfigurieren verschiedener Stile.

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.