Qt Quick Controls - Tutoriel sur le chat
Ce tutoriel explique comment créer une application de chat basique à l'aide de Qt Quick Controls. Il explique également comment intégrer une base de données SQL dans une application Qt.
Chapitre 1 : Mise en place
Lors de la création d’un nouveau projet, le plus simple est d’utiliser Qt Creator. Pour ce projet, nous avons choisi le modèle d'application «Qt Quick », qui crée une application « Hello World » basique comprenant les fichiers suivants :
CMakeLists.txt- Indique à CMake comment notre projet doit être compiléMain.qml- Fournit une interface utilisateur par défaut contenant une fenêtre videmain.cpp- Chargemain.qmlqtquickcontrols2.conf- Indique à l’application quel style elle doit utiliser
main.cpp
Le code par défaut de main.cpp comporte deux inclusions :
#include <QGuiApplication>
#include <QQmlApplicationEngine>La première nous donne accès à QGuiApplication. Toutes les applications Qt nécessitent un objet application, mais le type précis dépend de ce que fait l’application. QCoreApplication suffit pour les applications non graphiques. QGuiApplication suffit pour les applications graphiques qui n’utilisent pas Qt Widgets, tandis que ` QApplication ` est requis pour celles qui l’utilisent.
La deuxième inclusion rend QQmlApplicationEngine disponible, ce qui nous permet de charger notre QML.
Dans main(), nous configurons l’objet 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 un wrapper pratique autour de QQmlEngine, fournissant la fonction loadFromModule pour charger facilement le QML d’une application. Il facilite également l’utilisation des sélecteurs de fichiers.
Une fois la configuration effectuée 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.ControlsVous remarquerez que le Qt Quick module a déjà été importé. Cela nous donne accès à des primitives graphiques telles que Item, Rectangle, Text, etc. Pour la liste complète des types, consultez la Qt Quick QML Types documentation.
Ajoutez 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 offrant des fonctionnalités pratiques supplémentaires pour créer un header et 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 celles-ci définies, nous disposons d’une fenêtre vide, correctement dimensionnée, prête à être remplie de contenu.
Remarque : la propriété title du code par défaut a été supprimée.
Le premier « écran » de notre application sera une liste de contacts. Il serait intéressant d’afficher, en haut de chaque écran, un texte décrivant son objectif. Les propriétés header et footer de ApplicationWindow pourraient convenir dans ce cas. Elles présentent certaines caractéristiques qui les rendent idéales pour les éléments devant s’afficher sur tous les écrans d’une application :
- Elles sont respectivement ancrées en haut et en bas de la fenêtre.
- Elles occupent toute 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 que l’utilisateur consulte, il est beaucoup plus simple d’utiliser Page. Pour l’instant, nous allons simplement ajouter une page, mais dans le chapitre suivant, nous montrerons 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, dont la taille est définie pour occuper tout l’espace de la fenêtre à l’aide de la propriété anchors.fill.
Ensuite, nous attribuons une valeur à la propriété ` Label ` de son élément ` header `. La classe `Label` étend l’élément primitif ` Text ` du module ` Qt Quick ` en ajoutant des styles et un héritage de la classe ` font `. Cela signifie qu’un `Label` peut avoir un aspect différent selon le style utilisé, et qu’il peut également transmettre sa taille en pixels à ses éléments enfants.
Nous souhaitons laisser un peu d’espace entre le haut de la fenêtre de l’application et le texte ; nous définissons donc la propriété ` padding `. Cela alloue un espace supplémentaire de chaque côté de l’étiquette (dans les limites de celle-ci). 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 pourra être traduit par le système de traduction de Qt. Il s’agit d’une bonne pratique à suivre pour le texte 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 du sens naturel du texte ; par exemple, un texte qui se lit 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 ; nous alignons donc le texte au centre de ses limites, tant horizontalement que verticalement.
Le fichier de projet
Le fichier ` CMakeLists.txt ` contient toutes les informations nécessaires à CMake pour compiler notre projet en un exécutable que nous pouvons lancer.
Pour une explication détaillée de ce fichier, consultez la section « Compilation d’une application QML ».
Voici à quoi ressemble actuellement notre application lorsqu’elle est exécutée :

