Qt状态机框架(01):The State Machine Framework【官翻】

文章目录

  • 一、前言
  • 二、状态机框架中的类
  • 三、简单的状态机示例
    • 3.1 首先,我们创建状态机和状态:
    • 3.2 然后,我们使用QState::addTransition()函数来创建转换:
    • 3.3 然后,为QState指定关联对象的属性的值
    • 3.4 接下来,我们将状态添加到机器中,设置机器的初始状态:
    • 3.4 最后,我们启动状态机:
  • 四 、状态进入和退出时的信号
  • 五、状态机的结束
  • 六、通过分组状态共享转换
  • 七、使用历史状态保存和恢复当前状态
  • 八、使用并行状态来避免状态的组合爆炸
  • 九、检测复合状态已经完成
  • 十、无目标转换
  • 十一、事件、转换和保护
  • 十二、使用恢复策略自动恢复属性
  • 十三、动画属性赋值
  • 十四、检测所有属性已在状态中设置
  • 十五、如果在动画完成之前状态就退出了,会发生什么呢
  • 十六、默认的动画
  • 十七、嵌套状态机
  • 十八、总结

一、前言

态机框架提供了用于创建和执行状态图的类。概念和表示法基于Harel的状态图:一种复杂系统的可视化形式《Statecharts: A visual formalism for complex systems》,也是UML状态图的基础。状态机执行的语义基于状态图XML (SCXML)。

态图提供了一种图形化的方式来建模系统如何对刺激做出反应。这是通过定义系统可能处于的状态,以及系统如何从一种状态转移到另一种状态(状态之间的转换)来实现的。事件驱动系统(如Qt应用程序)的一个关键特征是,行为通常不仅依赖于最后的事件或当前事件,还依赖于之前的事件。使用statecharts,这些信息很容易表达。

态机框架提供了一个API和执行模型,可用于在Qt应用程序中有效地嵌入状态图的元素和语义。该框架与Qt的元对象系统集成紧密;例如,状态之间的转换可以由信号触发,状态可以配置为在{QObject}上设置属性和调用方法。Qt的事件系统用于驱动状态机。

态机框架中的状态图是分层的。状态可以嵌套在其他状态中,状态机的当前配置由当前处于活动状态的状态集组成。状态机的有效配置中的所有状态都有一个公共祖先。

二、状态机框架中的类

这些类由qt提供,用于创建事件驱动的状态机。

Class 简介
QAbstractState QStateMachine状态的基类
QAbstractTransition QAbstractState对象之间转换的基类
QEventTransition Qt事件的特定于qobject的转换
QFinalState 最终状态
QHistoryState 返回到以前活动的亚状态的方法
QKeyEventTransition 关键事件的转换
QMouseEventTransition 鼠标事件的转换
QSignalTransition 基于Qt信号的转换
QState qstatemine通用状态
QStateMachine 层次有限状态机
QStateMachine::SignalEvent 表示Qt信号事件
QStateMachine::WrappedEvent 继承QEvent并保存与QObject关联的事件的克隆

Qt状态机框架(01):The State Machine Framework【官翻】_第1张图片

三、简单的状态机示例

为了演示状态机API的核心功能,让我们看一个小示例:一个状态机具有三种状态,即s1、s2和s3。状态机由单个QPushButton控制;单击按钮时,机器转换到另一种状态。最初,状态机处于状态s1。这台机器的状态图如下:
Qt状态机框架(01):The State Machine Framework【官翻】_第2张图片

下面的代码片段显示了创建这样一个状态机所需的代码。

3.1 首先,我们创建状态机和状态:

     QStateMachine machine;
     QState *s1 = new QState();
     QState *s2 = new QState();
     QState *s3 = new QState();

3.2 然后,我们使用QState::addTransition()函数来创建转换:

     s1->addTransition(button, &QPushButton::clicked, s2);
     s2->addTransition(button, &QPushButton::clicked, s3);
     s3->addTransition(button, &QPushButton::clicked, s1);

