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.
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.
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 :
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 :
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 :
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...
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 !
-