Qt Quick Contrôles - Tutoriel sur le chat
Ce tutoriel montre comment écrire une application de chat basique en utilisant Qt Quick Controls. Il explique également comment intégrer une base de données SQL dans une application Qt SQL.
Chapitre 1 : Mise en place
Lors de la mise en place d'un nouveau projet, il est plus facile d'utiliser Qt Creator. Pour ce projet, nous avons choisi le modèle d'applicationQt Quick , qui crée une application de base "Hello World" avec les fichiers suivants :
CMakeLists.txt- Indique à CMake comment notre projet doit être construitMain.qml- Fournit une interface utilisateur par défaut contenant une fenêtre videmain.cpp- Chargemain.qmlqtquickcontrols2.conf- Indique à l'application le style qu'elle doit utiliser
main.cpp
Le code par défaut de main.cpp comporte deux includes :
#include <QGuiApplication> #include <QQmlApplicationEngine>
La première nous donne accès à QGuiApplication. Toutes les applications Qt XML ont besoin d'un objet d'application, mais le type précis dépend de ce que fait l'application. QCoreApplication est suffisant pour les applications non graphiques. QGuiApplication est suffisant pour les applications graphiques qui n'utilisent pas d'objet d'application, alors que est nécessaire pour celles qui en utilisent. Qt Widgetstandis que QApplication est nécessaire pour celles qui l'utilisent.
Le deuxième include rend QQmlApplicationEngine disponible, ce qui nous permet de charger notre QML.
Dans main(), nous configurons l'objet d'application et le moteur QML :
int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QQmlApplicationEngine engine; engine.loadFromModule("chattutorial", "Main"); return app.exec(); }
QQmlApplicationEngine est une enveloppe pratique au-dessus de QQmlEngine, fournissant la fonction loadFromModule pour charger facilement le QML pour une application. Il ajoute également une certaine commodité dans l'utilisation des sélecteurs de fichiers.
Une fois que nous avons configuré les choses en C++, nous pouvons passer à l'interface utilisateur en QML.
Main.qml
Modifions le code QML par défaut pour l'adapter à nos besoins.
import QtQuick import QtQuick.Controls
Vous remarquerez que le module Qt Quick a déjà été importé. Cela nous donne accès à des primitives graphiques telles que Item, Rectangle, Text, et ainsi de suite. Pour la liste complète des types, voir la Qt Quick QML Types documentation.
Ajouter une importation du module Qt Quick Controls. Cela permet notamment d'accéder à ApplicationWindow, qui remplacera le type racine existant, Window:
ApplicationWindow { width: 540 height: 960 visible: true ... }
ApplicationWindow est un Window avec une commodité supplémentaire pour la création d'un header et d'un footer. Il fournit également la base pour popups et prend en charge certains styles de base, tels que l'arrière-plan color.
Trois propriétés sont presque toujours définies lors de l'utilisation de ApplicationWindow: width, height et visible. Une fois ces propriétés définies, nous disposons d'une fenêtre vide de taille appropriée, prête à être remplie de contenu.
Remarque : la propriété title du code par défaut est supprimée.
Le premier "écran" de notre application sera une liste de contacts. Il serait intéressant d'avoir un texte en haut de chaque écran décrivant son objectif. Les propriétés header et footer de ApplicationWindow pourraient convenir à cette situation. Elles présentent certaines caractéristiques qui les rendent idéales pour les éléments qui doivent être affichés sur chaque écran d'une application :
- Elles sont ancrées respectivement en haut et en bas de la fenêtre.
- Ils remplissent la largeur de la fenêtre.
Cependant, lorsque le contenu de l'en-tête et du pied de page varie en fonction de l'écran regardé par l'utilisateur, il est beaucoup plus facile d'utiliser Page. Pour l'instant, nous nous contenterons d'ajouter une page, mais dans le prochain chapitre, nous verrons comment naviguer entre plusieurs pages.
Page { anchors.fill: parent header: Label { padding: 10 text: qsTr("Contacts") font.pixelSize: 20 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } }
Tout d'abord, nous ajoutons une page, qui est dimensionnée pour occuper tout l'espace de la fenêtre à l'aide de la propriété anchors.fill.
Ensuite, nous assignons un Label à sa propriété header. Label étend l'élément primitif Text du module Qt Quick en y ajoutant le style et l'héritage font. Cela signifie qu'une étiquette peut avoir un aspect différent selon le style utilisé et qu'elle peut également propager sa taille en pixels à ses enfants.
Nous voulons qu'il y ait une certaine distance entre le haut de la fenêtre de l'application et le texte, c'est pourquoi nous définissons la propriété padding. Celle-ci alloue de l'espace supplémentaire de chaque côté de l'étiquette (dans ses limites). Nous pouvons également définir explicitement les propriétés topPadding et bottomPadding à la place.
Nous définissons le texte de l'étiquette à l'aide de la fonction qsTr(), ce qui garantit que le texte peut être traduit par le système de traduction de Qt XML. C'est une bonne pratique à suivre pour le texte qui est visible par les utilisateurs finaux de votre application.
Par défaut, le texte est aligné verticalement sur le haut de ses limites, tandis que l'alignement horizontal dépend de la direction naturelle du texte ; par exemple, un texte qui est lu de gauche à droite sera aligné à gauche. Si nous utilisions ces valeurs par défaut, notre texte se trouverait dans le coin supérieur gauche de la fenêtre. Ce n'est pas souhaitable pour un en-tête, c'est pourquoi nous alignons le texte au centre de ses limites, à la fois horizontalement et verticalement.
Le fichier du projet
Le fichier CMakeLists.txt contient toutes les informations nécessaires à CMake pour construire notre projet en un exécutable que nous pouvons exécuter.
Pour une explication approfondie de ce fichier, voir Construire une application QML.
Voici à quoi ressemble actuellement notre application lorsqu'elle est exécutée :

