QT—状态机框架

概念

状态机框架提供了一些类来创建和执行(状态图state graphs) ,状态图为一个系统如何对外界激励进行反应提供了一个图形化模型,该模型是通过定义一些系统可能进入的状态以及系统怎样从一个状态切换到另一个状态来实现的。

事件驱动的系统(比如Qt应用程序)的一个关键特性就是**它的行为不仅仅依赖于最后一个或者当前的事件,而且也依赖于将要执行的事件**。
通过使用状态图,这些信息会非常容易地表达。

状态机框架提供了一个API和一个执行模型来有效地将状态图的元素和语义嵌人到Qt应用程序中。

该框架与Qt的元对象系统是紧密结合的,例如,状态间的切换可以由信号来触发。

Qt的事件系统用来驱动状态机。
状态机框架中的状态图是分层的,状态可以嵌套在其他状态中,状态机一个有效配置中的所有状态都拥有一个共同的祖先。

状态机框架在Qt 4.6中被引入。本节内容可以参考The State Machine Frame-work关键字。

创建状态机

下面先来看一个最简单的应用:

假定状态机由一个QPushButton控制,
包含3个状态:即s1、s2和 s3,其中,s1是初始状态。当单击按钮时,状态机切换到另一个状态。

图11-7是该状态机的状态图:

QT—状态机框架_第1张图片

下面通过代码实现:

新建空的Qt项目,名称为mystatemachine,完成后先在项目文件中添加“QT+= widgets”一行代码并保存该文件,然后添加新的 main.cpp文件,并更改其中内容为:

#include 
#include 
#include 
#include 

int main(int argc, char* argv[ ])
{
    QApplication app(argc, argv);
    //创建一个开始按钮
    QPushButton button("State Machine");
    // 创建状态机和三个状态,并将三个状态添加到状态机中
    QStateMachine machine;
    QState *s1 = new QState(&machine);
    QState *s2 = new QState(&machine);
    QState *s3 = new QState(&machine);
    // 为按钮部件的geometry属性分配一个值,当进入该状态时会设置该值
    s1->assignProperty(&button, "geometry", QRect(100, 100, 120, 50));
    s2->assignProperty(&button, "geometry", QRect(300, 100, 120, 50));
    s3->assignProperty(&button, "geometry", QRect(200, 200, 120, 50));
    // 使用按钮部件的单击信号来完成三个状态的切换
    //按下是
	//按下s1到s2,
	//按下s2到s3
	//按下s3到s1
     QSignalTransition *transition1 = s1->addTransition(&button,SIGNAL()),s2);
     QSignalTransition *transition1 = s1->addTransition(&button,SIGNAL()),s3);
     QSignalTransition *transition1 = s1->addTransition(&button,SIGNAL()),s1);
   
    // 设置状态机的初始状态并启动状态机
    machine.setInitialState(s1);
    machine.start();
    button.show();
    return app.exec();
}

要使用一个状态机,则需要先创建该状态机和使用到的状态,可以像这里在创建状态时直接将其添加到状态机中,也可以使用QStateMachine::addState()来添加状态。

创建完状态后要使用assignProperty()函数为QObject对象的属性分配值。

该状态时就可以为QObject对象的这个属性设置该值。然后要使用addTransition()函数来完成一个状态到另一个状态的切换,可以关联QObject对象的一个信号来触发切换

最后要为状态机设置初始状态并启动状态机,这样当状态机启动时就会自动进入初始状态。

状态机是 异步执行 (各语句执行结束的顺序与语句执行开始的顺序并不一定相同)
它会成为应用程序事件循环的一部分

现在可以运行程序,然后单击按钮,查看状态机的运行效果。

QT—状态机框架_第2张图片

当状态机进人一个状态时会发射QState:: entered()信号,而退出一个状态时会发射QState::exited()信号。

可以关联这两个信号来完成一些操作:
例如,在进入s3状态时将按钮最小化,那么可以在程序中调用setInitialState()函数,并在代码前添加如下代码:

      QObject::connect(s3, &QState::entered, &button, &QPushButton::showMinimized);//按下最小化

在状态机中使用动画

如果将状态机API和Qt中的动画API相关联,那么就可以使分配到状态上的属性自动实现动画效果。

在前面的程序中先添加头文件:

#include 
#include 