Chapitre 2 : Les listes
Dans ce chapitre, nous allons expliquer comment créer une liste d’éléments interactifs à l’aide de ` ListView ` et ` ItemDelegate`.
ListView provient du module Qt Quick et affiche une liste d’éléments alimentée à partir d’un modèle. ItemDelegate provient du module Qt Quick Controls et fournit un élément de vue standard destiné à être utilisé dans des vues et des contrôles tels que ListView et ComboBox. Par exemple, chaque ItemDelegate peut afficher du texte, être coché ou décoché, 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. Celle-ci doit occuper tout l’espace disponible sur la page ; nous utilisons donc anchors.fill. Notez que Page veille à ce qu’un espace suffisant soit réservé à son en-tête et à son pied de page ; dans ce cas précis, la vue sera donc placée sous l’en-tête, par exemple.
Ensuite, nous définissons des marges ( margins ) autour de l’ ListView afin de créer un espace entre celle-ci 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 toujours être « balayées » par l’utilisateur.
Les éléments doivent être bien espacés au sein de la vue ; la propriété ` spacing ` est donc 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’un des principaux atouts de QML réside dans sa capacité à permettre un prototypage extrêmement rapide d’une application, et cet exemple en est la preuve. Il est également possible d’attribuer 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 affiché 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 de vraies données. Pour cela, il est préférable d’utiliser un modèle C++ approprié disponible à l’adresse subclassing QAbstractItemModel.
Délégué
Passons maintenant à delegate. Nous attribuons le texte correspondant issu 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, consultez la section « Modèles et vues » sur Qt Quick.
Dans notre application, la largeur de chaque élément de la vue doit être identique à celle de la vue. Cela garantit à l'utilisateur un espace suffisant pour sélectionner un contact dans la liste, ce qui est un facteur important sur les appareils dotés de petits écrans tactiles, comme les téléphones mobiles. Cependant, la largeur de la vue inclut nos marges en pixels 48; nous devons donc en tenir compte lors de l’affectation de la propriété `width`.
Ensuite, nous définissons un délégué ` Image`. Celui-ci affichera une photo 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 celle de l’image, afin d’éviter tout espace vertical vide.

