De QtWidgets à QtQuick, la transition d’une application, partie 1 – L’histoire de deux architectures logicielles

Je travaille avec Qt régulièrement depuis plus de 20 ans et, pendant cette période, j’ai vu QtWidgets se développer, puis QtQuick faire son apparition. Cela signifie que vous disposez désormais de deux stacks graphiques différentes dans Qt.

Bien sûr, elles ont toutes deux leurs avantages et leurs inconvénients, mais si vous disposez d’un code QtWidgets legacy et que vous souhaitez passer à QtQuick, comment procéder au mieux ? Je ne trouvais pas de réponse satisfaisante à cette question dans les discussions publiques, d’où mon intervention lors de la QtWS25.

Aujourd’hui, je lance également cette courte série d’articles afin de proposer un format plus long que les gens peuvent lire à leur rythme. J’espère que cela profitera à l’ensemble de la communauté des utilisateurs de Qt.

Au cœur de la question du « portage de QtWidgets vers QtQuick », il y a une discussion à avoir sur l’architecture logicielle. Il s’avère qu’en tant que développeur polyglotte et tech lead chez enioka Haute Couture, j’ai été confronté à de nombreux contextes différents chez nos clients et nous avons vraiment un faible pour l’architecture logicielle.

Ce premier article présentera mon point de vue sur les motifs d’architecture logicielle utilisés dans les applications Qt.

Cet article est déjà apparu en anglais sur le Qt blog.

Architecture logicielle typique des applications QtWidgets

Voici une manière de visualiser l’architecture logicielle typique des applications QtWidgets :

Discussion

Bien sûr, nous devons imaginer plusieurs classes pour chacun des types décrits dans ce diagramme. Nous montrons ici trois classes en interaction, mais dans notre application, cette structure est probablement répétée à de nombreuses reprises.

Le motif architectural que cela indique est « presque » celui du Model View Controler (MVC). Nous y reviendrons plus tard dans cet article, car cela est important pour notre exploration. Mais d’abord, examinons les différentes parties.

Nous avons tout d’abord les classes de modèle, qui sont de bonnes vieilles sous-classes QObject ou des types valeur. Nous en avons probablement un certain nombre dans notre application. Elles interagissent avec les widgets (contrôleurs) principalement en émettant des signaux et via des appels directs depuis les widgets. Il existe probablement des relations complexes entre les classes de modèle et les classes de widget.

Ensuite, nous avons la partie vue. Il s’agit également de code C++, mais ce code est probablement généré. Dans la plupart des cas, Qt Designer (autonome ou intégré à Qt Creator) est utilisé pour concevoir graphiquement le contenu de la vue. Cela créera un fichier ui utilisé par uic pour générer le code C++ correspondant.

Enfin, nous avons la partie contrôleur. Elle créera sa vue correspondante (sauf dans des cas extrêmement rares où il y a un widget par vue et vice versa). La communication entre le contrôleur et la vue se fait à l’aide de signaux et de slots dans les deux sens. Nous appelons également ces contrôleurs des widgets, car dans les applications basées sur QtWidgets, ces classes héritent également de QWidget.

C’est pourquoi je n’appelle pas cette architecture logicielle MVC, mais « MVC avec un twist ». Dans une application MVC normale, il n’y a pas de couplage aussi fort entre le contrôleur et la vue, ni entre le contrôleur et la bibliothèque graphique.

Code simplifié

Dans le cas du code pour de telles applications, la majeure partie se trouve dans le widget/contrôleur et ressemblera probablement à ceci :

class Widget : public QWidget {
Q_OBJECT
public:
explicit Widget(QWidget *parent = nullptr): ui{std::make_unique<Ui::View>()} {
ui->setupUi(this);
// Probably a few connects between this and ui to slots manipulating m_model
}
void setModel(Model *model) {
// Maybe a few connects from model to slots impacting ui
m_model = model;
}
private:
std::unique_ptr<Ui::View> ui;
Model m_model = nullptr;
};

Si nous supposons l’existence de Model et que Ui::View a été généré par uic. Le code ci-dessus serait le strict minimum pour pouvoir connecter le modèle, la vue et le contrôleur.

Bien sûr, pour que tout fonctionne correctement, nous avons probablement quelque chose comme ce qui suit écrit quelque part :

auto model = new Model(parent); // Or called a function to get a pointer to an instance
auto widget = new Widget(parent);
widget->setModel(model);
widget->show();

L’architecture logicielle typique des applications QtQuick

Qu’en est-il de l’architecture logicielle typique des applications QtQuick ? Voici une représentation visuelle des composants impliqués :

Discussion

Une fois encore, nous devons imaginer plusieurs classes pour chacun des types décrits dans ce diagramme. Dans une application QtQuick classique, ce modèle d’interaction entre les classes se répète à de nombreuses reprises.

Le motif architectural ainsi décrit s’apparente à celui du Model View Presenter (MVP). Examinons les différentes parties d’un tel modèle.

