精华内容
下载资源
问答
  • QT状态机

    千次阅读 2015-03-27 15:13:15
    今天看见一篇将Qt状态机的好文,特地转载,原文出处:http://blog.csdn.net/fuyajun01/article/details/6106201 状态机框架提供了创建和执行状态图的一些类.这些概念和表示都是基于Harel状态图中的一些概念和记...

    今天看见一篇将Qt状态机的好文,特地转载,原文出处:http://blog.csdn.net/fuyajun01/article/details/6106201

    状态机框架提供了创建和执行状态图的一些类.这些概念和表示都是基于Harel状态图中的一些概念和记法.它也是UML状态图表的基础.状态机执行的语义是基于状态图XML(SCXML).

       状态图提供了一种图形化的方式来对一个系统建模,从而反映它怎么响应外部触发.这是通过定义系统可能进入的一些状态以及系统怎么从一个状态转换到另一个状态(不同状态之间转变)来实现的.事件驱动系统的一个关键的特征(例如Qt应用程序)就是行为通常不仅取决于上次或当前事件,还取决于在它之前的一些事件.用状态图,这个信息非常容易表达.

       状态机框架提供了一套API以及一种执行模型,可有效地将状态图的元素和语义嵌入到Qt应用程序当中.该框架与Qt的元对象系统结合紧密:例如,不同状态之间的转变可由信号触发且状态可配置用于设置QObject的属性和方法.Qt的事件系统用于驱动状态机.

       状态机框架中的状态图是分层的.状态可嵌套在另一个状态内.状态机的当前配置包含一些当前活跃的状态.状态机中的一个有效的配置中的所有状态都有一个共同的祖先.

    状态机框架中的类

    qt提供了这些类来创建事件驱动的状态机.

    QAbstractState

    The base class of states of a QStateMachine

    QAbstractTransition

    The base class of transitions between QAbstractState objects

    QEventTransition

    QObject-specific transition for Qt events

    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

    QState

    General-purpose state for QStateMachine

    QStateMachine

    Hierarchical finite state machine

    QStateMachine::SignalEvent

    Represents a Qt signal event

    QStateMachine::WrappedEvent

    Holds a clone of an event associated with a QObject

    一个简单的状态机

    为了演示状态机API的核心功能,让我们来看一个小例子:一个状态机有三个状态s1,s2和s3.状态机由一个按钮来控制;当点击按钮时,状态机转换到另一个状态.刚开始时,状态机处于状态s1.该状态机的状态图如下所示:

    下面代码段显示了创建一个这样的状态机所需的代码.首先,我们创建一个状态机和一些状态:

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

    最后,我们启动状态机:

    状态是异步执行的,例如,它成为你的应用程序事件循环的一部分.

    在状态入口和出口做有意义的工作

    上面的状态机仅仅从一个状态转换到另一个状态,并没有执行任何操作.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()信号.在下面的代码段中,按钮的showMaximize()槽在进入状态s3时被调用.当退出状态s3时调用showMinimized():

        QObject::connect(s3, SIGNAL(entered()), button, SLOT(showMaximized()));

        QObject::connect(s3, SIGNAL(exited()), button, SLOT(showMinimized()));

    自定义的状态可以重新实现方法QAbstractState::onEntry()QAbstractState::onExit().

    完成的状态机

    前面部分定义的状态机从不完成.为了使一个状态机能够完成,它需要拥有一个顶层的最终状态(QFinalState对象).当状态机进入一个顶层最终状态时,该状态机将会释放QStateMachine::finished()信号并停止.

    在图中引入一个最终状态,所有你需要做的就是创建一个QFinalState对象且使用它作为一个或多个转换的目标.

    通过对状态进行分组来共享转换

    假设我们想让用户能够通过点击Quit揿钮在任何时刻能够退出应用程序.为了完成这个目标,我们需要创建一个最终状态并将其作为与Quit按钮的clicked()信号相关联的转换的目标.我们可以从状态s1,s2,s3中添加一个转换;但是,这看起来像是多余的,并且,我们不得不记住从每个将来新加入的状态添加一个这样的转换.

    我们可以通过将状态s1,s2,s3分组取得相同的行为(即点击Quit按钮将退出状态机,无论该状态机处于哪个状态).这是通过创建一个新的顶层状态并使三个原先的状态成为新状态的孩子.如下图显示了新状态机.

     

    三个原先的状态已经重命名为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()槽.

    一个子状态可以覆盖一个继承过来的转换.例如,如下代码添加了一个转换,它有效地造成了当状态机处于状态s12时,Quit按钮将被忽略.

       s12->addTransition(quitButton, SIGNAL(clicked()), s12);

    一个转换可以将任何状态作为它的目标,例如,目标状态不一定要与源状态处于相同的层次. 

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

    假设我们要增加一个“中断”机制到前面提到的例子当中;用户应该能够点击一个按钮使状态机执行一些不相关的任务,任务完成后状态机应该能够恢复到之前执行的任何任务。(例如,返回到旧状态,在此例子中s11,s12,s13中的一个)。

    这样的行为很容易地使用历史状态建模。一个历史状态(QHistoryState对象)是一个伪状态,它代表父状态最后退出时所处的孩子状态。

    一个历史状态创建为某个状态的孩子,用于为其记录当前的孩子状态;当状态机在运行时检测到有这样的一个状态存在时,它在父状态退出时自动地记录当前的孩子状态。到该历史状态的一个转变实际上是到状态机之前保存的子状态的转变。状态机自动地“转发”到真正孩子状态的转变。

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

     

    下面的代码显示了怎么去实现这种机制;在本例中,我们在进入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);

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

    假设你想要在一个状态机中建立一些相互排斥的属性。比如说,我们感兴趣的属性是Clean VS Dirty和Moving VS Not moving。需要采用四个互斥的状态和八个转变才能描述该状态机,并能在各个可能的组合中自由的移动。

     

    如果我们增加第三个属性(比如,Red VS Blue),状态的总数将会翻倍,到8个,且如果我们添加第四个属性(比如,Enclosed VS Convertible),状态的总数将再次翻倍到16个。

    使用并行状态,状态的总数和转变数会随着属性的不断增加线性地增长,而不是指数地增长。而且,从并行状态中添加或移除状态不会影响它们的兄弟状态。

    为了创建一个并行状态组,传递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);

    当一个并行状态组进入时,所有的子状态将会同时进入。每个子状态里的转变正常执行。但是,任何一个子状态可以执行存在于父状态中的一个转变。当这发生时,父状态以及所有的子状态将退出。

    状态机框架的并行机制遵循如下一种交错的语义。所有并行操作将以单步,原子地进行,没有事件可以中断并行操作。但是,事件仍然会被顺序地处理,因为状态机本身是单线程的。举个例子:考虑这样的一个情形,有两个转变从相同的状态组中退出,并且它们的(退出)条件同时变为真。在这种情况下,被处理的事件中的后一个将不会产生任何效果,因为第一个事件已经促使状态机从并行状态中退出了。

    检测某个组合状态已经完成

    一个孩子状态可为最终状态(一个QFinalState对象)。当进入最终状态时,父状态发出QState::finished()信号。下图显示了一个组合状态s1,在进入最终状态之前执行一些处理:

    当进入s1的最终状态时,s1会自动地发出finished()。我们使用一个转变来促使这个事件触发一个状态改变:

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

    在组合状态中使用最终状态是有用的,当你想隐藏一个组合状态的内部细节时;例如,位于该组合状态之外的世界只需能进入到该状态并在该状态完成了其工作时获得通知。在构建复杂的状态机(深度嵌套)时,这是一种非常强大的抽象和封装机制。(在以上例子中,当然你可以创建一个直接从s1的done状态开始的一个转变,而不依赖s1的finished()信号,但是,会造成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,s1会退出并且每次点击的时候进入(例如,会发出QAbstractState::entered()QAbstractState::exited()信号)。

    事件,转变和哨卫

    一个QStateMachine运行在自己的事件循环里,对于信号转变(QSignalTransition对象),当它截获了相应地信号,QStateMachine会自动地发送一个QStateMachine::SignalEvent到自身。类似地,对于QObject事件转变(QEventTransition对象),会发送一个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<StringEvent*>(e);

            return (m_value == se->value);

        }

     

        virtual void onTransition(QEvent *) {}

     

     private:

        QString m_value;

     };

    eventTest()的重载中,我们首先检测了事件类型是否是我们想要的类型。如果是的,我们将事件转换为一个StringEvent并执行字符串比较操作。

    如下是一个使用了自定义事件和转变的状态图:

     

    该状态图的实现代码如下:

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

    没有被任何相关的转变处理的事件将自动由状态处理。这对于分组状态和提供这样的事件的一个默认处理是有用的;例如,如下状态图:

     

    对于深度嵌套的状态图,你可以添加这样的“回退(fallback)”转变

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

    在一些状态机中,在精力集中在对状态中的属性进行赋值是有用的,而不是当状态不再活跃时恢复它们。如果你知道当状态机进入某个状态时,并且在该状态下没有显式地给属性一个值,属性总是应该恢复到它的初始状态,你可以设置全局的策略为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拥有两个孩子:s2s3。当进入s2时,属性fooBar的值为2.0,因为该状态显式地定义了该值。当状态机处于状态s3时,该状态没有定义任何值,但是s1定义了属性的值为1.0,因此,这就是将被赋给fooBar的值。

    动画属性赋值

    状态机API与动画API的连接使得当在状态中设置动画属性时,自动地animating属性。比如,我们有如下代码:

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

    这里,我们定义了用户界面的两个状态,在状态s1中,button小些,在状态s2中,button大些。如果我们点击按钮,从状态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, SIGNAL(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, SIGNAL(entered()), messageBox, SLOT(exec()));

     

        s1->addTransition(button, SIGNAL(clicked()), s2);

    button点击后,状态机将转换到状态s2,它会设置按钮的geometry属性,然后弹出一个信息框来提示用户geometry已经改变。

    在正常情况下,没有使用动画时,该操作会以预期地方式执行。但是,如果在状态s1s2的转变中为button的属性geometry定义了一个动画,该动画将在进入s2时启动,但是,在动画结束运行之前,geometry属性并不会到达它定义的值。在这种情况下,在buttongeometry属性实际被设置之前,会弹出一个信息框。

    为了确保信息框直到geometry达到它的最终值的时候才弹出,我们可以使用状态的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, SIGNAL(entered()), messageBox, SLOT(exec()));

     

        s1->addTransition(button, SIGNAL(clicked()), s2);

        s2->addTransition(s2, SIGNAL(propertiesAssigned()), s3);

    在该例子中,当button点击时,状态机进入s2,当仍然处于状态s2直到属性geometry被设置为QRect(0, 0, 50, 50)。然后,它会转变到s3。当进入s3时,信息框会弹出。如果转变到s2有一个geometry属性的动画,那么状态机将会处于s2中直到动画完成。如果没有这样的动画,它会设置该属性并立即进入状态s3。

    不管什么方式,当状态机处于状态s3,可以保证属性geometry已经被赋予了定义的值。如果全局恢复策略设置为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赋值。注意,显式地设置转变动画比默认动画优先级大。

    今天看见一篇将QT状态机的好文,特地转载,原文出处:http://blog.csdn.net/fuyajun01/article/details/6106201

    状态机框架提供了创建和执行状态图的一些类.这些概念和表示都是基于Harel状态图中的一些概念和记法.它也是UML状态图表的基础.状态机执行的语义是基于状态图XML(SCXML).

       状态图提供了一种图形化的方式来对一个系统建模,从而反映它怎么响应外部触发.这是通过定义系统可能进入的一些状态以及系统怎么从一个状态转换到另一个状态(不同状态之间转变)来实现的.事件驱动系统的一个关键的特征(例如Qt应用程序)就是行为通常不仅取决于上次或当前事件,还取决于在它之前的一些事件.用状态图,这个信息非常容易表达.

       状态机框架提供了一套API以及一种执行模型,可有效地将状态图的元素和语义嵌入到Qt应用程序当中.该框架与Qt的元对象系统结合紧密:例如,不同状态之间的转变可由信号触发且状态可配置用于设置QObject的属性和方法.Qt的事件系统用于驱动状态机.

       状态机框架中的状态图是分层的.状态可嵌套在另一个状态内.状态机的当前配置包含一些当前活跃的状态.状态机中的一个有效的配置中的所有状态都有一个共同的祖先.

    状态机框架中的类

    qt提供了这些类来创建事件驱动的状态机.

    QAbstractState

    The base class of states of a QStateMachine

    QAbstractTransition

    The base class of transitions between QAbstractState objects

    QEventTransition

    QObject-specific transition for Qt events

    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

    QState

    General-purpose state for QStateMachine

    QStateMachine

    Hierarchical finite state machine

    QStateMachine::SignalEvent

    Represents a Qt signal event

    QStateMachine::WrappedEvent

    Holds a clone of an event associated with a QObject

    一个简单的状态机

    为了演示状态机API的核心功能,让我们来看一个小例子:一个状态机有三个状态s1,s2和s3.状态机由一个按钮来控制;当点击按钮时,状态机转换到另一个状态.刚开始时,状态机处于状态s1.该状态机的状态图如下所示:

    下面代码段显示了创建一个这样的状态机所需的代码.首先,我们创建一个状态机和一些状态:

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

    最后,我们启动状态机:

    状态是异步执行的,例如,它成为你的应用程序事件循环的一部分.

    在状态入口和出口做有意义的工作

    上面的状态机仅仅从一个状态转换到另一个状态,并没有执行任何操作.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()信号.在下面的代码段中,按钮的showMaximize()槽在进入状态s3时被调用.当退出状态s3时调用showMinimized():

        QObject::connect(s3, SIGNAL(entered()), button, SLOT(showMaximized()));

        QObject::connect(s3, SIGNAL(exited()), button, SLOT(showMinimized()));

    自定义的状态可以重新实现方法QAbstractState::onEntry()QAbstractState::onExit().

    完成的状态机

    前面部分定义的状态机从不完成.为了使一个状态机能够完成,它需要拥有一个顶层的最终状态(QFinalState对象).当状态机进入一个顶层最终状态时,该状态机将会释放QStateMachine::finished()信号并停止.

    在图中引入一个最终状态,所有你需要做的就是创建一个QFinalState对象且使用它作为一个或多个转换的目标.

    通过对状态进行分组来共享转换

    假设我们想让用户能够通过点击Quit揿钮在任何时刻能够退出应用程序.为了完成这个目标,我们需要创建一个最终状态并将其作为与Quit按钮的clicked()信号相关联的转换的目标.我们可以从状态s1,s2,s3中添加一个转换;但是,这看起来像是多余的,并且,我们不得不记住从每个将来新加入的状态添加一个这样的转换.

    我们可以通过将状态s1,s2,s3分组取得相同的行为(即点击Quit按钮将退出状态机,无论该状态机处于哪个状态).这是通过创建一个新的顶层状态并使三个原先的状态成为新状态的孩子.如下图显示了新状态机.

     

    三个原先的状态已经重命名为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()槽.

    一个子状态可以覆盖一个继承过来的转换.例如,如下代码添加了一个转换,它有效地造成了当状态机处于状态s12时,Quit按钮将被忽略.

       s12->addTransition(quitButton, SIGNAL(clicked()), s12);

    一个转换可以将任何状态作为它的目标,例如,目标状态不一定要与源状态处于相同的层次. 

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

    假设我们要增加一个“中断”机制到前面提到的例子当中;用户应该能够点击一个按钮使状态机执行一些不相关的任务,任务完成后状态机应该能够恢复到之前执行的任何任务。(例如,返回到旧状态,在此例子中s11,s12,s13中的一个)。

    这样的行为很容易地使用历史状态建模。一个历史状态(QHistoryState对象)是一个伪状态,它代表父状态最后退出时所处的孩子状态。

    一个历史状态创建为某个状态的孩子,用于为其记录当前的孩子状态;当状态机在运行时检测到有这样的一个状态存在时,它在父状态退出时自动地记录当前的孩子状态。到该历史状态的一个转变实际上是到状态机之前保存的子状态的转变。状态机自动地“转发”到真正孩子状态的转变。

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

     

    下面的代码显示了怎么去实现这种机制;在本例中,我们在进入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);

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

    假设你想要在一个状态机中建立一些相互排斥的属性。比如说,我们感兴趣的属性是Clean VS Dirty和Moving VS Not moving。需要采用四个互斥的状态和八个转变才能描述该状态机,并能在各个可能的组合中自由的移动。

     

    如果我们增加第三个属性(比如,Red VS Blue),状态的总数将会翻倍,到8个,且如果我们添加第四个属性(比如,Enclosed VS Convertible),状态的总数将再次翻倍到16个。

    使用并行状态,状态的总数和转变数会随着属性的不断增加线性地增长,而不是指数地增长。而且,从并行状态中添加或移除状态不会影响它们的兄弟状态。

    为了创建一个并行状态组,传递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);

    当一个并行状态组进入时,所有的子状态将会同时进入。每个子状态里的转变正常执行。但是,任何一个子状态可以执行存在于父状态中的一个转变。当这发生时,父状态以及所有的子状态将退出。

    状态机框架的并行机制遵循如下一种交错的语义。所有并行操作将以单步,原子地进行,没有事件可以中断并行操作。但是,事件仍然会被顺序地处理,因为状态机本身是单线程的。举个例子:考虑这样的一个情形,有两个转变从相同的状态组中退出,并且它们的(退出)条件同时变为真。在这种情况下,被处理的事件中的后一个将不会产生任何效果,因为第一个事件已经促使状态机从并行状态中退出了。

    检测某个组合状态已经完成

    一个孩子状态可为最终状态(一个QFinalState对象)。当进入最终状态时,父状态发出QState::finished()信号。下图显示了一个组合状态s1,在进入最终状态之前执行一些处理:

    当进入s1的最终状态时,s1会自动地发出finished()。我们使用一个转变来促使这个事件触发一个状态改变:

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

    在组合状态中使用最终状态是有用的,当你想隐藏一个组合状态的内部细节时;例如,位于该组合状态之外的世界只需能进入到该状态并在该状态完成了其工作时获得通知。在构建复杂的状态机(深度嵌套)时,这是一种非常强大的抽象和封装机制。(在以上例子中,当然你可以创建一个直接从s1的done状态开始的一个转变,而不依赖s1的finished()信号,但是,会造成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,s1会退出并且每次点击的时候进入(例如,会发出QAbstractState::entered()QAbstractState::exited()信号)。

    事件,转变和哨卫

    一个QStateMachine运行在自己的事件循环里,对于信号转变(QSignalTransition对象),当它截获了相应地信号,QStateMachine会自动地发送一个QStateMachine::SignalEvent到自身。类似地,对于QObject事件转变(QEventTransition对象),会发送一个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<StringEvent*>(e);

            return (m_value == se->value);

        }

     

        virtual void onTransition(QEvent *) {}

     

     private:

        QString m_value;

     };

    eventTest()的重载中,我们首先检测了事件类型是否是我们想要的类型。如果是的,我们将事件转换为一个StringEvent并执行字符串比较操作。

    如下是一个使用了自定义事件和转变的状态图:

     

    该状态图的实现代码如下:

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

    没有被任何相关的转变处理的事件将自动由状态处理。这对于分组状态和提供这样的事件的一个默认处理是有用的;例如,如下状态图:

     

    对于深度嵌套的状态图,你可以添加这样的“回退(fallback)”转变

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

    在一些状态机中,在精力集中在对状态中的属性进行赋值是有用的,而不是当状态不再活跃时恢复它们。如果你知道当状态机进入某个状态时,并且在该状态下没有显式地给属性一个值,属性总是应该恢复到它的初始状态,你可以设置全局的策略为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拥有两个孩子:s2s3。当进入s2时,属性fooBar的值为2.0,因为该状态显式地定义了该值。当状态机处于状态s3时,该状态没有定义任何值,但是s1定义了属性的值为1.0,因此,这就是将被赋给fooBar的值。

    动画属性赋值

    状态机API与动画API的连接使得当在状态中设置动画属性时,自动地animating属性。比如,我们有如下代码:

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

    这里,我们定义了用户界面的两个状态,在状态s1中,button小些,在状态s2中,button大些。如果我们点击按钮,从状态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, SIGNAL(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, SIGNAL(entered()), messageBox, SLOT(exec()));

     

        s1->addTransition(button, SIGNAL(clicked()), s2);

    button点击后,状态机将转换到状态s2,它会设置按钮的geometry属性,然后弹出一个信息框来提示用户geometry已经改变。

    在正常情况下,没有使用动画时,该操作会以预期地方式执行。但是,如果在状态s1s2的转变中为button的属性geometry定义了一个动画,该动画将在进入s2时启动,但是,在动画结束运行之前,geometry属性并不会到达它定义的值。在这种情况下,在buttongeometry属性实际被设置之前,会弹出一个信息框。

    为了确保信息框直到geometry达到它的最终值的时候才弹出,我们可以使用状态的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, SIGNAL(entered()), messageBox, SLOT(exec()));

     

        s1->addTransition(button, SIGNAL(clicked()), s2);

        s2->addTransition(s2, SIGNAL(propertiesAssigned()), s3);

    在该例子中,当button点击时,状态机进入s2,当仍然处于状态s2直到属性geometry被设置为QRect(0, 0, 50, 50)。然后,它会转变到s3。当进入s3时,信息框会弹出。如果转变到s2有一个geometry属性的动画,那么状态机将会处于s2中直到动画完成。如果没有这样的动画,它会设置该属性并立即进入状态s3。

    不管什么方式,当状态机处于状态s3,可以保证属性geometry已经被赋予了定义的值。如果全局恢复策略设置为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赋值。注意,显式地设置转变动画比默认动画优先级大。

    展开全文
  • qt状态机 animatedtiles.zip
  • qt 状态机

    2010-12-06 11:31:00
    #include "bird.h" #include <QtGui/QApplication> #include <QtGui/qlabel.h> #include #include #include #include #include #include

    展开全文
  • Qt状态机场景模拟

    2018-06-24 18:32:42
    Qt状态机场景模拟 具体详情查看雨田哥博客:https://blog.csdn.net/ly305750665/article/details/78734732
  • 上一篇博客中已经介绍了Qt状态机的基础概念和用法,文章在这里,接下来继续介绍Qt状态机的使用。 历史状态的保存和恢复 前一个示例中,我们通过一个按钮中断状态机,在此基础上,如果我们中断状态机过后想再次回到...

    前言

    上一篇博客中已经介绍了Qt状态机的基础概念和用法,文章在这里,接下来继续介绍Qt状态机的使用。

    历史状态的保存和恢复

    前一个示例中,我们通过一个按钮中断状态机,在此基础上,如果我们中断状态机过后想再次回到之前停下来的地方,这时候就需要使用到历史状态
    历史状态是一个假想的状态,它表示了父状态上次退出时的子状态。

    历史状态通常创建为想要保存的那个状态的子状态。这样在程序运行时,当状态机检测到这种状态的存在就会在父状态退出时自动记录当前的子状态。连接到历史状态的过渡实际上就是连接到状态机上次保存的子状态,状态机会自动的将过渡前移到正在的子状态。下面是执行流程:

    先看一下效果图:
    在这里插入图片描述

    代码如下:

    #include <QWidget>
    #include <QState>
    #include <QStateMachine>
    #include <QFinalState>
    #include <QHistoryState>
    
    namespace Ui {
    class Widget;
    }
    
    class Widget : public QWidget
    {
        Q_OBJECT
    
    public:
        explicit Widget(QWidget *parent = nullptr);
        ~Widget();
    
    private slots:
        void onOutputMessage();
    
    private:
        Ui::Widget *ui;
        QStateMachine * m_pStateMachine = nullptr;
        QState * m_pState1 = nullptr;
        QState * m_pState2 = nullptr;
        QState * m_pState3 = nullptr;
    
        QState * m_pStateParent = nullptr;
        QFinalState * m_pFinalState = nullptr;
        QHistoryState * m_pHistoryState = nullptr;
    };
    
    Widget::Widget(QWidget *parent) :
        QWidget(parent),
        ui(new Ui::Widget)
    {
        ui->setupUi(this);
    
        m_pStateMachine = new QStateMachine(this);
    
        m_pStateParent = new QState();
    
        m_pState1 = new QState(m_pStateParent);
        m_pState2 = new QState(m_pStateParent);
        m_pState3 = new QState(m_pStateParent);
    
        m_pStateParent->assignProperty(ui->label, "text", "In s1");
        m_pState1->assignProperty(ui->btn2,"pos",QPoint(20,40));
        m_pState2->assignProperty(ui->btn2,"pos",QPoint(80,40));
        m_pState3->assignProperty(ui->btn2,"pos",QPoint(120,40));
    
        m_pState1->addTransition(ui->btn1,SIGNAL(clicked()),m_pState2);
        m_pState2->addTransition(ui->btn1,SIGNAL(clicked()),m_pState3);
        m_pState3->addTransition(ui->btn1,SIGNAL(clicked()),m_pState1);
    
        m_pStateParent->setInitialState(m_pState1);
    
        m_pFinalState = new QFinalState();
    
        m_pStateParent->addTransition(ui->btn3,SIGNAL(clicked()),m_pFinalState);
        m_pStateMachine->addState(m_pStateParent);
        m_pStateMachine->addState(m_pFinalState);
    
        m_pStateMachine->setInitialState(m_pStateParent);
    
        m_pHistoryState = new QHistoryState(m_pStateParent);
    
        QState *s3 = new QState();
        s3->assignProperty(ui->label, "text", "In s3");
        QMessageBox *mbox = new QMessageBox(this);
        mbox->addButton(QMessageBox::Ok);
        mbox->setText("Interrupted!");
        mbox->setIcon(QMessageBox::Information);
        QObject::connect(s3, SIGNAL(entered()), mbox, SLOT(exec()));
        s3->addTransition(m_pHistoryState);
        m_pStateMachine->addState(s3);
    
        m_pStateParent->addTransition(ui->btn2, SIGNAL(clicked()), s3);
    
        m_pStateMachine->start();
    
    }
    
    Widget::~Widget()
    {
        delete ui;
    }
    
    

    使用并行状态来避免过多的状态组合

    当需要同步执行多个状态时,可以将状态机设置成并行状态组,进入到并行状态后,所有子状态都会同时开始运行,每个子状态的过渡都会正常执行。但是,每一个子状态都有可能退出父状态,如果这样,父状态和它所有的子状态都会结束。

    在Qt状态机框架的并行机制里有一个交错语义。所有的并行操作都是在一个事件处理中独立的、原子的被执行,所以没有事件能打断并行操作。但是,事件仍然是被顺序的处理的,因为状态机本身是单线程的。举个栗子,如果有两个过渡退出同一个并行状态组,并且它们的触发条件同时被满足。在这种情况下,第二个被处理的退出事件将没有任何实际的反应,因为第一个事件已经导致了状态机从并行状态中结束。

    并行状态示例:

          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状态;当进入一个final子状态时,父状态会发出finished() 信号。下图显示了一个组合状态s1在做了一系列的处理后进入了一个final状态:
    在这里插入图片描述

    当s1进入一个final子状态时,s1会自动发出finished() 信号。我们使用一个 信号过渡 来触发一个状态转换:

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

    在组合状态中使用final状态对应想隐藏组合状态的内部细节来说是非常有用的。也就是说,对应外部世界来说,只需要进入这个状态,然后等待这个状态的完成信号即可。这对于构建复杂的状态机来说是一种强有力的的封装和抽象机制。但是,对应并行状态组来说,finishe()信号只有在所以的子状态都进入final状态时才会发出。

    无目标状态的过渡

    一个Transition并不是一定要有一个目标状态,并且没有目标状态的过渡也可以像其他过渡一样被触发。区别是当一个没有目标状态的过渡被触发时,不会导致任何状态的改变。这运行你在状态机进入某个状态时响应一个信号或事件而不必离开那个状态。

    示例:

        m_pStateMachine = new QStateMachine(this);
    
        m_pState1 = new QState();
    
        QSignalTransition *trans = new QSignalTransition(ui->btn1, SIGNAL(clicked()));
    
        m_pState1->addTransition(trans);
    
        m_pStateMachine->addState(m_pState1);
    
        m_pStateMachine->setInitialState(m_pState1);
    
        QMessageBox *mbox = new QMessageBox(this);
        mbox->addButton(QMessageBox::Ok);
        mbox->setText("Interrupted!");
        mbox->setIcon(QMessageBox::Information);
        QObject::connect(trans, SIGNAL(triggered()), mbox, SLOT(exec()));
    
        m_pStateMachine->start();
    

    当每次点击按钮时,都会弹出消息框,但是状态会一直停留在m_pState1,如果显示的把状态机的状态设置为s1,s1状态会结束,然后重新进入该状态。

    展开全文
  • Qt状态机框架介绍(一)

    千次阅读 2019-10-16 14:42:47
    概述 状态机,简写为FSM(Finite State Machine),...Qt状态机的使用场景主要针对比较复杂的界面,或者需要切换不同状态的控件,比如三态按钮,每个状态对应不同的样式,如果自己做状态管理,那就比较麻烦了。 而...

    概述

    状态机,简写为FSM(Finite State Machine),状态机由状态寄存器和组合逻辑电路构成,能够根据控制信号按照预先设定的状态进行状态转移,是协调相关信号动作、完成特定操作的控制中心。
    简单来说,状态机,就是负责执行各种状态的切换。Qt状态机的使用场景主要针对比较复杂的界面,或者需要切换不同状态的控件,比如三态按钮,每个状态对应不同的样式,如果自己做状态管理,那就比较麻烦了。

    而状态机就是解决这种问题的,Qt中的状态机框架为我们提供了很多的API和类,使我们能更容易的在自己的应用程序中集成状态动画。这个框架是和Qt的元对象系统机密结合在一起的。比如,各个状态之间的转换是通过信号触发的,状态可被配置为用来设置QObject对象的属性以及调用其方法。可以说Qt中的状态机就是通过Qt自身的事件系统来驱动的。

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

    状态机框架中的类

    以下类由Qt提供,用于创建事件驱动的状态机。

    在这里插入图片描述

    一个简单的示例

    接下来直接通过一个简单的示例来了解Qt状态机的工作方式。

    最基础的Qt状态机使用流程如下:

    • 创建一个状态机QStateMachine和需要的状态QState
    • 使用QState::addTransition() 函数为这些状态之间添加过渡
    • 将状态添加到状态机进行管理,并为状态机设置一个初始状态
    • 启动状态机

    效果图:

    在这里插入图片描述
    这里btn2有三种状态,分别显示在不同的位置,通过点击Btn1进行状态的切换。

    其状态图如下:

    接下来看看代码:

    #include <QWidget>
    #include <QState>
    #include <QStateMachine>
    
    namespace Ui {
    class Widget;
    }
    
    class Widget : public QWidget
    {
        Q_OBJECT
    
    public:
        explicit Widget(QWidget *parent = nullptr);
        ~Widget();
    
    private slots:
        void onOutputMessage();
    
    private:
        Ui::Widget *ui;
        QStateMachine * m_pStateMachine = nullptr;
        QState * m_pState1 = nullptr;
        QState * m_pState2 = nullptr;
        QState * m_pState3 = nullptr;
    };
    
    
    #include "widget.h"
    #include "ui_widget.h"
    #include <QDebug>
    
    Widget::Widget(QWidget *parent) :
        QWidget(parent),
        ui(new Ui::Widget)
    {
        ui->setupUi(this);
    
        m_pStateMachine = new QStateMachine(this);
    
        m_pState1 = new QState();
        m_pState2 = new QState();
        m_pState3 = new QState();
    
        m_pState1->assignProperty(ui->btn2,"pos",QPoint(20,40));
        m_pState2->assignProperty(ui->btn2,"pos",QPoint(80,40));
        m_pState3->assignProperty(ui->btn2,"pos",QPoint(120,40));
    
        m_pState1->addTransition(ui->btn1,SIGNAL(clicked()),m_pState2);
        m_pState2->addTransition(ui->btn1,SIGNAL(clicked()),m_pState3);
        m_pState3->addTransition(ui->btn1,SIGNAL(clicked()),m_pState1);
    
        m_pStateMachine->addState(m_pState1);
        m_pStateMachine->addState(m_pState2);
        m_pStateMachine->addState(m_pState3);
        m_pStateMachine->setInitialState(m_pState1);
    
        m_pStateMachine->setInitialState(m_pState1);
        m_pStateMachine->start();
    }
    
    Widget::~Widget()
    {
        delete ui;
    }
    

    这样状态机就开始异步的运行了,也就是说,它成为了我们应用程序事件循环的一部分。这也对应了我们上面说的,Qt的状态机是通过Qt自身的事件机制来驱动的。

    状态转换时操作QObject对象

    使用QState::assignProperty() 函数当进入某个状态时让其去修改某个QObject对象的属性。也就是上面使用的m_pState1->assignProperty(ui->btn2,"pos",QPoint(20,40)); 执行该状态时,改变Btn2的位置属性。

    除了操作QObject对象的属性外,我们还能通过状态的转换来调用QObject对象的函数。这是通过使用状态转换时发出的信号完成的。其中,当进入某个状态时会发出QState::enterd() 信号,当退出某个状态时会发出QState::exited() 信号。

        connect(m_pState3,&QState::entered,this,[=](){
            qDebug() << __FUNCTION__ << "state3 entered..";
        });
        connect(m_pState3,&QState::exited,this,[=](){
            qDebug() << __FUNCTION__ << "state3 exited..";
        });
    

    当进入和离开m_pState3状态时,将会输出响应的日志。

    状态机结束

    以上的示例状态机是永远不会停止的,每次点击按钮会一直循环切换不同状态,那现在如果要停止状态机该怎么办呢。

    为了使一个状态机在某种条件下结束,我们需要创建一个顶层的final 状态(QFinalState object) 。当状态机进入一个顶层的final 状态时,会发出finished() 信号后结束。所以,我们只需要为上面的状态图引入一个final 状态,并把它设置为某个过渡的目标状态即可。这样当状态机在某种条件下转换到该状态时,整个状态机结束。

    如果我们想让用户随时通过点击退出按钮来退出整个应用程序。为了实现这个需求,我们需要创建一个final状态并使他成为和按钮的clicked()信号相关联的那个过渡的目标状态。有两种方案:

    • 方法一:为状态s1,s2,s3分别添加一个到final状态的过渡,但这看上去有点多余,代码臃肿,并且不利于将来的扩张。
    • 方法二:将状态s1,s2,s3分成一组。我们通过创建一个新的顶层状态并使s1,s2,s3成为其孩子来完成。下面是这种方法所对应的状态转换图:

    上面的三个状态被重命名为s11,s12,s13以此来表明它们是s1的孩子。子状态会隐式的继承父状态的过渡。这意味着我们目前可以只添加一个s1到final状态s2的过渡即可,s11,s12,s13会继承这个过渡,从而无论在什么状态均可退出应用程序。并且,将来新添加到s1的新的子状态也会自动继承这个过渡。

    所谓的分组,就是只需在创建状态时为其指定一个合适的父状态即可。当然,还需要为这组状态指定一个初始状态,即当s1是某个过渡的目标状态时,状态机应该进入哪个子状态。实现代码如下:

    #include <QWidget>
    #include <QState>
    #include <QStateMachine>
    #include <QFinalState>
    
    namespace Ui {
    class Widget;
    }
    
    class Widget : public QWidget
    {
        Q_OBJECT
    
    public:
        explicit Widget(QWidget *parent = nullptr);
        ~Widget();
    
    private slots:
        void onOutputMessage();
    
    private:
        Ui::Widget *ui;
        QStateMachine * m_pStateMachine = nullptr;
        QState * m_pState1 = nullptr;
        QState * m_pState2 = nullptr;
        QState * m_pState3 = nullptr;
    
        QState * m_pStateParent = nullptr;
        QFinalState * m_pFinalState = nullptr;
    };
    
    #include "widget.h"
    #include "ui_widget.h"
    #include <QDebug>
    
    Widget::Widget(QWidget *parent) :
        QWidget(parent),
        ui(new Ui::Widget)
    {
        ui->setupUi(this);
    
        m_pStateMachine = new QStateMachine(this);
    
        m_pStateParent = new QState();
    
        m_pState1 = new QState(m_pStateParent);
        m_pState2 = new QState(m_pStateParent);
        m_pState3 = new QState(m_pStateParent);
    
        m_pState1->assignProperty(ui->btn2,"pos",QPoint(20,40));
        m_pState2->assignProperty(ui->btn2,"pos",QPoint(80,40));
        m_pState3->assignProperty(ui->btn2,"pos",QPoint(120,40));
    
        m_pState1->addTransition(ui->btn1,SIGNAL(clicked()),m_pState2);
        m_pState2->addTransition(ui->btn1,SIGNAL(clicked()),m_pState3);
        m_pState3->addTransition(ui->btn1,SIGNAL(clicked()),m_pState1);
    
        m_pStateParent->setInitialState(m_pState1);
    
        m_pFinalState = new QFinalState();
        
    	//当点击Btn3时,停止状态机
        m_pStateParent->addTransition(ui->btn3,SIGNAL(clicked()),m_pFinalState);
        m_pStateMachine->addState(m_pStateParent);
        m_pStateMachine->addState(m_pFinalState);
    
        m_pStateMachine->setInitialState(m_pStateParent);
    
        connect(m_pState3,&QState::entered,this,[=](){
            qDebug() << __FUNCTION__ << "state3 entered..";
        });
        connect(m_pState3,&QState::exited,this,[=](){
            qDebug() << __FUNCTION__ << "state3 exited..";
        });
    
        connect(m_pStateMachine,&QStateMachine::finished,this,[=](){
            qDebug()  << __FUNCTION__ ;
        });
    
        m_pStateMachine->start();
    }
    
    Widget::~Widget()
    {
        delete ui;
    }
    

    效果图如下:

    在这里插入图片描述
    以上示例,当点击Btn3时,停止状态机。这时候再点击Btn1就不会再有状态切换。

    以上是基础的状态机使用演示。

    参考地址:https://doc.qt.io/qt-5/statemachine-api.html#?tdsourcetag=s_pctim_aiomsg

    展开全文
  • 认识一下Qt状态机QStateMachine

    千次阅读 2019-02-22 23:57:19
    关于Qt状态机的介绍就懒得说了,网络上一搜一大堆,反正也看不懂。我关心的就是怎么使用,毕竟我只是一个编写应用程序的程序员。简单粗暴的理解一下状态机就是一个管理很多状态的机器。 组成 一个最简单的状态机...
  • 在上一节的例子中,我们使用了Qt状态机框架的分组状态实现了共享状态迁移的功能,基于上一节的状态图,我们考虑这样一种情况,在S1中任意子状态时,我们按下一个按钮中断了状态机,做一些操作,操作完毕后再恢复到...
  • 如何保证Qt状态机的最佳性能 How to ensure the best Qt state machine performance 如果您使用Qt进行应用程序开发,并且使用状态机,那么很可能您正在使用Qt状态机框架。因此,您将使用普通C++或SCXML定义状态机。...
  • Qt状态机场景模拟-续

    千次阅读 2017-12-11 19:04:00
    Qt状态机场景模拟-续 Qt状态机场景模拟-续 简述 效果图 代码 结尾简述 在上篇中Qt状态机场景模拟,应用了状态机和动画消息,做了简单小车过交通灯的模拟效果。这里,我又稍稍做了些效果,小车在前进过程中,轮子...
  • qt 状态机小记

    2021-03-11 17:41:23
    状态机框架与Qt的元对象系统是紧密结合的,例如Qt的事件系统用来驱动状态机状态机中状态间的切换可以由信号来触发。关于状态机可以参考The State Machine Framework关键字。如下的示例中,状态机被一个按钮控制,...
  • Qt 状态机框架学习

    千次阅读 2016-10-21 12:56:30
    Qt状态机框架是基于状态图XML(SCXML) 实现的。从Qt4.6开始,它已经是QtCore模块的一部分。尽管它本身是蛮复杂的一套东西,但经过和Qt的事件系统(event system)、信号槽(signals and slots)及属性系统(property ...
  • 原创文章,可以转载,转载时请以超链接形式保留本文地址、...Qt状态机框架中,有一个特殊的类QFinalState,这个类定义了状态机的终止状态,我们仅需要简单的将QFinalState的对象A加入到状态机中,然后定义一个状态迁
  • Qt状态机框架的一个典型模型
  • Qt状态机代码测试.rar

    2020-06-01 11:27:38
    Qt 纯代码的状态机测试,测试了状态机间的多迁移。使用了字符串名作为事件,发送事件时只需要发送字符串的事件
  • Qt状态机框架

    千次阅读 2017-03-16 10:43:47
     Qt中的状态机框架为我们提供了很多的API和类,使我们能更容易的在自己的应用程序中集成状态动画。这个框架是和Qt的元对象系统机密结合在一起的。比如,各个状态之间的转换是通过信号触发的,状态可被配置为用来...
  • Qt状态机实现动画

    2012-07-16 15:57:08
    Qt使用状态机实现动画,代码简洁清晰,适合新手学习研究
  • Qt状态机实例

    2019-12-27 13:26:09
    Qt4.6引入了动画框架,摆脱了以往控件只能安静的呆在布局里的时代,利用Qt提供的动画框架,我们可以让控件跳起舞来,呵呵,很有趣啊… 在Qt4.7中又引入了Qt quick技术,其中的QML语言也是专门...
  • 一个最简单的状态机应该包括状态机(QStateMachine)、状态(QState)和过渡(QAbstractTransition子类)。 状态机就相当于一个容器,过渡就是将某一个状态切换到另一个状态(当然也可以不切换)。 (2)什么时候...
  • QT状态机的使用

    2015-10-31 11:15:50
    上面这两个类就是QT里面状态机使用所需要的类。 先把做出来的测试界面放上来,后面就好对比着界面来说了: 通过QState初始化几个状态,然后将这些状态通过QStateMachine的addState方法,添 加进状态机。 在正确...
  • Qt状态机有这样一个特性:进入某个状态时会触发QState::entered()信号,离开状态时触发QState::exited()信号,如此,我们使用槽函数关联这两个信号就可以做一些额外的操作了。还是之前的代码,增加两个槽函数,输出...
  • QT状态机框架

    2013-11-06 15:35:55
    状态机(The State Machine Framework) 作者:刘旭晖 Raymond 转载请注明出处 Email:colorant@163.com BLOG:http://blog.csdn.net/colorant/   QT的State Machine Framework是在Qt4.6中引入的,其理论...
  • Qt状态机QStateMachine使用

    千次阅读 2016-03-25 19:04:30
    状态机

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 449
精华内容 179
关键字:

qt状态机