Communication entre QML et C++/Qt

Louis du Verdier

Depuis sa sortie, Qt Quick met en avant la possibilité de séparer la partie graphique de la partie logique d'une application. La partie graphique, l'interface utilisateur, est codée avec QML tandis que la partie logique est codée en C++ avec Qt. Une question que l'on peut alors se poser est la suivante : comment faire communiquer la partie QML avec la partie C++/Qt ? Ce tutoriel a pour objectif de répondre à cette question.

N'hésitez pas à commenter cet article !
Commentez

- Titre : Communication entre QML et C++/Qt

- Auteur : Louis du Verdier

- Parution : 17 juillet 2011

- Mise à jour : 17 juillet 2011

- Licence : Copyright ® 2011 Louis du Verdier. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Problématique

- II. Contexte des méthodes

- III. Solution 1 : parcourir l'arborescence QML

- III-A. Démarche

- III-B. Inconvénients

- IV. Solution 2 : un nouveau composant

- IV-A. Démarche

- IV-B. Inconvénients

- V. Solution 3 : la bonne

- VI. Remerciements

I - Problématique

QML est un langage déclaratif lancé dans le but d'offrir aux développeurs un moyen simple de séparer la partie logique de la partie graphique d'une application. Il correspond à un outil puissant permettant de réaliser des interfaces graphiques fluides et ergonomiques, avec des transitions et des animations telles que les designers les pensent, sans autre limite que celle de leur imagination.

On pourrait aisément comparer ce concept à l'organisation d'une équipe de développement : la création, donc les designers, travaillent dans une aile des studios de l'entreprise tandis que les développeurs travaillent dans une autre. Mais durant toute la durée de réalisation d'un projet, il est nécessaire que les deux groupes communiquent, au début pour se mettre d'accord, pour que la création sache si telle animation est réalisable, pour que les développeurs demandent telle image manquante ; ils ont donc besoin d'un système faisant office d'une ancre, exactement comme la partie logique et la partie graphique d'une application.

Dans la pratique, le système « ancre » entre les deux groupes de l'équipe de développement peut être n'importe quoi : une boîte mail, un système de messagerie instantanée ou même la voix suite à l'utilisation des jambes. Du point de vue de Qt et de QML, la situation est identique, les moyens de relier la partie graphique avec la partie logique ne manquent pas. Plusieurs moyens seront traités à la suite, suivis de leurs points forts et de leurs points faibles.

II - Contexte des méthodes

Avant de pouvoir entrer dans les détails, il est nécessaire de savoir de quoi on parle. QML et C++/Qt sont des termes bien trop larges. Prenons donc un cas simple mais précis. On a un fichier main.qml que l'on affiche par le fichier main.cpp dans une QDeclarativeView.

Le fichier main.qml est celui-ci :

main.qml

import QtQuick 1.0

Rectangle {
id: main
width: 300
height: 200

color: "black"

property string text: ""

MouseArea {
anchors.fill: parent
}

Text {
id: main_text
text: parent.text
color: "white"
anchors.fill: parent
font.pointSize: 18
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}

Et le fichier main.cpp :

main.cpp

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

QmlApplicationViewer viewer;
viewer.setOrientation(QmlApplicationViewer::ScreenOrientationAuto);
viewer.setMainQmlFile(QLatin1String("qml/main.qml"));
viewer.showExpanded();

return app.exec();
}

Avec QmlApplicationViewer, la classe dérivée de QDeclarativeView qui est générée automatiquement par QtCreator. Après compilation et exécution, une fenêtre noire devrait apparaître. En changeant la valeur de la propriété text définie à une chaîne vide (""), du texte blanc devrait s'afficher au milieu de la fenêtre noire.

L'objectif de cet article sera de présenter les diverses manières de mettre en place un échange entre la partie C++ et la partie QML.

III - Solution 1 : parcourir l'arborescence QML

Cette solution est à sens unique. Elle permet d'accéder aux données QML depuis le C++/Qt pour récupérer ou assigner une valeur à une propriété.

III-A - Démarche

La première solution qui s'offre à nous pour accéder aux données présentes dans notre fichier main.qml est de parcourir l'arborescence QML, de récupérer le QObject associé à l'élément auquel on souhaite accéder, puis d'appeler la fonction property() pour récupérer la valeur d'une propriété ou setProperty() afin d'assigner une valeur à la propriété en question.

Pour parcourir l'arborescence QML, on passe par le rootObject() de la QDeclarativeView :

QObject *obj = viewer.rootObject();
qDebug() << obj->property("color").toString();

Cela affichera la couleur du rectangle à l'Id main, soit #000000, la couleur noire.

Toutefois, on n'accède pas directement avec rootObject() à toute l'arborescence QML mais uniquement au premier élément, l'élément à la racine. Il est bon de noter que l'on parle d'arborescence et ainsi de parenté. Dans le fichier QML, on peut voir que l'élément Text à l'Id main_text est englobé dans l'élément Rectangle à l'Id main. On dit que main_text a pour parent main.

