QML : user et abuser des types énumérés

Cette article tente de décrire les différents moyens d'exposer un type enum écrit en langage C++ dans le contexte du langage descriptif QML (Qt Modeling Language) de Qt. Travailler avec des types enums en QML présente de nombreux avantages :
  • une lecture explicite du code basée sur des types nommés plutôt que sur des chiffres sans signification précise.
  • une comparaison optimisée basée sur des valeurs entières plutôt que sur des chaînes de caractères.
  • le type énuméré est borné à un ensemble de valeurs constantes.
  • l'auto-complétion disponible dans Qt Creator, pour faciliter le choix de l'enum en QML.

Illustration avec un simple exemple

Voici un simple code qui montre l'usage de type enum en QML. Ce code permet de créer des ventilateurs (Fan) qui possèdent trois caractéristiques : une vitesse (speed), une taille (size) et un état (state). Ces trois propriétés sont traitées chacune à l'aide d'un type enum importé grâce à l'instruction "import FanImport 1.0".

Main.qml
import QtQuick 2.0
import FanImport 1.0
Rectangle {
    width: 360
    height: 100
    Row {
        spacing: 2
        Fan { speed : FanSpeed.Slow; size : FanSize.Big }
        Fan { speed : FanSpeed.Medium;  state : FanState.On }
        Fan { speed : FanSpeed.Fast;  size: FanSize.Small }
    }
} 

Le résultat c'est trois ventilateurs de différentes tailles dont celui du milieu est actif (non visible assurément dans l'image ci dessous, car le ventilateur du milieu est censé tourner à une vitesse moyenne).


Fan.qml : le code du composant Fan dont il est fait référence dans le code QML prédent.
import QtQuick 2.0
import FanImport 1.0

Fan {
    id: fan
    width: 100
    height: 100

    property int speed : FanSpeed.Medium
    property int size : FanSize.Medium
    property int state : FanState.Off

    Timer {
        interval: 100;
        running: fan.state === FanState.On ? true : false;
        repeat: true;
        onTriggered: {
            if (fan.speed === FanSpeed.Slow)
                fanText.rotation += 10
            else if (fan.speed === FanSpeed.Medium)
                fanText.rotation += 25
            else if (fan.speed === FanSpeed.Fast)
                fanText.rotation += 50
        }
    }
    Image {
        id: fanText
        source: "qrc:///qml/images/fan.jpg"
        anchors.centerIn: parent
        width: 20 * (fan.size+1)
        height: 20 * (fan.size+1)
    }
    MouseArea {
        anchors.fill: parent
        onClicked: fan.state === FanState.Off ? fan.state = FanState.On 
                                              : fan.state = FanState.Off
    }
}

Rendre accessible en QML un type enum défini en C++

Le type énuméré en C/C++

En C/C++ les types énumérés sont équivalents à des entiers, dont le premier de la liste prend comme valeur entière par défaut 0, le second 1 et ainsi de suite.

class FanState
{
    enum StateType {On, Off};
};
class FanSpeed
{
    enum Speed {Slow, Medium, Fast};
}; 
class Size 
{
    enum Size {Small, Medium, Big};
};

