【Qt】状态机框架

英文原文链接:http://doc.qt.io/archives/qt-4.8/statemachine-api.html

状态机框架提供用于创建和执行状态图的类。状态图的概念和符号基于Harel的Statecharts: A visual formalism for complex systems,这也是UML状态图起源。状态机执行的语义是基于State Chart XML (SCXML)

状态图提供了一种用图的形式建模的方法,描述一个系统如何对外界刺激进行反应。它通过定义系统可能的状态以及系统如何进行状态转换实现。事件驱动系统(比如Qt应用)的关键特征是,系统的行为不仅取决于最近一次或当前事件,还取决于更早的事件。使用状态图可以容易的表示这种信息。

状态机框架提供API和执行模型,它们可以有效地将状态图的元素和语义嵌入到Qt应用中。这个框架紧密集成了Qt元对象系统,比如状态转换可以通过信号触发,状态可以利用QObject配置和设置属性调用方法。Qt事件系统也能驱动状态机。

状态图在状态机框架中是分层的。状态可以嵌套在其他状态中,而且状态机的当前配置由一系列当前活动的状态组成。在状态机框架配置中所有有效的状态都有一个共同的原型。

状态机模型中的类

qt提供了以下类,用于创建事件驱动状态机。

QAbstractState

The base class of states of a QStateMachine

QStateMachine 的状态的基类

QAbstractTransition

The base class of transitions between QAbstractState objects

QAbstractState 对象之间转换关系的基类

QEventTransition

QObject-specific transition for Qt events

Qt事件驱动的转换关系

QFinalState

Final state

终态

QHistoryState

Means of returning to a previously active substate

返回到先前活动的子状态的方法

QKeyEventTransition

Transition for key events

按键事件驱动的转换关系

QMouseEventTransition

Transition for mouse events

鼠标事件驱动的转换关系

QSignalTransition

Transition based on a Qt signal

Qt信号驱动的转换关系

QState

General-purpose state for QStateMachine

QStateMachine 的一般用途状态

QStateMachine

Hierarchical finite state machine

分级有限状态机

QStateMachine::SignalEvent

Represents a Qt signal event

表示Qt信号事件

QStateMachine::WrappedEvent

Inherits QEvent and holds a clone of an event associated with a QObject

继承QEvent并持有与QObject相关联的事件的克隆

一个简单的状态机

下面的小栗子演示了状态机API的核心功能:一个状态机和三个状态s1, s2, s3。状态机由一个单独的QPushButton控制;当按钮被按下,状态机会转换到其他状态。最初,状态机的状态是s1。这个状态机的状态图如下:

【Qt】状态机框架_第1张图片

下面的代码片段演示如何创建这样的一个状态机。首先,创建状态机和状态:

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

然后,创建通过 QState::addTransition() 创建转换关系:

    s1->addTransition(button, SIGNAL(clicked()), s2);
    s2->addTransition(button, SIGNAL(clicked()), s3);
    s3->addTransition(button, SIGNAL(clicked()), s1);
接下来,给状态机添加状态并设置初始状态:
    machine.addState(s1);
    machine.addState(s2);
    machine.addState(s3);
    machine.setInitialState(s1);
最后,启动状态机:
    machine.start();
状态机是异步执行的,即它成为应用程序的事件循环的一部分。

状态的进入和退出

前面的状态机仅仅只能切换状态,但不运行任何操作。QState::assignProperty() 可以用于进入某个状态时,给一个 QObject 设置属性。下面的代码片段中,每个状态都为QLabel的文本属性指定了值:

    s1->assignProperty(label, "text", "In state s1");
    s2->assignProperty(label, "text", "In state s2");
    s3->assignProperty(label, "text", "In state s3");

当进入任意状态时,label的文本将会由此改变。

进入某状态时,会发射 QState::entered() 信号,离开某状态时,会发射 QState::exited()  信号。下面的代码中,当进入状态S3时,按钮的 showMaximized() 槽将被调用,离开时,将调用 showMinimized() 。

    QObject::connect(s3, SIGNAL(entered()), button, SLOT(showMaximized()));
    QObject::connect(s3, SIGNAL(exited()), button, SLOT(showMinimized()));
重载   QAbstractState::onEntry () 和  QAbstractState::onExit () 可以定义状态。

状态机的终结

前面定义的状态机不会终结。为了状态机能有终结,它需要有一个顶层的终态 (QFinalState object)。当状态机机进入一个顶层终态时,状态机将会发射信号 QStateMachine::finished() 并停止。

引入一个终态只需要创建一个  QFinalState 对象,并用其作为状态转换关系的目标。

通过分组状态共享转换关系

假设我们可以在任何时候点击Quit按钮退出应用。为了实现这个目的,需要创建一个终态,并通过Quit按钮的 clicked() 信号触发转换关系的目标。可以为s1, s2, s3 都增加转换关系,但这种方法十分冗长,而且还要记得以后为每种新增的状态都需要增加这种转换。

我们可以用过给状态s1, s2, s3 分组的方法实现相同的行为(即无论状态机处于哪种状态,点击Quit按钮均能退出状态机)。这是通过建立一个新的顶层状态实现的,之前的三个状态则作为这个顶层状态子状态。下图显示了新的状态机。

【Qt】状态机框架_第2张图片

之前的三个状态被重命名为了s11, s12, s13,表示它们现在时新的顶层状态s1的子状态。子状态隐式继承了父状态的转换关系。这意味着现在可以简单的增加一个从s1到s2的转换关系。添加到s1的新状态也将自动继承这一转换关系。