然后进行状态切换的代码更改如下:

    // 使用按钮部件的单击信号来完成三个状态的切换
       QSignalTransition *transition1 = s1->addTransition(&button,&QPushButton::clicked, s2);
       QSignalTransition *transition1 = s1->addTransition(&button,&QPushButton::clicked, s3);
       QSignalTransition *transition1 = s1->addTransition(&button,&QPushButton::clicked, s1);
    //设置按钮动画效果
      QPropertyAnimation *animation = new QPropertyAnimation(&button, "geometry");
      transition1->addAnimation(animation);
      transition2->addAnimation(animation);
      transition3->addAnimation(animation);

这样就可以在状态切换时使用动画效果了。

QT—状态机框架_第3张图片

在属性上添加动画,就意味着进入一个状态时分配的属性将无法立即生效,而是在进入时开始播放动画,然后以平滑的动画来达到属性分配的值。

这里无须为动画设置开始和结束的值,它们会被隐含地进行设置,开始值就是开始播放动画时属性的当前值,结束值就是状态分配的属性的值


1.默认动画

如果想对一个属性指定一个动画,从而使所有的切换都默认使用这个动画,那么可以在状态机中使用默认动画。

例如,可以将前面程序中3个调用addAnimation()函数的代码使用下面一行代码来代替:

machine.addDefaultAnimation(animation);

注意,如果一个属性明确指定了动画,那么它就会优先于该属性的任何默认动画。

2. 检测状态中的所有属性都已经被设置

首先来看代码:

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,05050));
connect(s2,&QState::entered,messageBox,&QMessageBox::exec);
sl->addTransition(&button,&QPushButton::clicked,s2);

connect(s2,&QState::entered,messageBox,&QMessageBox::exec);

当按钮被单击时,状态机便会进入状态s2,这时会设置按钮的 geometry属性,

s2->assignProperty(&button,"geometry" ,QRectF(0,05050));

QMessageBox * messageBox =new QMessageBox(mainWindow);
messageBox->addButton(QMessageBox::Ok);
messageBox->setText("Button geometry has been set!");
messageBox->setIcon(QMessageBox::Information);

然后弹出一个提示框来告诉用户 geometry属性已经改变。
在正常情况下,没有使用动画时,将会按照期望的操作进行。


然而,如果在s1向s2切换时对geometry属性使用了动画,那么动画将会在进入 s2时启动,而geometry属性不会在动画结束前达到指定的值

在这种情况下,提示框会在geometry属性获得指定的值之前弹出来,这就不是我们想要的结果了。

为了确保直到 geometry属性获得最终的值以后提示框才会弹出来,则可以使用状态的 propertiesAssigned()信号;该信号会在属性被分配到最终的值时被发射,而无论使用了动画与否。

再将上面的处理状态的几行代码更改如下:

QState *s1 = new QState();
QState *s2=new QState();
s2->assignProperty(&button,"geometry" ,QRectF(0,05050));
QState *s3=new QState();
connect(s3,&QState::entered,messageBox,&QMessageBox::exec);
sl->addTransition(&button,&QPushButton::clicked,s2);
s2->addTransition(&button,&QPushButton::clicked,s3);

这样当按钮被单击时,状态机会进入 s2,且它会留在s2直到geometry属性获得最终的值,然后切换到s3。进入s3后再弹出提示框。

如果进入到s2的切换时对geometry属性使用了动画,那么状态机会一直留在s2直到动画播放结束;
如果没有使用动画,则它会简单地设置属性的值,然后立即进入s3。


3.动画结束前退出状态会发生什么

如果一个状态在动画结束前退出了,那么状态机的行为会依赖于切换的目标状态

  • 如果目标状态明确地为该属性分配了一个值,那么该属性就会使用目标状态设置的这个值

  • 如果目标状态没有为该属性分配任何值,这样会有两种选择:
    默认的,该属性会被分配切换时离开的那个状态所定义的值;
    但是如果设置了全局恢复策略,那么,恢复策略指定的值优先。

状态机的其他特性

1. 为状态分组来共享切换

  • 假设要使一个退出按钮在任何时候都可以退出应用程序

那么可以创建一个QFinalState最终状态,然后让它作为切换的目标状态,并且将切换关联到退出按钮的单击信号上

这样虽然可以将最终状态和s1、s2以及s3状态分别进行切换,不过这样看起来很乱,而且如果以后再添加新的状态还要记得让它和最终状态进行切换。
其实可以将s1、s2和s3分组从而达到相同的效果。