Chapitre 3 : Navigation
Dans ce chapitre, vous apprendrez à utiliser l'StackView pour naviguer entre les pages d'une application. Voici le code révisé de main.qml:
import QtQuick.Controls
ApplicationWindow {
id: window
width: 540
height: 960
visible: true
StackView {
id: stackView
anchors.fill: parent
initialItem: ContactPage {}
}
}Navigation avec StackView
Comme son nom l’indique, StackView permet une navigation basée sur une pile. Le dernier élément « poussé » dans la pile est le premier à en être retiré, et l’élément situé tout en haut est toujours celui qui est visible.
De la même manière qu’avec `Page`, nous indiquons à ` StackView ` de remplir la fenêtre de l’application. Il ne reste plus qu’à lui fournir 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 ainsi dès que vous avez une idée générale des écrans que comportera votre application. Cela rend non seulement votre code plus lisible, mais garantit également que les éléments ne sont instanciés à partir d’un composant donné que lorsque cela est absolument nécessaire, ce qui réduit l’utilisation de la mémoire.
Remarque : Qt Creator propose plusieurs solutions 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 point à prendre en compte lors de l’utilisation de ListView est de savoir s’il faut y faire référence via id ou utiliser la propriété attachée ListView.view. La meilleure approche dépend de plusieurs facteurs. Attribuer un identifiant à la vue permet d’obtenir des expressions de liaison plus courtes et plus efficaces, car la propriété attachée entraîne un surcoût très faible. Cependant, 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 associées afin d’éviter de lier le délégué à une vue particulière. Par exemple, en utilisant les propriétés associées, l’affectation width dans notre délégué devient :
width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMarginAu chapitre 2, nous avons ajouté un élément ` ListView ` sous l’en-tête. Si vous exécutez l’application de 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 esthétique, surtout si le texte dans les délégués est suffisamment long pour atteindre celui de l’en-tête. L’idéal serait d’avoir un bloc de couleur uni sous le texte de l’en-tête, mais au-dessus de la vue. Cela garantit que le contenu de la liste ne peut pas interférer visuellement avec celui 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 sur ` true`, mais cela can affect performance.
ToolBar est l’outil idéal pour cette tâche. Il s’agit d’un conteneur regroupant à la fois des actions et des contrôles valables pour l’ensemble de l’application et contextuels, tels que des boutons de navigation et des champs de recherche. Mieux encore, sa couleur d’arrière-plan provient, comme d’habitude, du style de l’application. Le voici en action :
Comme il ne dispose pas de mise en page propre, nous centrons nous-mêmes l’étiquette à l’intérieur.
Le reste du code est identique à celui du chapitre 2, à l’exception du fait que nous avons tiré parti du signal « clicked » pour ajouter la page suivante à la vue de pile :
onClicked: root.StackView.view.push("ConversationPage.qml", { inConversationWith: modelData })Lorsqu’on ajoute un ` Component ` ou un ` url ` à ` StackView`, il est souvent nécessaire d’initialiser l’élément (qui sera éventuellement instancié) avec certaines variables. La fonction ` push()` de ` StackView` prend cela en compte, en acceptant un objet JavaScript comme deuxième argument. Nous utilisons cette fonction 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; celle-ci est nécessaire en raison du fonctionnement des propriétés associées.
Parcourons ensemble le fichier ` ConversationPage.qml`, en commençant par les importations :
import QtQuick
import QtQuick.Layouts
import QtQuick.ControlsElles sont identiques à celles d’auparavant, à l’exception de l’ajout de l’importation QtQuick.Layouts, que nous aborderons sous peu.
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é servira simplement à déterminer le texte affiché dans l’en-tête. 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 la méthode ` pop()` lorsqu’on clique dessus. Un « ToolButton » est fonctionnellement similaire à ` Button`, mais offre un rendu plus adapté à un « ToolBar ».
Il existe deux façons de disposer des éléments en QML : les positionneurs d’éléments et Qt Quick Layouts. Les positionneurs d’éléments (Row, Column, etc.) sont utiles lorsque la taille des éléments est connue ou fixe, et qu’il suffit de les positionner proprement selon une certaine disposition. Les mises en page de Qt Quick Layouts permettent à la fois de positionner et de redimensionner les éléments, ce qui les rend particulièrement adaptées 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
...
}Un « pane » est essentiellement un rectangle dont la couleur est définie par le style de l'application. Il est similaire à un « Frame », à la seule différence qu'il ne comporte pas de contour.
Les éléments qui sont des enfants directs d’une mise en page disposent de divers attached properties. Nous utilisons ` Layout.fillWidth ` et ` Layout.fillHeight ` sur l’élément ` ListView ` pour nous assurer qu’il occupe autant d’espace que possible au sein de la ` ColumnLayout `. La même procédure est appliquée au `Pane`. Comme ` ColumnLayout ` est une mise en page verticale, il n’y a pas d’éléments à gauche ou à droite de chaque enfant ; chaque élément occupera donc toute la largeur de la mise en page.
En revanche, l’instruction « Layout.fillHeight » dans le « ListView » lui permettra d’occuper l’espace restant après l’intégration du « Pane ».
Examinons la vue de liste 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 défini la largeur et la hauteur de son élément parent, nous avons également défini des marges pour la vue. Cela nous permet d’obtenir un alignement harmonieux avec le texte de remplacement dans le champ « rédiger un message » :