En spécifiant la valeur de la propriété objectName d'un enfant, on peut y accéder depuis le rootObject() par le biais de la fonction findChild() de QObject :

Text {
id: main_text
objectName: "main_text"
...
}

Et l'accès à l'objet :

QObject *obj = viewer.rootObject();
QObject *text = obj->findChild<QObject*>("main_text");
qDebug() << text->property("width");

Cela affichera la taille de l'élément main_text, 300.

Il est bon de noter que les exemples exposés présentent la fonction property() de QObject, permettant de récupérer la valeur d'une propriété. Pour définir la valeur d'une propriété, il suffit d'appeler la fonction setProperty(). On peut également caster l'objet en QDeclarativeItem et appeler directement l'une de ses fonctions :

QObject *obj = viewer.rootObject();
obj->setProperty("color", Qt::blue); // le rectangle devient bleu
QDeclarativeItem *item = qobject_cast<QDeclarativeItem *>(obj);
item->setWidth(200); // la longueur du rectangle devient 200
item->setProperty("color", Qt::green); // le rectangle devient vert

III-B - Inconvénients

Cette méthode n'est pas idéale dans le sens où elle possède deux inconvénients :

- elle fait tout l'inverse de ce pour quoi QML a été lancé puisque la partie C++ est forcée de connaître l'arborescence de l'interface utilisateur pour pouvoir accéder aux données ;

- elle est assez coûteuse sur le plan de la consommation en ressources du fait que parcourir une arborescence pour accéder à des données n'est pas un moyen idéal d'optimisation.

IV - Solution 2 : un nouveau composant

Cette solution permet d'accéder aux données QML depuis le C++/Qt pour récupérer ou assigner une valeur à une propriété. Elle relate la création d'un nouveau composant qui servirait de pont entre la partie graphique et la partie logique de l'application.

IV-A - Démarche

À la vue des inconvénients de la première solution, on pourrait envisager de toucher uniquement à un seul élément de l'arborescence QML plutôt qu'à tous à la fois. Cela nécessite la création d'un élément, par exemple nommé Anchor, permettant d'écrire ceci :

Rectangle {
id: main
// ...
color: anchor.color

Anchor {
id: anchor
objectName: "anchor"
}
// ...
Text {
text: anchor.text
// ...
}
}

On se servirait d'une instance de l'élément Anchor pour attribuer les valeurs à tous les éléments de l'interface QML et on ne toucherait qu'à l'objet anchor.

Afin d'aboutir à ce résultat, il est nécessaire de créer une classe héritant de QObject possédant les propriétés text, de type QString, et color, de type QColor.

anchor.h

#ifndef ANCHOR_H
#define ANCHOR_H

#include <QObject>
#include <QColor>
#include <QString>

class Anchor : public QObject
{
Q_OBJECT
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)

public:
Anchor() : _color(Qt::black), _text("") { }
QColor color() const {
return _color;
}
QString text() const {
return _text;
}
void setColor(const QColor &color) {
_color = color;
emit colorChanged();
}
void setText(const QString &text) {
_text = text;
emit textChanged();
}

signals:
void colorChanged();
void textChanged();

private:
QColor _color;
QString _text;
};

#endif // ANCHOR_H

Dans le main.cpp, on appelle alors la fonction qmlRegisterType() comme ceci :

qmlRegisterType<Anchor>("MyLib", 1, 0, "Anchor");

L'unique chose à faire pour pouvoir écrire le code QML donné plus haut est d'ajouter une ligne :

import MyLib 1.0

On accéderait aux données de cet élément exactement comme dans la solution précédente :

QObject *obj = viewer.rootObject();
QObject *anchor = obj->findChild<QObject*>("anchor");
anchor->setProperty("text", "L'ancre fonctionne.");

Note : le contenu de l'interface QML ne s'actualise qu'uniquement parce que les signaux NOTIFY sont envoyés lors de la modification des propriétés associées, conformément aux déclarations emit. Sans ces déclarations, l'interface ne broncherait pas.

Cette solution permet de mettre en place très facilement une communication signaux/slots réciproque entre le code QML et C++. Voici un exemple illustrant une réception du signal colorChanged() des deux côtés :

main.qml

import QtQuick 1.0
import MyLib 1.0

Rectangle {
id: main
width: 100; height: 100

Anchor {
id: anchor
onColorChanged: {
console.log("anchor.onColorChanged()");
}
}

Component.onCompleted: {
anchor.color = "green";
}
}

Et la partie C++ :

main.cpp

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

qmlRegisterType<Anchor>("MyLib", 1, 0, "Anchor");

QmlApplicationViewer viewer;

// ...

anchor.h