Dans l'exemple ci dessus, chaque type énuméré est créé dans une classe distincte, mais ce n'est pas obligatoire. En C++, il est courant de définir plusieurs types énumérés dans la même classe. L'inconvénient de cette pratique c'est qu'il n'est pas possible de déclarer deux types énumérés avec la même valeur (en l'occurence Medium). Le compilateur  remonte une erreur de type : "redeclaration of enum" :


Il y a d'autres avantages a créer une classe à part, et pour des raisons de lisibilité, c'est la façon qui a été adopté dans cette présentation. Après c'est un peu à l'appréciation de chacun et en fonction du contexte.

Enregistrer l'enum dans les meta-données d'un QObject

Pour rendre visible le type enum dans un contexte QML, on doit :
  1. hériter de la classe QObject.
  2. renseigner la macro Q_ENUMS(...) avec le nom du type énuméré. Cette macro va permettre d'enrichir les informations du meta-object.
  3. ne pas oublier d'appeler la macro Q_OBJECT, nécessaire à la génération des méta-données.
class State : public QObject
{
    Q_OBJECT
    Q_ENUMS(StateType)
public:
    enum StateType {On, Off};
};

Enregistrer le Type Enum dans le contexte QML

Avant de créer la QQuickView qui va interpréter le QML, chaque type énuméré doit être enregistré à l'aide de la fonction template qmlRegisterType.

int qmlRegisterType<T>(const char * uri,
                int versionMajor,
                int versionMinor,
                const char * qmlName)

  • T : le template est renseigné avec le type énuméré que l'on souhaite exporter dans le QML.
  • uri : représente le nom de la librairie que l'on va importer dans l'entête du fichier QML.
  • versionMajor, versionMinor : assignent une version à la librairie importée, de façon à maintenir l'évolution de la librairie
  • qmlName : le dernier paramètre, c'est le nom du type enum dans le contexte QML. Généralement, et pour simplifier, c'est le même nom que le type enum définit en C++.

main.cpp : enregistrement des enums à destination du contexte QML
int main(int argc, char *argv[])
{
    QApplication *app = new QApplication(argc, argv);

    qmlRegisterType<FanSize>("FanImport", 1, 0, "FanSize");
    qmlRegisterType<FanSpeed>("FanImport", 1, 0, "FanSpeed");
    qmlRegisterType<FanState>("FanImport", 1, 0, "FanState");

    QQuickView *view = new QQuickView();
    view->setSource(QUrl("qrc:///qml/Fans/main.qml"));
    view->show();

    return app->exec();
}

Utiliser les enum dans le QML

Pour accéder aux types énumérés, il faut d'abord importer la librairie grâce à la commande "import" au début du fichier. Il faudra également préciser la version que l'on souhaite utiliser.
import FanImport 1.0

A partir de maintenant, la complétion devient disponible dans l'éditeur QML de QtCreator. C'est une aide précieuse qui nous rappelle à tout moment quels sont les types enum disponibles. En QML, accéder à une des valeurs de l'enum ne se fait pas comme en C++, il faut utiliser la notation pointée.

property int speed : FanSpeed.Medium
property int size : FanSize.Medium
property int state : FanState.Off

Attention: contrairement au C++, en QML il n'y a pas de vérification de la cohérence des types énumérés. L'interpréteur QML considère que c'est une variable de type entière (int) et il est très facile d'écrire du code faux d'un point de vue sémantique, mais vrai d'un point de vue syntaxique :

fan.size = FanState.Off;


Echanger des enum entre le C++ et le QML

Accéder à un enum depuis une Q_PROPERTY

Quand un objet C++ est exposé dans le contexte QML, les propriétés marquées avec la macro Q_PROPERTY sont accessibles comme si c'était des données membres. Certaines de ces propriétés peuvent être de type enum.

Class Fan : cet objet expose la propriété speed de façon à ce que l'on puisse la lire, la modifier et être informé à tout moment d'un changement de valeur.

class Fan : public QObject
{
    Q_OBJECT
    Q_PROPERTY(FanSpeed::Speed speed READ speed WRITE setSpeed NOTIFY speedChanged)
 
public:
    explicit Fan(QObject *parent = 0) : QObject(parent), m_speed(FanSpeed::Medium) {} 

    FanSpeed::Speed speed() const { return m_speed; }

    void setSpeed(FanSpeed::Speed value)  
    {
        if (m_speed != value) {
            m_speed = value;
            emit speedChanged(value);
        }
    }

signals:
    void speedChanged(FanSpeed::Speed arg);

private:
    FanSpeed::Speed m_speed;
};

Une classe Fan doit être instanciée en C++, puis injectée dans le contexte QML grâce à la commande setContextProperty.

Fan MyFan;
QQuickView *view = new QQuickView();
view->rootContext()->setContextProperty("MyFan",&MyFan);

Coté QML, on crée un composant Fan qui va prendre comme paramètre la propriété speed de la classe C++ injectée dans le contexte. Ensuite, au niveau du composant slider, on change la valeur de cet objet, ce qui aura pour conséquence d'accélérer ou de réduire la vitesse du ventilateur.

import QtQuick 2.1
import QtQuick.Controls 1.1
import FanImport 1.0
 
Rectangle {
    width: 200; height: 200
    Fan { speed : MyFan.speed }
    Slider {
        id: slider
        anchors.bottom: parent.bottom
        anchors.left: parent.left
        minimumValue: FanSpeed.Slow
        maximumValue: FanSpeed.Fast
        value: FanSpeed.Medium
        onValueChanged:  MyFan.speed = slider.value
    }
}

Appeler une méthode C++ avec un paramètre enum depuis le QML

Pour appeler une méthode C++ depuis le QML, celle-ci doit être précédée du mot clé Q_INVOKABLE.

    Q_INVOKABLE void setEcoSpeed(FanSpeed::Speed value) ;
Depuis le QML, la méthode est appelée très simplement en passant le paramètre souhaitée.
    Fan { speed : MyFan.speed }

    Button {
        anchors.bottom: parent.bottom
        anchors.right: parent.right
        height: 20
        text: "Set Eco Speed"
        onClicked: {
            MyFan.setEcoSpeed(MyFan.speed);
        }
    }

Dans l'état, l'appel à la méthode ne fonctionnera pas et l'erreur suivante sera retournée au moment du runtime :
qrc:///qml/Fans/main.qml:23: Error: Unknown method parameter type: FanSpeed::Speed

L'interpréteur QML ne reconnaissant pas ce nouveau type préfixé par le nom de sa classe d'origine, il faut forcer explicitement sa reconnaissance en appelant la fonction qRegisterMetaType.

Dans l'exemple, il faut donc rajouter en C++ l'appel à cette méthode :
    qRegisterMetaType<FanSpeed::Speed>("FanSpeed::Speed");

Attention : du fait de la non vérification du typage en QML, il est possible de transmettre une valeur qui ne correspond à aucune valeur connue. Dans ce cas, il faut être prudent coté C++, et veiller à vérifier la valeur de l'enum transmise avant de le traiter.
onClicked: {
    MyFan.setEcoSpeed(17); //cette valeur n'existe pas!
}

Instancier un type enum directement depuis le QML

La méthode qmlRegisterType autorise l'instanciation de l'objet enregistré directement en QML. C'est pratique quand la classe propose des méthodes préfixées comme étant Q_INVOKABLE (c'est à dire qu'on peut les appeler directement depuis le QML).

Dans l'exemple ci dessous, la méthode toQString convertit la valeur de l'enum en chaîne de caractères. Elle est définit en tant que Q_INVOKABLE dans le code de l'enum FanSpeed
class FanSpeed : public QObject
{
    Q_OBJECT
    Q_ENUMS(Speed)
public:
    enum Speed {Slow, Medium, Fast};

    Q_INVOKABLE QString toQString(FanSpeed::Speed speed) const  
    {
        switch (speed) {
        case Slow: return "Slow";
        case Medium: return "Medium";
        case Fast: return "Fast";
        }
        return "";
    }
};

Pour appeler cette méthode, un composant QML doit être instancié au préalable avec un identifiant unique définit dans la propriété id. Ensuite, il suffit d'appeler la méthode toQString en faisant référence à l'id de ce composant.

FanSpeed {
    id: fanSpeed
}
Text {
    text: fanSpeed.toQString(MyFan.speed)
}

Fin de l'article.


Aucun commentaire:

Enregistrer un commentaire