Ensuite, nous définissons les propriétés ` 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 aux bords de celle-ci. Le moyen le plus simple de comprendre ce principe est de mettre ces propriétés en commentaire et d'observer ce qui se passe lorsque l'on fait défiler la vue.
Nous inversons ensuite la direction verticale de la vue, afin 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, en attendant que nous implémentions le véritable modèle au chapitre 4.
Au sein du délégué, nous déclarons un ` Row ` comme élément racine, car nous souhaitons 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é factice sentByMe, qui utilise simplement l’index du délégué pour alterner entre les différents auteurs. À l’aide de cette propriété, nous distinguons les différents auteurs de trois manières :
- Les messages envoyés par l’utilisateur sont alignés à droite de l’écran en définissant
anchors.rightsurlistView.contentItem.right. - En définissant la propriété `
visible` de l’avatar (qui n’est pour l’instant qu’un simple rectangle) en fonction de `sentByMe`, 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 du texte foncé sur un fond foncé, et inversement, nous définissons également la couleur du texte en fonction de l’auteur. Au chapitre 5, nous verrons comment le style (styling) gère ce genre de détails à notre place.
Au 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 recouvrir 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
}
}
}La zone de saisie « TextArea » doit occuper toute la largeur disponible de l’écran. Nous y ajoutons un texte de remplacement afin d’indiquer visuellement à l’utilisateur où il doit commencer à taper. Le texte saisi dans cette zone est renvoyé à la ligne automatiquement pour ne pas déborder de l’écran.
Enfin, le bouton n’est activé que lorsqu’il y a effectivement un message à envoyer.
Chapitre 4 : Modèles
Dans le chapitre 4, nous vous guiderons tout au long du processus de création de modèles SQL en lecture seule et en lecture-écriture en C++, puis de leur exposition à QML afin de remplir les vues.
QSqlQueryModel
Afin de simplifier ce tutoriel, nous avons choisi de rendre la liste des contacts de l'utilisateur non modifiable. QSqlQueryModel est le choix logique dans ce cas, car il fournit un modèle de données en lecture seule pour les ensembles de résultats SQL.
Jetons un œ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 n'y a pas grand-chose à voir ici, passons donc 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 ainsi que ceux dont nous avons besoin provenant de 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 encore), 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 passé à cette fonction, celle-ci renverra une « connexion par défaut », dont nous aborderons bientôt la création.
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 servira à remplir le modèle. Dans ce cas précis, nous nous intéressons simplement à toutes les lignes de la table 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 ` ; nous devons donc en informer moc à l'aide de la macro ` Q_OBJECT `.
La propriété recipient sera définie depuis QML afin d’indiquer au modèle pour quelle conversation il doit récupérer les messages.
Nous redéfinissons 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 souhaitons appeler depuis QML, d’où la macro ` Q_INVOKABLE `.
Jetons un œ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('Moi', 'Ernest Hemingway', '2016-01-07T14:36:06', 'Bonjour !')");
query.exec("INSERT INTO Conversations VALUES('Ernest Hemingway', 'Moi', '2016-01-07T14:36:16', 'Bonjour.')");
query.exec("INSERT INTO Conversations VALUES('Moi', 'Albert Einstein', '2016-01-01T11:24:53', 'Salut !')");
query.exec("INSERT INTO Conversations VALUES('Albert Einstein', 'Moi', '2016-01-07T14:36:16', 'Bonjour.')");
query.exec("INSERT INTO Conversations VALUES('Hans Gude', 'Moi', '2015-11-20T06:30:02', 'Bonjour. As-tu reçu mon tableau ?')");
query.exec("INSERT INTO Conversations VALUES('Moi', 'Hans Gude', '2015-11-20T08:21:03', 'Bonjour, Hans. Oui, c’est très beau. Merci beaucoup ! "
"Combien d'heures y as-tu consacré ?')");
}Ce code est très similaire à sqlcontactmodel.cpp, à l’exception près que nous opérons désormais sur la table Conversations. Nous définissons également conversationsTableName comme une variable constante 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 allons utiliser via la fonction ` setTable()`. Pour nous assurer que les messages les plus récents de la conversation s'affichent en premier, nous trions les résultats de la requête par le champ ` timestamp ` dans l'ordre décroissant. Cela va de pair avec la configuration de la propriété ` verticalLayoutDirection ` de ` ListView` sur ` ListView.BottomToTop ` (ce que nous avons abordé 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 appliquons un filtre aux 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() ` utilise l'implémentation de ` QSqlTableModel` si le rôle n'est pas un rôle utilisateur personnalisé. Si le rôle est un rôle utilisateur, nous pouvons lui soustraire ` Qt::UserRole ` pour obtenir l'index de ce champ, puis 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 un mappage entre les valeurs de nos rôles personnalisés et les noms de rôles. Cela nous permet d’utiliser ces rôles dans QML. Il peut être utile de déclarer une énumération 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 préoccupons 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 l'recipient fournie et un message pour insérer un nouvel enregistrement dans la base de données. Comme nous utilisons QSqlTableModel::OnManualSubmit, nous devons appeler manuellement la fonction submitAll().
Connexion à la base de données et enregistrement des types avec QML
Maintenant que nous avons défini les classes de modèle, examinons 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 disposons d'un emplacement accessible en écriture sur tous les appareils.
const QString fileName = writeDir.absolutePath() + "/chat-database.sqlite3";
// Lorsque l'on utilise le pilote SQLite, la fonction 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() établit la connexion à la base de données SQLite, en créant le fichier s'il n'existe pas encore.
Dans ` main()`, nous appelons ` qmlRegisterType()` pour enregistrer nos modèles en tant que types dans QML.
Utilisation des modèles dans QML
Maintenant que nos modèles sont disponibles en tant que types QML, quelques modifications mineures doivent être apportées au fichier ` ContactPage.qml`. Pour pouvoir utiliser ces types, nous devons d'abord les importer à l'aide de l'URI que nous avons défini dans ` main.cpp` :
import chattutorialNous remplaçons ensuite le modèle factice par le modèle approprié :
model: SqlContactModel {}Au sein du délégué, nous utilisons une syntaxe différente pour accéder aux données du modèle :
text: model.displayDans ` ConversationPage.qml`, nous ajoutons la même importation ` chattutorial ` et remplaçons le modèle factice :
model: SqlConversationModel {
recipient: root.inConversationWith
}Dans le modèle, nous définissons la propriété ` recipient ` sur le nom du contact pour lequel la page est affichée.
L'élément délégué racine passe d'une ligne (Row) à une colonne (Column), afin de pouvoir afficher l'horodatage 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 correct, nous pouvons utiliser son rôle « recipient » dans l’expression de la propriété « sentByMe ».
Le rectangle qui servait d’avatar a été converti en image. L’image ayant sa propre taille implicite, il n’est pas nécessaire 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 l’ source de l’image sur une URL vide au lieu d’utiliser la propriété visible.
Nous souhaitons que l’arrière-plan de chaque message soit légèrement plus large (12 pixels de chaque côté) que son texte. Cependant, s’il est trop long, nous souhaitons limiter sa largeur au bord de la liste, d’où l’utilisation de ` Math.min()`. Lorsque le message n’a pas été envoyé par nous, un avatar apparaît toujours devant celui-ci ; nous en tenons donc 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 correspond à la plus petite des deux valeurs. En revanche, dans l’image ci-dessous, le texte du message étant assez long, c’est la plus petite valeur (la largeur de la vue) qui est retenue, ce qui garantit que le texte s’arrête au bord opposé de l’écran :

Afin d’afficher l’horodatage de chaque message, comme nous l’avons vu précédemment, nous utilisons un Label. La date et l’heure sont formatées à l’aide de la fonction ` Qt.formatDateTime()` en utilisant un format personnalisé.
Le bouton « Envoyer » doit désormais réagir lorsqu’on clique dessus :
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 invokable sendMessage() du modèle, qui insère une nouvelle ligne dans la table de base de données « Conversations ». Ensuite, nous effaçons le champ de texte pour permettre une nouvelle saisie.
Chapitre 5 : Mise en forme
Les styles d’ Qt Quick Controls sont conçus pour fonctionner sur n’importe quelle plateforme. Dans ce chapitre, nous allons apporter quelques modifications visuelles mineures afin de nous assurer que notre application s’affiche correctement lorsqu’elle est exécutée avec les styles Basic, Material et Universal.
Jusqu'à présent, nous avons uniquement testé l'application avec le style « Basic ». Si nous l'exécutons avec le style « Material », par exemple, nous constaterons immédiatement certains problèmes. Voici la page « Contacts » :

Le texte de l'en-tête est noir sur un fond bleu foncé, ce qui le rend très difficile à lire. Le même problème se pose avec la page « Conversations » :

La solution consiste à indiquer à la barre d'outils qu'elle doit utiliser le thème « Dark », afin que cette information soit transmise à ses éléments enfants, leur permettant ainsi de changer la couleur de leur texte pour une teinte plus claire. La manière la plus simple de procéder est d'importer directement le style « Material » et d'utiliser la propriété attachée « Material » :
import QtQuick.Controls.Material 2.12
// ...
header: ToolBar {
Material.theme: Material.Dark
// ...
}Cependant, cela entraîne une dépendance stricte au style Material ; le plugin du style Material doit être déployé avec l'application, même si l'appareil cible ne l'utilise pas, sinon le moteur QML ne parviendra pas à trouver l'importation.
Il vaut mieux, à la place, s’appuyer sur la prise en charge intégrée par Qt Quick Controlsdes sélecteurs de fichiers basés sur les styles. Pour ce faire, nous devons déplacer le fichier ` ToolBar ` dans un fichier distinct. 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 autre n’est spécifié) est en vigueur. 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 Qt Quick Controls. Le code lui-même n’a pas changé par rapport à ce qu’il était dans ContactPage.qml, ce qui est normal ; pour la version par défaut du fichier, rien ne doit être différent.
De retour dans 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 « Material » 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 correctement nommés, situés à côté de la version par défaut du fichier. Cela signifie que nous devons ajouter un dossier nommé « +Material » dans le même répertoire que celui contenant ChatToolBar.qml : le dossier racine. Le signe « + » est requis par QFileSelector afin de garantir que la fonctionnalité de sélection ne soit pas déclenchée par inadvertance.
Voici +Material/ChatToolBar.qml:
import QtQuick.Controls
import QtQuick.Controls.Material
ToolBar {
Material.theme: Material.Dark
}Nous allons apporter 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 s'affichent désormais correctement :


Essayons le style « Universal » :


Tout fonctionne bien. Pour une application relativement simple comme celle-ci, le changement de style ne devrait nécessiter que très peu d'ajustements.
Essayons maintenant le thème sombre de chaque style. Le style « Basic » ne dispose pas de thème sombre, car cela ajouterait une légère surcharge à un style conçu pour être aussi performant que possible. Nous allons d'abord tester le style « Material » ; ajoutez donc une entrée dans ` qtquickcontrols2.conf ` pour lui indiquer d'utiliser son thème sombre :
[Material]
Primary=Indigo
Accent=Indigo
Theme=DarkUne fois cela fait, compilez et exécutez l’application. Voici ce que vous devriez voir :


Les deux pages s’affichent correctement. Ajoutez maintenant une entrée pour le style « Universal » :
[universal]
Theme=DarkAprès avoir compilé et exécuté l'application, vous devriez obtenir les résultats suivants :


Résumé
Dans ce tutoriel, nous vous avons guidé à travers les étapes suivantes de la création d'une application basique à l'aide d'Qt Quick Controls:
- Création d’un nouveau projet à l’aide d’ Qt Creator.
- Configuration d'une page ApplicationWindow de base.
- Définition des en-têtes et des pieds de page avec Page.
- Affichage du contenu dans une page ListView.
- Refactorisation des composants dans leurs propres fichiers.
- Navigation entre les écrans avec StackView.
- Utilisation de mises en page pour permettre à une application de s'adapter en douceur aux différents formats d'écran.
- Implémentation de modèles personnalisés en lecture seule et en écriture qui intègrent une base de données SQL dans l'application.
- Intégration de C++ à QML via Q_PROPERTY, Q_INVOKABLE et qmlRegisterType().
- Tester et configurer 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.