3.3 然后,为QState指定关联对象的属性的值

    s1->assignProperty(button, "pos", QPoint(10,10));
    s2->assignProperty(button, "pos", QPoint(337,10));
    s3->assignProperty(button, "pos", QPoint(173,100));

3.4 接下来,我们将状态添加到机器中,设置机器的初始状态:

     machine.addState(s1);
     machine.addState(s2);
     machine.addState(s3);
     machine.setInitialState(s1);

3.4 最后,我们启动状态机:

machine.start();

状态机异步执行,即它成为应用程序事件循环的一部分。

四 、状态进入和退出时的信号

述状态机从一种状态转换到另一种状态,使用了QState::assignProperty()函数在进入状态时将状态设置为QObject的属性。在下面的代码片段中,给QPushButton的pos属性的值在每个状态分配一个值:

    s1->assignProperty(button, "pos", QPoint(10,10));
    s2->assignProperty(button, "pos", QPoint(337,10));
    s3->assignProperty(button, "pos", QPoint(173,100));

进入任意一个状态时,按钮的位置将相应地改变。

入状态时发出QState::entered()信号,退出状态时发出QState::exited()信号。在下面的代码片段中,当进入和退出s3时,lamda表达式将被执行,打印相应的信息。

    QObject::connect(s3, &QState::entered, [=]{
        qDebug() << "QState::entered";
    });
    QObject::connect(s3, &QState::exited, [=]{
        qDebug() << "QState::exited";
    });

自定义状态可以重新实现QAbstractState::onEntry()和QAbstractState::onExit()。

Qt状态机框架(01):The State Machine Framework【官翻】_第3张图片

五、状态机的结束

上一节中定义的状态机永远不会结束。为了让状态机能够完成,它需要有一个顶级的最终状态(QFinalState对象)。当状态机进入顶级最终状态时,机器将发出QStateMachine::finished()信号并停止。

要在图中引入最终状态,您需要做的就是创建一个QFinalState对象,并将其用作一个或多个转换的目标。完整代码如下:

#include "Widget.h"

#include 

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;
    QStateMachine machine;
    QState *s1 = new QState();
    QState *s2 = new QState();
    QState *s3 = new QState();

    QPushButton *button = new QPushButton("SimpleStateMachine",&w);
    s1->addTransition(button, &QPushButton::clicked, s2);
    s2->addTransition(button, &QPushButton::clicked, s3);
//    s3->addTransition(button, &QPushButton::clicked, s1);
    QFinalState *fs = new QFinalState;
    s3->addTransition(button, &QPushButton::clicked, fs);

    s1->assignProperty(button, "pos", QPoint(10,10));
    s2->assignProperty(button, "pos", QPoint(337,10));
    s3->assignProperty(button, "pos", QPoint(173,100));

    QObject::connect(s3, &QState::entered, [=]{
        qDebug() << "QState::entered";
    });
    QObject::connect(s3, &QState::exited, [=]{
        qDebug() << "QState::exited";
    });
    machine.addState(s1);
    machine.addState(s2);
    machine.addState(s3);

    machine.addState(fs);

    machine.setInitialState(s1);

    machine.start();

    w.resize(500,200);
    w.show();
    return a.exec();
}

六、通过分组状态共享转换

假设我们希望用户能够通过单击quit按钮随时退出应用程序。为了实现这一点,我们需要创建一个最终状态,并使它成为与Quit按钮的clicked()信号相关联的转换的目标。我们可以从每一个s1 s2 s3添加一个过渡;然而,这似乎是多余的,而且还必须记住从将来添加的每个新状态添加这样的转换。

通过将状态s1、s2和s3分组,我们可以实现相同的行为(即单击Quit按钮退出状态机,而不管状态机处于哪种状态)。这是通过创建一个新的顶级状态并使三个原始状态成为新状态的子状态来实现的。下图显示了新的状态机。

Qt状态机框架(01):The State Machine Framework【官翻】_第4张图片

初的三个状态已被重新命名为s11、s12和s13,以反映它们现在是新的顶级状态s1的子状态。子状态隐式继承其父状态的转换。这意味着现在只需添加一个从s1到最终状态s2的转换即可。添加到s1的新状态也将自动继承这个转换。