这就是创建一个新的状态,然后将这3个状态作为新状态的子状态;

相对于子状态而言,前面直接添加到状态机中的状态都可以看作顶层状态

新的状态机如图11-8所示:

QT—状态机框架_第4张图片

----这里将那3个状态分别重命名为s11、s12和s13,从而表明它们是新的顶层状态s1
的子状态。

子状态会隐含地继承它们父状态的切换,这意味着现在只需要从s1到最终状态s2添加一个切换即可,而新添加到s1中的状态都会自动继承这个切换。

(项目源码mystatemachine)下面首先在前面的程序中添加头文件#include< QFinalState>
,然后更改主函数中的内容,更改后代码如下:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
int main(int argc, char* argv[ ])
{
    QApplication app(argc, argv);
 
    //设置按钮
    QPushButton button("State Machine");
    QPushButton quitButton("Quit");
    //创建状态机
    QStateMachine machine;
    QState *s1 = new QState(&machine); //s1为父状态
    QState *s11 = new QState(s1);
    QState *s12 = new QState(s1);
    QState *s13 = new QState(s1);
    //初始化和转换
    s1->setInitialState(s11);

    s11->assignProperty(&button, "geometry", QRect(100, 100, 120, 50));
    s12->assignProperty(&button, "geometry", QRect(300, 100, 120, 50));
    s13->assignProperty(&button, "geometry", QRect(200, 200, 120, 50));
    QSignalTransition *transition1 = s11->addTransition(&button,
                                                        &QPushButton::clicked, s12);
    QSignalTransition *transition2 = s12->addTransition(&button,
                                                        &QPushButton::clicked, s13);
    QSignalTransition *transition3 = s13->addTransition(&button,
                                                        &QPushButton::clicked, s11);

 
 //动画效果 s13按下最小化 
    QPropertyAnimation *animation = new QPropertyAnimation(&button, "geometry");
    transition1->addAnimation(animation);
    transition2->addAnimation(animation);
    transition3->addAnimation(animation);
    QObject::connect(s13, &QState::entered, &button, &QPushButton::showMinimized);

 //结束QFinalState  s1按下到s2
    QFinalState *s2 = new QFinalState(&machine);
    s1->addTransition(&quitButton, &QPushButton::clicked, s2);
    QObject::connect(&machine, QStateMachine::finished, qApp, &QApplication::quit);

 // 设置状态机的初始状态并启动状态机
    machine.setInitialState(s1);
    machine.start();
    button.show();
    quitButton.move(300, 300);
    quitButton.show();
    
    return app.exec();
}

这里在创建子状态时要指定父状态,而且还要指定初始子状态。

 QState *s1 = new QState(&machine); 

按下退出按钮时会切换到s2状态,为了可以退出应用程序,需要将状态机的finished()信号关联到quit()槽上。

子状态也可以覆盖继承的切换比如要在s12状态时忽略退出按钮,则可以添加如下一行代码:

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

QT—状态机框架_第5张图片
这样前面s12按下到s13被这个覆盖了,s12按下到s12不变

一个切换的目标状态可以是任意的状态,比如目标状态可以和源状态不在状态层次结构的同一个层中。

2. 使用历史状态来保存或者恢复当前状态

假设要在前面的例子中添加一个“中断”机制,当按下一个按钮后可以让状态机执行一些无关的工作,而完成后又可以恢复到以前的状态,这可以通过使用历史状态QHistoryState来完成。

历史状态是一个伪状态,它代表了当父状态退出时所在的那个子状态。历史状态应创建为一个状态的子状态,这个状态就是要记录状态的父状态。

例如,在s1的任何一个子状态(s11、s12、s13)进行了中断,那么要记录的状态就是这个发生中断的子状态,而历史状态应该创建为s1的子状态。当父状态(如s1)退出时,则自动记录当前的子状态(如s11或s12或s13),切换到历史状态实际上就是切换到状态机先前保存的子状态。

添加了中断机制的状态机如图11-9所示:

QT—状态机框架_第6张图片

(项目源码mystatemachine)首先,在前面的程序中添加头文件:#include < QFinalState>

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

然后更改主代码:

int main(int argc, char* argv[ ])
{
    QApplication app(argc, argv);

    QPushButton button("State Machine");
    QPushButton quitButton("Quit");
    QStateMachine machine;
    QState *s1 = new QState(&machine);
    QState *s11 = new QState(s1);
    QState *s12 = new QState(s1);
    QState *s13 = new QState(s1);
    s1->setInitialState(s11);

    s11->assignProperty(&button, "geometry", QRect(100, 100, 120, 50));
    s12->assignProperty(&button, "geometry", QRect(300, 100, 120, 50));
    s13->assignProperty(&button, "geometry", QRect(200, 200, 120, 50));
    QSignalTransition *transition1 = s11->addTransition(&button,
                                                        &QPushButton::clicked, s12);
    QSignalTransition *transition2 = s12->addTransition(&button,
                                                        &QPushButton::clicked, s13);
    QSignalTransition *transition3 = s13->addTransition(&button,
                                                        &QPushButton::clicked, s11);

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

    QPropertyAnimation *animation = new QPropertyAnimation(&button, "geometry");
    transition1->addAnimation(animation);
    transition2->addAnimation(animation);
    transition3->addAnimation(animation);
    QObject::connect(s13, &QState::entered, &button, &QPushButton::showMinimized);

    QFinalState *s2 = new QFinalState(&machine);
    s1->addTransition(&quitButton, &QPushButton::clicked, s2);
    QObject::connect(&machine, QStateMachine::finished, qApp, &QApplication::quit);

    QPushButton interruptButton("interrupt");
    interruptButton.show();
    QHistoryState *s1h = new QHistoryState(s1);
    QState *s3 = new QState(&machine);
    QMessageBox mbox;
    mbox.addButton(QMessageBox::Ok);
    mbox.setText("Interrupted!");
    mbox.setIcon(QMessageBox::Information);
    QObject::connect(s3, &QState::entered, &mbox, &QMessageBox::exec);
    s3->addTransition(s1h);
    s1->addTransition(&interruptButton, &QPushButton::clicked, s3);

    machine.setInitialState(s1);
    machine.start();
    button.show();
    quitButton.move(300, 300);
    quitButton.show();


    return app.exec();
}

这里就跟上面说的,建立一个s3新子状态,然后新建一个按钮interrupt,按下后显示提示框,
QT—状态机框架_第7张图片
完成后进入 s3->addTransition(s1h) ,s1h
这里当进入s3状态时只是简单地显示一个提示框,然后通过历史状态立即返回到先前的子状态。

QT—状态机框架_第8张图片

3. 使用并行状态来避免组合爆炸


假定在一个单一的状态机中包含了一个汽车的一组互斥的属性,例如,clean对
dirty、moving对not moving,则可以使用4个互斥的状态和8个切换来表示所有可能出现的组合,

如图11- 10所示:(二对互斥属性的状态机)
QT—状态机框架_第9张图片

但是如果再添加第三个属性(比如Red对Blue),总的状态数就会翻倍变为8;而如果再添加第四个属性,那么状态总数就会变为16。(公式=2^{n})

使用并行状态就可以使状态的总数线性增长而不是指数增长,而且向并行状态中添加或者移除状态都不会影响其他的兄弟状态,如图11-11所示:

QT—状态机框架_第10张图片
本小节采用的项目源码:mystatemachine。
将前面的代码进行更改.添加头文件# include < QLabel> .然后将main()函数更改如下:

int main(int argc, char* argv[ ])
{
    QApplication app(argc, argv);

    QPushButton button1("clean or not");
    QPushButton button2("moving or not");
    QLabel label;
    QLabel label1(&label);
    QLabel label2(&label);

    QStateMachine machine;
    QState *s1 = new QState(QState::ParallelStates);
    QState *s11 = new QState(s1);
    QState *clean = new QState(s11);
    QState *dirty = new QState(s11);
    
    s11->setInitialState(clean);
    clean->assignProperty(&label1, "text", "clean");
    dirty->assignProperty(&label1, "text", "dirty");
    clean->addTransition(&button1, &QPushButton::clicked, dirty);
    dirty->addTransition(&button1, &QPushButton::clicked, clean);
    QState *s12 = new QState(s1);
    QState *moving = new QState(s12);
    QState *notMoving = new QState(s12);
    s12->setInitialState(notMoving);
    moving->assignProperty(&label2, "text", "moving");
    notMoving->assignProperty(&label2, "text", "not moving");
    moving->addTransition(&button2, &QPushButton::clicked, notMoving);
    notMoving->addTransition(&button2, &QPushButton::clicked, moving);

    machine.addState(s1);
    machine.setInitialState(s1);
    machine.start();
    button1.move(100, 300);
    button1.show();
    button2.move(300, 300);
    button2.show();
    label1.resize(100, 20);
    label2.resize(100, 20);
    label2.move(0, 20);
    label.move(180, 120);
    label.resize(100, 50);
    label.show();

    return app.exec();
}

