Aller au contenu

ELCa11 - Tuto : faire communiquer C++ et QML


Objectifs

  • Construire une application Qt Quick combinant une interface en QML et une logique métier en C++.
  • Maîtriser les quatre mécanismes essentiels de communication QML ↔ C++ :
    • setContextProperty — exposer un objet C++ à QML ;
    • Q_INVOKABLE — appeler une méthode C++ depuis QML ;
    • Q_PROPERTY — exposer un attribut comme propriété QML ;
    • signaux Qt — notifier QML d’un changement côté C++.

L’exemple : un compteur affiché à l’écran, que l’on incrémente ou décrémente avec les flèches du clavier (↑ / ↓).

Prérequis


Aparté : signaux et slots Qt

Qt met à disposition un mécanisme original de communication entre objets, appelé signaux et slots (signals and slots) :

  • un signal est émis par un objet quand un événement se produit (valeur changée, bouton cliqué, etc.) ;
  • un slot est une méthode qui s’exécute en réaction à un signal qu’on lui a connecté ;
  • la connexion entre les deux se fait avec QObject::connect(...).

C’est plus souple que les callbacks C++ classiques et permet à des objets de se notifier sans dépendre directement les uns des autres. C’est aussi le mécanisme qui permet à QML de réagir automatiquement quand un attribut C++ change : la classe C++ émet un signal, le moteur QML l’attrape et rafraîchit l’affichage.


Étape 1 — Créer le projet et l’arbre de scène QML (partie statique)

  1. Lancer Qt Creator. Menu Fichier > Nouveau projet….
  2. Choisir Application (Qt) > Application Qt Quick.
  3. Nom : Compteur (sans année — garder un nom générique). Système de build : CMake.
  4. Valider les écrans suivants par défaut.

Compiler et exécuter (▶ ou Ctrl+R / Cmd+R) pour vérifier que la fenêtre vide s’affiche.

Ouvrir Main.qml et basculer en mode Conception (icône en bas à gauche, ou Mode > Design).

Construire l’interface :

  • Fenêtre (Window) : taille 320 × 240, titre Compteur.
  • Rectangle principal (anchors.fill: parent) avec une couleur de fond au choix.
  • Rectangle centré, taille 100 × 50, couleur contrastante, identifiant boite.
  • Élément Text centré dans la boite, taille 20 pixels, gras, police Tahoma (ou autre).
  • Cocher focus: true sur le rectangle principal (panneau Propriétés > Avancé, en bas).

focus: true est indispensable pour que la fenêtre reçoive les événements clavier (sinon, les flèches ne déclencheront rien).


Étape 2 — Créer la classe C++ Compteur

  1. Clic droit sur le projet dans la barre latérale > Ajouter un nouveau….
  2. Choisir C++ > Classe C++ > Choisir….
  3. Nom de la classe : Compteur. Cocher Hériter de QObject (la macro Q_OBJECT sera ajoutée automatiquement).
  4. Valider.

Qt Creator crée compteur.h et compteur.cpp. Hériter de QObject est indispensable pour utiliser les signaux/slots, Q_INVOKABLE et Q_PROPERTY.

Dans compteur.h, ajouter :

#ifndef COMPTEUR_H
#define COMPTEUR_H

#include <QObject>
#include <QString>

class Compteur : public QObject {
    Q_OBJECT
    Q_PROPERTY(QString cptQML READ readCompteur NOTIFY cptChanged)

public:
    explicit Compteur(QObject* parent = nullptr);

    Q_INVOKABLE void increment();
    Q_INVOKABLE void decrement();

signals:
    void cptChanged();

private:
    QString readCompteur() const;
    int fCompteur;
};

#endif // COMPTEUR_H

Quelques explications :

  • Q_OBJECT : macro indispensable dans toute classe utilisant signaux/slots/properties Qt.
  • Q_INVOKABLE : marque une méthode comme appelable depuis QML.
  • Q_PROPERTY(QString cptQML READ readCompteur NOTIFY cptChanged) : déclare une propriété QML nommée cptQML, de type QString, dont :

    • la valeur s’obtient en appelant readCompteur() (READ) ;
    • les changements sont annoncés par le signal cptChanged (NOTIFY).

    Pas de WRITE : la propriété est lecture seule depuis QML (on ne peut pas écrire vueObjetCpt.cptQML = "..."). On y reviendra plus bas.

Pas de ; à la fin de la ligne Q_PROPERTY — c’est une macro, pas une déclaration.

Implémentation dans compteur.cpp :

#include "compteur.h"

Compteur::Compteur(QObject* parent) : QObject(parent), fCompteur(10) {}

void Compteur::increment() {
    ++fCompteur;
    emit cptChanged();
}

void Compteur::decrement() {
    --fCompteur;
    emit cptChanged();
}

QString Compteur::readCompteur() const {
    return QString::number(fCompteur);
}