Chapitre 2 : Listes
Dans ce chapitre, nous allons expliquer comment créer une liste d'éléments interactifs à l'aide de ListView et ItemDelegate.
ListView Le module ItemDelegate provient du module Qt Quick et affiche une liste d'éléments remplie à partir d'un modèle. Le module provient du module Qt Quick Controls et fournit un élément de vue standard à utiliser dans les vues et les contrôles tels que ListView et ComboBox. Par exemple, chaque ItemDelegate peut afficher du texte, être activé ou désactivé et réagir aux clics de souris.
Voici notre 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"
}
}
}
...Dimensionnement et positionnement
La première chose à faire est de définir la taille de la vue. Elle doit remplir l'espace disponible sur la page, nous utilisons donc anchors.fill. Notez que la page s'assure que l'en-tête et le pied de page ont suffisamment d'espace réservé, de sorte que l'affichage dans ce cas se situera sous l'en-tête, par exemple.
Ensuite, nous définissons margins autour de ListView pour mettre une certaine distance entre lui et les bords de la fenêtre. Les propriétés de marge réservent de l'espace à l'intérieur des limites de la vue, ce qui signifie que les zones vides peuvent encore être "feuilletées" par l'utilisateur.
Les éléments doivent être bien espacés dans la vue, c'est pourquoi la propriété spacing est définie sur 20.
Modèle
Afin de remplir rapidement la vue avec quelques éléments, nous avons utilisé un tableau JavaScript comme modèle. L'une des plus grandes forces de QML est sa capacité à rendre le prototypage d'une application extrêmement rapide, et ceci en est un exemple. Il est également possible d'assigner simplement un nombre à la propriété du modèle pour indiquer le nombre d'éléments dont vous avez besoin. Par exemple, si vous attribuez 10 à la propriété model, le texte d'affichage de chaque élément sera un nombre compris entre 0 et 9.
Cependant, dès que l'application dépasse le stade du prototype, il devient rapidement nécessaire d'utiliser des données réelles. Pour cela, il est préférable d'utiliser un modèle C++ approprié en subclassing QAbstractItemModel.
Délégué
Passons à delegate. Nous attribuons le texte correspondant du modèle à la propriété text de ItemDelegate. La manière exacte dont les données du modèle sont mises à la disposition de chaque délégué dépend du type de modèle utilisé. Pour plus d'informations, voir Modèles et vues à l' adresse Qt Quick.
Dans notre application, la largeur de chaque élément de la vue doit être identique à la largeur de la vue. L'utilisateur dispose ainsi d'une grande marge de manœuvre pour sélectionner un contact dans la liste, ce qui est important sur les appareils dotés d'un petit écran tactile, comme les téléphones portables. Cependant, la largeur de la vue inclut nos marges de 48 pixels, et nous devons donc en tenir compte dans notre affectation à la propriété width.
Ensuite, nous définissons une page Image qui affichera une image du contact de l'utilisateur. L'image aura une largeur de 40 pixels et une hauteur de 40 pixels. Nous baserons la hauteur du délégué sur la hauteur de l'image, afin de ne pas avoir d'espace vertical vide.

