Cours sur l’utilisation de QHistoryState du framework QStateMachine

Daniel Génon

Pour ce troisième cours portant sur les machines à états avec la bibliothèque Qt, il est nécessaire d’avoir lu le premier cours sur QStateMachine, car nous allons étudier l’état d’historisation (sauvegarde ou interruption temporaire) QHistoryState, dont l’utilisation dans les applications pilotant des automatismes peut s’avérer avantageuse. Nous examinerons aussi l’envoi d’un ordre à la « machine » en provoquant soi-même un événement, et l’utilisation de transitions déclenchées par la souris.

Commentez

- Titre : Cours sur l’utilisation de QHistoryState du framework QStateMachine

- Auteur : Daniel Génon

- Parution : 29 juin 2022

- Mise à jour : 10 juillet 2022

- Public visé :

- Licence : Le contenu de cet article est rédigé par Daniel Génon et est mis à disposition selon les termes de la Licence Creative Commons Attribution 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Voir également :

- Documentation QStateMachine

- Utiliser le framework QStateMachine

- Utiliser les états parallèles dans QStateMachine

- Cours sur le langage UML

- Autres tutoriels sur la bibliothèque Qt

Sommaire

- I. Récupérer les fichiers sources

- II. Principe

- III. Petit rappel de classes usuelles

- III-A. postEvent()

- III-B. QMouseEventTransition

- IV. Mise en place du test : étape 01

- IV-A. Classe Scene

- IV-B. Classe Cible

- IV-B-1. Classe EvntPerso

- IV-B-2. Classe de transition TransCouleur

- IV-B-3. Classe EtatCouleur

- IV-B-4. Constructeur de la classe Cible

- IV-B-5. Mise en œuvre de postEvent

- IV-C. Classe Declencheur

- V. Test simple : étape 02

- V-A. Modifier la classe Cible

- V-A-1. Nouvelle fonction Change

- VI. États historiques en cascade

- VII. Étape 03

- VII-A. Modifier la classe Scene

- VII-B. Les cibles

- VIII. Pause dans un programme avec l’état historique

- VIII-A. Étape 04 : interruption d’une animation

- VIII-A-1. L’état Deplacement

- VIII-A-2. La classe Scene

- VIII-B. Étape 05 : profondeur d’historisation et états parallèles

- IX. Remerciements

I - Récupérer les fichiers sources

Un dépôt public GitHub est disponible, il contient 5 projets de travaux pratiques représentant chacun un programme pour tester les démonstrations de ce cours. En vous rendant sur ce dépôt, cliquez sur « code » (bouton vert) et choisissez « download ZIP ». Ensuite, chaque répertoire projetxx contient un .pro à ouvrir avec Qt Creator. La version de Qt utilisée est la 6.2.2, mais les projets ont été aussi testés en version 5.15.2 et 6.21.

La documentation officielle de Qt 6.2 en anglais sur « The State Machine Framework » est consultable en ligne.

L’API d’origine Qt 5 n’a pas subi de modifications et la migration de développements en version Qt 5 vers la version Qt 6.1 et ultérieures, suivant cette documentation, ne devrait occasionner aucun souci. Seules, l’adaptation des #include et l’ajout de QT += statemachinedans le fichier .pro seront nécessaires. Voir le paragraphe « Adaptations pour Qt 6.2 » du premier cours sur QStateMachine pour plus de précisions.

Pour faciliter l’étude, les diagrammes non normalisés sont réalisés pour la programmation, mais pour l’analyse d’une application basée sur l’API QstateMachine, je recommanderai vivement d’utiliser une schématisation par le langage de modélisation UML dont l’apprentissage fait l’objet d’un cours sur le site.

Vous trouverez les liens sur les différents tutoriels « Developpez » concernant Qt en fin de document.

II - Principe

L’état historique est un état enfant fictif qui, lorsqu’il est activé à un instant particulier, permet à son état parent, dans un premier temps, de sauvegarder l’état de ses enfants puis, dans le deuxième temps, de restaurer leurs états sauvegardés antérieurement. Les diagrammes ci-dessous exposent ce principe.

Dans l’état parent exclusif, l’état E3 est actif ; à cet instant, un événement active l’état historique H :

la machine sauve l’historique du parent, c’est-à-dire l’état E3 actif, et restaure celui précédemment historisé en activant l’état E1. À présent, au cours du déroulement ordinaire du programme, l’état E4 est activé,

puis cet événement déclenche l’état historique H, la machine sauve l’historique du parent, ici l’état E4 actif, et restaure l’activation de l’état E3 : le précédent historique de parent.

Nous avons maintenant un aperçu schématique du fonctionnement de QHistoryState, dans un contexte d’états exclusifs, sans une arborescence parent/enfant qui obscurcirait la compréhension.

Une application donnée en exemple dans la documentation de Qt montre l’utilisation familière de cette classe : exemple Qt Padavigator, dans laquelle l’utilisateur change l’écran affiché en appuyant sur la touche entrée et revient à l’écran précédent avec un nouvel appui sur cette touche. Vous constaterez qu’en quelques lignes cette opération devient très simple.

III - Petit rappel de classes usuelles

Jusqu’à présent, nous nous sommes contentés de gérer le comportement de la machine à états de la classe QSignalTransition, pour la commander avec les signaux, et de QKeyEventTransition, pour la piloter en utilisant les touches du clavier. Les deux classes décrites ci-dessous et utilisées dans les logiciels de travaux pratiques vont étoffer ce panel de possibilités. Nous aurons aussi l’occasion d’utiliser des fonctions membres de QState et QStateMachine pour nous familiariser avec ces classes.

III-A - postEvent()

Cette fonction, membre de QStateMachine, nous permet d’envoyer un événement, créé avec la classe QEvent, directement vers la machine à états. Cet événement pourra être traité avec la fonction eventTest d’une classe héritant de QAbstractTransition. La mise en œuvre étant complexe, elle sera détaillée dans l’étape 01.

void QStateMachine::postEvent( QEvent *event,
QStateMachine::EventPriority priority = NormalPriority )

III-B - QMouseEventTransition

Cette classe permet d'utiliser un événement émis par la souris comme déclencheur dans une ou plusieurs transitions pour commander la machine à états. Dans les tests du cours, nous utiliserons la classe QEventTransition, qui est plus adaptée aux objets GraphicsItem. Néanmoins, vous trouverez un sujet d’étude avec le projet test Bonus01, basé sur l’exemple Qt Event Transitions et modifié en utilisant la classe QMouseEventTransition.

IV - Mise en place du test : étape 01

