Qt State Machine C++-Leitfaden
Das State Machine Framework bietet Klassen zur Erstellung und Ausführung von Zustandsgraphen. Diese Seite veranschaulicht die wichtigsten Funktionen des Frameworks in C++.
C++-Klassen im State Machine Framework
Die vollständige Liste der C++-Klassen im State Machine Framework finden Sie unter Qt State Machine C++ Classes
Ein einfacher Zustandsautomat
Um die Kernfunktionalität der State Machine API zu demonstrieren, betrachten wir ein kleines Beispiel: Ein Zustandsautomat mit drei Zuständen, s1
, s2
und s3
. Der Zustandsautomat wird von einem einzigen QPushButton gesteuert; wenn die Schaltfläche angeklickt wird, wechselt der Automat in einen anderen Zustand. Zu Beginn befindet sich der Automat im Zustand s1
. Das Zustandsdiagramm für diesen Automaten sieht wie folgt aus:
Der folgende Ausschnitt zeigt den Code, der benötigt wird, um einen solchen Zustandsautomaten zu erstellen. Zunächst erstellen wir den Zustandsautomaten und die Zustände:
QStateMachine machine; QState *s1 = new QState(); QState *s2 = new QState(); QState *s3 = new QState();
Dann erstellen wir die Übergänge mit der Funktion QState::addTransition():
s1->addTransition(button, &QPushButton::clicked, s2); s2->addTransition(button, &QPushButton::clicked, s3); s3->addTransition(button, &QPushButton::clicked, s1);
Anschließend fügen wir die Zustände zum Automaten hinzu und setzen den Anfangszustand des Automaten:
machine.addState(s1); machine.addState(s2); machine.addState(s3); machine.setInitialState(s1);
Schließlich starten wir den Zustandsautomaten:
machine.start();
Der Zustandsautomat wird asynchron ausgeführt, d. h. er wird Teil der Ereignisschleife Ihrer Anwendung.
Nützliche Arbeit beim Eintritt und Verlassen des Zustands
Der obige Zustandsautomat geht lediglich von einem Zustand in einen anderen über, er führt keine Operationen aus. Die Funktion QState::assignProperty() kann verwendet werden, um einen Zustand eine Eigenschaft von QObject setzen zu lassen, wenn der Zustand betreten wird. Im folgenden Ausschnitt wird der Wert, der der Texteigenschaft von QLabel zugewiesen werden soll, für jeden Zustand angegeben:
s1->assignProperty(label, "text", "In state s1"); s2->assignProperty(label, "text", "In state s2"); s3->assignProperty(label, "text", "In state s3");
Wenn einer der Zustände eingegeben wird, wird der Text des Etiketts entsprechend geändert.
Das Signal QState::entered() wird ausgegeben, wenn der Zustand betreten wird, und das Signal QState::exited() wird ausgegeben, wenn der Zustand verlassen wird. Im folgenden Ausschnitt wird der showMaximized()-Slot der Schaltfläche aufgerufen, wenn der Zustand s3
erreicht wird, und der showMinimized()-Slot der Schaltfläche wird aufgerufen, wenn s3
verlassen wird:
QObject::connect(s3, &QState::entered, button, &QPushButton:showMaximized); QObject::connect(s3, &QState::exited, button, &QPushButton::showMinimized);
Benutzerdefinierte Zustände können QAbstractState::onEntry() und QAbstractState::onExit() reimplementieren.
Zustandsautomaten, die beenden
Der im vorigen Abschnitt definierte Zustandsautomat wird nie beendet. Damit ein Zustandsautomat beendet werden kann, muss er einen Endzustand auf oberster Ebene haben (QFinalState object). Wenn der Zustandsautomat in einen Endzustand der obersten Ebene eintritt, gibt er das Signal QStateMachine::finished() aus und hält an.
Um einen Endzustand in den Graphen einzuführen, müssen Sie lediglich ein QFinalState Objekt erstellen und es als Ziel eines oder mehrerer Übergänge verwenden.
Gemeinsame Nutzung von Übergängen durch Gruppierung von Zuständen
Nehmen wir an, der Benutzer soll die Anwendung jederzeit durch Anklicken einer Beenden-Schaltfläche beenden können. Um dies zu erreichen, müssen wir einen Endzustand erstellen und ihn zum Ziel eines Übergangs machen, der mit dem Signal clicked() der Schaltfläche Quit verbunden ist. Wir könnten jeweils einen Übergang von s1
, s2
und s3
hinzufügen; dies erscheint jedoch redundant, und man müsste auch daran denken, einen solchen Übergang von jedem neuen Zustand hinzuzufügen, der in Zukunft hinzugefügt wird.
Wir können dasselbe Verhalten erreichen (nämlich, dass das Anklicken der Quit-Schaltfläche den Zustandsautomaten beendet, unabhängig davon, in welchem Zustand sich der Zustandsautomat befindet), indem wir die Zustände s1
, s2
und s3
gruppieren. Dazu wird ein neuer Top-Level-Zustand erstellt und die drei ursprünglichen Zustände werden zu Kindzuständen des neuen Zustands. Das folgende Diagramm zeigt den neuen Zustandsautomaten.
Die drei ursprünglichen Zustände wurden in s11
, s12
und s13
umbenannt, um zu verdeutlichen, dass sie nun untergeordnete Zustände des neuen Top-Level-Zustands s1
sind. Untergeordnete Zustände erben implizit die Übergänge ihres übergeordneten Zustands. Das bedeutet, dass es jetzt ausreicht, einen einzigen Übergang von s1
zum Endzustand s2
hinzuzufügen. Neue Zustände, die zu s1
hinzugefügt werden, erben auch automatisch diesen Übergang.
Um Zustände zu gruppieren, müssen Sie bei der Erstellung des Zustands nur noch den richtigen Elternteil angeben. Außerdem müssen Sie angeben, welcher der untergeordneten Zustände der Ausgangszustand ist (d. h. in welchen untergeordneten Zustand der Zustandsautomat eintreten soll, wenn der übergeordnete Zustand das Ziel eines Übergangs ist).
QState *s1 = new QState(); QState *s11 = new QState(s1); QState *s12 = new QState(s1); QState *s13 = new QState(s1); s1->setInitialState(s11); machine.addState(s1); QFinalState *s2 = new QFinalState(); s1->addTransition(quitButton, &QPushButton::clicked, s2); machine.addState(s2); machine.setInitialState(s1); QObject::connect(&machine, &QStateMachine::finished, QCoreApplication::instance(), &QCoreApplication::quit);
In diesem Fall möchten wir, dass die Anwendung beendet wird, wenn der Zustandsautomat fertig ist, daher ist das Signal finished() des Automaten mit dem Slot quit() der Anwendung verbunden.
Ein untergeordneter Zustand kann einen geerbten Übergang außer Kraft setzen. Der folgende Code fügt beispielsweise einen Übergang hinzu, der bewirkt, dass die Schaltfläche Beenden ignoriert wird, wenn sich der Zustandsautomat im Zustand s12
befindet.
s12->addTransition(quitButton, &QPushButton::clicked, s12);
Ein Übergang kann einen beliebigen Zustand als Ziel haben, d.h. der Zielzustand muss sich nicht auf der gleichen Ebene in der Zustandshierarchie befinden wie der Ausgangszustand.
Verwendung von Verlaufszuständen zum Speichern und Wiederherstellen des aktuellen Zustands
Stellen Sie sich vor, dass wir das im vorigen Abschnitt besprochene Beispiel um einen "Unterbrechungs"-Mechanismus erweitern wollen; der Benutzer sollte auf eine Schaltfläche klicken können, um den Zustandsautomaten eine nicht damit zusammenhängende Aufgabe ausführen zu lassen, woraufhin der Zustandsautomat das fortsetzen sollte, was er zuvor getan hat (d. h. in den alten Zustand zurückkehren, der in diesem Fall einer von s11
, s12
und s13
ist).
Ein solches Verhalten kann leicht mit Hilfe von History-States modelliert werden. Ein History-Status (QHistoryState object) ist ein Pseudo-Status, der den Child-Status repräsentiert, in dem sich der Parent-Status beim letzten Verlassen des Parent-Status befand.
Wenn der Zustandsautomat zur Laufzeit das Vorhandensein eines solchen Zustands erkennt, zeichnet er automatisch den aktuellen (echten) Unterzustand auf, wenn der übergeordnete Zustand verlassen wird. Ein Übergang in den History-Zustand ist in Wirklichkeit ein Übergang in den Child-Zustand, den der Zustandsautomat zuvor gespeichert hatte; der Zustandsautomat "leitet" den Übergang automatisch in den realen Child-Zustand weiter.
Das folgende Diagramm zeigt den Zustandsautomaten, nachdem der Unterbrechungsmechanismus hinzugefügt wurde.
Der folgende Code zeigt, wie er implementiert werden kann; in diesem Beispiel wird einfach ein Meldungsfenster angezeigt, wenn s3
eingegeben wird, und dann sofort über den Verlaufszustand zum vorherigen Unterzustand von s1
zurückgekehrt.
QHistoryState *s1h = new QHistoryState(s1); QState *s3 = new QState(); s3->assignProperty(label, "text", "In s3"); QMessageBox *mbox = new QMessageBox(mainWindow); mbox->addButton(QMessageBox::Ok); mbox->setText("Interrupted!"); mbox->setIcon(QMessageBox::Information); QObject::connect(s3, &QState::entered, mbox, &QMessageBox::exec); s3->addTransition(s1h); machine.addState(s3); s1->addTransition(interruptButton, &QPushButton::clicked, s3);
Parallele Zustände verwenden, um eine kombinatorische Explosion von Zuständen zu vermeiden
Nehmen wir an, Sie wollten eine Reihe von sich gegenseitig ausschließenden Eigenschaften eines Autos in einem einzigen Zustandsautomaten modellieren. Nehmen wir an, die Eigenschaften, an denen wir interessiert sind, sind sauber vs. schmutzig und in Bewegung vs. nicht in Bewegung. Man bräuchte vier sich gegenseitig ausschließende Zustände und acht Übergänge, um alle möglichen Kombinationen darzustellen und sich frei zwischen ihnen zu bewegen.
Wenn wir eine dritte Eigenschaft hinzufügen (z. B. Rot vs. Blau), würde sich die Gesamtzahl der Zustände auf acht verdoppeln; und wenn wir eine vierte Eigenschaft hinzufügen (z. B. Geschlossen vs. Umwandelbar), würde sich die Gesamtzahl der Zustände noch einmal verdoppeln, auf 16.
Bei der Verwendung von Parallelzuständen wächst die Gesamtzahl der Zustände und Übergänge linear mit dem Hinzufügen weiterer Eigenschaften und nicht exponentiell. Außerdem können Zustände zu einem parallelen Zustand hinzugefügt oder daraus entfernt werden, ohne dass sich dies auf einen ihrer Geschwisterzustände auswirkt.
Um eine parallele Zustandsgruppe zu erstellen, übergeben Sie QState::ParallelStates an den Konstruktor QState.
QState *s1 = new QState(QState::ParallelStates); // s11 and s12 will be entered in parallel QState *s11 = new QState(s1); QState *s12 = new QState(s1);
Wenn eine parallele Zustandsgruppe betreten wird, werden alle ihre Unterzustände gleichzeitig betreten. Übergänge innerhalb der einzelnen Unterzustände funktionieren normal. Allerdings kann jeder der Unterzustände einen Übergang annehmen, der den übergeordneten Zustand verlässt. In diesem Fall werden der übergeordnete Zustand und alle seine untergeordneten Zustände verlassen.
Die Parallelität im State Machine Framework folgt einer verschachtelten Semantik. Alle parallelen Operationen werden in einem einzigen, atomaren Schritt der Ereignisverarbeitung ausgeführt, so dass kein Ereignis die parallelen Operationen unterbrechen kann. Die Ereignisse werden jedoch weiterhin sequentiell verarbeitet, da die Maschine selbst einen einzigen Thread hat. Ein Beispiel: Nehmen wir an, es gibt zwei Übergänge, die dieselbe parallele Zustandsgruppe verlassen, und ihre Bedingungen werden gleichzeitig wahr. In diesem Fall hat das zuletzt verarbeitete Ereignis keine Auswirkungen, da das erste Ereignis bereits dazu geführt hat, dass die Maschine den parallelen Zustand verlässt.
Erkennen, dass ein zusammengesetzter Zustand beendet ist
Ein untergeordneter Zustand kann endgültig sein (ein QFinalState Objekt); wenn ein endgültiger untergeordneter Zustand erreicht wird, sendet der übergeordnete Zustand das Signal QState::finished(). Das folgende Diagramm zeigt einen zusammengesetzten Zustand s1
, der einige Verarbeitungen durchführt, bevor er in einen endgültigen Zustand übergeht:
Wenn der Endzustand von s1
erreicht wird, sendet s1
automatisch das Signal finished(). Wir verwenden einen Signalübergang, damit dieses Ereignis einen Zustandswechsel auslöst:
s1->addTransition(s1, &QState::finished, s2);
Die Verwendung von Endzuständen in zusammengesetzten Zuständen ist nützlich, wenn Sie die internen Details eines zusammengesetzten Zustands verbergen wollen; d.h. das Einzige, was die Außenwelt tun können sollte, ist, den Zustand zu betreten und eine Benachrichtigung zu erhalten, wenn der Zustand seine Arbeit beendet hat. Dies ist ein sehr mächtiger Abstraktions- und Kapselungsmechanismus beim Aufbau komplexer (tief verschachtelter) Zustandsautomaten. (Im obigen Beispiel könnte man natürlich einen Übergang direkt aus dem Zustand s1
's done
erzeugen, anstatt sich auf das Signal s1
's finished() zu verlassen, aber mit der Konsequenz, dass die Implementierungsdetails von s1
offengelegt werden und davon abhängig sind).
Bei parallelen Zustandsgruppen wird das Signal QState::finished() ausgegeben, wenn alle untergeordneten Zustände in den endgültigen Zustand übergegangen sind.
Ziellose Übergänge
Ein Übergang muss keinen Zielzustand haben. Ein Übergang ohne Ziel kann auf die gleiche Weise wie jeder andere Übergang ausgelöst werden; der Unterschied besteht darin, dass ein zielloser Übergang keine Zustandsänderungen verursacht, wenn er ausgelöst wird. So können Sie auf ein Signal oder Ereignis reagieren, wenn sich Ihre Maschine in einem bestimmten Zustand befindet, ohne diesen Zustand verlassen zu müssen. Beispiel:
QStateMachine machine; QState *s1 = new QState(&machine); QPushButton button; QSignalTransition *trans = new QSignalTransition(&button, &QPushButton::clicked); s1->addTransition(trans); QMessageBox msgBox; msgBox.setText("The button was clicked; carry on."); QObject::connect(trans, QSignalTransition::triggered, &msgBox, &QMessageBox::exec); machine.setInitialState(s1);
Die Messagebox wird jedes Mal angezeigt, wenn die Schaltfläche angeklickt wird, aber der Zustandsautomat bleibt in seinem aktuellen Zustand (s1). Würde der Zielzustand jedoch explizit auf s1 gesetzt, würde s1 jedes Mal verlassen und wieder betreten werden (z. B. würden die Signale QAbstractState::entered() und QAbstractState::exited() ausgegeben).
Ereignisse, Übergänge und Wachen
Ein QStateMachine führt seine eigene Ereignisschleife aus. Bei Signalübergängen (QSignalTransition Objekte) sendet QStateMachine automatisch ein QStateMachine::SignalEvent an sich selbst, wenn es das entsprechende Signal abfängt; in ähnlicher Weise wird bei QObject Ereignisübergängen (QEventTransition Objekte) ein QStateMachine::WrappedEvent gesendet.
Sie können Ihre eigenen Ereignisse mit QStateMachine::postEvent() an den Zustandsautomaten senden.
Wenn Sie ein benutzerdefiniertes Ereignis an den Zustandsautomaten senden, haben Sie normalerweise auch einen oder mehrere benutzerdefinierte Übergänge, die durch Ereignisse dieses Typs ausgelöst werden können. Um einen solchen Übergang zu erstellen, untergliedern Sie die Klasse QAbstractTransition und reimplementieren eventTest(), wobei Sie prüfen, ob ein Ereignis mit Ihrem Ereignistyp übereinstimmt (und optional andere Kriterien, z. B. Attribute des Ereignisobjekts).
Hier definieren wir unseren eigenen benutzerdefinierten Ereignistyp, StringEvent
, um Strings an den Zustandsautomaten zu senden:
struct StringEvent : public QEvent { StringEvent(const QString &val) : QEvent(QEvent::Type(QEvent::User+1)), value(val) {} QString value; };
Als Nächstes definieren wir einen Übergang, der nur ausgelöst wird, wenn die Zeichenkette des Ereignisses mit einer bestimmten Zeichenkette übereinstimmt (ein geschützter Übergang):
class StringTransition : public QAbstractTransition { Q_OBJECT public: StringTransition(const QString &value) : m_value(value) {} protected: bool eventTest(QEvent *e) override { if (e->type() != QEvent::Type(QEvent::User+1)) // StringEvent return false; StringEvent *se = static_cast<StringEvent*>(e); return (m_value == se->value); } void onTransition(QEvent *) override {} private: QString m_value; };
In der Neuimplementierung von eventTest() prüfen wir zunächst, ob der Ereignistyp der gewünschte ist; wenn ja, wandeln wir das Ereignis in eine StringEvent
um und führen den Stringvergleich durch.
Es folgt ein Zustandsdiagramm, das das benutzerdefinierte Ereignis und den Übergang verwendet:
So sieht die Implementierung des Zustandsdiagramms aus:
QStateMachine machine; QState *s1 = new QState(); QState *s2 = new QState(); QFinalState *done = new QFinalState(); StringTransition *t1 = new StringTransition("Hello"); t1->setTargetState(s2); s1->addTransition(t1); StringTransition *t2 = new StringTransition("world"); t2->setTargetState(done); s2->addTransition(t2); machine.addState(s1); machine.addState(s2); machine.addState(done); machine.setInitialState(s1);
Sobald die Maschine gestartet ist, können wir Ereignisse an sie senden.
machine.postEvent(new StringEvent("Hello")); machine.postEvent(new StringEvent("world"));
Ein Ereignis, das nicht von einer relevanten Transition behandelt wird, wird vom Zustandsautomaten stillschweigend konsumiert. Es kann sinnvoll sein, Zustände zu gruppieren und eine Standardbehandlung solcher Ereignisse vorzusehen, wie beispielsweise im folgenden Zustandsdiagramm dargestellt:
Bei tief verschachtelten Zustandsdiagrammen können Sie solche "Fallback"-Übergänge auf der am besten geeigneten Granularitätsebene hinzufügen.
Verwendung der Wiederherstellungsrichtlinie zur automatischen Wiederherstellung von Eigenschaften
In einigen Zustandsautomaten kann es sinnvoll sein, die Aufmerksamkeit auf die Zuweisung von Eigenschaften in Zuständen zu richten und nicht auf deren Wiederherstellung, wenn der Zustand nicht mehr aktiv ist. Wenn Sie wissen, dass eine Eigenschaft immer auf ihren Anfangswert zurückgesetzt werden soll, wenn die Maschine in einen Zustand eintritt, der der Eigenschaft nicht explizit einen Wert zuweist, können Sie die globale Wiederherstellungsrichtlinie auf QStateMachine::RestoreProperties setzen.
QStateMachine machine; machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties);
Wenn diese Wiederherstellungsrichtlinie gesetzt ist, wird der Rechner automatisch alle Eigenschaften wiederherstellen. Tritt er in einen Zustand ein, in dem eine bestimmte Eigenschaft nicht gesetzt ist, sucht er zunächst in der Hierarchie der Vorgänger nach, ob die Eigenschaft dort definiert ist. Ist dies der Fall, wird die Eigenschaft auf den vom nächsten Vorfahren definierten Wert zurückgesetzt. Ist dies nicht der Fall, wird der ursprüngliche Wert wiederhergestellt (d. h. der Wert der Eigenschaft, bevor die Zuweisung von Eigenschaften in den Zuständen ausgeführt wurde).
Nehmen Sie den folgenden Code:
QStateMachine machine; machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties); QState *s1 = new QState(); s1->assignProperty(object, "fooBar", 1.0); machine.addState(s1); machine.setInitialState(s1); QState *s2 = new QState(); machine.addState(s2);
Nehmen wir an, die Eigenschaft fooBar
hat beim Start der Maschine den Wert 0,0. Wenn sich die Maschine im Zustand s1
befindet, hat die Eigenschaft den Wert 1.0, da der Zustand ihr diesen Wert ausdrücklich zuweist. Wenn sich die Maschine im Zustand s2
befindet, ist kein Wert für die Eigenschaft explizit definiert, so dass sie implizit auf 0,0 zurückgesetzt wird.
Wenn wir verschachtelte Zustände verwenden, definiert der übergeordnete Zustand einen Wert für die Eigenschaft, der an alle nachfolgenden Zustände vererbt wird, die der Eigenschaft nicht explizit einen Wert zuweisen.
QStateMachine machine; machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties); QState *s1 = new QState(); s1->assignProperty(object, "fooBar", 1.0); machine.addState(s1); machine.setInitialState(s1); QState *s2 = new QState(s1); s2->assignProperty(object, "fooBar", 2.0); s1->setInitialState(s2); QState *s3 = new QState(s1);
Hier hat s1
zwei Kinder: s2
und s3
. Wenn s2
eingegeben wird, hat die Eigenschaft fooBar
den Wert 2.0, da dieser explizit für den Zustand definiert ist. Wenn sich die Maschine im Zustand s3
befindet, ist kein Wert für den Zustand definiert, aber s1
definiert die Eigenschaft als 1.0, so dass dies der Wert ist, der fooBar
zugewiesen wird.
Animationen und Zustandsautomaten
Die Zustandsautomaten-API ist mit dem The Animation Framework verbunden, um die automatische Animation von Eigenschaften zu ermöglichen, wenn sie in Zuständen zugewiesen werden.
Der Zustandsautomat bietet einen speziellen Zustand, der eine Animation abspielen kann. Ein QState kann auch Eigenschaften setzen, wenn der Zustand betreten oder verlassen wird, und dieser spezielle Animationszustand wird zwischen diesen Werten interpolieren, wenn er eine QPropertyAnimation erhält.
Wir können eine oder mehrere Animationen mit einem Übergang zwischen Zuständen verknüpfen, indem wir eine QSignalTransition oder QEventTransition Klasse verwenden. Diese Klassen sind beide von QAbstractTransition abgeleitet, das die Komfortfunktion addAnimation() definiert, die das Anhängen einer oder mehrerer Animationen ermöglicht, die beim Auftreten des Übergangs ausgelöst werden.
Wir haben auch die Möglichkeit, Eigenschaften mit den Zuständen zu verknüpfen, anstatt die Start- und Endwerte selbst zu setzen.
Nehmen wir an, wir haben den folgenden Code:
QState *s1 = new QState(); QState *s2 = new QState(); s1->assignProperty(button, "geometry", QRectF(0, 0, 50, 50)); s2->assignProperty(button, "geometry", QRectF(0, 0, 100, 100)); s1->addTransition(button, &QPushButton::clicked, s2);
Hier definieren wir zwei Zustände einer Benutzeroberfläche. In s1
ist button
klein, und in s2
ist es größer. Wenn wir auf die Schaltfläche klicken, um von s1
zu s2
zu wechseln, wird die Geometrie der Schaltfläche sofort eingestellt, wenn ein bestimmter Zustand erreicht wurde. Wenn der Übergang jedoch fließend sein soll, brauchen wir nur eine QPropertyAnimation zu erstellen und diese dem Übergangsobjekt hinzuzufügen.
QState *s1 = new QState(); QState *s2 = new QState(); s1->assignProperty(button, "geometry", QRectF(0, 0, 50, 50)); s2->assignProperty(button, "geometry", QRectF(0, 0, 100, 100)); QSignalTransition *transition = s1->addTransition(button, &QPushButton::clicked, s2); transition->addAnimation(new QPropertyAnimation(button, "geometry"));
Das Hinzufügen einer Animation für die betreffende Eigenschaft bedeutet, dass die Zuweisung der Eigenschaft nicht mehr sofort wirksam wird, wenn der Zustand betreten wird. Stattdessen beginnt die Animation mit dem Eintritt in den Zustand und animiert die Zuweisung der Eigenschaft sanft. Da wir den Start- und Endwert der Animation nicht festlegen, werden diese implizit festgelegt. Der Startwert der Animation ist der aktuelle Wert der Eigenschaft, wenn die Animation beginnt, und der Endwert wird auf der Grundlage der für den Zustand definierten Eigenschaftszuweisungen festgelegt.
Wenn die globale Wiederherstellungsrichtlinie des Zustandsautomaten auf QStateMachine::RestoreProperties gesetzt ist, ist es möglich, auch Animationen für die Wiederherstellung von Eigenschaften hinzuzufügen.
Erkennen, dass alle Eigenschaften in einem Zustand gesetzt wurden
Wenn Animationen zur Zuweisung von Eigenschaften verwendet werden, definiert ein Zustand nicht mehr die genauen Werte, die eine Eigenschaft haben wird, wenn sich die Maschine in dem gegebenen Zustand befindet. Während die Animation läuft, kann die Eigenschaft potenziell jeden beliebigen Wert haben, je nach Animation.
In einigen Fällen kann es nützlich sein, zu erkennen, wann die Eigenschaft tatsächlich den durch einen Zustand definierten Wert erhalten hat.
Nehmen wir an, wir haben den folgenden Code:
QMessageBox *messageBox = new QMessageBox(mainWindow); messageBox->addButton(QMessageBox::Ok); messageBox->setText("Button geometry has been set!"); messageBox->setIcon(QMessageBox::Information); QState *s1 = new QState(); QState *s2 = new QState(); s2->assignProperty(button, "geometry", QRectF(0, 0, 50, 50)); connect(s2, &QState::entered, messageBox, SLOT(exec())); s1->addTransition(button, &QPushButton::clicked, s2);
Wenn button
angeklickt wird, geht der Rechner in den Zustand s2
über, in dem die Geometrie der Schaltfläche festgelegt wird, und zeigt dann ein Meldungsfenster an, um den Benutzer darauf hinzuweisen, dass die Geometrie geändert wurde.
Im Normalfall, wenn keine Animationen verwendet werden, funktioniert dies wie erwartet. Wenn jedoch eine Animation für die geometry
von button
am Übergang zwischen s1
und s2
eingestellt ist, wird die Animation gestartet, wenn s2
eingegeben wird, aber die Eigenschaft geometry
erreicht ihren definierten Wert nicht, bevor die Animation beendet ist. In diesem Fall wird das Meldungsfenster angezeigt, bevor die Geometrie der Schaltfläche tatsächlich festgelegt wurde.
Um sicherzustellen, dass das Meldungsfenster erst erscheint, wenn die Geometrie tatsächlich ihren endgültigen Wert erreicht hat, können wir das Signal propertiesAssigned() des Zustands verwenden. Das Signal propertiesAssigned() wird ausgegeben, wenn der Eigenschaft der endgültige Wert zugewiesen wird, unabhängig davon, ob dies sofort oder erst nach Beendigung der Animation geschieht.
QMessageBox *messageBox = new QMessageBox(mainWindow); messageBox->addButton(QMessageBox::Ok); messageBox->setText("Button geometry has been set!"); messageBox->setIcon(QMessageBox::Information); QState *s1 = new QState(); QState *s2 = new QState(); s2->assignProperty(button, "geometry", QRectF(0, 0, 50, 50)); QState *s3 = new QState(); connect(s3, &QState::entered, messageBox, SLOT(exec())); s1->addTransition(button, &QPushButton::clicked, s2); s2->addTransition(s2, &QState::propertiesAssigned, s3);
Wenn in diesem Beispiel button
angeklickt wird, wechselt die Maschine in den Zustand s2
. Er bleibt im Zustand s2
, bis die Eigenschaft geometry
auf QRect(0, 0, 50, 50)
gesetzt wurde. Dann geht er in s3
über. Wenn s3
eingegeben wird, wird das Meldungsfenster eingeblendet. Wenn der Übergang zu s2
eine Animation für die Eigenschaft geometry
enthält, bleibt der Rechner im Zustand s2
, bis die Animation abgespielt wurde. Wenn es keine solche Animation gibt, wird die Eigenschaft einfach gesetzt und sofort in den Zustand s3
übergegangen.
Wenn sich der Rechner im Zustand s3
befindet, ist in jedem Fall gewährleistet, dass der Eigenschaft geometry
der definierte Wert zugewiesen wurde.
Wenn die globale Wiederherstellungsrichtlinie auf QStateMachine::RestoreProperties gesetzt ist, wird der Zustand das Signal propertiesAssigned() erst dann ausgeben, wenn auch diese ausgeführt wurden.
Was passiert, wenn ein Zustand verlassen wird, bevor die Animation beendet ist?
Wenn ein Zustand Eigenschaftszuweisungen hat und der Übergang in den Zustand Animationen für die Eigenschaften enthält, kann der Zustand möglicherweise verlassen werden, bevor die Eigenschaften den vom Zustand definierten Werten zugewiesen wurden. Dies ist insbesondere dann der Fall, wenn es Übergänge aus dem Zustand heraus gibt, die nicht vom propertiesAssigned()-Signal abhängen, wie im vorherigen Abschnitt beschrieben.
Die State Machine API garantiert, dass eine vom Zustandsautomaten zugewiesene Eigenschaft entweder:
- einen Wert hat, der der Eigenschaft explizit zugewiesen ist.
- gerade zu einem Wert animiert wird, der der Eigenschaft explizit zugewiesen ist.
Wenn ein Zustand verlassen wird, bevor die Animation abgeschlossen ist, hängt das Verhalten des Zustandsautomaten vom Zielzustand des Übergangs ab. Wenn der Zielzustand der Eigenschaft explizit einen Wert zuweist, wird keine weitere Aktion durchgeführt. Der Eigenschaft wird der durch den Zielzustand definierte Wert zugewiesen.
Wenn der Zielzustand der Eigenschaft keinen Wert zuweist, gibt es zwei Möglichkeiten: Standardmäßig wird der Eigenschaft der Wert zugewiesen, der durch den Zustand definiert ist, den sie verlässt (der Wert, der ihr zugewiesen worden wäre, wenn die Animation zu Ende abgespielt worden wäre). Wenn jedoch eine globale Wiederherstellungsrichtlinie festgelegt ist, hat diese Vorrang, und die Eigenschaft wird wie üblich wiederhergestellt.
Standard-Animationen
Wie bereits beschrieben, können Sie Animationen zu Übergängen hinzufügen, um sicherzustellen, dass Eigenschaftszuweisungen im Zielzustand animiert sind. Wenn Sie möchten, dass eine bestimmte Animation für eine bestimmte Eigenschaft verwendet wird, unabhängig davon, welcher Übergang genommen wird, können Sie diese als Standardanimation zum Zustandsautomaten hinzufügen. Dies ist insbesondere dann nützlich, wenn die Eigenschaften, die durch bestimmte Zustände zugewiesen (oder wiederhergestellt) werden, bei der Konstruktion des Automaten nicht bekannt sind.
QState *s1 = new QState(); QState *s2 = new QState(); s2->assignProperty(object, "fooBar", 2.0); s1->addTransition(s2); QStateMachine machine; machine.setInitialState(s1); machine.addDefaultAnimation(new QPropertyAnimation(object, "fooBar"));
Wenn sich der Automat im Zustand s2
befindet, spielt er die Standardanimation für die Eigenschaft fooBar
ab, da diese Eigenschaft von s2
zugewiesen wird.
Beachten Sie, dass Animationen, die explizit bei Übergängen gesetzt werden, Vorrang vor jeder Standardanimation für die jeweilige Eigenschaft haben.
Verschachtelte Zustandsautomaten
QStateMachine ist eine Unterklasse von QState. Dies ermöglicht es einem Zustandsautomaten, ein Kindzustand eines anderen Automaten zu sein. QStateMachine implementiert QState::onEntry() neu und ruft QStateMachine::start() auf, so dass der untergeordnete Zustandsautomat automatisch gestartet wird, wenn er eingegeben wird.
Der übergeordnete Zustandsautomat behandelt den untergeordneten Automaten als atomaren Zustand im Zustandsautomatenalgorithmus. Der Child-Zustandsautomat ist in sich geschlossen; er verwaltet seine eigene Ereigniswarteschlange und Konfiguration. Insbesondere ist zu beachten, dass die configuration() des untergeordneten Automaten nicht Teil der Konfiguration des übergeordneten Automaten ist (nur der untergeordnete Automat selbst ist Teil der Konfiguration).
Zustände des untergeordneten Zustandsautomaten können nicht als Ziele von Übergängen im übergeordneten Zustandsautomaten angegeben werden; dies kann nur der untergeordnete Zustandsautomat selbst. Umgekehrt können Zustände des übergeordneten Zustandsautomaten nicht als Ziele von Übergängen im untergeordneten Zustandsautomaten angegeben werden. Das Signal finished() des untergeordneten Zustandsautomaten kann verwendet werden, um einen Übergang im übergeordneten Automaten auszulösen.
Siehe auch Qt State Machine Overview und Qt State Machine QML Guide.
© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.