I. De quoi s’agit-il ?▲
Je propose ci-dessous les éléments recueillis lors de l'utilisation de l'outil graphique de conception des machines d'états SCXML dans Qt Creator. J’en suis venu à envisager l’utilisation de machines d’états lors d’un développement.
Après avoir passé un certain temps à comprendre le bout de framework utile, j’ai rédigé ceci afin d’en garder une trace. Car la documentation susceptible de me mettre le pied à l’étrier concernant l’outil de conception graphique de Qt Creator est plutôt rare.
Concernant la mise en œuvre du framework QStateMachine, il y a bien sûr les cours de Daniel Géron :
- Cours sur le framework QStateMachine ;
- Cours sur les états parallèles avec le framework QStateMachine ;
- Cours sur l’utilisation de QHistoryState du framework QStateMachine.
J’ai voulu creuser du côté de SCXML pour profiter de l’outil de conception graphique et ainsi me décoller du code. Cet outil suppose l’utilisation du framework QScxmlStateMachine. Je suis classiquement parti de la documentation.
Je n’aborde ici que quelques parties du framework, celles qui m’ont été utiles.
Enfin, j’ai rédigé pour « les pauvres p’tits gars comme moi », développeurs du dimanche, qu’il faut guider pas à pas.
II. Notes de versions logicielles▲
Ce qui suit a été réalisé sous Debian 12 Bookworm avec Qt Creator 9.0.2, Qt 6.4.2 et qmake6.
J’ai appris l’abandon prochain de qmake et son remplacement par CMake lors de l’écriture de ces lignes (merci Thibaut !). Une mise à jour sera donc nécessaire avec la mise en œuvre de ce qui suit avec CMake, pour lequel le lecteur pourra se référer à la documentation Qt.
III. Préambule▲
Le sigle SCXMLState Chart XML désigne un langage de description de machines d’états basé sur XML et défini en 2015 par un standard du W3C. Qt offre une implémentation de ce langage au travers de la classe QScxmlStateMachine. Qt Creator intègre un outil de conception graphique de machines d’états, lesquelles sont sauvegardées sous la forme d’un fichier « .scxml ». Ce fichier peut soit :
- être compilé par l’outil qscxmlc (cf. ci-après) qui génère alors les fichiers « .h » et « .cpp », qui seront intégrés au projet de façon transparente ;
- soit lu dynamiquement par un objet QScxmlStateMachine au sein du programme, ce qui offre une souplesse supplémentaire pour adapter à la volée une machine d’états.
Pour activer la prise en compte de SCXML dans Qt Creator, il faut activer le plugin idoine via le menu « Aide/À propos des outils ».
L’outil de conception graphique de Qt Creator n’est pas opérationnel de suite. Il lui faut la classe QScxmlStateMachine qui n’est pas incluse dans Qt Core.
Sous Debian, les paquets suivants sont nécessaires (hors QML) :
- qt6-scxml-dev ;
- libqt6scxml6 ;
- libqt6scxml6-bin ;
- libqt6statemachine6.
IV. Ajout d’une machine d’états à un projet▲
Pour inclure des machines d’états dans un projet, il faut ajouter la première ligne ci-dessous au « .pro » du projet afin de charger le module de Qt correspondant :
Pour les utilisateurs de CMake, voir le point d’information du § II ci-dessous.
La première ligne est à ajouter à la main. La rubrique « STATECHARTS » est complétée par Qt Creator lors de l’ajout des machines d’états au projet. Ici, quatre machines d’états sont définies. Les deux dernières sont présentées dans les chapitres suivants.
L’ajout d’une machine d’états dans Qt Creator se fait via un clic droit sur l’icône du projet (fenêtre Projets). Sélectionner ensuite « Ajouter Nouveau... ». Dans la fenêtre qui s’ouvre, choisir « Modeling » puis « State Chart ». Poursuivre comme pour l’ajout d’une classe au projet. Le nom donné à la machine d’états sera le nom de la classe héritée de QScxmlStateMachine.
La machine d’états ajoutée apparaîtra dans la section « State Charts » de l’arborescence du projet. La sélection de la nouvelle machine d’états ouvre l’éditeur permettant de concevoir graphiquement une machine d’états.
Avec l’installation décrite plus haut, quand la machine d’états est définie ou modifiée, je dois soit lancer l’outil externe qscxmlc (ajouté aux outils externes dans le menu Qt Creator « Outils/Externes... ») pour générer les fichiers sources, soit compiler deux fois le projet. Pour plus de facilité, j’ai créé un raccourci clavier vers cet outil (« Alt+S » était disponible pour mon installation).
Ne pas ajouter manuellement ces fichiers sources à l’arborescence du projet, car cela provoquerait des problèmes lors de l’édition de liens (vécu…).
V. Mise en œuvre d’une machine d’états dans Qt Creator▲
V-A. Un exemple simple▲
La capture d’écran suivante montre la machine d’états « SM_SauvegardeBase.scxml ». Elle permet de suivre l’état de sauvegarde d’une base de données.
J’ai défini quatre états :
-
« Base_Non_Modifiee » qui contient deux autres états :
- « Base_vide » quand la base de données est vide,
- « Base_sauvee » : état de la base lorsqu’elle a été sauvée ;
- « Base_Modifiee » lorsque la base de données a été modifiée sans avoir été sauvegardée.
Les flèches entre ces états décrivent des transitions qui correspondent aux actions de l’utilisateur. En particulier la transition « Ajouter_retirer_lire_donneurs » permet de passer de l’état global « Base_Non_Modifiee » à l’état « Base_Modifiee », quel que soit l’état antérieur « Base_vide » ou « Base_sauvee ».
À gauche du schéma, le cercle plein correspond au départ de la machine d’états. Ce point peut se retrouver uniquement dans un état comme ici dans « Base_Vide ».
À droite du schéma, l’outil permet d’accéder aux constituants de la machine d’états et de définir leurs attributs (cf. capture d’écran ci-dessous). Dans cette fenêtre, un clic gauche sur l’état « Base_sauvee » permet de définir des actions lors de l’entrée dans l’état (balise <onentry>). Ici, j’ai décidé de lancer un évènement (balise <send>) lors de l’entrée dans l’état « Base_Sauvee ».
En partie basse, nous trouvons les attributs de l’action <send>. J’ai précisé le nom de l’évènement (« baseSauvee ») qui est généré lors de l’entrée dans l’état « Base_Sauvee ». Nous le verrons plus bas, cet évènement correspond à un signal qu’il est possible de gérer dans le code de l’application.
Il est également possible de définir un <send> lors de la sortie d’un état ou lors d’une transition. Il est ainsi possible d’exécuter du code en fonction de l’état de la machine.
Après compilation, la machine d’états est désormais disponible dans le projet sous la forme d’une classe héritée de QScxmlStateMachine. Le nom de cette classe correspond à celui donné à la machine d’états, ici SM_SauvegardeBase.
L’objet est instancié dans une MainWindow comme un membre, sous forme de pointeur ou non :
2.
3.
4.
5.
6.
class
MainWindow : public
QMainWindow
{
Q_OBJECT
private
:
SM_SauvegardeBase *
sm_etat_base_donneur;
…
2.
3.
4.
5.
6.
7.
8.
9.
MainWindow::
MainWindow(QTranslator *
transl, QWidget *
parent)
:
QMainWindow(parent)
, ui(new
Ui::
MainWindow)
{
ui→setupUi(this
);
...
sm_etat_base_donneur =
new
SM_SauvegardeBase();
lancer_machines_etats();
}
Enfin, il reste à « câbler » les états et les évènements de la machine d’états à des méthodes. C’est le rôle de ma méthode lancer_machine_etats().
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
void
MainWindow::
lancer_machines_etats()
{
sm_etat_base_donneur->
connectToState("Base_vide"
,[this
](bool
active){
methodeOfThisN1(); }
);
sm_etat_base_donneur->
connectToState("Base_non_vide"
,[this
](bool
active){
methodeOfThisN2(); }
);
sm_etat_base_donneur->
connectToState("Base_Modifiee"
,[this
](bool
active){
methodeOfThisN3(); }
);
sm_etat_base_donneur->
connectToState("Base_Non_Modifiee"
,[this
](bool
active){
methodeOfThisN4(); }
);
sm_etat_base_donneur->
connectToEvent("baseSauvee"
,this
, &
MainWindow::
mySlot );
sm_etat_base_donneur→start(); // Lance la machine d’états
}
La méthode connectToState() permet de relier un état de la machine à une fonction lambda qui sera exécutée soit inconditionnellement, soit à l’entrée de l’état (active = true) ou à la sortie de l’état (active = false). Dans ce listing, les noms entre les guillemets correspondent aux noms des états.
En cette phase d’apprentissage des machines d’états, cela m’a permis de suivre les changements d’état. Nous verrons plus bas qu’il y avait beaucoup plus simple à faire (cf. § VIII).
La méthode connectToEvent() de la ligne 8 permet de lier un évènement de la machine d’états à une méthode, ici MySlot(), définie ainsi :
Le paramètre (const QScxmlEvent &event) est obligatoire.
2.
3.
4.
5.
void
MainWindow::
mySlot(const
QScxmlEvent &
event)
{
majUI();
boiteMessage->
coucou("test event : "
+
event.name());
}
Pour finir, la machine d’états est lancée avec la méthode start().
Le passage d’un état à un autre se fait en déclenchant les transitions grâce à la méthode submitEvent(). Le nom des transitions est entre guillemets :
2.
3.
4.
5.
6.
7.
8.
9.
10.
void
MainWindow::
import
_fichier_donneurs()
{
if
(donneurs.get_nb_donneurs() >
0
) {
sm_etat_base_donneur->
submitEvent("Ajouter_lire_donneurs"
);
}
else
{
sm_etat_base_donneur->
submitEvent("Lire_donneurs"
);
}
...
}
Le changement d’état n’est effectif qu’après le traitement des évènements de la boucle des évènements de l’application.
Aussi, le traitement des évènements est réalisé après l’exécution du code utilisateur et avant de redonner la main à l’utilisateur. (cf. § VI « Gestion des évènements »).
Par la suite, j’interroge la machine d’états pour savoir si la base de données nécessite une sauvegarde avant de quitter l’application. Ici, je teste l’activation de l’état « Base_Modifiee » avec la méthode isActive() :
V-B. Ajout d’une temporisation à un évènement▲
Dans la machine d’états ci-dessus, l’état « Base_sauvee » succède à l’état « Base_Modifiee » par la transition « Sauvegarder_base ». L’activation de cette transition envoie un évènement « baseSauvee ».
Il est possible de définir un délai à l’envoi de cet évènement en renseignant « delay » dans les attributs de <send>. Pour définir cinq secondes, on tape « 5s » :
V-C. Utilisation de <send> et de <raise>▲
Dans divers cas, comme l’activation d’une transition ou l’entrée dans un état, il est possible d’envoyer des évènements. Ceux-ci peuvent être des évènements internes ou externes à la machine d’états.
L’action <send> permet d’envoyer des évènements au programme hôte, lesquels sont traités par la méthode connectToEvent() comme nous l’avons vu plus haut.
L’action <raise> permet de déclencher des transitions de la machine d’états (cf. § VII « Le modèle de données »).
VI. Gestions des évènements▲
Après l’appel de la méthode submitEvent(), le changement d’état n’a pas lieu immédiatement. Aussi, si j’ordonne le passage de l’état E1 à l’état E2, et si juste après je conditionne l’exécution d’une partie de code C à l’activation de l’état E2, alors C ne sera pas exécuté.
Une solution consiste à appeler la méthode ci-dessous après chaque submitEvent(), mais elle est déconseillée par la documentation Qt.
La solution, qui suit la logique de programmation par évènements, consiste à exécuter C lors de l’entrée dans l’état E2. (Merci le forum Qt).
VII. Allons plus loin : le modèle de données▲
Au travers d’un petit exemple, je vais évoquer le modèle de données d’une machine d’états.
VII-A. Le besoin▲
Mon application permet de filtrer une base de données, c’est-à-dire extraire uniquement les enregistrements qui correspondent à certains critères.
Mon application compte sept filtres. Je peux passer de l’un à l’autre directement. Je souhaite également adapter visuellement le style du filtre activé et remettre le filtre précédent dans un style normal.
Lorsque j’active un filtre, je me connecte classiquement à un slot qui va réaliser ce filtre et adapter visuellement le filtre. Il reste à remettre le filtre précédent dans un style normal. Or, ne sachant pas quel était le filtre précédent, je suis obligé de remettre tous les autres filtres dans un style normal, en excluant le filtre sélectionné. C’est lourd et pénible.
D’où l’idée d’utiliser une machine d’états.
VII-B. Mise en œuvre▲
Il faut définir sept états, dont un initial. Chaque état devrait alors compter six transitions vers les autres états. Donc, il faudrait définir quarante-deux transitions. C’est possible, mais pénible. D’autant que, s’il faut ajouter un filtre, ce n’est plus drôle du tout.
J’ai donc étudié la solution suivante :
Nous retrouvons nos sept états « filtre » en périphérie, un état « pivot » au centre et il n’y a plus que quatorze transitions. Les sept transitions qui partent des états « filtre » portent un nom identique : « Changer_filtre ». L’état pivot est chargé d’activer la transition vers l’état filtre cible. Il faut donc que la transition « Changer_filtre » porte le nom de l’état cible.
La norme SCXML permet d’inclure un modèle de données unique à une machine d’états, grâce auquel il est possible de définir un certain nombre de variables. Ce modèle de données ne peut pas être partagé avec une autre machine d’états. C’est cette capacité que je vais exploiter pour transmettre la valeur de l’état cible.
Pour cela, j’ai dû définir le type modèle de données utilisé par la machine d’états. À la racine des constituants de la machine d’états <scxml>, pour répondre à mon besoin, j’ai défini le type de modèle de données « ecmascript » (de la norme ECMAScript). C’est lui que je vais utiliser pour faire porter à mes transitions « Changer_filtre » l’information du filtre destinataire.
La norme SCXML précise qu’il existe d’autres modèles de données. Qt implémente également au moins NullDataModel et CppDataModel. Voir la référence au § IX.E.
Ensuite, il faut utiliser une autre forme de la méthode submitEvent() vue plus haut, celle qui permet de transmettre une valeur au modèle de données de la machine d’états :
Cette valeur va permettre de lancer la bonne transition dans l’état pivot. Pour cela, retour dans l’outil State Machine de Qt Creator :
Commençons par la flèche rouge du haut. Nous retrouvons notre état pivot. Juste en dessous, nous trouvons les sept transitions qui vont du pivot vers chaque état filtre. La flèche en dessous précise qu’il va se passer des choses à l’entrée dans l’état pivot. Il s’agit d’une série de <if>, <elseif>, <else> qui vont permettre de tester la valeur retournée par « _event.data », définie dans le modèle de données ECMAScript. C’est cette variable qui va contenir la valeur transmise comme second argument de submitEvent(). Les deux dernières flèches montrent le test de cette valeur avec la valeur transmise par la ligne de code plus haut.
Dans le test, notez l’encadrement de la rvalue avec de simples quotes. Je pense que sans ces simples quotes, la rvalue est considérée comme une variable du modèle de données.
Juste après le <elseif> précédent, en cas de succès du test, un <raise> permet d’activer la bonne transition :

VII-C. Utilisation▲
Le code suivant montre que je souhaite activer le filtre sur un formulaire en cliquant sur un bouton radio. Le code est le suivant :
2.
3.
void
on_radioButton_filtre_formulaire_clicked() {
sm_etat_filtre.submitEvent("Changer_filtre"
,"formulaire"
);
}
J’active la transition « Changer_filtre » connue par tous les filtres et qui aura pour effet d’activer l’état pivot. J’ai également précisé en second argument que je veux activer le filtre formulaire, ce qui sera traité par le pivot qui activera alors la transition idoine.
Il me reste à définir pour chaque état filtre les actions à réaliser à leur entrée et à leur sortie (mise en forme), comme vu au § V.A.
VII-D. Et les variables ?▲
Alors, me direz-vous, je n’ai pas utilisé de variable dans le modèle de données de la machine d’états. Dans mon cas, je n’en ai pas eu besoin et cela reste pour moi un thème à creuser. Sachez tout de même que pour définir des variables dans la machine d’états, il faut ajouter un <datamodel> (clic droit sur la racine <scxml>) puis ajouter des <data> dans ce <datamodel> (clic droit). Ici, j’ai défini une variable « filtre » dont il est possible de préciser la source (cf. spécifications du SCXML : standard du W3C) :
VIII. Un petit « log » pour finir ?▲
Je vous ai parlé plus haut, au § V.A, d’une façon de tracer l’activité d’une machine d’états en levant des exceptions par <send> dans la machine d’états, puis en les traitant dans le code avec la méthode connectToEvent().
J’ai découvert qu’il y a beaucoup beaucoup plus simple pour faire cela grâce à la balise <log> du SCXML.
Retournons dans Qt Creator et éditons la machine d’état de mes filtres. Dans l’arborescence <scxml> de ma machine d’états, je fais un clic droit sur la balise <onentry> de mon « Etat_Pivot » et je choisis « log ». Une entrée <log> s’ajoute alors à l’arborescence et je peux préciser deux éléments : un label et une expression.

À l’exécution de l’application, cela aura pour effet de provoquer la sortie suivante lors de l’entrée dans l’état pivot :

Pratique… mais il y a mieux : il est aussi possible de récupérer ces deux paramètres dans le programme en connectant le signal log(QString&, QString&) émis par la machine d’états.
IX. Enseignements▲
IX-A. Pas d’autocomplétion▲
Contrairement à l’écriture de code avec Qt Creator, il n’y a aucune aide à la saisie des noms d’état ou d’évènement. Or, l’édition graphique de la machine d’états masque le code en cours d’écriture. Il est donc commode d’éditer le fichier « .scxml » produit avec un éditeur tiers pour disposer des noms exacts.
Une autre solution consiste à utiliser la fonction de génération d’une image de la machine d’états sous sa forme graphique (clic droit sur le fond).
La moindre erreur de saisie de ces noms provoque logiquement le dysfonctionnement de la machine d’états. Le compilateur et les fonctions de QScxmlStateMachine ne signalent aucun problème en cas d’erreur dans un nom.
IX-B. Méthode de raisonnement▲
Le tracé d’une machine d’états permet de se « décoller » du code et d’appréhender la dynamique de certains pans de l’application. Pour certaines de mes machines d’états, la définition a été incrémentale et cela aurait été moins simple de travailler sur du code.
IX-C. Simplification du code▲
Une fois le mécanisme des machines d’états compris, j’ai pu réaliser ce que je voulais beaucoup plus simplement et lisiblement. Il suffit de provoquer les changements d’état avec submitEvent("transition")et de tester les états actifs avec isActive("etat")pour adapter les actions disponibles à l’utilisateur pour chaque état d’une base de données. Ce serait sans doute moins simple dans d’autres cas et demanderait une utilisation plus poussée des State Machines.
IX-D. Un domaine à explorer▲
Il est certainement possible d’aller beaucoup plus loin avec les machines d’états. Toutefois, hors celle de Qt, je n’ai pas trouvé beaucoup de documentation, notamment en français. Il faut se plonger dans le standard du W3C ou encore dans le tutoriel donné en troisième référence ci-dessous.
IX-E. Autres sources▲
- Document de base du SCXML : standard du W3C. Les exemples fournis à la fin du document m’ont été utiles ;
- Documentation Qt : aperçu de Qt SCXML ;
- SCXML Overview ;
- Page Wikipédia du standard SCXML ;
-
Exposé sur SCXML par les apprentis de l’école Ingénieurs 2000.
Concernant les modèles de données (merci dourouc05) :
- Implémentation Qt.