分组状态只需要在创建状态时指定其父状态,还需要指定一个子状态作为初始状态(例如,当转换目标为父状态时,状态机将进入哪一个子状态)。

    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, SIGNAL(clicked()), s2);
    machine.addState(s2);

    QObject::connect(&machine, SIGNAL(finished()), QApplication::instance(), SLOT(quit()));

这个例子中,我们希望当状态机结束时,退出程序,因此状态机的 finished() 信号与应用的 quit() 槽相连。

子状态可以覆盖继承的转换。例如,下面的代码增加了这样的转换关系,将使得 Quit 按钮会在s12状态下无视。

    s12->addTransition(quitButton, SIGNAL(clicked()), s12);
一个转换关系可以以任何状态作为其目标,例如,目标状态不必与源状态的层级相同。

用历史状态保存和还原当前状态

试想我们需要对前面的状态机增加一个“中断”机制;用户应可以点击一个按钮,让状态机执行一些不相关的任务,在这之后,状态机应该重新开始之前的工作(例如,在这个例子中,回到s1, s2, s3中的之前的状态)。

这种行为可以很容易的使用历史状态(history states)建模。一个历史状态(QHistoryState 对象)是一个伪状态,表示上次离开父状态是的子状态。

创建历史状态使其作为一个记录当前子状态的子状态;当状态机运行时检测到这种状态存在时,若父状态存在,它将自动将当前的(真)子状态记录在历史状态中。向历史状态的状态转换实际上是向其记录的子状态转换;状态机自动地“向前”转换到真正的子状态。

下图显示增加了中断机制的状态机。

【Qt】状态机框架_第3张图片

下面的代码展示了它如何实现;在这个例子中,进入s3时简单地显示一个消息框,然后立即离开,通过历史状态返回s1先前的子状态。

    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, SIGNAL(entered()), mbox, SLOT(exec()));
    s3->addTransition(s1h);
    machine.addState(s3);

    s1->addTransition(interruptButton, SIGNAL(clicked()), s3);

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

假设您想在一台状态机中模拟一组互斥的汽车属性。假设我们感兴趣的属性是干净的和脏的,移动的和不移动的。它需要四个互斥的状态和八个转换才能在所有可能的组合之间自由移动。

【Qt】状态机框架_第4张图片


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

使用并行状态,状态的总数和转换的数量是线性增长的,因为我们增加了更多的属性,而不是指数级的。而且,状态可以被添加到或从并行状态中删除,而不会影响它们的任何兄弟状态。

【Qt】状态机框架_第5张图片

为了创建一个并行的状态组,要传递 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);
当进入一个并行状态时,它所有的子状态将同时被进入。单个子状态的转移通常是正常的。然而,任何子状态都可能会经历一个退出父状态的转换。当这种情况发生时,父状态及其子状态都会退出。

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

检测组合状态已完成

子状态可以是终态;进入是终态的子状态时,其父状态会发射 QState::finished() 信号。下图显示了组合状态s1做了某些操作后进入了终态:

【Qt】状态机框架_第6张图片

当进入s1的终态时,s1会自动发射 finished() 信号。用一个信号触发的状态转换关系使其触发一个状态转换:

  s1->addTransition(s1, SIGNAL(finished()), s2);

这是隐藏组合状态内在细节的很实用的方法;比如,外部世界唯一工作时进入状态,然后当状态完成是收到通知。当构建复杂(深度网络)状态机时,这是一种非常有力的抽象和封装机制。(在前面的例子中,当然可以直接创建从s1的完成状态的转换,而 不使用s1的 finish() 信号,但这会导致暴露和依赖s1的实现细节)。

对于并行状态组,所有的子状态都进入终态时, QState::finished() 才会被发射。

无目标的转换关系

一个转换关系不必有一个目标状态。无目标的转换可以与其他转换关系一样被触发;不同之处在于,它不会导致任何状态变化。这会允许状态机对一个信号或事件做出反应时,保留原有状态,不会离开状态。例子:

QStateMachine machine;
QState *s1 = new QState(&machine);

QPushButton button;
QSignalTransition *trans = new QSignalTransition(&button, SIGNAL(clicked()));
s1->addTransition(trans);

QMessageBox msgBox;
msgBox.setText("The button was clicked; carry on.");
QObject::connect(trans, SIGNAL(triggered()), &msgBox, SLOT(exec()));

machine.setInitialState(s1);
每次点击按钮都会展示信息框,状态机也会保持当前状态(s1)。如果转换关系的目标状态被明确设置为了s1本身,则每次会退出并重入状态( 比如,会发射   QAbstractState::entered () 和  QAbstractState::exited () 信号)。

事件、转换关系和守卫

状态机(QStateMachine)运行它自己的事件循环(event loop)。对于信号转换(QSignalTransition objects),状态机截取到对应的信号时,会自动地布置一个状态机信号事件(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
{
public:
    StringTransition(const QString &value)
        : m_value(value) {}

protected:
    virtual bool eventTest(QEvent *e) const
    {
        if (e->type() != QEvent::Type(QEvent::User+1)) // StringEvent
            return false;
        StringEvent *se = static_cast(e);
        return (m_value == se->value);
    }

    virtual void onTransition(QEvent *) {}

private:
    QString m_value;
};

在 eventTest()  的重写中,我们首先检查事件类型,如果事件类型匹配,我们将事件转换为 StringEvent 并进行字符串比对。

下面是使用自定义事件和转换的状态图:

【Qt】状态机框架_第7张图片

状态图的实现如下:

    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】状态机框架_第8张图片

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

使用恢复策略来自动恢复属性




未完待续。。。




你可能感兴趣的:(QT)