创建QState对象时使用QState::ParallelStates作为参数来创建一个并行状态组

    QState *s1 = new QState(QState::ParallelStates);
    QState *s11 = new QState(s1);
    QState *clean = new QState(s11);
    QState *dirty = new QState(s11);

进入了一个并行状态组,也就同时进入了它的所有子状态

在独立的子状态间的切换操作可以正常进行,然而任何一个子状态都可以通过切换而退出父状态,这时将退出父状态以及它所有的子状态。


4. 检测复合状态的结束信号

一个子状态可以是一个最终状态,进入了一个最终子状态时,其父状态就会发射 QState::finished()信号

图11-12显示了一个复合状态s1在进入最终状态前进行了一些处理工作。
最终进入最终状态s2
QT—状态机框架_第11张图片

当进入s1的最终状态时,s1会自动发射finished()信号,可以使用信号切换来使这个事件触发一个状态变化:

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

如果想隐藏一个复合状态的内部细节,那么使用复合状态的最终状态是非常有效的。

对于外界来说需要做的只是进人这个状态,然后等该状态完成工作后获得一个通知。当要创建一个复杂的(深嵌套的)状态机时,这将是一个非常强大的抽象和封装机制。


5. 无目标切换

一个切换也可以没有目标状态,一个没有目标状态的切换也可以像其他切换那样被触发。

其不同之处在于,当一个没有目标的切换被触发时,它不会引起任何的状态变化,这样便可以让状态机在一个特定的状态时响应信号或者事件而不用离开这个状态。

例如:

新建一个状态s1,添加按钮,一个信号过渡切换trans,按下按钮后,作用trans,
发射QSignalTransition::triggered对msgBox,即信息框出现,然后trans是s1的过渡切换
即显示"The button was clicked; carry on.

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.");
Q0bject::connect(trans,&QSignalTransition::triggered,&msgBox,&QMessageBox::exec);

machine.setInitialState(s1);

每当按下按钮时都会显示提示框,但是状态机仍然会留在它当前的状态。

但是如果将目标状态显式的设置为s1,那么每次s1都会退出然后重新进入(例如,每次都会发射entered( )和exited()信号)。


6. 事件、切换和守护

QStateMachine在它自己的事件循环中运行。

  • 对于信号切换(QSignalTransition对象),当状态机截获相应的信号时.QStateMachine自动发送QStateMachine::SignalEvent事件给它自己;

  • 相似的,对于QObject 事件切换( QEventTransition对象), QStateMachine将会发送QStateMachine::WrappedEvent事件。

  • 另外,还可以使用QStateMachine::postEvent()发送自定义的事件给状态机,当发送自定义事件时,还需要继承QAbstractTransition类来创建自定义的切换。

更详细的介绍可以参考TheState Machine Framework关键字文档中的相应内容。


7. 使用恢复策略自动恢复属性

当状态分配的属性不再活动时,可能希望将其恢复到初始值,通过设置全局的恢复策略可以使状态机进入一个状态而不用明确指定属性的值。

//设置全局恢复策略
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);

这里设置了恢复策略QStateMachine: :RestoreProperties,假定在状态机开始时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->ssignProperty(object,"fooBar",2.0);
s1->setInitialState(s2);
QState* s3 = new QState(s1);

这里s1拥有s2和s3两个子状态。

  • 当进入s2时,属性fooBar的值为2.0;而当进入s3时,因为没有定义该属性的值,但是s1定义了该属性的值为1.0,所以s3中该属性的值也为1. 0。

状态机的应用可以参考State Machine Examples中的几个示例程序。要将图形视图框架、动画框架和状态机框架综合起来应用,可以参考一下Sub-attaq 演示程序,当然,也可以参考《Qt及QtQuick开发实战精解(第2版)》中的方块游戏实例。


你可能感兴趣的:(QT基础入门,qt,开发语言)