Show me the way to the next Qt bar

Il était une fois le QML

L’écosystème Qt est littéralement énorme, c’est une galaxie ou on peut virtuellement tout faire, ou presque. Du desktop, du mobile, de l’embarqué et même du web ! Bref Qt est partout.

Bulma dans DBZ Abridged : It's everywhere !!

Par contre niveau interface on a un choix à faire. Soit on tape dans du Widget, à l’ancienne, et donc dans du natif, soit on tâte du QML.
La méthode varie radicalement, dans le second cas on a un langage de programmation déclaratif associé à du code en JS ou en C++.

Mais t'avais dit qu'on ferait du natif !

Là déjà normalement ça fait grincer les dents des puristes.

L’expérience rappel indéniablement le développement Web, on définit l’interface comme on écrirait un JSON et on peut décorer tout ça un peu comme on le ferait en CSS. On pourrait penser que le résultat devrait être lourd ou peu performant mais tout ce bousin peut se compiler en un bytecode assez efficace (via le Qt Quick Compiler).

Les performances ne sont pas dégueux et le rendu très rapide et stable. Et si ça ne suffisait pas tonton Trolltech a mentionné un futur compilateur QML / JS -> C++ pour Qt 6.X. Bon on est à la 6.2 et on en a pas encore vu la couleur mais l’espoir fait vivre !

Alors, non, on aura pas de composants natifs. Mais déjà un composant natif, ça s’imite, et puis de nos jours qui se formalise d’une application qui ne suis pas le thème graphique ou les guidelines du système ?

À l’heure ou on mange de l’electron à toutes les sauces et ou même en natif on n’a pas un framework unique (coucou Windows) ? On s’en fout dans une certaine mesure, tant que ça marche et que c’est propre. Discord en est le parfait exemple.

L'essentiel est dans Lactel

Bon après cette introduction mi-figue mi-raisin voyons un peu de quoi on parle, avec un exemple simple :

import QtQuick 2.3

Rectangle {
    width: 200
    height: 100
    color: "red"

    Text {
        anchors.centerIn: parent
        text: "Hello, World!"
    }
}

Je vais partir du principe que vous ne connaissez pas le QML et faire de mon mieux pour vous le présenter et vous faire comprendre pourquoi c’est le bien.

Enfonçons les portes ouvertes, on notera l’import en début de fichier de QtQuick c’est la bibliothèque de composants basiques et l’API de base, ensuite je ne veux pas avoir l’air de minimiser mais le reste est assez limpide. On crée un rectangle, de 200 pixels par 100, de couleur rouge, et à l’intérieur on place un texte, centré grâce à une ancre et qui adresse une salutation au monde entier (rien que ça!).

Normalement deux choses sautent aux yeux, premièrement…

DIEU QUE C'EST BOH!1!

Naan mais vraiment, c'est propre, explicite, élégant, ça sent bon !

De nos jours on est de moins en moins étonné par l’approche déclarative, mais le QML est sorti des cartons en 2009. Il y a 12 ans… Dans un second temps, on pourra éventuellement se demander si ce composant Rectangle n'impliquerait pas une méthodologie trop simpliste et laborieuse. Après tout j'ai rarement eu à utiliser un composant aussi simple pour faire mon IHM (sauf en Web ou en Canvas). Ce qui n'est pas fondamentalement faux, le concept est de partir de briques simples et de bâtir toute une hiérarchie de composants pour arriver à des choses aussi complexes que les TableView ou du rendu 3D (parce que oui on peut faire du fucking rendu 3D, et cette démo a 10 ans, on est plus sur ça aujourd’hui). C’est à la fois un avantage et un inconvénient, parfois quand on ne trouve pas de composant tout fait pour ce qu’on veut ça peut devenir un vrai parcours du combattant, mais j’apprécie cette approche soviétique.

Bon, on a à peine gratté la surface. L'exemple est bien moche, c'est pas du natif, on ajoute la perte de performance et la nécessité de se frapper l’implémentation de certains composants et ça parait moins sexy. Mais ce serait faiblesse infâme que de s'arrêter là.