L’environnement ressemble à celui des deux précédents cours, mais le contenu de la zone graphique est changé. Le principe est un curseur mobile qui se déplace automatiquement de gauche à droite et recommence ce trajet tant que le logiciel est actif. Ce curseur est représenté dans l’image ci-dessous par un petit disque jaune. Au centre de l’écran se trouve une cible représentée par un témoin coloré en dessous duquel dépasse un contacteur.

Il n’y a que deux boutons pour le contrôle : quitter et configuration, ce dernier n’est pas utilisé dans les tests.

Le fonctionnement est simple : dès le démarrage du logiciel, le curseur se déplace sur un trajet qui provoque sa collision avec le contacteur de la cible et, à chaque rencontre, la couleur du témoin change. Le diagramme ci-dessous précise ce principe.

IV-A - Classe Scene

De même que dans les cours précédents, le constructeur de la classe met en place l’image du fond et deux objets graphiques pour le test.

Un objet cible qui sera composé de deux images :

- un témoin qui peut prendre quatre couleurs différentes ;

- un contacteur qui change la couleur du témoin à chaque passage. Vous observerez que ce contacteur disparaît après le premier contact et réapparaît à chaque nouveau trajet du curseur. La raison vous sera donnée dans le paragraphe de la classe cible.

ObjetImage *tmptemoin = new ObjetImage();
addItem( tmptemoin );
cible = new Cible( tmptemoin,  460, 350 );
addItem( cible );

Un objet déclencheur correspond à l’image du curseur se déplaçant de gauche à droite.

declencheur = new Declencheur( cible );
addItem( declencheur );
declencheur->hide();

La scène est définie au moment de l’appel à sa fonction InitialiserScene à la fin du constructeur de la classe Fenetre, point réel de démarrage du logiciel de test. Cette fonction initialise et démarre la machine à états, propre à la gestion de la classe Scene et les éléments de contrôles, bouton et ligne info.

void Scene::InitialiserScene( QToolButton *quitterlejeu, QLineEdit *linformation )
{
linformation->setText("mise en place");
QStateMachine *machine = new QStateMachine( this );
ZoneAnime *zoneanime = new ZoneAnime( this, machine );
QFinalState *final = new QFinalState( machine );
zoneanime→addTransition( quitterlejeu, SIGNAL(clicked()), final );
connect( machine, SIGNAL(finished()), qApp, SLOT(quit()) );
machine→setInitialState( zoneanime );
machine->start();
}

L’animation de l’objet déclencheur, c’est-à-dire le graphique mobile du curseur, est amorcée dans la fonction DemarrerScene :

void Scene::DemarrerScene()
{
setFocusItem( declencheur, Qt::OtherFocusReason );
declencheur->Initialiser();
declencheur->BougeToi();
}

Jusque-là, nous avons côtoyé des contrées que nous connaissons bien, et nous avons cheminé sans être déconcertés. Le contexte est maintenant établi, nous pouvons commencer les nouveautés par la description de la classe Cible.

IV-B - Classe Cible

La machine de cette classe gère quatre états symbolisés chacun par une couleur, et la transition d’un état à l’autre est activée par un contact entre le déclencheur et la cible, comme décrit précédemment.

Dans ce diagramme, l’index couleur, incrémenté lorsqu’il y a contact, est traité par une classe héritière de QEvent que nous n’avons pas encore utilisée dans ces cours, ou pour être plus juste, nous l’avons un peu utilisée, mais seulement en tant que paramètre dans la fonction onEntry des classes d’états et de transitions. Avant de décrire cette nouvelle classe, regardons en quoi elle nous sera utile. Quand le contact est opéré, il nous faut déclencher une transition pour changer la couleur du témoin, jusqu’à présent nous envoyions l’ordre soit par l’émission d’un signal, soit par action sur les touches du clavier. Cette fois, nous allons programmer notre événement directement dans la machine en utilisant la fonction postEvent de QStateMachine.

IV-B-1 - Classe EvntPerso

Pour créer un événement personnel, notre classe hérite de QEvent et nous lui donnons le Type « User ». Notez que nous avons la possibilité de créer grand un nombre d’événements personnels (voir la documentation Qt à ce sujet). C’est ce type utilisateur que nous testerons dans la fonction onEntry de la transition, ainsi que la valeur entière que nous passons en paramètre à notre classe EvntPerso pour déterminer la position de la couleur à afficher dans le témoin.

EvntPerso::EvntPerso( int vcoul ) : QEvent(QEvent::Type(QEvent::User))
{
couleur = vcoul;
}
int EvntPerso::ValCouleur()
{
return couleur;
}

Nous indiquons le type de l’événement dans la liste d’initialisations du constructeur.

IV-B-2 - Classe de transition TransCouleur

La classe de transition TransCouleur contient le numéro d’ordre d’affichage de la couleur pour valider la transition vers l’état concerné. Dans sa fonction onEntry, un premier test valide si l’appel est bien provoqué par un événement User :

TransCouleur::TransCouleur( int vcoul)
{
evtcouleur = vcoul;
}
bool TransCouleur::eventTest(QEvent *evt )
{
if(evt->type() == QEvent::Type( QEvent::User ))
{
return ( evtcouleur == ( (EvntPerso*) evt )->ValCouleur() );
}
return false;
}
void TransCouleur::onTransition(QEvent *){}

Ensuite, en cas de confirmation, nous vérifions que la valeur entière fournie par l’événement personnel est bien le numéro d’ordre d’affichage de l’état à traiter.

IV-B-3 - Classe EtatCouleur

Cette classe est minimale, elle contient l’adresse de l’instance ObjetImage et le nom du fichier stocké dans les ressources à charger dans cet objet.

EtatCouleur::EtatCouleur( const QString &ficoul, ObjetImage *vtem, QState *vparent ) : QState(vparent)
{
temoin  = vtem;
couleur = ficoul;
}
void EtatCouleur::onEntry(QEvent *)
{
temoin->hide();
temoin->ChangeImage(couleur);
temoin->show();
}

Comme enseigné dans le premier cours, lorsque la validation de la transition est approuvée, la fonction onEntry de l’état ciblé est activée, alors nous changeons l’image de l’objet témoin.

Pour recharger l’image dans un objet de la classe QPixmap, nous devons masquer l’objet, changer l’image et la réafficher pour rafraîchir la zone.

IV-B-4 - Constructeur de la classe Cible

Cette classe hérite de ObjetImage. Elle contient sa propre instance d’image pour figurer la cible, représentée par un trait qui servira de contact pour engager le changement d’état, et aussi un pointeur vers l’image témoin pour l’affichage de la couleur.

