Qt Quick Controls - Chat-Tutorial
Dieses Tutorial zeigt, wie man eine einfache Chat-Anwendung mit Qt Quick Controls schreibt. Es wird auch erklärt, wie man eine SQL-Datenbank in eine Qt-Anwendung integriert.
Kapitel 1: Einrichten
Wenn Sie ein neues Projekt einrichten, ist es am einfachsten, wenn Sie Qt Creator. Für dieses Projekt haben wir die AnwendungsvorlageQt Quick gewählt, die eine einfache "Hello World"-Anwendung mit den folgenden Dateien erstellt:
CMakeLists.txt
- Weist CMake an, wie unser Projekt gebaut werden sollMain.qml
- Bietet eine Standard-Benutzeroberfläche mit einem leeren Fenstermain.cpp
- Lädtmain.qml
qtquickcontrols2.conf
- Sagt der Anwendung, welchen Stil sie verwenden soll
main.cpp
Der Standardcode in main.cpp
hat zwei Includes:
#include <QGuiApplication> #include <QQmlApplicationEngine>
Das erste gibt uns Zugriff auf QGuiApplication. Alle Qt-Anwendungen benötigen ein Anwendungsobjekt, aber der genaue Typ hängt davon ab, was die Anwendung tut. QCoreApplication ist ausreichend für nicht-grafische Anwendungen. QGuiApplication ist ausreichend für grafische Anwendungen, die nicht Qt Widgetsverwenden, während QApplication für solche benötigt wird, die dies tun.
Das zweite Include macht QQmlApplicationEngine verfügbar und ermöglicht es uns, unser QML zu laden.
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 über QQmlEngine, der die Funktion loadFromModule zum einfachen Laden von QML für eine Anwendung bereitstellt. Sie bietet auch einige Annehmlichkeiten für die Verwendung von Dateiselektoren.
Sobald wir die Dinge in C++ eingerichtet haben, können wir mit der Benutzeroberfläche in QML fortfahren.
Main.qml
Lassen Sie uns den Standard-QML-Code an unsere Bedürfnisse anpassen.
import QtQuick import QtQuick.Controls
Sie werden feststellen, dass das Qt Quick Modul bereits importiert wurde. Dies ermöglicht uns den Zugriff auf grafische Primitive wie Item, Rectangle, Text, und so weiter. Die 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. Dies ermöglicht unter anderem den Zugriff auf ApplicationWindow, das den bestehenden Root-Typ Window
ersetzen wird:
ApplicationWindow { width: 540 height: 960 visible: true ... }
ApplicationWindow ist ein Window mit einigen zusätzlichen Annehmlichkeiten für die Erstellung eines header und eines footer. Es bietet auch die Grundlage für popups und unterstützt einige grundlegende Gestaltungsmöglichkeiten, wie den Hintergrund color.
Es gibt drei Eigenschaften, die bei der Verwendung von ApplicationWindow fast immer gesetzt werden: width, height und visible. Sobald wir diese eingestellt haben, haben wir ein leeres Fenster in der richtigen Größe, das mit Inhalt gefüllt werden kann.
Hinweis: Die Eigenschaft title
aus dem Standardcode wird entfernt.
Der erste "Bildschirm" in unserer Anwendung wird eine Liste von Kontakten sein. Es wäre schön, wenn am oberen Rand jedes Bildschirms ein Text stehen würde, der seinen Zweck beschreibt. Die Kopf- und Fußzeileneigenschaften von ApplicationWindow könnten in dieser Situation funktionieren. Sie haben einige Eigenschaften, die sie ideal für Elemente machen, die auf jedem Bildschirm einer Anwendung angezeigt werden sollen:
- Sie sind am oberen bzw. unteren Rand des Fensters verankert.
- Sie füllen die Breite des Fensters aus.
Wenn jedoch der Inhalt der Kopf- und Fußzeile je nach Bildschirm variiert, ist es viel einfacher, Page zu verwenden. Vorerst fügen wir nur eine Seite hinzu, aber im nächsten Kapitel werden wir zeigen, wie man zwischen mehreren Seiten navigieren kann.
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, die mit Hilfe der Eigenschaft anchors.fill so dimensioniert wird, dass sie den gesamten Platz im Fenster einnimmt.
Dann weisen wir der Eigenschaft header ein Label zu. Label erweitert das primitive Element Text aus dem Modul Qt Quick durch Hinzufügen von Styling und font Vererbung. Das bedeutet, dass ein Label unterschiedlich aussehen kann, je nachdem, welcher Stil verwendet wird, und auch seine Pixelgröße an seine Kinder weitergeben kann.
Wir wollen einen gewissen Abstand zwischen dem oberen Rand des Anwendungsfensters und dem Text, also setzen wir die Eigenschaft padding. Dadurch wird auf jeder Seite des Etiketts (innerhalb seiner Grenzen) zusätzlicher Platz zugewiesen. Wir können stattdessen auch die Eigenschaften topPadding und bottomPadding explizit festlegen.
Wir setzen den Text der Beschriftung mit der Funktion qsTr()
, die sicherstellt, dass der Text vom Übersetzungssystem von Qt übersetzt werden kann. Dies ist eine gute Praxis für Text, der für die Endbenutzer Ihrer Anwendung sichtbar ist.
Standardmäßig wird Text vertikal am oberen Rand seiner Begrenzungen ausgerichtet, während die horizontale Ausrichtung von der natürlichen Richtung des Textes abhängt; zum Beispiel wird Text, der von links nach rechts gelesen wird, nach links ausgerichtet. Würden wir diese Standardeinstellungen verwenden, würde sich unser Text in der linken oberen Ecke des Fensters befinden. Da dies für eine Kopfzeile nicht wünschenswert ist, richten wir den Text sowohl horizontal als auch vertikal an der Mitte seiner Begrenzungen aus.
Die Projektdatei
Die Datei CMakeLists.txt
enthält alle Informationen, die CMake benötigt, um unser Projekt in eine ausführbare Datei zu verwandeln, 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 aus, wenn sie ausgeführt wird:
Kapitel 2: Listen
In diesem Kapitel wird erklärt, wie man mit ListView und ItemDelegate eine Liste mit interaktiven Elementen erstellt.
ListView Qt Quick ItemDelegate stammt aus dem Modul Qt Quick Controls und stellt ein Standard-Ansichtelement zur Verwendung in Ansichten und Steuerelementen wie ListView und ComboBox bereit. Jedes ItemDelegate kann zum Beispiel Text anzeigen, ein- und ausgeschaltet 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ößenbestimmung und Positionierung
Als Erstes legen wir eine Größe für die Ansicht fest. Sie sollte den verfügbaren Platz auf der Seite ausfüllen, daher verwenden wir anchors.fill. Beachten Sie, dass die Seite sicherstellt, dass ihre Kopf- und Fußzeilen genügend eigenen Platz haben, so dass die Ansicht in diesem Fall z. B. unter der Kopfzeile sitzen wird.
Als Nächstes legen wir margins um ListView herum fest, um einen gewissen Abstand zwischen ihm und den Rändern des Fensters zu schaffen. Die Randeigenschaften reservieren Platz innerhalb der Grenzen der Ansicht, was bedeutet, dass die leeren Bereiche immer noch vom Benutzer "angeklickt" werden können.
Die Elemente sollten innerhalb der Ansicht gut verteilt sein, daher wird die Eigenschaft spacing auf 20
gesetzt.
Modell
Um die Ansicht schnell mit einigen Elementen aufzufüllen, haben wir ein JavaScript-Array als Modell verwendet. Eine der größten Stärken von QML ist seine Fähigkeit, das Prototyping einer Anwendung extrem schnell zu machen, und dies ist ein Beispiel dafür. Es ist auch möglich, der Modelleigenschaft einfach eine Zahl zuzuweisen, um anzugeben, wie viele Elemente Sie benötigen. Wenn Sie beispielsweise der Eigenschaft model
die Zahl 10
zuweisen, wird der Anzeigetext für jedes Element eine Zahl zwischen 0
und 9
sein.
Sobald die Anwendung jedoch über das Prototypenstadium hinausgeht, wird es schnell notwendig, echte Daten zu verwenden. Hierfür ist es am besten, ein richtiges C++-Modell von subclassing QAbstractItemModel zu verwenden.
Delegieren Sie
Weiter zu delegate. Wir weisen den entsprechenden Text aus dem Modell der Eigenschaft text von ItemDelegate zu. Die genaue Art und Weise, wie die Daten aus dem Modell den einzelnen Delegaten zur Verfügung gestellt werden, hängt von der Art des verwendeten Modells ab. Weitere Informationen finden Sie unter Modelle und Ansichten in Qt Quick.
In unserer Anwendung sollte die Breite der einzelnen Elemente in der Ansicht gleich der Breite der Ansicht sein. Dadurch wird sichergestellt, dass der Benutzer viel Platz hat, um einen Kontakt aus der Liste auszuwählen, was auf Geräten mit kleinen Touchscreens, wie z. B. Mobiltelefonen, ein wichtiger Faktor ist. Die Breite der Ansicht umfasst jedoch unsere 48
Pixelränder, so dass wir dies bei der Zuweisung der Eigenschaft width berücksichtigen müssen.
Als Nächstes definieren wir ein Image. Dieses wird ein Bild des Kontakts des Benutzers anzeigen. Das Bild wird 40
Pixel breit und 40
Pixel hoch sein. Die Höhe des Delegaten richtet sich nach der Höhe des Bildes, damit es keinen leeren vertikalen Raum gibt.
Kapitel 3: Navigation
In diesem Kapitel lernen Sie, wie Sie StackView verwenden, um zwischen den Seiten einer Anwendung zu navigieren. Hier ist die überarbeitete main.qml
:
import QtQuick.Controls ApplicationWindow { id: window width: 540 height: 960 visible: true StackView { id: stackView anchors.fill: parent initialItem: ContactPage {} } }
Navigieren mit StackView
Wie der Name schon sagt, bietet StackView eine stapelbasierte Navigation. Das letzte Element, das auf den Stapel "geschoben" wird, ist das erste, das entfernt wird, und das oberste Element ist immer dasjenige, das sichtbar ist.
Auf die gleiche Weise wie bei Page weisen wir StackView an, das Anwendungsfenster zu füllen. Danach müssen wir ihm nur noch ein Element geben, das es anzeigen soll, und zwar über initialItem. StackView akzeptiert items, components und URLs.
Sie werden feststellen, dass wir den Code für die Kontaktliste in ContactPage.qml
verschoben haben. Es ist eine gute Idee, dies zu tun, sobald Sie eine allgemeine Vorstellung davon haben, welche Bildschirme Ihre Anwendung enthalten wird. Auf diese Weise wird Ihr Code nicht nur leichter lesbar, 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 bequeme Refactoring-Optionen für QML, von denen eine es Ihnen ermöglicht, einen Codeblock in eine separate Datei zu verschieben (Alt + Enter > Move Component into Separate File
).
Eine weitere Überlegung bei der Verwendung von ListView ist, ob man sich mit id
darauf beziehen oder die angehängte Eigenschaft ListView.view verwenden soll. Welcher Ansatz am besten ist, hängt von einigen verschiedenen Faktoren ab. Wenn Sie der Ansicht eine ID geben, führt dies zu kürzeren und effizienteren Bindungsausdrücken, da die angehängte Eigenschaft nur einen sehr geringen Overhead hat. Wenn Sie jedoch planen, den Delegaten in anderen Ansichten wiederzuverwenden, ist es besser, die angehängten Eigenschaften zu verwenden, um den Delegaten nicht an eine bestimmte Ansicht zu binden. Wenn Sie zum Beispiel die angehängten Eigenschaften verwenden, wird die width
Zuordnung in unserem Delegaten:
width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin
In Kapitel 2 haben wir ein ListView unterhalb der Kopfzeile hinzugefügt. Wenn Sie die Anwendung für dieses Kapitel ausführen, werden Sie sehen, dass der Inhalt der Ansicht über die Kopfzeile gescrollt werden kann:
Das ist nicht sehr schön, vor allem wenn der Text in den Delegierten so lang ist, dass er den Text in der Kopfzeile erreicht. Was wir idealerweise tun wollen, ist ein fester Farbblock unter dem Kopfzeilentext, aber oberhalb der Ansicht. Dadurch wird sichergestellt, dass der Inhalt der Listenansicht nicht mit dem Inhalt der Kopfzeile in Konflikt gerät. Beachten Sie, dass es auch möglich ist, dies zu erreichen, indem Sie die Eigenschaft clip der Ansicht auf true
setzen, aber dafür ist can affect performance das richtige Werkzeug.
ToolBar ist das richtige Werkzeug für diese Aufgabe. Es ist ein Container für anwendungsweite und kontextabhängige Aktionen und Steuerelemente, wie z. B. Navigationsschaltflächen und Suchfelder. Das Beste daran ist, dass sie eine Hintergrundfarbe hat, die wie üblich aus dem Anwendungsstil stammt. Hier ist er in Aktion zu sehen:
header: ToolBar { Label { text: qsTr("Contacts") font.pixelSize: 20 anchors.centerIn: parent } }
Es hat kein eigenes Layout, also zentrieren wir das Etikett selbst darin.
Der Rest des Codes ist derselbe wie in Kapitel 2, mit der Ausnahme, dass wir das Signal clicked nutzen, um die nächste Seite auf den 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 (eventuell) instanziierte Element mit einigen Variablen zu initialisieren. StackView Die Funktion push() berücksichtigt dies, indem sie ein JavaScript-Objekt als zweites Argument annimmt. Wir verwenden dies, um der nächsten Seite den Namen eines Kontakts zu übermitteln, den sie dann zur Anzeige der entsprechenden Konversation verwendet. Beachten Sie die root.StackView.view.push
-Syntax; dies ist aufgrund der Funktionsweise von angehängten Eigenschaften erforderlich.
Gehen wir nun ConversationPage.qml
durch, beginnend mit den Importen:
import QtQuick import QtQuick.Layouts import QtQuick.Controls
Diese sind dieselben wie zuvor, mit Ausnahme des zusätzlichen Imports QtQuick.Layouts
, den wir in Kürze behandeln 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 Wurzelelement dieser Komponente ist eine andere Seite, die eine benutzerdefinierte Eigenschaft namens inConversationWith
hat. Diese Eigenschaft legt vorerst nur fest, was die Beschriftung in der Kopfzeile anzeigt. Später werden wir sie in der SQL-Abfrage verwenden, die die Liste der Nachrichten in der Unterhaltung auffüllt.
Damit der Benutzer zur Kontaktseite zurückkehren kann, fügen wir ein ToolButton hinzu, das pop() aufruft, wenn es angeklickt wird. Ein ToolButton ist funktionell ähnlich wie Button, bietet aber ein Aussehen, das besser zu ToolBar passt.
Es gibt zwei Möglichkeiten, Elemente in QML anzuordnen: Element-Positionierer und Qt Quick Layouts. Element-Positionierer (Row, Column usw.) sind nützlich, wenn die Größe der Elemente bekannt oder festgelegt ist und sie nur in einer bestimmten Formation angeordnet werden müssen. Die Layouts in Qt Quick Layouts können Elemente sowohl positionieren als auch in der Größe verändern und eignen sich daher gut für größenveränderbare Benutzeroberflächen. Im Folgenden 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 ... }
Pane ist im Grunde ein Rechteck, dessen Farbe vom Stil der Anwendung abhängt. Es ist ähnlich wie Frame, mit dem einzigen Unterschied, dass es keinen Strich um seinen Rand hat.
Für Elemente, die direkte Kinder eines Layouts sind, stehen verschiedene attached properties zur Verfügung. Wir verwenden Layout.fillWidth und Layout.fillHeight für ListView, um sicherzustellen, dass es innerhalb von ColumnLayout so viel Platz wie möglich einnimmt. Das Gleiche gilt für den Bereich. Da es sich bei ColumnLayout um ein vertikales Layout handelt, gibt es keine Elemente links oder rechts von jedem untergeordneten Element, so dass jedes Element die gesamte Breite des Layouts einnimmt.
Andererseits ermöglicht die Anweisung Layout.fillHeight in ListView, den verbleibenden Platz zu belegen, der nach der Unterbringung des Fensters übrig geblieben ist.
Schauen wir uns nun die Listenansicht 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 Objekts gefüllt haben, legen wir auch einige Ränder für die Ansicht fest. Dadurch erhalten wir eine schöne Ausrichtung mit dem Platzhaltertext im Feld "Nachricht verfassen":
Als nächstes legen wir displayMarginBeginning und displayMarginEnd fest. Diese Eigenschaften stellen sicher, dass die Delegierten außerhalb der Grenzen der Ansicht nicht verschwinden, wenn sie an den Rändern der Ansicht scrollen. Es ist am einfachsten, dies zu verstehen, indem Sie die Eigenschaften auskommentieren und sehen, was beim Scrollen der Ansicht passiert.
Anschließend drehen wir die vertikale Richtung der Ansicht um, so dass sich die ersten Elemente unten befinden. Die Delegierten sind in einem Abstand von 12 Pixeln angeordnet, und ein "Dummy" -Modell wird zu Testzwecken zugewiesen, bis wir das echte Modell in Kapitel 4 implementieren.
Innerhalb des Delegaten deklarieren wir ein Row als Root-Element, da wir wollen, dass der Avatar vom Inhalt der Nachricht gefolgt wird, wie im Bild oben gezeigt.
Die vom Benutzer gesendeten Nachrichten sollten von denen eines Kontakts unterschieden werden. Vorerst setzen wir eine Dummy-Eigenschaft sentByMe
, die einfach den Index des Delegaten verwendet, um zwischen verschiedenen Autoren zu wechseln. Mit dieser Eigenschaft können wir auf drei Arten zwischen verschiedenen Autoren unterscheiden:
- Vom Benutzer gesendete Nachrichten werden am rechten Rand des Bildschirms ausgerichtet, indem
anchors.right
auflistView.contentItem.right
gesetzt wird. - Indem wir die Eigenschaft
visible
des Avatars (der im Moment einfach ein Rechteck ist) basierend aufsentByMe
einstellen, zeigen wir ihn nur an, wenn die Nachricht von einem Kontakt gesendet wurde. - Wir ändern die Farbe des Rechtecks in Abhängigkeit vom Autor. Da wir nicht wollen, dass dunkler Text auf dunklem Hintergrund angezeigt wird und umgekehrt, stellen wir auch die Textfarbe in Abhängigkeit vom Autor ein. In Kapitel 5 werden wir sehen, wie das Styling solche Dinge für uns regelt.
Am unteren Rand des Bildschirms platzieren wir ein Element TextArea, um eine mehrzeilige Texteingabe zu ermöglichen, und 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 den Seitenkopf beeinträchtigt:
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 } } }
Die TextArea sollte die verfügbare Breite des Bildschirms ausfüllen. Wir weisen einen Platzhaltertext zu, um dem Benutzer einen visuellen Hinweis darauf zu geben, wo er mit der Eingabe beginnen soll. Der Text innerhalb des Eingabebereichs wird umbrochen, um sicherzustellen, dass er nicht außerhalb des Bildschirms steht.
Schließlich wird die Schaltfläche nur aktiviert, wenn tatsächlich eine Nachricht zu senden ist.
Kapitel 4: Modelle
In Kapitel 4 führen wir Sie durch den Prozess der Erstellung von SQL-Modellen in C++, die sowohl schreibgeschützt als auch schreibgeschützt sind, und stellen sie QML zur Verfügung, um Ansichten zu füllen.
QSqlQueryModel
Um das Tutorial einfach zu halten, haben wir uns dafür entschieden, die Liste der Benutzerkontakte nicht editierbar zu machen. QSqlQueryModel ist die logische Wahl für diesen Zweck, da es ein schreibgeschütztes Datenmodell für SQL-Ergebnismengen bietet.
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 ist nicht viel los, also gehen wir weiter zur Datei .cpp
:
#include "sqlcontactmodel.h"#include <QDebug>#include <QSqlError>#include <QSqlQuery>static void createTable() { if (QSqlDatabase::database().tables().contains(QStringLiteral("Kontakte"))) { // Die Tabelle existiert bereits; wir brauchen nichts zu tun. return; } QSqlQuery query; if (!query.exec( " CREATE TABLE IF NOT EXISTS 'Kontakte' (" " 'name' TEXT NOT NULL," " PRIMARY KEY(name)" ")")) ){ qFatal("Failed to query database: %s", qPrintable(query.lastError().text())); } query.exec("INSERT INTO Kontakte VALUES('Albert Einstein')"); query.exec("INSERT INTO Kontakte VALUES('Ernest Hemingway')"); query.exec("INSERT INTO Kontakte VALUES('Hans Gude')"); }
Wir binden die Header-Datei unserer Klasse ein und die, die wir von Qt benötigen. Dann 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() sieht vielleicht etwas verwirrend aus, weil wir noch keine spezifische Datenbank eingerichtet haben. Wenn dieser Funktion kein Verbindungsname übergeben wird, gibt sie eine "Standardverbindung" zurück, deren Erstellung wir in Kürze behandeln werden.
SqlKontaktModell::SqlKontaktModell(QObject *parent) : QSqlQueryModel(parent) { createTable(); QSqlQuery query; if (!query.exec("SELECT * FROM Kontakte")) 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 konstruieren wir eine Abfrage, die zum Auffüllen des Modells verwendet werden soll. In diesem Fall sind wir einfach an allen Zeilen der Tabelle Contacts
interessiert.
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 das Q_PROPERTY
- als auch das Q_INVOKABLE
-Makro und müssen daher moc mit Hilfe des Q_OBJECT
-Makros 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(), so dass wir unsere benutzerdefinierten Rollen in QML verwenden können.
Wir definieren auch die Funktion sendMessage()
, die wir von QML aus aufrufen wollen, 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 brauchen nichts zu 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('Me', 'Ernest Hemingway', '2016-01-07T14:36:06', 'Hello!')"); query.exec("INSERT INTO Conversations VALUES('Ernest Hemingway', 'Me', '2016-01-07T14:36:16', 'Good afternoon.')"); query.exec("INSERT INTO Conversations VALUES('Me', 'Albert Einstein', '2016-01-01T11:24:53', 'Hi!')"); query.exec("INSERT INTO Conversations VALUES('Albert Einstein', 'Me', '2016-01-07T14:36:16', 'Guten Morgen.')"); query.exec("INSERT INTO Conversations VALUES('Hans Gude', 'Me', '2015-11-20T06:30:02', 'God morgen. Har du fått mitt maleri?')"); query.exec("INSERT INTO Conversations VALUES('Me', 'Hans Gude', '2015-11-20T08:21:03', 'God morgen, Hans. Ja, det er veldig fint. Tusen takk! " "Hvor mange timer har du brukt på den?')"); }
Dies ist sqlcontactmodel.cpp
sehr ähnlich, mit dem Unterschied, dass wir jetzt auf der Tabelle Conversations
arbeiten. Außerdem definieren wir conversationsTableName
als statische Konstantenvariable, 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
ist das erste, was wir im Konstruktor tun, die Erstellung der Tabelle. Wir teilen QSqlTableModel über die Funktion setTable() 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
(die 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()
setzen wir einen Filter über die von der Datenbank zurückgegebenen Ergebnisse.
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. Wenn die Rolle eine Benutzerrolle ist, können wir Qt::UserRole davon subtrahieren, um den Index dieses Feldes zu erhalten, 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 uns die Verwendung dieser Rollen in QML. Es kann sinnvoll sein, ein Enum zu deklarieren, das alle Rollenwerte enthält, aber da wir uns außerhalb dieser Funktion nicht auf einen bestimmten Wert beziehen, machen wir uns diese Mühe nicht.
void SqlConversationModel::sendMessage(const QString &recipient, const QString &message) { const QString timestamp = QDateTime::currentDateTime().toString(Qt::ISODate); QSqlRecord newRecord = record(); newRecord.setValue("author", "Me"); 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 angegebene recipient
und eine message
, um einen neuen Datensatz in die Datenbank einzufügen. Aufgrund unserer Verwendung von QSqlTableModel::OnManualSubmit müssen wir submitAll() manuell aufrufen.
Verbinden mit der Datenbank und Registrieren von Typen mit QML
Nachdem wir nun die Modellklassen eingerichtet 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())); // Stellen Sie sicher, dass wir einen beschreibbaren Speicherort auf allen Geräten haben. const QString fileName = writeDir.absolutePath() + "/chat-database.sqlite3"; // Bei Verwendung des SQLite-Treibers erstellt open() die SQLite-Datenbank, wenn sie 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 sie noch nicht existiert.
Innerhalb von main()
rufen wir qmlRegisterType() auf, um unsere Modelle als Typen in QML zu registrieren.
Verwendung der Modelle in QML
Jetzt, da die Modelle als QML-Typen verfügbar sind, müssen wir noch einige kleinere Änderungen an ContactPage.qml
vornehmen. Um die Typen verwenden zu können, müssen wir sie zunächst über die URI importieren, die wir in main.cpp
festgelegt haben:
import chattutorial
Dann ersetzen wir das Dummy-Modell durch das richtige Modell:
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 chattutorial
import 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 werden soll.
Das Root-Delegate-Element ändert sich von einer Zeile in eine Spalte, um den Zeitstempel aufzunehmen, der unter jeder Nachricht angezeigt werden soll:
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 } }
Da wir nun ein geeignetes Modell haben, 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 seine eigene implizite Größe, so dass wir sie nicht explizit angeben müssen. Wie zuvor zeigen wir das Avatar nur an, wenn der Autor nicht der Benutzer ist, nur dass wir dieses Mal die source
des Bildes auf eine leere URL setzen, anstatt die visible
Eigenschaft zu verwenden.
Wir möchten, dass der Hintergrund jeder Nachricht etwas breiter ist (12 Pixel auf jeder Seite) als ihr Text. Wenn sie jedoch zu lang ist, wollen wir ihre Breite auf den Rand der Listenansicht begrenzen, daher die Verwendung von Math.min()
. Wenn die Nachricht nicht von uns gesendet wurde, wird immer ein Avatar davor stehen, also berücksichtigen wir das, indem wir die Breite des Avatars und den Zeilenabstand abziehen.
Im obigen Bild zum Beispiel ist die implizite Breite des Nachrichtentextes der kleinere Wert. In der Abbildung unten ist der Nachrichtentext jedoch recht lang, so dass der kleinere Wert (die Breite der Ansicht) gewählt wird, um sicherzustellen, dass der Text am gegenüberliegenden Bildschirmrand endet:
Um den Zeitstempel für jede Nachricht anzuzeigen, den wir bereits besprochen haben, verwenden wir ein Label. Das Datum und die Uhrzeit werden mit Qt.formatDateTime() formatiert, wobei ein benutzerdefiniertes Format verwendet wird.
Die Schaltfläche "Senden" muss nun auf das Anklicken reagieren:
Button { id: sendButton text: qsTr("Send") enabled: messageField.length > 0 Layout.fillWidth: false onClicked: { listView.model.sendMessage(root.inConversationWith, messageField.text) messageField.text = "" } }
Zuerst rufen wir die aufrufbare Funktion sendMessage()
des Modells auf, die eine neue Zeile in die Datenbanktabelle Conversations einfügt. Dann löschen wir das Textfeld, um Platz für zukünftige Eingaben zu schaffen.
Kapitel 5: Styling
Die Stile in Qt Quick Controls sind so konzipiert, dass sie auf jeder Plattform funktionieren. In diesem Kapitel werden wir einige kleinere visuelle Anpassungen vornehmen, um sicherzustellen, dass unsere Anwendung gut aussieht, wenn sie mit den Stilen Basic, Material und Universal ausgeführt wird.
Bis jetzt haben wir die Anwendung nur mit dem Basic-Stil getestet. Wenn wir sie zum Beispiel mit dem Material-Stil ausführen, werden wir sofort einige Probleme feststellen. Hier ist die Seite Kontakte:
Der Kopftext ist schwarz auf dunkelblauem Hintergrund, was sehr schwer zu lesen ist. Das Gleiche gilt für die Seite "Konversationen":
Die Lösung besteht darin, der Symbolleiste mitzuteilen, dass sie das Thema "Dunkel" verwenden soll, so dass diese Information an ihre untergeordneten Elemente weitergegeben wird und diese ihre Textfarbe auf etwas Helleres umstellen können. Am einfachsten ist es, den Materialstil direkt zu importieren und die Eigenschaft Material attached zu verwenden:
import QtQuick.Controls.Material 2.12 // ... header: ToolBar { Material.theme: Material.Dark // ... }
Dies bringt jedoch eine harte Abhängigkeit zum Materialstil mit sich; das Materialstil-Plugin muss mit der Anwendung bereitgestellt werden, auch wenn das Zielgerät es nicht verwendet, da die QML-Engine den Import sonst nicht findet.
Stattdessen ist es besser, sich auf die integrierte Unterstützung von Qt Quick Controls für stilbasierte Dateiselektoren zu verlassen. Zu diesem Zweck müssen wir ToolBar in eine eigene Datei auslagern. Wir werden sie ChatToolBar.qml
nennen. Dies wird die "Standard"-Version der Datei sein, was bedeutet, dass sie verwendet wird, wenn der Basic-Stil (der Stil, der verwendet wird, wenn kein Stil angegeben ist) in Gebrauch ist. Hier ist die neue Datei:
import QtQuick.Controls
ToolBar {
}
Da wir in dieser Datei nur den Typ ToolBar verwenden, benötigen wir nur den Import Qt Quick Controls. Der Code selbst hat sich im Vergleich zu ContactPage.qml
nicht geändert, und das ist auch gut so; für die Standardversion der Datei muss nichts geändert werden.
Zurück in ContactPage.qml
, aktualisieren wir den Code, um den neuen Typ zu verwenden:
header: ChatToolBar { Label { text: qsTr("Contacts") font.pixelSize: 20 anchors.centerIn: parent } }
Jetzt müssen wir die Materialversion der Symbolleiste hinzufügen. Dateiselektoren erwarten, dass sich Varianten einer Datei in entsprechend benannten Verzeichnissen befinden, die neben der Standardversion der Datei existieren. Das bedeutet, dass wir einen Ordner mit dem Namen "+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 werden die gleichen Änderungen an ConversationPage.qml
vornehmen:
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:
Probieren wir den Universal-Stil aus:
Hier gibt es keine Probleme. Bei einer relativ einfachen Anwendung wie dieser sollten nur sehr wenige Anpassungen erforderlich sein, wenn Sie den Stil wechseln.
Probieren wir nun das dunkle Thema der einzelnen Stile aus. Der Stil Basic hat kein dunkles Thema, da es einen leichten Overhead zu einem Stil hinzufügen würde, der so performant wie möglich sein soll. Wir werden zuerst den Stil Material testen, also fügen Sie einen Eintrag zu qtquickcontrols2.conf
hinzu, der ihm sagt, dass er sein dunkles Thema verwenden soll:
[Material] Primary=Indigo Accent=Indigo Theme=Dark
Sobald dies erledigt ist, erstellen Sie die Anwendung und führen Sie sie aus. Das sollten Sie nun sehen:
Beide Seiten sehen gut aus. Fügen Sie nun einen Eintrag für den Stil Universal hinzu:
[universal] Theme=Dark
Nachdem Sie die Anwendung erstellt und ausgeführt haben, sollten Sie diese Ergebnisse sehen:
Zusammenfassung
In diesem Lernprogramm haben wir Sie durch die folgenden Schritte geführt, um eine einfache Anwendung mit Qt Quick Controls zu schreiben:
- Erstellen eines neuen Projekts mit Qt Creator.
- Einrichten einer grundlegenden ApplicationWindow.
- Definieren von Kopf- und Fußzeilen mit Page.
- Anzeigen von Inhalten in einer ListView.
- Refactoring von Komponenten in ihre eigenen Dateien.
- Navigieren zwischen Bildschirmen mit StackView.
- Verwendung von Layouts, um die Größe einer Anwendung anständig zu ändern.
- Implementierung von benutzerdefinierten schreibgeschützten und beschreibbaren Modellen, die eine SQL-Datenbank in die Anwendung integrieren.
- Integration von C++ mit QML über Q_PROPERTY, Q_INVOKABLE und qmlRegisterType().
- Testen und Konfigurieren mehrerer Stile.
© 2025 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.