emit cptChanged(); : envoie le signal à tous les objets qui y sont connectés. QML est connecté automatiquement à ce signal grâce à NOTIFY dans la Q_PROPERTY — il rafraîchira l’affichage tout seul.


Étape 3 — Exposer l’objet C++ à QML (main.cpp)

Remplacer le contenu de main.cpp par :

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "compteur.h"

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

    Compteur aCompteur;                     // notre objet C++

    QQmlApplicationEngine engine;
    QObject::connect(
        &engine,
        &QQmlApplicationEngine::objectCreationFailed,
        &app,
        []() { QCoreApplication::exit(-1); },
        Qt::QueuedConnection);

    // Rend aCompteur accessible côté QML sous le nom "vueObjetCpt"
    engine.rootContext()->setContextProperty("vueObjetCpt", &aCompteur);

    engine.loadFromModule("Compteur", "Main");
    return app.exec();
}

Les points-clés :

  • Compteur aCompteur; crée l’objet C++.
  • engine.rootContext()->setContextProperty("vueObjetCpt", &aCompteur); rend cet objet visible côté QML, sous le nom vueObjetCpt. Vous y accéderez avec ce nom dans tout le code QML.
  • engine.loadFromModule("Compteur", "Main"); charge Main.qml (le "Compteur" est le nom du module, ici le nom du projet).

Compiler et exécuter — aucun changement visible à ce stade, c’est normal : on a exposé l’objet, mais QML ne l’utilise pas encore.


Étape 4 — Utiliser l’objet C++ depuis QML

Ouvrir Main.qml. Modifier l’élément Text pour lire la valeur du compteur, et ajouter la gestion des flèches sur le rectangle principal :

Text {
    id: txt
    anchors.centerIn: parent
    text: vueObjetCpt.cptQML
    font.pixelSize: 20
    font.bold: true
    font.family: "Tahoma"
}

Et sur le rectangle principal (qui a focus: true) :

Keys.onPressed: (event) => {
    switch (event.key) {
        case Qt.Key_Up:
            vueObjetCpt.increment();
            break;
        case Qt.Key_Down:
            vueObjetCpt.decrement();
            break;
    }
}

Compiler et exécuter :

  • la valeur initiale 10 doit s’afficher ;
  • ↑ doit incrémenter, ↓ décrémenter ;
  • la mise à jour est instantanée grâce au signal cptChanged connecté automatiquement à la propriété QML.

Étape 5 — Code final attendu

compteur.h et compteur.cpp : voir Étape 2.

main.cpp : voir Étape 3.

Main.qml :

import QtQuick

Window {
    width: 320
    height: 240
    visible: true
    title: qsTr("Compteur")

    Rectangle {
        id: fond
        anchors.fill: parent
        color: "lightyellow"
        focus: true

        Keys.onPressed: (event) => {
            switch (event.key) {
                case Qt.Key_Up:
                    vueObjetCpt.increment();
                    break;
                case Qt.Key_Down:
                    vueObjetCpt.decrement();
                    break;
            }
        }

        Rectangle {
            id: boite
            anchors.centerIn: parent
            width: 100
            height: 50
            color: "steelblue"

            Text {
                id: txt
                anchors.centerIn: parent
                text: vueObjetCpt.cptQML
                font.pixelSize: 20
                font.bold: true
                font.family: "Tahoma"
                color: "white"
            }
        }
    }
}

Pour aller plus loin

Rendre la propriété modifiable depuis QML (WRITE)

Si l’on veut pouvoir écrire la valeur du compteur depuis QML (par ex. via un champ texte), il faut ajouter WRITE à la Q_PROPERTY :

Q_PROPERTY(QString cptQML READ readCompteur WRITE writeCompteur NOTIFY cptChanged)

et fournir la méthode correspondante :

void Compteur::writeCompteur(const QString& s) {
    int nouvelle = s.toInt();
    if (nouvelle != fCompteur) {
        fCompteur = nouvelle;
        emit cptChanged();
    }
}

Bonne pratique : dans une fonction WRITE, toujours vérifier que la nouvelle valeur diffère de l’ancienne avant d’émettre le signal — sinon, on risque des boucles d’événements et des mises à jour inutiles.

Exercice — borner le compteur

Dans sa version actuelle, le compteur peut prendre n’importe quelle valeur entière. Étendre l’application pour qu’il reste dans un intervalle borné, par exemple [CPT_MIN, CPT_MAX] = [0, 20].

Pistes :

  • Modifier increment() et decrement() pour ne rien faire si la nouvelle valeur sortirait des bornes.
  • Faut-il avertir l’utilisateur quand on tente de dépasser une borne (ex. changer brièvement la couleur du texte) ?
  • Exposer CPT_MIN et CPT_MAX à QML (via deux nouvelles Q_PROPERTY en lecture seule) pour pouvoir les afficher dans l’interface.