Cible::Cible( ObjetImage *vtemoin, qreal posx, qreal posy)
{
temoin = vtemoin;
…
InitImage(QString(":/motif/cible.png"));
…

Puis nous instancions les quatre états en leur fournissant les chemins des images.

machine = new QStateMachine(this);
EtatCouleur *Cbleue = new EtatCouleur( ":/motif/bleue.png", temoin, machine);
EtatCouleur *Crouge = new EtatCouleur( ":/motif/rouge.png", temoin, machine);
EtatCouleur *Cverte = new EtatCouleur( ":/motif/verte.png", temoin, machine);
EtatCouleur *Cjaune = new EtatCouleur( ":/motif/jaune.png", temoin, machine);

Chaque transition est paramétrée par le nombre entier qui caractérise son ordre d’apparition.

TransCouleur *JauneVersBleue = new TransCouleur(1);
Cjaune->addTransition(JauneVersBleue);
JauneVersBleue->setTargetState(Cbleue);
TransCouleur *BleueVersRouge = new TransCouleur(2);
Cbleue->addTransition(BleueVersRouge);
BleueVersRouge->setTargetState(Crouge);
TransCouleur *RougeVersVerte = new TransCouleur(3);
Crouge->addTransition(RougeVersVerte);
RougeVersVerte->setTargetState(Cverte);
TransCouleur *VerteVersJaune = new TransCouleur(4);
Cverte->addTransition(VerteVersJaune);
VerteVersJaune->setTargetState(Cjaune);

Et pour finir, nous indiquons l’état initial à la machine machine->setInitialState(Cbleue);et nous la démarrons.machine->start();.

IV-B-5 - Mise en œuvre de postEvent

C’est le moment d’explorer comment inciter la machine à changer d’état.

À chaque contact, le déclencheur appelle la fonction Change :

void Cible::Change()
{
indcouleur++;
if(indcouleur>4)indcouleur=1;
machine->postEvent(new EvntPerso(indcouleur));
}

Dans cette fonction, nous incrémentons l’index de la couleur et celui-ci est envoyé à postEvent par l’intermédiaire d’un pointeur dynamique éphémère sur notre classe EvntPerso. La machine prend possession de l’objet, en utilisant son pointeur, et se chargera de le détruire dès qu’il sera utilisé. Il est donc plus pratique d’utiliser la notation : machine->postEvent(new EvntPerso(indcouleur));

plutôt que de créer un pointeur et de l’initialiser à chaque passage :

EvntPerso *toto = new EvntPerso(indcouleur);

machine->postEvent( toto );

IV-C - Classe Declencheur

Si vous avez lu le premier cours sur l'utilisation du framework QstateMachine, vous remarquerez la similarité avec la classe Predateur du paragraphe VII. Le curseur, en forme de disque jaune, effectue une trajectoire horizontale et sans fin : lorsqu’il atteint le bord droit de la scène, il reprend son parcours depuis son origine. La classe Declencheur réalise cette animation en utilisant les deux objets avance et arret, issus des classes d’états DeplaceDch et ArretDch, reliés par la transition reprend qui est une instance de la classe QSignalTransition.

Quand vous ouvrirez les fichiers sources des deux classes d’état, vous comprendrez, par leur contenu très simple, qu’il est inutile de les commenter.

Nous entrons dans le vif du sujet en analysant le constructeur de la classe Declencheur. Mises de côté les initialisations usuelles, nous créons nos deux états, avance et arrete, puis leur transition reprend qui sera activée par le signal finished, de l’animation du curseur, lorsqu’il atteint le bord droit de la zone. Cette transition est ajoutée à l’état avance et cible l’état arrete.

Declencheur::Declencheur( Cible *vcbl )
{
…
machine = new QStateMachine(this);
DeplaceDch *avance = new DeplaceDch( this, machine);
ArretDch   *arrete = new ArretDch( this, machine );

QSignalTransition *reprend = new QSignalTransition( animationdelobjet, SIGNAL(finished()) );
avance->addTransition(reprend);
reprend→setTargetState(arrete);

arrete->addTransition( arrete, SIGNAL( RePositionner()), avance );

connect( animationdelobjet, SIGNAL(valueChanged(QVariant)), this, SLOT(EnDeplacement()) );

Lorsque l’état arrete est activé, il émet le signal repositionner pour redémarrer le curseur, nous devons donc écrire l’instruction  arrete->addTransition( arrete, SIGNAL( RePositionner()), avance );pour ajouter une transition ciblant l’état avance qui, au signal repositionner, se réactivera pour recommencer le trajet.

La ligne qui suit appelle la fonction EnDeplacement à chaque changement de position du curseur, pour permettre le test de collision.

Nous finissons le constructeur par l’initialisation de la machine machine->setInitialState(avance);, car son déclenchement est provoqué dans la fonction DemarrerScene de la classe Scene.

Nous avons imposé le passage en paramètre du pointeur de l’objet cible dans la définition du constructeur, ceci dans le but de détecter le contact entre notre objet déclencheur et la cible grâce à la fonction SLOT EnDeplacement appelée à chaque mouvement du curseur.

Cette fonction commence par un test sur la visibilité de la cible ; en effet, il serait possible suivant la taille de la cible et celle du curseur associées à la valeur de animationdelobjet->setDuration(2500);que la collision soit validée plusieurs fois de suite. Cette anomalie existait dans le deuxième cours sur les états parallèles, mais elle était laissée telle quelle, car elle n’impactait pas le fonctionnement des tests. Ici, cette déficience fausserait le test, puisque la succession des contacts provoquerait un défilé des états.

void Declencheur::EnDeplacement()
{
if(pcible->isVisible())
{
if( TestCollision() )
{
pcible->Change();
pcible->setVisible(false);
}
}
}

Si la collision est réalisée, nous appelons la fonction Change de la classe Cible, où sera provoqué notre événement personnel pour changer la couleur, puis l’image de la cible est rendue invisible jusque la fin du trajet du curseur.

La détection de la collision se réduit à l’utilisation de la fonction collidingItems de la classe QGraphicsItem en utilisant l’intersection des contours :

bool Declencheur::TestCollision()
{
QList<QGraphicsItem*> lstobjet = collidingItems( Qt::IntersectsItemBoundingRect ) ;
return (lstobjet.contains(pcible));
}

À chaque démarrage du trajet du curseur, l’état arret appelle la fonction Initialiser qui replace les coordonnées du curseur à leur origine,

void Declencheur::Initialiser()
{
hide();
pntfin=QPointF( (scene()->width() - size().width()), 0.5*((scene()->height()) - size().height()) );
pntdepart=QPointF( 0, 0.5*((scene()->height()) - size().height()) );
setPos(pntdepart);
show();
pcible->setVisible(true);
}

avec, en fin de procédure, la réapparition de la cible pcible->setVisible(true);

Nous en avons terminé avec la mise en place des objets pour l’environnement de test de QHistoryState et nous avons appris à créer un événement dans la programmation pour influencer le fonctionnement de la machine à états. Nous pouvons maintenant passer au test.

V - Test simple : étape 02

QHistoryState se résume à saisir un instantané d’un état et à avoir la possibilité de le restituer. Dans la pratique, c’est un état fictif, obligatoirement enfant de l’état à historiser, qui copie l’état de son parent. Pour le test, nous provoquerons cette copie en cliquant sur un bouton spécifique que nous devrons ajouter à la classe Fenetre.

Dans le fichier d’entête, nous ajoutons une instance de QToolButton en private pour la transmettre à la classe Scene et une fonction pour positionner ce bouton dans la fenêtre bien séparé des deux autres :

class Fenetre : public QWidget
{
...
private :
...
QtoolButton *quitter,*configuration,*btnhisto;
...
QVBoxLayout *BarreBoutonHisto();
};

Nous pouvons examiner le détail de la fonction pour placer cette nouvelle barre de boutons
ci-dessous qui ne révèle pas de grandes nouveautés :

QVBoxLayout* Fenetre::BarreBoutonHisto()
{
int htico = 35;
QVBoxLayout *barrevert = new QVBoxLayout;
btnhisto = new QToolButton;
CreerUnBouton( btnhisto, ":/general/ouvert.png", "pause cible 1", htico );
barrevert->addWidget(btnhisto);
return barrevert;
}

Et nous insérons dans la création du menu la ligne d’instructions pour la solliciter :

void Fenetre::CreerLeMenu()
{
...
layoutdroite->addLayout(BarreBouton(),1,0);
layoutdroite->addLayout(BarreBoutonHisto(),2,0);
...
}

Ce qui nous placera une barre distincte à droite de la zone graphique, sous la barre actuelle.

Et, pour terminer ces modifications, nous rectifions l’appel à l’initialisation de la scène dans le constructeur :

Fenetre::Fenetre(QWidget *parent) : QWidget(parent)
{
...
pscene->InitialiserScene( quitter, ligneinfo1, btnhisto);
}

Ce paramètre, btnhisto, transitera par la classe Scene pour être transmis à la classe Cible au moyen de la fonction InitialiserScene :

void Scene::InitialiserScene( QToolButton *quitterlejeu, QLineEdit *linformation, QToolButton *histo)
{
linformation->setText("mise en place");
cible->InitHistorique(histo);
...
}

Le diagramme affiché au début de l’étape 01 sur le fonctionnement du test devient alors :

Dans notre étape 02, je n’aborderai pas la classe Declencheur, puisqu’elle reste inchangée du fait que son objectif reste exclusivement le contact avec la cible. Nous procéderons donc, sans délai, aux modifications de la classe Cible.

V-A - Modifier la classe Cible

Le but est de sauvegarder l’état couleur actif pendant la succession de ces couleurs, et, lorsque nous restaurons l’état sauvé, de faire revenir le témoin à la couleur historisée. Mais ensuite, nous disposons de deux possibilités pour continuer l’enchaînement des états.

La première, nous l’avons utilisée dans la mise en place de l’environnement du test. Le passage d’un EtatCouleur à un autre était assujetti à un index implémenté à chaque contact ; dans ce cas, la série se perpétue en respectant la suite prévue au début par incrémentation de l’index :

Quelle que soit la position du curseur, nous restaurons l’état historique : l’état bleu s’affiche immédiatement à la place de l’état rouge qui était en cours d’activité. Dès que le curseur, qui est toujours en mouvement, produit un contact, l’état vert est réactivé parce que l’état rouge le précède dans la successivité imposée par les index. C’est le cas le plus simple ; toutefois, si nous conservions ce principe, nous risquerions d’être désorientés dans l’observation du test puisque l’état restauré s’intercalerait dans l’enchaînement des états, sans en modifier la chronologie.

La deuxième possibilité est de restaurer l’état et de reprendre le déroulement à compter de cet état :

Quelle que soit la position du curseur, nous restaurons l’état historique : l’état bleu s’affiche. L’état jaune qui était en activé à cet instant est sauvegardé. Au prochain contact, l’état rouge apparaîtra, car il suit l’état bleu, et nous avons imposé que la suite reprenne avec, comme origine, l’état historique. Ce qui nous semblera, dans le déroulement du test, plus logique.

Vous remarquerez que le premier état de restauration est bleu, nous verrons plus loin qu’il faut définir un état historique par défaut pour l’état QHistoryState et, pour notre test, nous définirons l’état bleu.

Pour la suite de cette étape 02, nous chercherons la complication, uniquement pour utiliser des fonctions intéressantes des classes QState et QAbstractState.

Le diagramme de notre machine à états est déterminé comme suit :

Dès lors que l’état QHistoryState doit avoir un état parent, nous créons l’état groupecouleur qui contient les objets de la classe EtatCouleur en plus de l’état historique. Lorsqu’il y a contact, la classe Declencheur commande la procédure Change de la classe Cible qui récupère l’index, que nous avons placé dans les transitions, pour le transmettre à la fonction Machine->postEvent(...);. Vous devez bien noter que le conteneur groupecouleur n’est pas créé dans le but d’un partage de transitions comme nous allons le constater dans l’étude du constructeur de la classe Cible :

Cible::Cible( ObjetImage *vtemoin, qreal posx, qreal posy)
{
...
machine = new QstateMachine(this);
groupecouleur = new QState(machine);
EtatCouleur *Cbleue = new EtatCouleur( ":/motif/bleue.png", temoin, groupecouleur);
EtatCouleur *Crouge = new EtatCouleur( ":/motif/rouge.png", temoin, groupecouleur);
EtatCouleur *Cverte = new EtatCouleur( ":/motif/verte.png", temoin, groupecouleur);
EtatCouleur *Cjaune = new EtatCouleur( ":/motif/jaune.png", temoin, groupecouleur);

lstetat << Cbleue << Crouge << Cverte << Cjaune ;

groupecouleur->setInitialState(Cbleue);

Les objets EtatCouleur sont déclarés enfant de l’état groupecouleur. Les pointeurs de ces états sont stockés dans une liste typée QList<EtatCouleur*> lstetat;, pour faciliter des test ultérieurs et l’état Cbleue est déclaré comme état initial de groupecouleur.

Dans l’initialisation des transitions, on passe en paramètre l’index de l’état suivant :

TransCouleur *JauneVersBleue = new TransCouleur(1);
Cjaune->addTransition(JauneVersBleue);
JauneVersBleue->setTargetState(Cbleue);
TransCouleur *BleueVersRouge = new TransCouleur(2);
Cbleue->addTransition(BleueVersRouge);
BleueVersRouge->setTargetState(Crouge);
TransCouleur *RougeVersVerte = new TransCouleur(3);
Crouge->addTransition(RougeVersVerte);
RougeVersVerte->setTargetState(Cverte);
TransCouleur *VerteVersJaune = new TransCouleur(4);
Cverte->addTransition(VerteVersJaune);
VerteVersJaune->setTargetState(Cjaune);
lsttrans << JauneVersBleue << BleueVersRouge << RougeVersVerte << VerteVersJaune ;

Toutes les transitions sont ajoutées à un état enfant et non à leur parent groupecouleur, car nous ne sommes pas en partage de transitions. Nous constatons qu’à part le paramètre index, ces déclarations sont identiques à celles de la précédente étape 01. De même que les états, nous stockons les pointeurs des objets dans une liste typée QList<TransCouleur*> lsttrans;et nous terminons par l’initialisation de l’état historique et le démarrage de la machine :

historique = new QHistoryState(groupecouleur);
historique->setDefaultState(Cbleue);
machine->setInitialState(groupecouleur);
machine→start();

L’état Cbleue est défini comme état par défaut, il est nécessaire pour que le premier appel à l’état historique ait une restauration en réserve. Sa valeur reste inchangée tout au long de l’existence de l’état historique, sauf si nous en définissons un autre par la fonction setDefaultState. L’état par défaut de l’état historique ne doit pas être confondu avec l’état initial du parent.

V-A-1 - Nouvelle fonction Change

Cette fonction est totalement modifiée et vous devez être conscient que, dans notre programme, nous n’utilisons que des états simples pour éviter une multitude de tests inutiles pour cet apprentissage.

Dans la classe QStateMachine, la fonction configuration produit une table contenant tous les états actifs de la machine :

void Cible::Change()
{
int  indcouleur=1;
QSet<QAbstractState*> setdetat = machine->configuration();

Ce sont les états actifs au moment de l’appel à la fonction. Ceci signifie que, dans une application plus complexe, il est possible qu’entre le moment de la réception des éléments et leur traitement, des états de la machine aient changé d’activité, puisque celle-ci continue son action.

Notre table ne contient que deux états actifs, le parent groupecouleur et l’un de ses enfants en plus de l’état historique.

setdetat.remove(groupecouleur);
if(!setdetat.isEmpty())
{
if(lstetat.contains(*setdetat.begin()))
{

Nous supprimons le parent de la table et si le premier état restant se trouve dans la liste de nos objets EtatCouleur, nous en extrayons son pointeur dans un type QabstractState*.

QAbstractState* etatentete = *setdetat.begin();
QList<QAbstractTransition*> lsttransition = ((QState*)etatentete)->transitions();

Et nous pouvons alors utiliser une fonction de la classe QState pour obtenir la liste des transitions attachées à cet état actif et vérifier qu’elle est bien présente dans notre liste d’objets TransCouleur, pour récupérer l’index contenu dans cette transition.

if(lsttrans.contains(lsttransition.first()))
indcouleur = ((TransCouleur*)lsttransition.first())->ValCouleur();

Dans notre test, il n’y a qu’une seule transition par état qui est facilement récupérable avec lsttransition.first(). En transmettant l’index comme événement à la machine machine->postEvent(new EvntPerso(indcouleur));la transition de l’état actif en cours sera activée pour passer à l’état suivant. Ce qui implique que si l’état en cours est un état restauré, le prochain état sera son suivant dans la succession prévue à l’origine.

Pour terminer, n’oublions pas d’implémenter la fonction d’initialisation de la transition, attachée au bouton historique, qui est appelée au démarrage du logiciel par la classe Scene dans InitialiserScene :

void Cible::InitHistorique(QToolButton *histo)
{
groupecouleur->addTransition(histo, SIGNAL(clicked()), historique);
}

Cette partie, rendue compliquée pour les besoins de l’apprentissage, nous a permis de connaître le principe initial de l’état historique et d’utiliser quelques fonctions utiles pour la gestion de la machine à états.

VI - États historiques en cascade

La première idée qui nous vient quand nous évoquons les états historiques, c’est que nous pourrons en créer plusieurs dans un état parent afin de sauver des états au cours de l’activité de l’objet machine. Hé bien, ce n’est pas une bonne idée !

historique01 = new QHistoryState(groupecouleur);
historique02 = new QhistoryState(groupecouleur);

QKeyEventTransition *his01 = new QKeyEventTransition(this, QEvent::KeyPress, Qt::Key_4);
groupecouleur->addTransition(his01);
his01→setTargetState(historique01);

QKeyEventTransition *his02 = new QKeyEventTransition(this, QEvent::KeyPress, Qt::Key_6);
groupecouleur->addTransition(his02);
his02->setTargetState(historique02);

En effet, dans les lignes d’instructions ci-dessus, l’appui sur la touche 4 historise l’état en cours et restitue l’état « sauvegardé » ultérieurement. Quand nous appuierons sur la touche 6, l’état restitué sera celui historisé par la touche 4, car le parent est destiné à ne posséder qu’un seul état historique. Si nous multiplions ce type d’état dans un parent, cela ne change rien parce que nous pouvons considérer que ce sont des interrupteurs qui allument ou éteignent la même ampoule.

Avant d’approfondir ce cours, nous devons remarquer que :

- les fonctions onEntry et onExit de la classe QHistoryState sont inopérantes, malgré leur présence dans la documentation technique. Il n’est donc pas nécessaire de les réimplémenter dans des classes dérivées : n’oublions pas que c’est un pseudo-état dont l’unique fonction est de rétablir un état « interrompu » ;

- les fonctions evenTest et onTransition de la transition appliquée à un état historique sont fonctionnelles. L’indication de la documentation générale est dubitative dans son propos.

Vous pourrez analyser ces observations en utilisant le projet test bonus02.

VII - Étape 03

Nous allons étudier le programme test de l’étape 03 où trois états regroupent chacun trois états cibles, lesquels ont la faculté de prendre 6 états révélés chacun par une couleur. Chaque état groupe contient un bouton pour historiser les cibles, et dans le menu général figure aussi un bouton historique pour les états groupes.

Commençons par étudier une petite nouveauté dans la fenêtre. Le bouton historique au bas du côté droit affichera deux images, l’une pour la position enfoncée et l’autre pour la position relevée, ce qui nous permettra de mieux suivre l’action. Ces images seront placées dans une classe QIcon et attribuées à l’objet btnhisto, vous remarquerez l’attribution à la position par on ou off.

void Fenetre::CreerLeMenu()
{
...
QIcon iconetmp;
iconetmp.addFile(":/general/ouvert.png", QSize(40,40), Qicon::Normal, QIcon::Off );
iconetmp.addFile(":/general/ferme.png", QSize(40,40), Qicon::Normal, QIcon::On );
btnhisto = new QToolButton;
CreerUnBouton( btnhisto, "", "restaure/historise le groupe encours", 40 );
btnhisto->setCheckable(true);
btnhisto->setIcon(iconetmp);
barretmp->addWidget(btnhisto);
...

L’instructionbtntmp->setCheckable(true);transforme le bouton simple en bouton-poussoir qui changera d’icône à chaque clic.

VII-A - Modifier la classe Scene

Dans cette étape nous modifions l’état ZoneAnime de la classe Scene en lui ajoutant trois instances de la classe d’états EtatGroupe qui sont chargées de piloter, chacune, un groupe de cibles. Nous activerons un groupe en cliquant sur son nom affiché dans sa zone, ce qui cachera les cibles du groupe en cours et affichera les cibles du groupe activé. Le bouton historique effectuera l’opération sauver/restaurer.

Au démarrage du test, la classe Fenetre organise la classe Scene en sollicitant sa fonction InitialiserScene. Dans celle-ci, après les instructions habituelles pour créer l’objet machine, l’état QFinalState et ses connexions pour quitter le test, nous créons les états groupeA, B et C en leur spécifiant par la suite leur parent commun zoneanime avec l’instructionsetParent.

void Scene::InitialiserScene(QToolButton *quitterletest, QLineEdit *linformation, QToolButton *bhisto )
{
...
QStateMachine *machine =new QStateMachine(this);
ZoneAnime *zoneanime = new ZoneAnime( this, machine );
QFinalState *final = new QFinalState(machine);
zoneanime->addTransition( quitterletest, SIGNAL(clicked()), final );
connect( machine, SIGNAL(finished()), qApp, SLOT(quit()));

EtatGroupe *groupeA = new EtatGroupe( QString(":/motif/fondgrpA.png"), 50, 60, this );
EtatGroupe *groupeB = new EtatGroupe( QString(":/motif/fondgrpB.png"), 50, 320, this );
EtatGroupe *groupeC = new EtatGroupe( QString(":/motif/fondgrpC.png"), 50, 580, this );
groupeA->setParent(zoneanime);
groupeB->setParent(zoneanime);
groupeC→setParent(zoneanime);

QEventTransition *okA = new QEventTransition( groupeA->NoGroupe(), QEvent::GraphicsSceneMousePress, zoneanime );
okA->setTargetState(groupeA);
QEventTransition *okB = new QEventTransition( groupeB->NoGroupe(), QEvent::GraphicsSceneMousePress, zoneanime );
okB->setTargetState(groupeB);
QEventTransition *okC = new QEventTransition( groupeC->NoGroupe(), QEvent::GraphicsSceneMousePress, zoneanime );
okC→setTargetState(groupeC);

QHistoryState *historique = new QHistoryState( zoneanime );
historique->setDefaultState(groupeA);
zoneanime->addTransition(bhisto, SIGNAL(clicked()), historique);

zoneanime→setInitialState(groupeA);
machine->setInitialState(zoneanime);
machine->start();
}

L’initialisation des transitions entre la souris et les états, avec QEventTransition, réclame le numéro d’entité de l’objet qui provoque cette transition. Puisque l’objet est créé dans la classe état, nous récupérons ce numéro avec une fonction que nous observerons plus loin. Et nous finissons le paramétrage en donnant l’objet état qui contient cette transition, au lieu d’utiliser l’instruction addTransitioncomme auparavant.

En fin de constructeur, nous déterminons l’état historique et la connexion de sa transition au bouton du menu général.

L’état groupe est schématisé ci-dessous, observez que contrairement aux autres étapes, les groupes de cibles sont des objets inclus dans cette classe état.

Dans le constructeur, le paramètre nomfichier doit recevoir l’image du texte du groupe sur laquelle nous cliquerons pour afficher le groupe. Les apparitions et occultations des cibles seront réalisées simplement en changeant la position dans l’axe Z de l’image du fond du groupe de cible, récupéré par pfond = groupecible->NoFond();. Lorsque l’état est activé, onEntry place le fond derrière les cibles, et lorsque nous sortons de l’état, onExit le replace devant.

EtatGroupe::EtatGroupe(const QString &nomfichier, qreal vx, qreal vy , QGraphicsScene *pscene )
{
groupecible = new GroupeCible( nomfichier, vx, vy, pscene );
pfond = groupecible->NoFond();
}
void EtatGroupe::onEntry(QEvent *)
{
pfond->setZValue(2);
}
void EtatGroupe::onExit(QEvent *)
{
pfond->setZValue(5);
}
QObject *EtatGroupe::NoGroupe()
{
return static_cast<QObject*>(groupecible);
}

En fin du fichier source, nous plaçons l’instruction utilisée par la création des objets QEventTransition dans le constructeur de la classe Scene exposé précédemment, pour récupérer l’identité de l’objet créé, converti en pointeur QObject.

La classe GroupeCible met en place les trois cibles de la classe EtatGroupe. Sans trop la détailler, car pour nous son constructeur est devenu routinier, nous remarquerons à la fin de son fichier source la fonction InsererBoutonHistorique qui positionne le bouton dans la zone du groupe et envoie le numéro du pointeur de l’objet QToolButton à chacune de ses cibles. De cette façon, chaque action sur ce bouton provoquera dans toutes les cibles du groupe l’activation de son état historique, il y aura donc simultanéité de l’historisation.

void GroupeCible::InsererBoutonHistorique( qreal vx, qreal vy )
{
QToolButton *btnhisto = new QToolButton;
QIcon iconetmp;
iconetmp.addFile(":/general/ouvert.png",QSize(40,40),QIcon::Normal,QIcon::Off );
iconetmp.addFile(":/general/ferme.png",QSize(40,40),QIcon::Normal,QIcon::On );
btnhisto->setCheckable(true);
btnhisto->setIcon(iconetmp);
btnhisto->setIconSize( QSize (80,80));
btnhisto->setToolTip("restaure/historise le groupe");
BoutonDansScene = pscene->addWidget(btnhisto);
BoutonDansScene->setPos( vx, vy );
BoutonDansScene->setZValue(3);
for( int i=0; i<lstcible.size(); ++i ){ lstcible[i]->InitHistorique(btnhisto); }
}

Puisque BoutonDansScene est un QWidget placé dans un objet QGraphicsScene, pour que les événements qui l’affectent soient traités à travers la Scene, nous utilisons la classe QGraphicsProxyWidget pour son typage. Vous trouverez plus de détails sur l’application des insertions de QWidget dans QGraphicsScene avec la documentation de Qt.

VII-B - Les cibles

La classe Cible contient une zone image qui peut afficher six couleurs représentant chacune un état. Ces états sont reliés l’un à l’autre par une transition déclenchée avec le clic du bouton gauche de la souris dans la zone cible.

Nous remarquons, dans la machine, la présence de l’état groupecouleur qui sert de conteneur pour les six états et l’état historique, puisque QStateMachine ne doit pas contenir d’état historique.

Le début du constructeur de la classe Cible est similaire à ce que nous avons vu jusqu’à présent. L’ajout de l’instructionsetAcceptedMouseButtonsnous permet d’utiliser la souris pour provoquer un événement depuis cet objet, héritier de la classe QGraphicsObject. Vous noterez que l’événement contrôléQEvent::GraphicsSceneMousePressprovoque une alerte « QGraphicsItem::ungrabMouse: not a mouse grabber » dans l’affichage de la sortie de l’application de QtCreator, mais ne perturbe pas le déroulement du test.

Cible::Cible(qreal posx, qreal posy)
{
…
setAcceptedMouseButtons( Qt::LeftButton );
QStateMachine *machine = new QStateMachine(this);
groupecouleur  = new QState(machine);
...
EtatCouleur *e0 = new EtatCouleur( ":/motif/bleue.png", this, groupecouleur );
...
EtatCouleur *e5 = new EtatCouleur( ":/motif/mauve.png", this, groupecouleur );
TransEventSouris *BvR = new TransEventSouris( this, QEvent::GraphicsSceneMousePress, e0 );
BvR->setTargetState(e1);
…
TransEventSouris *MvB = new TransEventSouris( this, QEvent::GraphicsSceneMousePress, e5 );
MvB->setTargetState(e0);
historique = new QHistoryState(groupecouleur);
historique→setDefaultState(e0);
machine->setInitialState(groupecouleur);
machine->start();
}

Les cinq états de Cible sont définis par la classe EtatCouleur dont les instanciations de e0 à e5 permettront de permuter d’une couleur à la suivante et en continu, grâce à sa fonction onEntry :

void EtatCouleur::onEntry(QEvent *)
{
temoin->hide();
temoin->ChangeImage(couleur);
temoin->show();
temoin->setFlag( QGraphicsItem::ItemIsFocusable, true );
}

Lors de l’activation, l’image est modifiée par l’entrée dans l’état. N’oublions pas : étant donné que la classe Cible est héritière de la classe QGraphicsObject, pour valider la nouvelle image, un hide et un show sont nécessaires et, de ce fait, il faut rendre l’objet cible « focusable », car cette propriété a été annulée par le hide.

Pour la transition entre états, nous créons la classe TransEventSouris, dérivée de QEventTransition. Cette tribulation n’est pas nécessaire, mais nous permet, si le besoin se fait sentir, de tester sa fonction eventTest.

VIII - Pause dans un programme avec l’état historique

VIII-A - Étape 04 : interruption d’une animation

Dans cette étape une balle défile de haut en bas avec une abscisse différente à chaque parcours. L’appui sur le bouton pause, stoppe le défilement et affiche un message. La fin du message provoque la reprise du défilement, depuis la position de la balle au moment de l’arrêt.

Si vous le souhaitez, en testant les programmes bonus03 et bonus04, vous observerez que dans une utilisation simple de la fonction assignProperty() affectée à un état qui lance une animation, celle-ci continue jusqu’à son aboutissement, même si vous changez d’état en cours de déroulement. C’est pourquoi, dans cette etape04, nous réimplémenterons les fonctions onEntry et onExit pour interrompre et reprendre l’animation dans une classe état que nous examinons de suite.

VIII-A-1 - L’état Deplacement

Cinq instances de cette classe seront créées. L’initialisation de la machine à la première d’entre elles déclenchera l’animation de la balle dès le début du test.

Dans le constructeur, nous initialisons une instance de la classe QPropertyAnimation avec les positions début et fin du trajet, ainsi que la durée. Ces propriétés de base ne changeront pas pendant l’exécution du programme.

Deplacement::Deplacement(ObjetImage *vimage, Scene *vscene, qreal vposx,  QState *vparent ) : QState(vparent)
{
...
animationdelobjet = new QPropertyAnimation( vimage, "pos" );
animationdelobjet->setDuration(900);
animationdelobjet->setStartValue(ptdebut);
animationdelobjet->setEndValue(QPointF(vposx, vscene->height()+vimage→size().height()));
}

L’implémentation de la fonction virtuelle onEntry consiste simplement à provoquer l’animation suivant sa posture précédente. À noter que, si un objet est à sa position initiale, la fonction resume() n’engendre pas son mouvement.

Pour la fonction onExit, suivant le comportement de l’animation, elle comptabilise une fin de trajet ou met en attente le déplacement.

Dans la rédaction du fichier source, vous remarquerez le pointeur sur l’objet scene que nous avons conservé pour l’appel à sa fonction de comptage.

void Deplacement::onEntry(QEvent *)
{
if(animationdelobjet->state()==QAbstractAnimation::Paused)
animationdelobjet->resume();
else
animationdelobjet->start();
}
void Deplacement::onExit(QEvent *)
{
if(animationdelobjet->state()==QAbstractAnimation::Running)
animationdelobjet->pause();
else
{
pscene->Comptage();
animationdelobjet->stop();
}
}

VIII-A-2 - La classe Scene

Le schéma de notre test expose les états des 5 parcours verticaux de vert1 à vert5 reliés par une transition formant un circuit bouclé, et l’état pause qui nous autorisera l’interruption de l’animation et l’affichage du message. Le défilement de la balle démarre dès le début du test, un appui sur le bouton « pause » déclenche la transition ajoutée à zoneanime et active l’état « étatpause ».

C’est ici que nous découvrons que la classe QHistoryState peut être utilisée en tant que transition. N’oublions pas qu’elle est un état hybride et n’a vraiment ni les fonctions d’un état ni celles d’une transition. Dans notre utilisation, son instance sert de transition pour historiser l’état en cours dans zoneanime, avant d’afficher le message, de façon à reprendre l’exécution de la machine à cet état et à la position de la balle dans cet état dès l’extinction du message.

Le constructeur de la classe Scene ne contient que l’initialisation des images du fond et de la balle, nous passons donc tout de suite à la fonction InitialiserScene. Après les instructions habituelles de mise en œuvre de la machine, nous créons les cinq instanciations des objets de la classe Deplacement puis leurs cinq transitions pour former le bouclage, ensuite nous créons l’objet état historique dont le parent est zoneanime. Vous ne serez pas surpris de voir qu’il n’y a pas d’initialisation d’état par défaut pour l’historique dans ce programme du fait de son utilisation en tant que transition.

Le reste de la fonction est composé d’instructions maintenant usuelles pour nous.

void Scene::InitialiserScene(QToolButton *quitterletest, QLineEdit *linformation, QToolButton *bhisto)
{
...
Deplacement *horz1 = new Deplacement( image, this,  250, zoneanime );
Deplacement *horz2 = new Deplacement( image, this,  450, zoneanime );
Deplacement *horz3 = new Deplacement( image, this,  600, zoneanime );
Deplacement *horz4 = new Deplacement( image, this,  800, zoneanime );
Deplacement *horz5 = new Deplacement( image, this, 1000, zoneanime );
QSignalTransition *vers2 =
new QSignalTransition( horz1->rtanimation(), SIGNAL(finished()), horz1 );
vers2->setTargetState(horz2);
QSignalTransition *vers3 =
new QSignalTransition( horz2->rtanimation(), SIGNAL(finished()), horz2 );
vers3->setTargetState(horz3);
QSignalTransition *vers4 =
new QSignalTransition( horz3->rtanimation(), SIGNAL(finished()), horz3 );
vers4->setTargetState(horz4);
QSignalTransition *vers5 =
new QSignalTransition( horz4->rtanimation(), SIGNAL(finished()), horz4 );
vers5->setTargetState(horz5);
QSignalTransition *vers1 =
new QSignalTransition( horz5->rtanimation(), SIGNAL(finished()), horz5 );
vers1->setTargetState(horz1);

QHistoryState *historique = new QHistoryState( zoneanime );

Pause *etatpause = new Pause( (char*)&resultat, machine );
etatpause->addTransition( historique );
zoneanime->addTransition( bhisto, SIGNAL(clicked()), etatpause );
zoneanime->setInitialState( horz1 );
machine->setInitialState( zoneanime );
machine->start();
}

La classe d’état Pause n’est pas détaillée puisqu’elle n’est qu’un conteneur pour la boîte du message et que ses instructions sont conventionnelles, mais dans le fichier source, je propose une alternative à cette classe, alternative mise en commentaire ici, en créant une boîte de message dans la classe Scene avec deux boutons continuer ou quitter le test.

QState *etatpause = new QState(machine);
etatpause->addTransition( historique );

QMessageBox *raz = new QMessageBox();
raz->addButton( QString("continuer"), QMessageBox::AcceptRole );
QPushButton *sortir = raz->addButton( QString("quitter"), QMessageBox::NoRole );
raz->setText("une pause.");

zoneanime->addTransition(bhisto, SIGNAL(clicked()), etatpause);
connect( etatpause, SIGNAL(entered()), raz, SLOT(exec()) );
zoneanime->addTransition( sortir, SIGNAL(clicked()), final );

Dans cette solution, dès l’activation de l’état etatpause le signal entered() est envoyé pour afficher la boîte de dialogue. En sortant de cette boîte de message si le bouton labellisé « quitter » est sollicité, la connexion appelle l’état final et termine le test.

Vous ne trouverez pas le nombre de balles tombées, car la procédure aurait demandé une structure plus élaborée qui sortirait du cadre de cet apprentissage.

Le cours est bouclé, nous avons maintenant les connaissances nécessaires pour interrompre une machine à état en utilisant QHistoryState. Pour parfaire cette étude, la dernière étape va nous montrer l’effet de son paramètre HistoryType utilisé avec les états parallèles.

VIII-B - Étape 05 : profondeur d’historisation et états parallèles

Dans la déclaration d’une instance de la classe QHistoryState, le paramètre HistoryType peut prendre deux valeurs :

- ShallowHistory qui est la valeur par défaut avec laquelle les états enfants immédiats de l'état parent sont historisés ;

- DeepHistory où les états enfants le plus profondément imbriqués sont enregistrés et restitués.

Dans l’étude du programme test de cette étape, je décris uniquement les instructions créant l’état historique, le reste du test étant une réplique de tout ce que nous avons vu jusque-là.

Avec le diagramme qui suit, l’historique fait, qu’après la pause, les animations reprennent là où elles ont été interrompues.

Puisque l’état vertical est un état parallèle, ses enfants, qui sont exclusifs, sont tous actifs. L’état historique qui est enfant de vertical n’est pas affecté par ce parallélisme, n’oublions pas que c’est un pseudo état.

La séquence d’instructions pour ce test est :

QHistoryState *historique = new QHistoryState( QHistoryState::DeepHistory, vertical );

La valeur du paramètre HistoryType, fixée à DeepHistory, implique une recherche des états actifs dans les imbrications d’états enfants. Dans notre cas, il recherche dans chaque état balle son état enfant en fonctionnement, descente ou montée, afin de le restituer à la reprise.

Si nous modifions le paramètre avec la valeur par défaut, ShallowHistory,

QHistoryState *historique = new QHistoryState( QHistoryState::ShallowHistory, vertical );

Nous obtiendrons, après chaque interruption, un comportement insolite lors de la reprise de l’animation de chaque état, car la recherche d’activité se bornera aux états balle, et comme nous sommes dans un environnement parallèle, leurs propres enfants actifs ne seront pas détectés à la pause.

Pour affiner la compréhension de ce paramètre HistoryType, nous changeons le parent de l’état historique en le plaçant dans l’état zoneanime.

Et nous lui donnons le type DeepHistory.

QHistoryState *historique = new( QHistoryState( QHistoryState::DeepHistory, zoneanime );

Nous obtiendrons le même résultat qu’au premier cas. La commande ira chercher les états actifs dans les enfants de l’état vertical, donc tous puisqu’il est état parallèle, et dans chacun d’eux cherchera l’état descente ou montée en cours.

Avec ce troisième et dernier cours, plus aucun obstacle concernant la classe QStateMachine de la bibliothèque Qt sur les machines à états ne vous résistera. Gardez en mémoire les projets « bonus » ils pourraient vous être utiles comme rampe de lancement pour vos tests.

IX - Remerciements

Merci à dourouc05 pour ses conseils et à escartefigue pour la correction.