Chapitre 3 : Navigation
Dans ce chapitre, vous apprendrez à utiliser StackView pour naviguer entre les pages d'une application. Voici la version révisée de main.qml:
import QtQuick.Controls ApplicationWindow { id: window width: 540 height: 960 visible: true StackView { id: stackView anchors.fill: parent initialItem: ContactPage {} } }
Naviguer avec StackView
Comme son nom l'indique, StackView permet une navigation par pile. Le dernier élément à être "poussé" sur la pile est le premier à être retiré, et l'élément le plus haut est toujours celui qui est visible.
De la même manière qu'avec Page, nous demandons à StackView de remplir la fenêtre d'application. Il ne reste plus qu'à lui donner un élément à afficher, via initialItem. StackView accepte items, components et URLs.
Vous remarquerez que nous avons déplacé le code de la liste de contacts dans ContactPage.qml. Il est conseillé de procéder à cette opération dès que vous avez une idée générale des écrans que contiendra votre application. Non seulement cela facilite la lecture de votre code, mais cela garantit que les éléments ne sont instanciés à partir d'un composant donné qu'en cas de nécessité absolue, ce qui réduit l'utilisation de la mémoire.
Remarque : Qt Creator fournit plusieurs solutions rapides et pratiques pour QML, dont l'une vous permet de déplacer un bloc de code dans un fichier séparé (Alt + Enter > Move Component into Separate File).
Un autre élément à prendre en compte lors de l'utilisation de ListView est de savoir s'il faut s'y référer par id ou utiliser la propriété ListView.view qui y est attachée. La meilleure approche dépend de plusieurs facteurs. L'attribution d'un identifiant à la vue se traduira par des expressions de liaison plus courtes et plus efficaces, car la propriété attached n'entraîne qu'une très faible surcharge. Toutefois, si vous prévoyez de réutiliser le délégué dans d'autres vues, il est préférable d'utiliser les propriétés attachées pour éviter de lier le délégué à une vue particulière. Par exemple, en utilisant les propriétés attachées, l'affectation width de notre délégué devient :
width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin
Dans le chapitre 2, nous avons ajouté un ListView sous l'en-tête. Si vous exécutez l'application pour ce chapitre, vous verrez que le contenu de la vue peut défiler au-dessus de l'en-tête :
Ce n'est pas très agréable, surtout si le texte des délégués est suffisamment long pour atteindre le texte de l'en-tête. L'idéal est d'avoir un bloc de couleur solide sous le texte de l'en-tête, mais au-dessus de la vue. Cela permet d'éviter que le contenu de la liste n'interfère visuellement avec le contenu de l'en-tête. Notez qu'il est également possible d'obtenir ce résultat en définissant la propriété clip de la vue à true, mais can affect performance.
ToolBar est l'outil idéal pour cette tâche. Il s'agit d'un conteneur d'actions et de contrôles à l'échelle de l'application et sensibles au contexte, tels que les boutons de navigation et les champs de recherche. Mieux encore, il possède une couleur d'arrière-plan qui, comme d'habitude, provient du style de l'application. Le voici en action :
Il n'a pas de mise en page propre, nous devons donc centrer l'étiquette à l'intérieur de celle-ci.
Le reste du code est le même qu'au chapitre 2, sauf que nous avons tiré parti du signal clicked pour pousser la page suivante dans la vue de pile :
onClicked: root.StackView.view.push("ConversationPage.qml", { inConversationWith: modelData })
Lorsque l'on pousse un Component ou un url sur un StackView, il est souvent nécessaire d'initialiser l'élément (éventuellement) instancié avec quelques variables. StackView La fonction push() d'EMC en tient compte, en prenant un objet JavaScript comme second argument. Nous l'utilisons pour fournir à la page suivante le nom d'un contact, qu'elle utilise ensuite pour afficher la conversation correspondante. Notez la syntaxe root.StackView.view.push; elle est nécessaire en raison du fonctionnement des propriétés attachées.
Parcourons ConversationPage.qml, en commençant par les importations :
import QtQuick import QtQuick.Layouts import QtQuick.Controls
Ceux-ci sont les mêmes que précédemment, à l'exception de l'ajout de l'importation QtQuick.Layouts, que nous aborderons prochainement.
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 } } ...
L'élément racine de ce composant est une autre page, qui possède une propriété personnalisée appelée inConversationWith. Pour l'instant, cette propriété détermine simplement ce que l'étiquette de l'en-tête affiche. Plus tard, nous l'utiliserons dans la requête SQL qui alimente la liste des messages de la conversation.
Pour permettre à l'utilisateur de revenir à la page Contact, nous ajoutons un ToolButton qui appelle pop() lorsqu'il est cliqué. Un ToolButton est fonctionnellement similaire à Button, mais offre un aspect qui convient mieux à un ToolBar.
Il existe deux façons de disposer les éléments en QML : les positionneurs d'éléments et les dispositionsQt Quick . Les positionneurs d'éléments (Row, Column, etc.) sont utiles dans les situations où la taille des éléments est connue ou fixe, et où il suffit de les positionner proprement dans une certaine formation. Les modèles de Qt Quick Layouts peuvent à la fois positionner et redimensionner les éléments, ce qui les rend bien adaptés aux interfaces utilisateur redimensionnables. Ci-dessous, nous utilisons ColumnLayout pour disposer verticalement un ListView et un Pane:
ColumnLayout { anchors.fill: parent ListView { id: listView Layout.fillWidth: true Layout.fillHeight: true ... } ... Pane { id: pane Layout.fillWidth: true ... }
Le volet est essentiellement un rectangle dont la couleur provient du style de l'application. Il est similaire à Frame, à la seule différence qu'il n'a pas de trait autour de sa bordure.
Les éléments qui sont des enfants directs d'une mise en page disposent de plusieurs attached properties. Nous utilisons Layout.fillWidth et Layout.fillHeight pour ListView afin de nous assurer qu'il occupe le plus d'espace possible dans ColumnLayout. Il en va de même pour le volet. Comme ColumnLayout est une présentation verticale, il n'y a pas d'éléments à gauche ou à droite de chaque enfant, de sorte que chaque élément occupe toute la largeur de la présentation.
D'autre part, la déclaration Layout.fillHeight dans le site ListView lui permettra d'occuper l'espace restant après avoir accueilli le volet.
Examinons le listview en détail :
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 {} }
Après avoir rempli la largeur et la hauteur de son parent, nous avons également défini des marges sur la vue. Cela nous donne un bon alignement avec le texte de remplacement dans le champ "composer un message" :

