状态机框架
状态机框架提供了用于创建和执行状态图的类。其概念和符号基于哈瑞尔的《Statecharts: A visual formalism for complex systems》,该文档也是UML状态图的基础。状态机执行的语义基于State Chart XML(SCXML)。
状态图提供了一种图形化建模系统对刺激的反应方式。这是通过定义系统可能处于的 状态,以及系统如何从一个状态转移到另一个状态(状态之间的转换)来实现的。事件驱动系统(如Qt应用程序)的一个关键特征是行为往往不仅取决于最后或当前事件,还取决于先前的事件。使用状态图,可以轻松表达这些信息。
状态机框架提供了一个API和执行模型,可以有效地在Qt应用程序中嵌入状态图的元素和语义。该框架与Qt的元对象系统紧密集成;例如,状态之间的转换可以由信号触发,并且可以配置状态以在QObject上设置属性和调用方法。Qt的事件系统用于驱动状态机。
状态机框架中的状态图是分层的。状态可以嵌套在其他状态内部,并且状态机的当前配置包括当前活动的状态集。状态机的有效配置中的所有状态将具有共同的祖先。
状态机框架中的类
这些类由Qt提供,用于创建基于事件驱动的状态机。
QAbstractState | QStateMachine 的状态的基类 |
---|---|
QAbstractTransition | QAbstractState 对象之间转换的基类 |
QEventTransition | 专门用于 Qt 事件的转换 |
QFinalState | 最终状态 |
QHistoryState | 返回到之前活动的子状态的方式 |
QKeyEventTransition | 键盘事件的转换 |
QMouseEventTransition | 鼠标事件的转换 |
QSignalTransition | 基于 Qt 信号的转换 |
QState | QStateMachine 的通用状态 |
QStateMachine | 分层有限状态机 |
QStateMachine::SignalEvent | 表示 Qt 信号事件 |
QStateMachine::WrappedEvent | 继承自 QEvent 并持有与 QObject 关联的事件的克隆 |
一个简单的状态机
为了演示状态机 API 的核心功能,让我们看一个小例子:一个拥有三个状态 s1
、s2
和 s3
的状态机。这个状态机由一个单独的 QPushButton 控制;当按钮被点击时,状态机会转移到另一个状态。最初,状态机处于状态 s1
。这个状态机的状态图如下:
以下代码片段显示了创建这样一个状态机所需的代码。首先,我们创建状态机和状态:
QStateMachine machine;
QState *s1 = new QState();
QState *s2 = new QState();
QState *s3 = new QState();
然后,我们使用 QState::addTransition() 函数创建转换:
s1->addTransition(button, &QPushButton::clicked, s2);
s2->addTransition(button, &QPushButton::clicked, s3);
s3->addTransition(button, &QPushButton::clicked, s1);
接下来,我们将状态添加到状态机中,并设置状态机的初始状态:
machine.addState(s1);
machine.addState(s2);
machine.addState(s3);
machine.setInitialState(s1);
最后,我们启动状态机:
machine.start();
状态机是异步执行的,即它成为应用程序事件循环的一部分。
在状态进入和退出时执行有用的工作
上面的状态机仅仅是从一个状态转移到另一个状态,它并没有执行任何操作。可以使用 QState::assignProperty()
函数在状态进入时设置 QObject 的属性。在下面的代码片段中,为每个状态指定了应该分配给 QLabel 的 text 属性的值:
s1->assignProperty(label, "text", "In state s1");
s2->assignProperty(label, "text", "In state s2");
s3->assignProperty(label, "text", "In state s3");
当任何状态被进入时,标签的文本将相应更改。
当状态被进入时,会发射 QState::entered()
信号,当状态被退出时,会发射 QState::exited()
信号。在下面的代码片段中,当状态 s3
被进入时,将调用按钮的 showMaximized()
槽,并且当 s3
被退出时,将调用按钮的 showMinimized()
槽:
QObject::connect(s3, &QState::entered, button, &QPushButton::showMaximized);
QObject::connect(s3, &QState::exited, button, &QPushButton::showMinimized);
自定义状态可以重新实现 QAbstractState::onEntry()
和 QAbstractState::onExit()
。
完成的状态机
在上一节中定义的状态机永远不会完成。为了使状态机能够完成,它需要有一个顶级的 最终 状态(QFinalState 对象)。当状态机进入顶级最终状态时,状态机将发射 finished() 信号并停止。
要在图中引入最终状态,你只需创建一个 QFinalState 对象,并将其用作一个或多个转换的目标即可。
通过分组状态共享转换
假设我们希望用户能够随时通过单击“退出”按钮退出应用程序。为了实现这一点,我们需要创建一个最终状态,并将其作为与“退出”按钮的 clicked() 信号关联的转换的目标。我们可以从 s1
、s2
和 s3
中的每个状态添加一个转换;然而,这似乎是冗余的,并且还需要记住在未来添加的每个新状态中添加这样的转换。
我们可以通过将状态 s1
、s2
和 s3
分组来实现相同的行为(即,单击“退出”按钮退出状态机,无论状态机当前处于哪个状态)。这是通过创建一个新的顶级状态,并使三个原始状态成为新状态的子状态来完成的。下面的图表显示了新的状态机。
原来的三个状态已经被重命名为 s11
、s12
和 s13
,以反映它们现在是新的顶级状态 s1
的子状态。子状态隐式地继承其父状态的转换。这意味着现在只需要从 s1
到最终状态 s2
添加一个转换就足够了。添加到 s1
的新状态也将自动继承这个转换。
要将状态分组,只需在创建状态时指定正确的父状态即可