class Anchor : public QObject
{
Q_OBJECT
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)

public:
Anchor() : _color(Qt::black) {
connect(this, SIGNAL(colorChanged()), qApp, SLOT(aboutQt()));
}

Remarque : les signaux du côté du C++ et de QML sont déclarés sous la forme signalName, avec signalName correspondant à la valeur inscrite dans la macro Q_PROPERTY() ou derrière une déclaration signal. Quant à eux, les gestionnaires QML sont de la forme onSignalName, tout comme les gestionnaires JS :

Q_PROPERTY(... NOTIFY signalName) // Propriété côté C++
Q_SIGNAL T signalName(); // Décaration du signal côté C++
signal signalName // Déclaration du signal côté QML
onSignalName: { ... } // Gestionnaire associé au signal côté QML

IV-B - Inconvénients

Cette solution en appelle toujours à une connaissance de l'arborescence QML graphique par la partie logique de l'application. Le fait lui-même que la partie C++ ait besoin d'accéder à la partie QML est conceptuellement incorrect, l'idéal étant de ne pas avoir à intervenir sur le rootObject() ou autre du QDeclarativeView. Bien que plus ouverte, elle possède les mêmes inconvénients que la solution précédente.

V - Solution 3 : la bonne

À la vue des deux exemples précédents, on peut aisément déduire que la bonne solution serait un moyen d'instaurer un objet qui serait à la fois présent dans la partie logique et dans la partie graphique, une solution permettant d'écrire ceci dans le fichier main.qml :

main.qml

import QtQuick 1.0

Rectangle {
id: main
width: 100; height: 100
color: anchor.color
}

Et cela dans main.cpp, avec anchor une instance de la classe Anchor :

anchor.setProperty("color", Qt::red);

Cela reviendrait à définir une variable globale à notre arborescence QML, de type Anchor, et d'interagir avec à notre guise depuis le QML et le C++, que ce soit pour appeler une de ses fonctions, modifier ou récupérer une valeur de propriété. Qt Quick met à notre disposition les contextProperties qui permettent ce genre de chose :

Anchor anchor;

QmlApplicationViewer viewer;
viewer.setOrientation(QmlApplicationViewer::ScreenOrientationAuto);
viewer.rootContext()->setContextProperty("anchor", &anchor);
viewer.setMainQmlFile(QLatin1String("qml/main.qml"));
viewer.showExpanded();

anchor.setProperty("color", Qt::red);

De là, on peut se servir de la propriété contextuelle anchor à notre guise, par exemple, pour appeler une de ses fonctions définies Q_INVOKABLE depuis QML :

class Anchor : public QObject
{
Q_OBJECT
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)

public:
Anchor() : _color(Qt::black) { }
Q_INVOKABLE void clicked() { qDebug() << "Clicked !"; }
// ...

Et côté QML :

import QtQuick 1.0

Rectangle {
id: main
width: 100; height: 100
color: anchor.color

MouseArea {
anchors.fill: parent
onClicked: anchor.clicked();
}
}

L'instance de la classe Anchor, bien qu'utilisée comme propriété contextuelle, peut être manipulée comme une instance de toute classe C++.

À la fin de la partie traitant de la deuxième solution, un exemple de réception des signaux a été donné. Pour pouvoir obtenir un résultat similaire avec les propriétés contextuelles, une connexion identique se fait du côté C++, avec QObject::connect(). Par contre, du côté QML, il est nécessaire d'utiliser l'élément Connections en lui passant une target afin de pouvoir utiliser onColorChanged() :

main.cpp

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

Anchor anchor;
QmlApplicationViewer viewer;
QObject::connect(&anchor, SIGNAL(colorChanged()), &app, SLOT(aboutQt()));
viewer.rootContext()->setContextProperty("anchor", &anchor);

main.qml

import QtQuick 1.0

Rectangle {
id: main
width: 100; height: 100

Connections {
target: anchor
onColorChanged: {
console.log("anchor.onColorChanged()");
}
}

Component.onCompleted: {
anchor.color = "green";
}
}

Note : l'élément Connections n'est pas limité aux propriétés contextuelles. Il peut également être utilisé sur tout type d'élément à même d'envoyer des signaux, comme un Rectangle, TextInput, MouseArea ou même Flickable. Par exemple :

Rectangle {
id: main
width: 100; height: 100

Connections {
target: main
onWidthChanged: {
console.log("main.onWidthChanged()");
}
}
}

On peut donc en conclure qu'une contextProperty est une solution idéale au problème de la communication entre la partie C++ et QML d'une application. Attention toutefois à ce que l'objet défini en tant que propriété contextuelle ne soit pas détruit pendant l'exécution pour éviter un crash non nécessaire.

VI - Remerciements

Merci à yan pour ses conseils durant la rédaction et à dourouc05 ainsi qu'à jacques_jean pour la relecture !