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 :
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 :
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.
#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 :
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++ :
int
main(int
argc, char
*
argv[])
{
QApplication
app(argc, argv);
qmlRegisterType<
Anchor>
("MyLib"
, 1
, 0
, "Anchor"
);
QmlApplicationViewer viewer;
// ...
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 :
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() :
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);
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 !