状态进行分组所需要的只是在创建状态时指定适当的父状态。您还需要指定哪个子状态是初始状态(即,当父状态是转换的目标时,状态机应该进入哪个子状态)。

#include "Widget.h"
#include 

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;

    QStateMachine machine;
    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);

    QPushButton *button = new QPushButton("SimpleStateMachine",&w);
    s11->addTransition(button, &QPushButton::clicked, s12);
    s12->addTransition(button, &QPushButton::clicked, s13);
    s13->addTransition(button, &QPushButton::clicked, s11);


    s11->assignProperty(button, "pos", QPoint(10,10));
    s12->assignProperty(button, "pos", QPoint(337,10));
    s13->assignProperty(button, "pos", QPoint(173,100));

    QPushButton *quitButton = new QPushButton("Quit",&w);
    QFinalState *s2 = new QFinalState();
    s1->addTransition(quitButton, &QPushButton::clicked, s2);
    machine.addState(s2);
    machine.setInitialState(s1);
  
    //子状态覆盖了s1的过渡,表现为在s12状态下,点击quitButton无效
    s12->addTransition(quitButton, &QPushButton::clicked, s12);

    QObject::connect(&machine, &QStateMachine::finished,
                     QCoreApplication::instance(), &QCoreApplication::quit);
    machine.start();

    w.resize(500,200);
    w.show();
    return a.exec();
}

在本例中,我们希望应用程序在状态机完成时退出,因此机器的finished()信号连接到应用程序的quit()插槽。

子状态可以覆盖继承的过渡。例如,下面的代码添加了一个转换,该转换在状态机处于状态s12时有效地导致忽略Quit按钮。

 s12->addTransition(quitButton, &QPushButton::clicked, s12);

转换可以将任何状态作为其目标,即目标状态不必与源状态在状态层次结构中的同一级别。

七、使用历史状态保存和恢复当前状态

设我们想在上一节讨论的示例中添加一个“中断”机制;用户应该能够单击一个按钮,让状态机执行一些无关的任务,之后,状态机应该恢复它之前正在做的事情(即返回到旧状态,在本例中是s11、s12和s13的状态之一)。

样的行为可以很容易地使用历史状态建模。历史状态(QHistoryState对象)是一种伪状态,它表示上次退出父状态时父状态所处的子状态。

史状态被创建为我们希望为其记录当前子状态的状态的子状态;当状态机在运行时检测到存在这样的状态时,当父状态退出时,它会自动记录当前的(真实的)子状态。向历史状态的转换实际上是向状态机先前保存的子状态的转换;状态机自动将转换“转发”到真实的子状态。
下图显示了添加中断机制后的状态机。
Qt状态机框架(01):The State Machine Framework【官翻】_第5张图片

下面的代码显示了如何实现它;在本例中,我们只是在输入s3时显示一个消息框,然后通过历史状态立即返回到s1的上一个子状态。

Qt状态机框架(01):The State Machine Framework【官翻】_第6张图片

#include "Widget.h"

#include 

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;

    QStateMachine machine;
    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);
    machine.setInitialState(s1);

    QLabel *label = new QLabel("SimpleStateMachine",&w);
    label->setStyleSheet("background:cyan");

    s11->assignProperty(label, "text", "In s11");
    s12->assignProperty(label, "text", "In s12");
    s13->assignProperty(label, "text", "In s13");

    QPushButton *button = new QPushButton("S1",&w);
    button->move(400,0);
    s11->addTransition(button,&QPushButton::clicked,s12);
    s12->addTransition(button,&QPushButton::clicked,s13);
    s13->addTransition(button,&QPushButton::clicked,s11);

    QPushButton *quitButton = new QPushButton("quit",&w);
    quitButton->move(400,170);
    QFinalState *s2 = new QFinalState();
    machine.addState(s2);
    s1->addTransition(quitButton, &QPushButton::clicked, s2);
    QObject::connect(&machine, &QStateMachine::finished,
                     QCoreApplication::instance(), &QCoreApplication::quit);

    QPushButton *interruptButton = new QPushButton("interrupt",&w);
    interruptButton->move(400,30);

    // 为s1 记录历史状态
    QHistoryState *s1h = new QHistoryState(s1);

    QState *s3 = new QState();
    s3->assignProperty(label, "text", "In s3");
    s1->addTransition(interruptButton, &QPushButton::clicked, s3);
    s3->addTransition(s1h);
    machine.addState(s3);

    QMessageBox *mbox = new QMessageBox(&w);
    mbox->addButton(QMessageBox::Ok);
    mbox->setText("Interrupted!");
    mbox->setIcon(QMessageBox::Information);
    QObject::connect(s3, &QState::entered, mbox, &QMessageBox::exec);

    machine.start();

    w.resize(500,200);
    w.show();
    return a.exec();
}