Tout d’abord, nous avons les classes de modèle qui, comme dans notre cas MVC, sont de bonnes vieilles sous-classes QObject ou des types valeur. Elles interagissent avec le proxy (parfois appelé service) principalement en émettant des signaux et via des appels directs depuis le proxy. De ce côté de la communication, cela ressemble à ce que nous avons vu précédemment pour le modèle/contrôleur, et de la même manière, il existe probablement des relations complexes entre les classes de modèle et de proxy.

La partie vue est très différente dans le cas de QtQuick. Au lieu de code C++, nous avons du code QML. Il peut être obtenu via un éditeur graphique, mais il est probablement écrit à la main. Il aura également la responsabilité de créer le proxy ou d’utiliser un proxy préexistant. C’est la responsabilité opposée à celle que nous avons vue dans notre « MVC avec un twist » où le contrôleur créait la vue.

Enfin, nous avons la partie proxy. Elle aura besoin d’un moyen de trouver son modèle correspondant. La communication entre le proxy et la vue se fait à l’aide de property bindings et d’appels de méthodes directs par la vue.

Code simplifié

Du point de vue du code, le proxy ressemblera à ceci :

class Proxy : public QObject {
Q_OBJECT
Q_PROPERTY(QString modelId READ modelId WRITE setModelId NOTIFY modelIdChanged)
Q_PROPERTY(QString value READ value WRITE setValue NOTIFY valueChanged)
QML_ELEMENT
public:
using QObject::QObject
// Getter and setters for the properties above
private:
// Locate or create the model parts we need based on modelId
Model *model() const;
};

Une différence notable par rapport au cas QtWidgets est que notre proxy est cette fois-ci un simple QObject. Il est intégré au runtime QML afin que son interface soit disponible dans ce langage, mais il n’y a pas de couplage fort avec la bibliothèque graphique.

Le code QML pour la vue ressemblera alors à ceci :

import QtQuick 2.0 as QQ
// Assuming Proxy has been registered in the com.enioka.hc.app namespace
import com.enioka.hc.app 1.0 as App
QQ.Item {
App.Proxy {
id: proxy
modelId: "whatWeNeed"
}
QQ.Text {
anchors.centerIn: parent
text: proxy.value
}
}

Cette vue crée le proxy et transmet les informations nécessaires pour trouver le ou les objets modèles pertinents à l’aide de la propriété modelId. L’interface graphique est également liée à la propriété proxy.value pour l’afficher.

Pour obtenir une application fonctionnelle, nous avons maintenant quelque chose comme ce qui suit dans la fonction main :

QQmlApplicationEngine engine;
// Assuming the View.qml has been registered in the com.enioka.hc.app module
engine.loadFromModule("com.enioka.hc.app", "View");

Et maintenant ?

Nous avons confirmé que les applications QtWidgets et QtQuick ont généralement des architectures logicielles différentes. Voyons ce que nous pouvons en conclure.

La première chose qui saute aux yeux est que l’architecture logicielle typique des applications basées sur QtWidgets nuit à la réutilisabilité. Ce « MVC avec un twist » est excessivement adapté au schéma de composition des widgets. D’une certaine manière, c’est une bonne chose, car cela permet de composer rapidement ces widgets dans des interfaces graphiques plus grandes. Malheureusement, cela implique également de capturer une grande partie de la logique métier dans l’interface graphique. Cela ne peut que poser des problèmes lorsque cette logique doit être réutilisée dans un contexte différent… comme le portage vers QtQuick.

Mon parti pris ici est clair, je trouve que le modèle d’architecture logicielle utilisé par les applications QtQuick est supérieur. Il conduit à beaucoup moins de couplage entre la vue et le proxy. Ou du moins, le couplage restant va dans la bonne direction (la vue connaît le proxy, mais pas l’inverse). Ce modèle d’architecture MVP est plus pérenne et permettrait d’avoir différentes interfaces graphiques attachées au même ensemble de proxys.

La situation actuelle des applications QtWidgets est regrettable, mais comment en sommes-nous arrivés là ? Après tout, nous n’étions pas obligés de suivre le modèle « MVC avec un twist ». Nous avons fini par le faire collectivement pour des raisons historiques. Au début, seul QtWidgets existait et nous ne connaissions rien d’autre (je plaide coupable aussi). De plus, la documentation officielle et le tutoriel de QtWidgets ne présentent que des exemples suivant le modèle « MVC avec un twist ». Cela convient pour de petits exemples, mais comme nous l’avons vu, cela ne résistera pas à l’épreuve du temps lorsque l’application deviendra plus importante.

Heureusement, je pense que nous avons un moyen de sortir du statu quo.

Ce qui va suivre…

Dans le prochain article de cette série, nous verrons comment faire passer de manière responsable votre application QtWidgets legacy de l’architecture classique « MVC avec un twist » à une architecture MVP. Nous nous occuperons de préparer tout ce qui est nécessaire avant la transition à proprement parler afin d’éviter autant que possible toute disruption de votre projet.

Réactions dans le fédivers

Publié

dans

par

Étiquettes :

Commentaires

4 réponses à « De QtWidgets à QtQuick, la transition d’une application, partie 1 – L’histoire de deux architectures logicielles »

En savoir plus sur enioka

Abonnez-vous pour poursuivre la lecture et avoir accès à l’ensemble des archives.

Poursuivre la lecture