Ensuite, nous définissons displayMarginBeginning et displayMarginEnd. Ces propriétés garantissent que les délégués situés en dehors des limites de la vue ne disparaissent pas lors du défilement sur les bords de la vue. Il est plus facile de comprendre cela en commentant les propriétés et en observant ce qui se passe lors du défilement de la vue.
Nous inversons ensuite la direction verticale de la vue, de sorte que les premiers éléments se trouvent en bas. Les délégués sont espacés de 12 pixels et un modèle "factice" est attribué à des fins de test, jusqu'à ce que nous implémentions le vrai modèle au chapitre 4.
Dans le délégué, nous déclarons un Row comme élément racine, car nous voulons que l'avatar soit suivi du contenu du message, comme le montre l'image ci-dessus.
Les messages envoyés par l'utilisateur doivent être distingués de ceux envoyés par un contact. Pour l'instant, nous définissons une propriété fictive sentByMe, qui utilise simplement l'index du délégué pour alterner entre les différents auteurs. Grâce à cette propriété, nous pouvons distinguer les différents auteurs de trois façons :
- Les messages envoyés par l'utilisateur sont alignés sur le côté droit de l'écran en définissant
anchors.rightsurlistView.contentItem.right. - En définissant la propriété
visiblede l'avatar (qui est simplement un rectangle pour l'instant) en fonction desentByMe, nous ne l'affichons que si le message a été envoyé par un contact. - Nous modifions la couleur du rectangle en fonction de l'auteur. Comme nous ne voulons pas afficher un texte sombre sur un fond sombre, et vice versa, nous définissons également la couleur du texte en fonction de l'auteur. Au chapitre 5, nous verrons comment le stylisme s'occupe de ce genre de choses pour nous.
En bas de l'écran, nous plaçons un élément TextArea pour permettre la saisie de texte sur plusieurs lignes, ainsi qu'un bouton pour envoyer le message. Nous utilisons Pane pour couvrir la zone située sous ces deux éléments, de la même manière que nous utilisons ToolBar pour empêcher le contenu de la listview d'interférer avec l'en-tête de la page :
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 } } }
Le site TextArea doit occuper toute la largeur de l'écran. Nous attribuons un texte de remplacement pour indiquer visuellement à l'utilisateur l'endroit où il doit commencer à taper. Le texte à l'intérieur de la zone de saisie est enveloppé pour s'assurer qu'il ne sort pas de l'écran.
Enfin, le bouton n'est activé que lorsqu'il y a un message à envoyer.
Chapitre 4 : Modèles
Dans le chapitre 4, nous allons vous expliquer comment créer des modèles SQL en lecture seule et en lecture-écriture en C++ et les exposer à QML pour remplir des vues.
QSqlQueryModel
Pour que le tutoriel reste simple, nous avons choisi de rendre la liste des contacts de l'utilisateur non modifiable. QSqlQueryModel est le choix logique à cette fin, car il fournit un modèle de données en lecture seule pour les ensembles de résultats SQL.
Jetons un coup d'œil à notre classe SqlContactModel qui dérive de QSqlQueryModel:
#include <QQmlEngine> #include <QSqlQueryModel> class SqlContactModel : public QSqlQueryModel { Q_OBJECT QML_ELEMENT public: SqlContactModel(QObject *parent = nullptr); };
Il ne se passe pas grand-chose ici, alors passons au fichier .cpp:
#include "sqlcontactmodel.h"#include <QDebug>#include <QSqlError>#include <QSqlQuery>static void createTable() { if (QSqlDatabase::database().tables().contains(QStringLiteral("Contacts"))) { // La table existe déjà ; nous n'avons rien à faire. 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')") ; }
Nous incluons le fichier d'en-tête de notre classe et ceux que nous demandons à Qt. Nous définissons ensuite une fonction statique nommée createTable() que nous utiliserons pour créer la table SQL (si elle n'existe pas déjà), puis pour la remplir avec quelques contacts fictifs.
L'appel à database() peut sembler un peu déroutant car nous n'avons pas encore configuré de base de données spécifique. Si aucun nom de connexion n'est transmis à cette fonction, elle renverra une "connexion par défaut", dont la création sera abordée prochainement.
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())); }
Dans le constructeur, nous appelons createTable(). Nous construisons ensuite une requête qui sera utilisée pour alimenter le modèle. Dans ce cas, nous sommes simplement intéressés par toutes les lignes du tableau Contacts.
QSqlTableModel
SqlConversationModel est plus complexe :
#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; };
Nous utilisons à la fois les macros Q_PROPERTY et Q_INVOKABLE, et nous devons donc le faire savoir à moc en utilisant la macro Q_OBJECT.
La propriété recipient sera définie à partir de QML pour indiquer au modèle pour quelle conversation il doit récupérer les messages.
Nous surchargeons les fonctions data() et roleNames() afin de pouvoir utiliser nos rôles personnalisés dans QML.
Nous définissons également la fonction sendMessage() que nous voulons appeler depuis QML, d'où la macro Q_INVOKABLE.
Jetons un coup d'œil au fichier .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)) { // La table existe déjà ; nous n'avons rien à faire. 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', 'Bonjour!')") ; query.exec("INSERT INTO Conversations VALUES('Albert Einstein', 'Me', '2016-01-07T14:36:16', 'Bonjour.')") ; 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 ?")") ; }
Ceci est très similaire à sqlcontactmodel.cpp, à l'exception du fait que nous opérons maintenant sur la table Conversations. Nous définissons également conversationsTableName comme une variable const statique, car nous l'utilisons à plusieurs endroits dans le fichier.
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); }
Comme pour SqlContactModel, la première chose que nous faisons dans le constructeur est de créer la table. Nous indiquons à QSqlTableModel le nom de la table que nous utiliserons via la fonction setTable(). Pour que les derniers messages de la conversation soient affichés en premier, nous trions les résultats de la requête en fonction du champ timestamp dans l'ordre décroissant. Cela va de pair avec la définition de la propriété verticalLayoutDirection de ListView à ListView.BottomToTop (que nous avons abordée au chapitre 3).
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(); }
Dans setRecipient(), nous définissons un filtre sur les résultats renvoyés par la base de données.
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); }
La fonction data() revient à l'implémentation de QSqlTableModel si le rôle n'est pas un rôle d'utilisateur personnalisé. Si le rôle est un rôle d'utilisateur, nous pouvons soustraire Qt::UserRole pour obtenir l'index de ce champ et l'utiliser pour trouver la valeur que nous devons renvoyer.
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; }
Dans roleNames(), nous renvoyons une correspondance entre les valeurs de nos rôles personnalisés et les noms des rôles. Cela nous permet d'utiliser ces rôles en QML. Il peut être utile de déclarer un enum pour contenir toutes les valeurs de rôle, mais comme nous ne faisons référence à aucune valeur spécifique dans le code en dehors de cette fonction, nous ne nous en soucions pas.
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; }
La fonction sendMessage() utilise la donnée recipient et une message pour insérer un nouvel enregistrement dans la base de données. En raison de notre utilisation de QSqlTableModel::OnManualSubmit, nous devons appeler manuellement submitAll().
Connexion à la base de données et enregistrement des types avec QML
Maintenant que nous avons établi les classes de modèle, jetons un coup d'œil à 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())); // S'assurer que nous avons un emplacement accessible en écriture sur tous les périphériques. const QString fileName = writeDir.absolutePath() + "/chat-database.sqlite3"; // Lors de l'utilisation du pilote SQLite, open() créera la base de données SQLite si elle n'existe pas.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() crée la connexion à la base de données SQLite, en créant le fichier actuel s'il n'existe pas déjà.
Dans main(), nous appelons qmlRegisterType() pour enregistrer nos modèles en tant que types dans QML.
Utilisation des modèles en QML
Maintenant que les modèles sont disponibles en tant que types QML, il reste quelques modifications mineures à apporter à ContactPage.qml. Pour pouvoir utiliser les types, nous devons d'abord les importer à l'aide de l'URI que nous avons défini à main.cpp:
import chattutorial
Nous remplaçons ensuite le modèle fictif par le modèle approprié :
model: SqlContactModel {}
Dans le délégué, nous utilisons une syntaxe différente pour accéder aux données du modèle :
text: model.display
Dans ConversationPage.qml, nous ajoutons le même import chattutorial et remplaçons le modèle fictif :
model: SqlConversationModel { recipient: root.inConversationWith }
Dans le modèle, nous fixons la propriété recipient au nom du contact pour lequel la page est affichée.
L'élément délégué racine passe d'une ligne à une colonne, pour tenir compte de l'horodatage que nous voulons afficher sous chaque message :
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 } }