Déjà, on l'a dit, la performance n'est plus un facteur prioritaire depuis un bon moment. En plus, dans les faits on est pas mal ! Le rendu est accéléré, précompilé et on se retrouve avec des apps très fluide. Niveau poids on est aussi assez bas (quelque KB pour une petite app, auquel s'ajoute environ +- 40MB si on emballe Qt avec). Faut compter aussi qu'on gagne en temps de développement par rapport à l’approche Widget/C++. C'est pas négligeable non plus ! Et pour les parties critique il y a toujours moyen de faire le boulot avec ledit C++.

Bref, on est loin de la perfection, mais ça fait le café. Et surtout c'est chouette.

Teal'c qui boit une pleine cafetière d'un café très chaud et O'neill faisant une blague - SG1

Tu prends un voleur qui loupe son coup par exemple, et bah, ils lui coupent les doigts

Pour vous illustrer ça j'ai eu envie de refaire la fameuse Todo App en QML.
Et tant qu'à rester simple, on n'utilisera pas de C++, on se limitera à un seul fichier et on essayera autant que possible d'avoir un look natif.

Ouvrons Qt Creator, nouveau projet, c'est parti !

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.12

ApplicationWindow {
    id: app
    visible: true
    title: "Le Qml c'est pas sorcier"
    width : 480
    height: 600

    ColumnLayout {
        id: mainLayout
        anchors.fill: parent
        anchors.margins: 10

        ListView {
            id: list
            Layout.fillWidth: true
            Layout.fillHeight: true
            spacing: 5
            clip: true
        }

        RowLayout {
            Layout.fillWidth: true

            TextField {
                id: textField
                Layout.fillWidth: true
                placeholderText: "Inscrire une tache..."
                horizontalAlignment: TextEdit.AlignHCenter
                Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
            }

            Button {
                id: button
                text: "Todo !"
                Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
            }
        }
    }
}

On a déjà la structure ! Ça donne ça :

Un exemple avec les couleurs par défaut

Je préfère de loin l'usage des Layouts aux Anchors, parce que j'ai tendance à écrire moins de code pour arranger mes éléments, mais c'est subjectif.
Disons ça comme ça.
Normalement à la vue de Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter il y a des développeurs Web qui versent une larme.
Oui c'est aussi simple que ça de centrer un élément. Avec les ancres c'est même plus simple encore : anchors.centerIn: parent.

Par contre c'est pas super natif comme look, c'est le thème par défaut il faut dire, ça s'arrange facilement en ajoutant un fichier qtquickcontrols2.conf au projet avec ça dedans :

[Controls]
Style=System

Et qui donne :

Un exemple avec les couleurs du système

Voilà là ça colle déjà beaucoup mieux à mon thème système (dans mon cas il utilise la palette de couleur définie par KDE).
Sous Windows ou macOS le rendu sera différent, mais utilisera les mêmes composants, seule la couleur changera.
Pour avoir un rendu encore plus proche du natif sur ces systèmes il faudra au moins Qt6 qui propose des thèmes supplémentaires.

Tonight we dine in hell

Continuons, on va utiliser un modèle pour "nourrir" notre ListView, je vais intentionnelement le laisser vide, il sera défini lors de l'ajout du premier élément. On va aussi définir l'apparence des delegates, ou l'apparence des éléments de la liste. QtQuick fait en effet un usage assez poussé du Model View Delegate.

ListModel {
    id: model
}

...

ListView {
    id: list
    anchors.fill: parent
    spacing: 5

    model: model
    delegate: Rectangle {
        width: list.width; height: 70

        RowLayout {
            anchors.fill: parent
            anchors.margins: 10

            CheckBox { }

            ColumnLayout {
                Text { text: '<b>Tache :</b> ' + title; }
                Text { text: '<b>Status :</b> ' + (status ? "Terminé" : "En cours"); }
            }
        }
    }
}

Analysons un peu tout ça, pour le modèle j'utilise un ListModel, ça va me permettre de stocker les éléments de ma liste tout en évitant d'écrire ce modèle en C++. C'est moins performant et plus limité mais c'est pour l'exemple et pour garder le code simple.

J'ai utilisé deux mots, title et status dans mon delegate, ce sont les champs de mon modèle J'aurai pu les définir explicitement dans mon modèle, mais j'avais pas envie. Je suis une grosse feignasse c'est comme ça. Et puis c'est du code en moins et ça, ça n'a pas de prix.

Bien maintenant que j'ai une UI capable d'afficher mon modèle on va le peupler ! Je veux que sur l'appuie du bouton ou bien sur un appuie de la touche entrée sur le textfield on ajoute un élément, c'est assez simple, aussi simple qu'un onClick :

TextField {
    id: textField
    ...
    onAccepted: button.clicked()
}

Button {
    id: button
    text: "Todo !"
    onClicked: if(textField.text !== "") { addModel(textField.text, false); textField.text = ""; }
    Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
function addModel(title, status) {
    model.append({"title": title, "status": status})
    list.positionViewAtEnd()
}

function removeModel(index) {
    model.remove(index, 1)
    list.hoveredIndex = -1
}

function setStatus(index, status) {
    model.set(index, {"title": model.get(index).title, "status": status})
}

Je vous l'avais dit, on se prend pas la tête. Dans la foulée j'ai aussi ajouté de quoi supprimer et éditer une entrée, on reste minimaliste.
On va être moins feng shui pour intégrer ça au delegate, je vous remets le ListView en entier pour plus de clarté :

ListView {
    id: list
    property int hoveredIndex: -1

    anchors.fill: parent
    spacing: 5

    model: model
    delegate: Rectangle {
        width: list.width; height: 70

        RowLayout {
            anchors.fill: parent
            anchors.margins: 10

            CheckBox {
                checked: status
                onCheckStateChanged: setStatus(index, checked)
            }

            ColumnLayout {
                Text { text: '<b>Tache :</b> ' + title; }
                Text { text: '<b>Status :</b> ' + (status ? "Terminé" : "En cours"); }
            }
        }

        Image {
           id: close
           height: 20
           width: 20
           anchors.verticalCenter: parent.verticalCenter
           x: index === list.hoveredIndex ? parent.width - width - 10 : parent.width + 10

           Behavior on x { NumberAnimation { duration: 150 } }

           source: "https://upload.wikimedia.org/wikipedia/commons/8/8f/Flat_cross_icon.svg"
           fillMode: Image.PreserveAspectFit
           mipmap: true

           MouseArea {
               anchors.fill: parent
               onClicked: removeTodo(index)
           }
        }

        MouseArea {
            propagateComposedEvents: true
            anchors.fill: parent
            hoverEnabled: true
            onHoveredChanged: list.hoveredIndex = index
            onPressAndHold: list.hoveredIndex = index
            onExited: list.hoveredIndex = -1
            onPressed: mouse.accepted = false
        }
    }

    clip: true

    // Animations
    add: Transition {
        NumberAnimation {
            easing {
                type: Easing.OutElastic
                amplitude: 1.0
                period: 0.5
            }
            properties: "y";
            duration: 1000
        }
    }
    removeDisplaced: Transition { NumberAnimation { properties: "y"; duration: 150 } }
    remove: Transition { NumberAnimation { property: "opacity"; to: 0; duration: 150 } }
}

Ça donne ça :

Une animation montrant le résultat des étapes précédentes

Mais Jamy ! Quelle est cette sorcellerie ?!

Examinons un peu comment ça marche, quand on change l'état du CheckBox on appelle setStatus() et le modèle est mis à jour lorsqu’il émet en interne un événement comme dataChanged(), toutes les propriétés en QML fonctionnent comme ça, et tout ça se base sur les QProperty de Qt.

La partie concernant la suppression est un poil plus complexe.

J'ai d'abord ajouté une propriété hoveredIndex à ma liste, qui me sert à savoir au-dessus de quel item on se trouve, je renseigne cette valeur avec un MouseArea. Son utilisation est assez commune, on a accès à différents événements, le onPressed: mouse.accepted = false et le propagateComposedEvents: true permettent de transmettre les événements que traitera pas et qui seraient sinon "consommés" par le MouseArea. Ça va permettre de continuer utiliser le défilement de la liste à la souris et au doigt. Parce que oui notre app pourra aussi tourner sur mobile.

Quand l'index d'un delegate correspondra à hoveredIndex on affichera le bouton de suppression. J'ai fait ce bouton avec un composant Image et un autre MouseArea pour le onClick. Vous noterez que j'ai simplement renseigné l'URL d'un SVG en source pour mon composant. C'est possible en QML et ça me permet de ne pas surcharger l'exemple avec des ressources supplémentaires. En fait on peut renseigner n'importe qu'elle ressource QML avec une URL, même des composants entiers !

Bref, par défaut sa position est en dehors du ListView, il est donc non affiché grâce à la propriété clip à true qui coupe tout ce qui dépasse du composant, mais quand on passe la souris sur l'item ou qu'on le maintient sur mobile on révèle l'image. J'aurai bien sur pu simplement utiliser la variable visible mais cette façon de cacher l'élément nous permet d'ajouter une petite animation sur l'axe x, histoire de frimer. On peut d'ailleurs animer beaucoup de choses en QML et ce facilement. J'ai également ajouté des animations sur les événements add, removeDisplaced et remove de la liste.

Il y a des choses qui ne veulent juste pas mourir

Bon, déjà ce moment on a une application fonctionnelle, on pourrait presque l'utiliser en vrai !
Mais pour ça ce serait bien que le modèle soit persistant ! Qu'à cela ne tienne, ça peut se faire très rapidement avec le LocalStorage (je vous avais dis que ça ressemblait quand même pas mal à du développement web) !

C'est un objet qui permet d'utiliser une base sqlite stockée en local :

property var db

function addDB(title, status) {
    db.transaction( function(tx) {
        var rs = tx.executeSql('INSERT INTO Todo VALUES(?, ?)', [ title, status ]);
        addModel(rs.insertId, title, status)
    })
}

function editDB(id, title, status) {
    db.transaction( function(tx) {
        tx.executeSql('UPDATE Todo SET title = ?, status = ? WHERE rowid = ?', [ title, status, id ]);
    })
}

function removeDB(id) {
    db.transaction( function(tx) {
        tx.executeSql('DELETE FROM Todo WHERE rowid = ?', id);
    })
}

function initDB() {
    db = LocalStorage.openDatabaseSync("QMLTodoExampleDB", "1.0", "The Example QML SQL!", 1000000);
    db.transaction(
        function(tx) {
            tx.executeSql('CREATE TABLE IF NOT EXISTS Todo(title TEXT, status BOOL)');
            var rs = tx.executeSql('SELECT rowid, * FROM Todo');
            for (var i = 0; i < rs.rows.length; i++) {
                addModel(rs.rows.item(i).rowid, rs.rows.item(i).title, rs.rows.item(i).status)
            }
        }
    )
}

La méthode initDB va vérifier qu'une table Todo existe, sinon elle va la créer, et récupérera son contenu pour l'ajouter au modèle. Cette méthode sera appelée au lancement de l'application avec un Component.onCompleted: initDB().

On modifiera addModel et setStatus pour tenir compte du fait que les booléens sont traités comme des entiers par la base sqlite et on ajoutera des méthodes addTodo et removeTodo pour appeler plus commodément les méthodes du modèle et de la base de donnée :

function addTodo(title, status) {
    addDB(title, status)
}

function removeTodo(index) {
    removeDB(model.get(index).id)
    removeModel(index)
}

function setStatus(index, status) {
    model.set(index, {"id": model.get(index).id, "title": model.get(index).title, "status": status ? 1 : 0})
    editDB(model.get(index).id, model.get(index).title, model.get(index).status)
}

function addModel(id, title, status) {
    model.append({"id": Number(id), "title": title, "status": status ? 1 : 0})
    list.positionViewAtEnd()
}

function removeModel(index) {
    model.remove(index, 1)
    list.hoveredIndex = -1
}

Et c'est tout.

Ça suffit à gérer nos données ! Alors oui, ça reste très basique mais si on a besoin de plus on pourra se tourner vers le C++ pour faire tout ce que l'on veut ! Ça convient d'ailleurs plus pour du prototypage à mon avis, mais bon ça marche !

Le mot de la fin

Le code est sur Github.

J'ai fait quelques ajouts esthétiques, notamment j'utilise un composant SystemPalette pour obtenir des couleurs de fond ou pour certains textes qui ne suivent pas le thème par défaut.

SystemPalette {
    id: myPalette
    colorGroup: SystemPalette.Active
}

Text {
    anchors.centerIn: parent
    color: myPalette.text
    text: "La liste de tache est vide, ajoutez en une !"
}

Vous l'aurez remarqué dans l'animation plus haut, j'ai aussi ajouté de la transparence à ma fenêtre, parce que j'aime bien ça.
En tout cas chez moi avec la composition et le flou activé ça rend bien !

Le TextArea a subit quelques modifications pour lui permettre de sélectionner du texte et je lui ai ajouté un menu au clic droit, en effet le composant n'a pas de menu par défaut...

Scully se demandant si tout ça est bien sérieux

C'est le cas de nombreux composants en QML. On peut s'en étonner mais c'est par design soi-disant. Dans tous les cas c'est assez simple à contourner même si c'est dommage je trouve !

En conclusion j'espère que j'ai pu vous donner envie de faire du QML, c'est une technologie qui a ces limites et ces inconvénients, mais je crois qu'elle a quelques avantages qui méritent qu'on s'y attarde.

On a pas écrit beaucoup de code (vos petits doigts boudinés sont intacts) et pourtant on a une application assez sympa et qui fonctionne ! J'ai connu des technos plus avares en récompense !

Et si j'avais un système de commentaire vous pourriez me dire en quoi je me trompe et pourquoi cet article est un ramassis de connerie, mais j'ai pas encore eu le temps / l'envie de m'y coller alors dans le cul lulu. La prochaine fois !

Edit 10/07/2022 : En théorie, si rien ne foire, vous pouvez créer un compte et poster des commentaires !