八、使用并行状态来避免状态的组合爆炸

设您希望在单个状态机中对car的一组互斥属性进行建模。假设我们感兴趣的属性是干净与脏,移动与不移动。它需要四种互斥状态和八种转换才能表示并在所有可能的组合之间自由移动。

Qt状态机框架(01):The State Machine Framework【官翻】_第7张图片

果我们添加第三个属性(比如,红色vs蓝色),状态总数将翻倍,达到8个;如果我们添加第四个属性(比如,封闭的vs可转换的),状态的总数将再次翻倍,达到16。

使用并行状态时,状态和转移的总数会随着属性的增加而线性增长,而不是指数增长。此外,可以在并行状态中添加状态或从并行状态中删除状态,而不影响它们的任何兄弟状态。

Qt状态机框架(01):The State Machine Framework【官翻】_第8张图片

创建并行状态组,请将QState::ParallelStates传递给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);

输入一个并行状态组时,它的所有子状态将同时输入。单个子状态中的转换操作正常。但是,任何子状态都可以进行退出父状态的转换。发生这种情况时,父状态及其所有子状态都将退出。

态机框架中的并行性遵循交叉语义。所有并行操作都将在事件处理的单个原子步骤中执行,因此没有事件可以中断并行操作。但是,事件仍将按顺序处理,因为机器本身是单线程的。例如:考虑这样一种情况:有两个转换退出相同的并行状态组,并且它们的条件同时为真。在这种情况下,两个事件中最后一个处理的事件将不会有任何效果,因为第一个事件已经导致机器退出并行状态。

九、检测复合状态已经完成

状态可以是final(一个QFinalState对象);当进入最后一个子状态时,父状态发出QState::finished()信号。下图显示了复合状态s1,它在进入最终状态之前进行了一些处理:

Qt状态机框架(01):The State Machine Framework【官翻】_第9张图片

当进入s1的最终状态时,s1将自动发出finished()。我们使用信号转换来导致该事件触发状态改变:

 s1->addTransition(s1, &QState::finished, s2);

您想隐藏复合状态的内部细节时,在复合状态中使用最终状态非常有用;也就是说,外部世界应该能够做的唯一一件事就是进入状态,并在状态完成工作时获得通知。在构建复杂(深嵌套)状态机时,这是一种非常强大的抽象和封装机制。(在上面的示例中,您当然可以直接从s1的done状态创建转换,而不是依赖于s1的finished()信号,但其结果是暴露并依赖于s1的实现细节)。

于并行状态组,当所有子状态都进入最终状态时,将发出QState::finished()信号。

十、无目标转换

换不需要有目标状态。没有目标的转换可以像任何其他转换一样被触发;不同之处在于,当触发无目标转换时,它不会导致任何状态更改。这允许您在机器处于某种状态时对信号或事件作出反应,而不必离开该状态。例子:

 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);

每次单击按钮时都会显示消息框,但状态机将保持当前状态(s1)。但是,如果目标状态被显式地设置为s1,则每次都会退出并重新进入s1(例如,会发出QAbstractState::entered()和QAbstractState::exited()信号)。

十一、事件、转换和保护