Maintenant que nous disposons d'un modèle approprié, nous pouvons utiliser son rôle recipient dans l'expression de la propriété sentByMe.
Le rectangle utilisé pour l'avatar a été converti en image. L'image a sa propre taille implicite, nous n'avons donc pas besoin de la spécifier explicitement. Comme précédemment, nous n'affichons l'avatar que lorsque l'auteur n'est pas l'utilisateur, sauf que cette fois-ci, nous définissons source de l'image à une URL vide au lieu d'utiliser la propriété visible.
Nous voulons que l'arrière-plan de chaque message soit légèrement plus large (12 pixels de chaque côté) que le texte. Cependant, s'il est trop long, nous voulons limiter sa largeur au bord de la listview, d'où l'utilisation de Math.min(). Lorsque le message n'a pas été envoyé par nous, un avatar le précède toujours, ce dont nous tenons compte en soustrayant la largeur de l'avatar et l'espacement entre les lignes.
Par exemple, dans l'image ci-dessus, la largeur implicite du texte du message est la plus petite valeur. Cependant, dans l'image ci-dessous, le texte du message est assez long, c'est donc la plus petite valeur (la largeur de la vue) qui est choisie, ce qui garantit que le texte s'arrête au bord opposé de l'écran :

Pour afficher l'horodatage de chaque message dont nous avons parlé plus haut, nous utilisons une étiquette. La date et l'heure sont formatées avec Qt.formatDateTime(), en utilisant un format personnalisé.
Le bouton "envoyer" doit maintenant réagir au clic :
Button { id: sendButton text: qsTr("Send") enabled: messageField.length > 0 Layout.fillWidth: false onClicked: { listView.model.sendMessage(root.inConversationWith, messageField.text) messageField.text = "" } }
Tout d'abord, nous appelons la fonction invocable sendMessage() du modèle, qui insère une nouvelle ligne dans la table de la base de données Conversations. Ensuite, nous effaçons le champ de texte pour faire place à de futures entrées.
Chapitre 5 : Style
Les styles des contrôles Qt Quick sont conçus pour fonctionner sur n'importe quelle plate-forme. Dans ce chapitre, nous allons procéder à quelques modifications visuelles mineures pour nous assurer que notre application a une bonne apparence lorsqu'elle est exécutée avec les styles Basic, Material et Universal.
Jusqu'à présent, nous avons testé l'application avec le style Basic. Si nous l'exécutons avec le style Material, par exemple, nous verrons immédiatement quelques problèmes. Voici la page Contacts :

Le texte de l'en-tête est noir sur fond bleu foncé, ce qui est très difficile à lire. Il en va de même pour la page Conversations :

La solution consiste à indiquer à la barre d'outils qu'elle doit utiliser le thème "Dark", afin que cette information soit propagée à ses enfants, ce qui leur permet de changer la couleur de leur texte pour quelque chose de plus clair. La façon la plus simple de le faire est d'importer directement le style Material et d'utiliser la propriété Material attached :
import QtQuick.Controls.Material 2.12
// ...
header: ToolBar {
Material.theme: Material.Dark
// ...
}Le plugin de style Material doit être déployé avec l'application, même si l'appareil cible ne l'utilise pas, sinon le moteur QML ne trouvera pas l'importation.
Au lieu de cela, il est préférable de s'appuyer sur le support intégré de Qt Quick Controls pour les sélecteurs de fichiers basés sur le style. Pour ce faire, nous devons déplacer le site ToolBar dans son propre fichier. Nous l'appellerons ChatToolBar.qml. Il s'agira de la version "par défaut" du fichier, ce qui signifie qu'elle sera utilisée lorsque le style Basic (qui est le style utilisé lorsqu'aucun style n'est spécifié) est utilisé. Voici le nouveau fichier :
import QtQuick.Controls
ToolBar {
}Comme nous n'utilisons que le type ToolBar dans ce fichier, nous n'avons besoin que de l'importation de Qt Quick Controls. Le code lui-même n'a pas changé par rapport à ContactPage.qml, ce qui est normal ; pour la version par défaut du fichier, rien ne doit être différent.
De retour à ContactPage.qml, nous mettons à jour le code pour utiliser le nouveau type :
header: ChatToolBar { Label { text: qsTr("Contacts") font.pixelSize: 20 anchors.centerIn: parent } }
Nous devons maintenant ajouter la version matérielle de la barre d'outils. Les sélecteurs de fichiers s'attendent à ce que les variantes d'un fichier se trouvent dans des répertoires nommés de manière appropriée et qui existent en même temps que la version par défaut du fichier. Cela signifie que nous devons ajouter un dossier nommé "+Material" dans le même répertoire que ChatToolBar.qml : le dossier racine. Le "+" est requis par QFileSelector afin de s'assurer que la fonction de sélection n'est pas déclenchée accidentellement.
Voici +Material/ChatToolBar.qml:
import QtQuick.Controls import QtQuick.Controls.Material ToolBar { Material.theme: Material.Dark }
Nous apporterons les mêmes modifications à ConversationPage.qml:
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 } }
Les deux pages sont maintenant correctes :