QStateMachine运行自己的事件循环。对于信号转换(QSignalTransition objects), QStateMachine在截取相应信号时,自动向自身发送一个QStateMachine::SignalEvent;类似地,对于QObject事件转换(QEventTransition objects),发布一个QStateMachine::WrappedEvent。

可以使用QStateMachine::postEvent()将自己的事件发布到状态机。

向状态机发布自定义事件时,通常还会有一个或多个可从该类型事件触发的自定义转换。要创建这样的转换,您需要子类化QAbstractTransition并重新实现QAbstractTransition::eventTest(),在这里您检查一个事件是否与您的事件类型匹配(以及可选的其他标准,例如事件对象的属性)。

里我们定义了自己的自定义事件类型StringEvent,用于向状态机发送字符串:

 struct StringEvent : public QEvent
 {
     StringEvent(const QString &val) 
       : QEvent(QEvent::Type(QEvent::User+1)),
         value(val) {}

     QString value;
 };

下来,我们定义一个转换,它只在事件字符串匹配特定字符串时触发(一个被保护的转换):

 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;
 };

eventTest()重新实现中,我们首先检查事件类型是否是所需的事件类型;如果是,我们将事件转换为StringEvent并执行字符串比较。
下面是使用自定义事件和转换的状态图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y1KAK5U4-1603287578536)(https://doc.qt.io/qt-5/images/statemachine-customevents.png)]

statechart的实现是这样的:

     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);

一旦机器启动,我们就可以向它发布事件。

     machine.postEvent(new StringEvent("Hello"));
     machine.postEvent(new StringEvent("world"));

被任何相关转换处理的事件将被状态机静默使用。它可以用于对状态进行分组,并提供此类事件的默认处理;例如,如下图所示:

Qt状态机框架(01):The State Machine Framework【官翻】_第10张图片

于深度嵌套的状态图,您可以在最合适的粒度级别上添加这种“回退”转换。

十二、使用恢复策略自动恢复属性

在某些状态机中,将注意力集中在分配状态中的属性上,而不是在状态不再活动时恢复它们,这可能很有用。如果您知道当机器进入没有显式地给属性赋值的状态时,属性应该始终恢复到初始值,那么您可以将全局恢复策略设置为QStateMachine::RestoreProperties。

 QStateMachine machine;
 machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties);

设置此恢复策略后,计算机将自动恢复所有属性。如果它进入了未设置给定属性的状态,它将首先搜索祖先的层次结构,以查看是否在那里定义了属性。如果是,则该属性将恢复到最近的祖先定义的值。如果没有,它将恢复到初始值(即执行状态中的任何属性分配之前的属性值)。
取以下代码:

     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);

假设在机器启动时属性fooBar为0.0。当机器处于状态s1时,属性将为1.0,因为该状态显式地将该值赋给它。当机器处于状态s2时,没有显式地为该属性定义值,因此它将隐式地恢复为0.0。
如果我们使用嵌套状态,父属性将为属性定义一个值,该值将由没有显式分配值给该属性的所有后代继承。

     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);

这里s1有两个子结点:s2和s3。当输入s2时,属性fooBar的值将为2.0,因为这是为状态显式定义的。当机器处于状态s3时,没有为状态定义值,但是s1将属性定义为1.0,因此这是将分配给fooBar的值。

十三、动画属性赋值

态机API与Qt中的动画API连接,允许在属性以状态分配时自动进行动画化。

我们有以下代码:

     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);

里我们定义了用户界面的两种状态。在s1中按钮很小,而在s2中按钮更大。如果我们单击按钮从s1转换到s2,当进入给定状态时,按钮的几何形状将立即被设置。但是,如果我们想要平滑过渡,我们所需要做的就是制作一个QPropertyAnimation并将其添加到过渡对象中。

     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"));

有问题的属性添加动画意味着属性分配在进入状态后将不再立即生效。相反,当进入状态后,动画将开始播放,并平稳地动画属性分配。由于我们没有设置动画的起始值或结束值,因此将隐式地设置它们。动画的开始值将是动画开始时属性的当前值,结束值将根据为状态定义的属性分配来设置。

果状态机的全局恢复策略被设置为QStateMachine::RestoreProperties,那么还可以为属性恢复添加动画。

十四、检测所有属性已在状态中设置

动画用于分配属性时,状态不再定义机器处于给定状态时属性的确切值。在动画运行时,属性可能有任何值,这取决于动画。在某些情况下,能够检测属性何时实际被分配了由状态定义的值是很有用的。
说我们有以下代码:

     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);

单击按钮时,机器将转换到状态s2,设置按钮的几何形状,然后弹出消息框,通知用户几何形状已被更改。

不使用动画的正常情况下,这将按预期操作。但是,如果在s1和s2之间的转换中设置了按钮几何图形的动画,则动画将在输入s2时启动,但在动画运行完成之前,几何属性实际上不会达到其定义值。在本例中,消息框将在按钮的几何形状实际设置之前弹出。

了确保在几何图形实际达到最终值之前不会弹出消息框,我们可以使用状态的propertiesAssigned()信号。propertiesAssigned()信号将在属性被分配最终值时发出,无论这是立即执行还是在动画完成播放之后执行。

     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);

本例中,当单击按钮时,机器将输入s2。它将保持状态s2,直到几何属性被设置为QRect(0,0,50, 50)。然后它将过渡到s3。当s3被输入时,消息框将弹出。如果到s2的转换有一个几何属性的动画,那么机器将停留在s2中,直到动画完成播放。如果没有这样的动画,它将简单地设置属性并立即进入状态s3。

论哪种方式,当机器处于状态s3时,都可以保证属性几何已经被分配了定义的值。

果将全局恢复策略设置为QStateMachine::RestoreProperties,则该状态将不会发出propertiesAssigned()信号,直到这些也被执行。

十五、如果在动画完成之前状态就退出了,会发生什么呢

果一个状态具有属性分配,并且转换到该状态的属性具有动画,则该状态可能在属性被分配给该状态定义的值之前退出。这在不依赖于propertiesAssigned()信号的状态转换时尤其适用,如前一节所述。

状态机API保证状态机分配的属性:

  • 具有显式分配给该属性的值。

  • 当前正在动画化为显式分配给该属性的值。

在动画完成之前退出状态时,状态机的行为取决于转换的目标状态。如果目标状态显式地给属性赋值,则不会采取任何附加操作。属性将被分配目标状态定义的值。

果目标状态没有给属性分配任何值,那么有两个选项:默认情况下,属性将被分配由它离开的状态定义的值(如果动画被允许完成播放,它将被分配的值)。但是,如果设置了全局恢复策略,则优先执行该策略,并且该属性将像往常一样恢复。

十六、默认的动画

前所述,可以向转换添加动画,以确保目标状态中的属性分配是动画的。如果您希望为给定属性使用特定的动画,而不管采用哪种转换,您可以将其作为默认动画添加到状态机中。在构造计算机时,当特定状态分配(或恢复)的属性不知道时,这尤其有用。

 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"));

机器处于s2状态时,机器将播放属性fooBar的默认动画,因为该属性是由s2分配的。

意,在过渡上显式设置的动画将优先于给定属性的任何默认动画。

十七、嵌套状态机

QStateMachine是QState的一个子类。这允许状态机成为另一台机器的子状态。QStateMachine重新实现了QState::onEntry()并调用QStateMachine::start(),这样当进入子状态机时,它将自动开始运行。

状态机算法中,父状态机将子机器视为原子状态。子状态机是自包含的;它维护自己的事件队列和配置。特别要注意的是,子计算机的configuration()不是父计算机配置的一部分(只有子计算机本身是)。

状态机的状态不能指定为父状态机中的转换目标;只有子状态机本身可以。相反,不能将父状态机的状态指定为子状态机中的转换目标。子状态机的finished()信号可用于触发父机器中的转换。

请参阅声明性状态机框架。

十八、总结

这个大纲着实太啰嗦,应该分成两块来讲,而且也不具备大纲的性质,有零有整的解释,感觉有点杂乱。

你可能感兴趣的:(#,Qt,动画框架)