Essayons le style universel :


Aucun problème. Pour une application relativement simple comme celle-ci, il ne devrait y avoir que très peu d'ajustements nécessaires lorsque l'on passe d'un style à l'autre.
Essayons maintenant le thème sombre de chaque style. Le style Basic n'a pas de thème sombre, car cela ajouterait une légère surcharge à un style qui est conçu pour être aussi performant que possible. Nous allons tester le style Material en premier, donc ajoutez une entrée à qtquickcontrols2.conf qui lui indique d'utiliser son thème sombre :
[Material] Primary=Indigo Accent=Indigo Theme=Dark
Une fois que c'est fait, construisez et exécutez l'application. Voici ce que vous devriez voir :


Les deux pages ont l'air correctes. Ajoutez maintenant une entrée pour le style Universal :
[universal] Theme=Dark
Après avoir créé et exécuté l'application, vous devriez obtenir les résultats suivants :


Résumé
Dans ce tutoriel, nous vous avons présenté les étapes suivantes de l'écriture d'une application de base à l'aide des contrôles Qt Quick:
- Création d'un nouveau projet à l'aide de Qt Creator.
- Mise en place d'une application de base ApplicationWindow.
- Définition des en-têtes et des pieds de page avec Page.
- Affichage du contenu dans une page ListView.
- Refonte des composants dans leurs propres fichiers.
- Naviguer entre les écrans avec StackView.
- Utiliser des mises en page pour permettre à une application de se redimensionner de manière gracieuse.
- Mise en œuvre de modèles personnalisés en lecture seule et en écriture qui intègrent une base de données SQL dans l'application.
- Intégrer C++ avec QML via Q_PROPERTY, Q_INVOKABLE, et qmlRegisterType().
- Test et configuration de plusieurs styles.
© 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.