文章目录
- 前言
- 例子一 :官方文档的例子
- 1. 抓住重点:初步认识状态机
- 完整代码
- 2. 抓住重点:状态机中的状态共享 (进一步完善)
- 完整代码
- 3. 抓住重点:使用历史状态保存和恢复当前状态(进一步完善)
- 完整代码
- 4. 抓住重点:使用并行状态来避免状态组合爆炸(进一步完善)
- 完整代码(我没看懂这个部分,代码运行后也没有任何效果)
- 5.检测复合状态是否完成
- 完整代码
- 6.Targetless转换(无目标的转换)
- 完整代码
- 7. Events, Transitions and Guards(事件,过渡和警卫
- 完整代码(从这一部分开始有意思了,与自定义事件进行了结合)
- 8.使用还原策略自动还原属性
- 完整代码
- 9.给属性赋予动画
- 完整代码
- 10.检测所有属性都处于某个状态
- 完整代码
- 11.如果状态在动画完成之前退出会发生什么
- 12.默认的动画(这个不是很懂)
- 完整代码
- 13. 嵌套状态机
- 完结撒花!!!
前言
关于Qt状态机,维基百科是这么讲的:有限状态机,原来状态机又称"有限状态机".
关于qt状态机,我的理解是:qt程序在进入到某一状态后(比如状态A),就会一直保持这个状态(可以简单的理解为:进入了while循环),除非你修改了状态(比如改成了状态B),那么程序才会停止状态A的行为,转而执行状态B的行为…
再者,Qt状态机的执行是基于事件驱动的。状态转换通常与事件相关联,当特定事件发生时,状态机会根据转换定义执行相应的状态转换。这些事件可以来自用户输入、定时器到期、信号触发等等。所以,为了掌握状态机,你还需要复习一个qt的事件驱动.
The State Machine Framework(状态机参考)[在qtCreator中搜索QState即可找到下面的文档!!!]
-
状态之间的转换可以由信号触发
-
Qt的事件系统用于驱动状态机。
-
下面所有与状态机有关的类,都是由事件驱动的状态机类:
QAbstractState 状态机的基类
QAbstractTransition 在QAbstractState对象之间,进行转换的基类
QEventTransition qobject特定于Qt事件的转换
QFinalState 最终状态(我试了一下,如果只设置几个QState,而不设置QFinalState ,程序在关闭后,停顿几秒,然后崩溃)
QHistoryState 返回到先前活动的子状态的方法
QKeyEventTransition 关键事件的转换
QMouseEventTransition 鼠标事件的过渡
QSignalTransition 基于Qt信号的过渡
QState QStateMachine的通用状态
QStateMachine 层次有限状态机
QStateMachine::SignalEvent 表示一个Qt信号事件
QstateMachine::WrappedEvent 继承QEvent并保存与QObject关联的事件的一个克隆
为了更好的理解状态机:我写了下面几个例子:
例子一 :官方文档的例子
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);//表示一开始是状态s1
Finally, we start the state machine:
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进入时将调用按钮的showmaximization()槽
,而当状态s3退出时将调用按钮的show()槽
:
QObject::connect(s3, SIGNAL(entered()), button, SLOT(showMaximized()));
QObject::connect(s3, SIGNAL(exited()), button, SLOT(showMinimized()));
自定义状态可以重新实现QAbstractState::onEntry()和QAbstractState::onExit()。
完整代码
//mainwindow.h加上头文件
#include <QStateMachine>
#include <QState>
#include <QFinalState>
#include <QSignalTransition>
#include <QDebug>
//在MainWindow类中声明一个状态机
QStateMachine _machine;
//mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
_machine;
QState *s1 = new QState();
QState *s2 = new QState();
QState *s3 = new QState();
s1->addTransition(ui->button, SIGNAL(clicked()), s2);
s2->addTransition(ui->button, SIGNAL(clicked()), s3);
s3->addTransition(ui->button, SIGNAL(clicked()), s1);
_machine.addState(s1);
_machine.addState(s2);
_machine.addState(s3);
_machine.setInitialState(s1);//表示一开始是状态s1
s1->assignProperty(ui->label, "text", "In state s1");
s2->assignProperty(ui->label, "text", "In state s2");
s3->assignProperty(ui->label, "text", "In state s3");
QObject::connect(s3, SIGNAL(entered()), ui->button, SLOT(showMaximized()));
QObject::connect(s3, SIGNAL(exited()), ui->button, SLOT(showMinimized()));
_machine.start();
}
MainWindow::~MainWindow()
{
delete ui;
}
mainwindow.ui中加一个button和一个label
效果:然后你每次点击一下PushButton,label的文本就会更改
2. 抓住重点:状态机中的状态共享 (进一步完善)
上面的的状态机已经基本完善了,但是还有一个缺点:永远不会结束。为了使状态机能够完成,它需要有一个顶级最终状态(QFinalState对象)。当状态机进入顶层最终状态时,该机器将发出QStateMachine::finished()信号并停止。
要在图中引入最终状态,您需要做的就是创建一个QFinalState对象,并将其用作一个或多个转换的目标:
通过分组状态共享转换:
假设我们希望用户可以随时通过单击退出按钮退出应用程序。为了实现这一点,我们需要创建一个最终状态,并将其作为与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);//先定义四个状态,s11,s12,s13都认s1为其父
s1->setInitialState(s11);//s11是s1的老大
machine.addState(s1);//machine搞成一个私有成员变量,将s1加入状态机
QFinalState *s2 = new QFinalState();//最好的状态,到了这里就终止咯
s1->addTransition(quitButton, SIGNAL(clicked()), s2);//搞一个quitButton,用来触发最终状态[也就是:当quitButton被点击时,状态切换为s2]
machine.addState(s2);//将s2加入状态机
machine.setInitialState(s1);//一开始显示的是s11这个状态
QObject::connect(&machine, SIGNAL(finished()), QApplication::instance(), SLOT(quit()));//状态机发射结束信号,程序终止
在本例中,我们希望在状态机结束时退出应用程序,因此机器的finished()信号连接到应用程序的quit()插槽。
子状态可以覆盖继承的转换。例如,下面的代码添加了一个转换,当状态机处于状态s12时,该转换会有效地导致Quit按钮被忽略。
s12->addTransition(quitButton, SIGNAL(clicked()), s12);//s12状态添加了一个转换,该转换的条件是当Quit按钮被点击时,状态机会转换到自身[也就是s12]
//这意味着当状态机处于状态s12时,如果点击了Quit按钮,状态机会忽略该转换,不会进入终止状态s2。
//进一步解释:当状态机处于s12时,无论点击Quit按钮多少次,都不会导致应用程序退出。
转换可以有任何状态作为它的目标,即目标状态不必与源状态在状态层次结构中的同一层
完整代码
//mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QStateMachine>
#include <QState>
#include <QFinalState>
#include <QSignalTransition>
#include <QDebug>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
QStateMachine _machine;
};
#endif // MAINWINDOW_H
//mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
QState *s1 = new QState();
QState *s11 = new QState(s1);
QState *s12 = new QState(s1);
QState *s13 = new QState(s1);//先定义四个状态,s11,s12,s13都认s1为其父
//大环境是s1,但是s1内部的s11,s12,s13还是可以互相转换的
s11->addTransition(ui->button, SIGNAL(clicked()), s12);
s12->addTransition(ui->button, SIGNAL(clicked()), s13);
s13->addTransition(ui->button, SIGNAL(clicked()), s11);
s11->assignProperty(ui->label, "text", "In state s1:s11");
s12->assignProperty(ui->label, "text", "In state s1:s12");
s13->assignProperty(ui->label, "text", "In state s1:s13");
s1->setInitialState(s11);//s11是s1的老大
_machine.addState(s1);//machine搞成一个私有成员变量,将s1加入状态机
QFinalState *s2 = new QFinalState();//最好的状态,到了这里就终止咯
s1->addTransition(ui->quitButton, SIGNAL(clicked()), s2);//搞一个quitButton,用来触发最终状态[也就是:当quitButton被点击时,状态切换为s2]
_machine.addState(s2);//将s2加入状态机
_machine.setInitialState(s1);//一开始显示的是s11这个状态
QObject::connect(&_machine, SIGNAL(finished()), QApplication::instance(), SLOT(quit()));//状态机发射结束信号,程序终止
s12->addTransition(ui->quitButton, SIGNAL(clicked()), s12);
_machine.start();
}
MainWindow::~MainWindow()
{
delete ui;
}
mainwindow.ui中
加:
一个button
一个label
一个quitButton
效果:
很明显,当处于s1:s12这个状态时,quitButton失效!无法达到最终状态,来使得程序退出(这里如果使用动态图片,展示的效果更好,可惜我太笨了,百度了半天,都没学会具体怎么操作)
3. 抓住重点:使用历史状态保存和恢复当前状态(进一步完善)
假设我们想要在上一节讨论的例子中添加一个“中断”机制。用户应该能够点击一个按钮让状态机执行一些不相关的任务,之后状态机应该恢复它之前所做的任何事情(即回到旧状态,在本例中是s11, s12和s13之一)。
这种行为可以很容易地使用历史状态进行建模。历史状态(QHistoryState对象)是一个伪状态,表示父状态在最近一次退出父状态时所处的子状态。
历史状态是作为我们希望记录当前子状态的状态的子状态创建的;当状态机在运行时检测到存在这样的状态时,它会在父状态退出时自动记录当前(真实)子状态。转换到历史状态实际上是转换到状态机之前保存的子状态;状态机自动将转换“转发”到真正的子状态。
下图显示了添加中断机制后的状态机。
如果你不认识interrupt
这个单词,那你就要吃大亏了,你可能看不懂上面这张图了!抓住重点:interrupt就是中断的意思,interruptButton应该是中断按钮的意思,我猜测应该会设计一个中断按钮,点击就会触发中断操作!
下面的代码展示了如何实现它。在本例中,我们只是在输入s3时显示一个消息框,然后通过历史状态立即返回到s1的前一个子状态。
QHistoryState *s1h = new QHistoryState(s1);
QState *s3 = new QState();
s3->assignProperty(label, "text", "In s3");//处于s3状态时,label会显示:In s3
QMessageBox *mbox = new QMessageBox(mainWindow);//认mainwindow为父
mbox->addButton(QMessageBox::Ok);
mbox->setText("Interrupted!");
mbox->setIcon(QMessageBox::Information);//设置消息框的图标为信息图标(QMessageBox::Information)。这将在消息框的标题栏或内容区域显示一个信息图标,以便与不同类型的消息进行视觉区分。
QObject::connect(s3, SIGNAL(entered()), mbox, SLOT(exec()));
s3->addTransition(s1h);
machine.addState(s3);
s1->addTransition(interruptButton, SIGNAL(clicked()), s3);//按下addTransition,s1状态转s3
完整代码
//mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QStateMachine>
#include <QState>
#include <QFinalState>
#include <QSignalTransition>
#include <QDebug>
#include <QHistoryState>
#include <QMessageBox>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
QStateMachine _machine;
};
#endif // MAINWINDOW_H
//mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
QState *s1 = new QState();
QState *s11 = new QState(s1);
QState *s12 = new QState(s1);
QState *s13 = new QState(s1);//先定义四个状态,s11,s12,s13都认s1为其父
//大环境是s1,但是s1内部的s11,s12,s13还是可以互相转换的
s11->addTransition(ui->button, SIGNAL(clicked()), s12);
s12->addTransition(ui->button, SIGNAL(clicked()), s13);
s13->addTransition(ui->button, SIGNAL(clicked()), s11);
s11->assignProperty(ui->label, "text", "In state s1:s11");
s12->assignProperty(ui->label, "text", "In state s1:s12");
s13->assignProperty(ui->label, "text", "In state s1:s13");
s1->setInitialState(s11);//s11是s1的老大
_machine.addState(s1);//machine搞成一个私有成员变量,将s1加入状态机
QFinalState *s2 = new QFinalState();//最好的状态,到了这里就终止咯
s1->addTransition(ui->quitButton, SIGNAL(clicked()), s2);//搞一个quitButton,用来触发最终状态[也就是:当quitButton被点击时,状态切换为s2]
_machine.addState(s2);//将s2加入状态机
_machine.setInitialState(s1);//一开始显示的是s11这个状态
QObject::connect(&_machine, SIGNAL(finished()), QApplication::instance(), SLOT(quit()));//状态机发射结束信号,程序终止
s12->addTransition(ui->quitButton, SIGNAL(clicked()), s12);
//
QHistoryState *s1h = new QHistoryState(s1);//s1h状态认s1为父
QState *s3 = new QState();
s3->assignProperty(ui->label, "text", "In s3");//处于s3状态时,label会显示:In s3
QMessageBox *mbox = new QMessageBox(this);//认mainwindow为父
mbox->addButton(QMessageBox::Ok);
mbox->setText("Interrupted!");
mbox->setIcon(QMessageBox::Information);//设置消息框的图标为信息图标(QMessageBox::Information)。这将在消息框的标题栏或内容区域显示一个信息图标,以便与不同类型的消息进行视觉区分。
QObject::connect(s3, SIGNAL(entered()), mbox, SLOT(exec()));
s3->addTransition(s1h);
_machine.addState(s3);
s1->addTransition(ui->interruptButton, SIGNAL(clicked()), s3);//按下addTransition,s1状态转s3
_machine.start();
}
MainWindow::~MainWindow()
{
delete ui;
}
mainwindow.ui中
加:
一个button
一个label
一个quitButton
一个interruptButton
效果:可以看到,当我们按下interruptButton时,会转为s3状态:将label的文字改为In s3;然后一旦进入s3状态,就会弹出如下弹窗:
当我按下ok时,程序的状态又会返回到之前的状态[应该是这句的作用:s3->addTransition(s1h);
]
然后就是这句:_machine.addState(s3);
(想想一般什么情况下需要加入状态机?1.父状态加入,他的子状态就不用加了;2.自己没有父状态也无子状态,需要加入状态机[不管这个状态是普通切换的状态,还是产生中断的状态])
4. 抓住重点:使用并行状态来避免状态组合爆炸(进一步完善)
假设您想在单个状态机中对汽车的一组互斥属性进行建模。假设我们感兴趣的属性是干净和脏,移动和不移动。它需要4个互斥的状态和8个转换才能表示并在所有可能的组合之间自由移动。
如果我们添加第三个属性(比如红色vs蓝色),状态的总数将翻倍,达到8个;如果我们添加第四个属性(例如,封闭vs可转换),状态的总数将再次翻倍,达到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);
当进入一个并行状态组时,它的所有子状态将同时进入。各个子状态内部的转换正常运行。但是,任何子状态都可以进行退出父状态的转换。当发生这种情况时,父状态及其所有子状态都将退出。
状态机框架中的并行性遵循交错语义。所有并行操作都将在事件处理的单个原子步骤中执行,因此没有事件可以中断并行操作。但是,由于机器本身是单线程的,事件仍然会顺序处理。举个例子:考虑这样一种情况:有两个转换退出同一个并行状态组,并且它们的条件同时为真。在这种情况下,最后一个处理的事件不会产生任何影响,因为第一个事件已经导致机器退出并行状态。
完整代码(我没看懂这个部分,代码运行后也没有任何效果)
我没看懂这个部分,代码运行后也没有任何效果
5.检测复合状态是否完成
子状态可以是final(一个QFinalState对象);当进入最后一个子状态时,父状态发出QState::finished()信号。下图显示了一个复合状态s1,它在进入最终状态之前进行了一些处理:
当s1的最终状态进入时,s1会自动触发finished()。我们使用信号转换让这个事件触发状态改变:
s1->addTransition(s1, SIGNAL(finished()), s2);//s1发射finished信号,就会转为s2状态
当你想隐藏组合状态的内部细节时,在组合状态中使用最终状态是有用的;也就是说,外部世界唯一能做的就是进入该状态,并在该状态完成其工作时获得通知。在构建复杂(深度嵌套)状态机时,这是一种非常强大的抽象和封装机制。(在上面的例子中,你当然可以直接从s1的done状态创建一个转换,而不是依赖s1的finished()信号,但结果是暴露和依赖s1的实现细节)。
对于并行状态组,当所有子状态都进入最终状态时,会发出QState::finished()信号
。
完整代码
这个代码运行后也没有任何效果
6.Targetless转换(无目标的转换)
转换不需要目标状态。没有目标的过渡可以像其他过渡一样被触发;不同之处在于,当触发无目标转换时,它不会导致任何状态变化
。这允许您在机器处于特定状态时对信号或事件做出反应,而不必离开该状态。例子:
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()信号)。
完整代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QMessageBox>
#include <QPushButton>
#include <QSignalTransition>
#include <QState>
#include <QStateMachine>
#include <QVBoxLayout>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
QStateMachine _machine;
QMessageBox _msgBox;
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
QState *s1 = new QState(&_machine);
QSignalTransition *trans = new QSignalTransition(ui->_button, SIGNAL(clicked()));
s1->addTransition(trans);
_msgBox.setText("The button was clicked; carry on.");
QObject::connect(trans, SIGNAL(triggered()), &_msgBox, SLOT(exec()));
_machine.setInitialState(s1);
_machine.start();
}
MainWindow::~MainWindow()
{
delete ui;
}
ui文件中加:
一个_button
7. Events, Transitions and Guards(事件,过渡和警卫
QStateMachine运行自己的事件循环。对于信号转换(QSignalTransition对象),当QStateMachine(状态机)拦截到相应的信号时,它会自动向自己发送一个QStateMachine::SignalEvent;
类似地,对于QObject事件转换(QEventTransition对象),发布了QStateMachine::WrappedEvent。
您可以使用QStateMachine::postEvent()
将自己的事件发送到状态机。
当将自定义事件提交到状态机时,通常还可以从该类型的事件触发一个或多个自定义转换。要创建这样的转换,你需要继承QAbstractTransition并重新实现QAbstractTransition::eventTest(),在这里你可以检查一个事件是否与你的事件类型匹配(以及可选的其他条件,例如event对象的属性)。
在这里,我们定义了自定义事件类型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
并执行字符串比较
。
下面是一个使用自定义事件和转换的状态图:
状态图的实现如下:
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"));
未被任何相关转换处理的事件将由状态机静默地使用。它可以对状态进行分组,并提供对此类事件的默认处理;例如,如下面的状态图所示:
对于深度嵌套的状态图,您可以在最合适的粒度级别上添加这种“备用”转换。
完整代码(从这一部分开始有意思了,与自定义事件进行了结合)
//mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QAbstractTransition>
#include <QEvent>
#include <QStateMachine>
#include <QFinalState>
#include <QString>
#include <QApplication>
#include <QApplication>
#include <QMainWindow>
#include <QStateMachine>
#include <QState>
#include <QFinalState>
#include <QAbstractTransition>
#include <QDebug>
#include <QGraphicsView>
#include <QGraphicsScene>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void on_button_clicked();
private:
Ui::MainWindow *ui;
QStateMachine _machine;
int _count = 0;
};
/********
* 声明了一个名为 StringEvent 的结构体,它是继承自 QEvent 的自定义事件类型,用于传递字符串类型的事件数据。
***********/
//重写一个字符串事件
struct StringEvent : public QEvent
{
public:
StringEvent(const QString &val)
: QEvent(QEvent::Type(QEvent::User+1)),
value(val) {
qDebug()<<" StringEvent(const QString &val)\
: QEvent(QEvent::Type(QEvent::User+1)),\
value(val)"<<",value:["<<value<<"]";
}
QString value;
};
/***********
* 声明了一个名为 StringTransition 的类,
* 它是继承自 QAbstractTransition 的自定义转换类型,
* 用于根据接收到的字符串事件值切换状态。
*****************/
//QAbstractTransition 是 Qt 框架中的抽象类,用于定义状态机中的转换。它是 QSignalTransition 和 QAbstractState 的基类。
class StringTransition : public QAbstractTransition//自定义一个字符串状态转换类,StringTransition
{
public:
StringTransition(const QString &value)
: m_value(value) {
qDebug()<<" StringTransition(const QString &value)\
: m_value(value)"<<",value:["<<value<<"]";
}
protected:
/*****
* eventTest(QEvent *e) 函数:
* 作用:用于检测事件是否满足转换条件。
* 参数:QEvent *e 是待检测的事件指针。
* 返回值:布尔值,表示事件是否满足转换条件。
* 实现:首先,通过比较事件的类型是否为自定义的 StringEvent 类型来判断事件类型是否符合预期。
* 然后,将事件指针转换为 StringEvent* 类型,并与转换对象的字符串值进行比较。如果相等,则转换条件满足,返回 true;否则,返回 false。
***********/
bool eventTest(QEvent *e) override
{
qDebug()<<"bool eventTest(QEvent *e) override,return:[";
qDebug()<<"e->type():{"<<e->type()<<"},QEvent::Type(QEvent::User+1):{"<<QEvent::Type(QEvent::User+1)<<"}";
/*
* 知识补充:
* 根据代码顺序可以确保 QEvent::Type(QEvent::User+1) 对应的是第一个自定义事件,
* 而 QEvent::Type(QEvent::User+2) 对应的是第二个自定义事件。
*/
if (e->type() != QEvent::Type(QEvent::User+1)) // StringEvent
{
qDebug()<<"false]";
return false;
}
StringEvent *se = static_cast<StringEvent*>(e);
qDebug()<<(m_value == se->value)<<"]...";
return (m_value == se->value);
}
void onTransition(QEvent *) override {
qDebug()<<"void onTransition(QEvent *) override";
}
private:
QString m_value;
};
#endif // MAINWINDOW_H
//mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
QState *s1 = new QState();
QState *s2 = new QState();
QFinalState *done = new QFinalState();
QObject::connect(&_machine, SIGNAL(finished()), QApplication::instance(), SLOT(quit()));
StringTransition *t1 = new StringTransition("Hello");//创建 StringTransition 对象 t1,将其事件值设置为 "Hello"
t1->setTargetState(s2);//t1的目标状态是s2
s1->addTransition(t1);//将转换 t1 添加到状态 s1,表示当接收到事件值为 "Hello" 的字符串事件时,从状态 s1 切换到状态 s2。
StringTransition *t2 = new StringTransition("world");//创建 StringTransition 对象 t2,将其事件值设置为 "world"
t2->setTargetState(done);//t2的目标状态是done
s2->addTransition(t2);//将转换 t2 添加到状态 s2,表示当接收到事件值为 "world" 的字符串事件时,从状态 s2 切换到最终状态 done。
//s1->addTransition(ui->button, SIGNAL(clicked()), s2);
//s2->addTransition(ui->button, SIGNAL(clicked()), done);
s1->assignProperty(ui->label, "text", "In state s1");
s2->assignProperty(ui->label, "text", "In state s2");
_machine.addState(s1);
_machine.addState(s2);
_machine.addState(done);
_machine.setInitialState(s1);//一开始是s1状态
_machine.start();
// _machine.postEvent(new StringEvent("world"));
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_button_clicked()
{
_count ++;
if(_count % 2 == 1)
{
_machine.postEvent(new StringEvent("Hello"));//根据上面的代码,我们知道:hello事件触发,会切换为s2
}else{
_machine.postEvent(new StringEvent("world"));//根据上面的代码,我们知道:world事件触发,会切换为done
}
}
ui文件加:
一个label
一个button
8.使用还原策略自动还原属性
在某些状态机中,将注意力集中在状态中的属性分配上是很有用的,而不是在状态不再活跃时恢复它们。如果您知道当计算机进入一个没有明确给属性值的状态时,属性应该总是恢复到它的初始值,那么您可以将全局恢复策略设置为QStateMachine::RestoreProperties。
QStateMachine machine;
machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties);
设置此还原策略后,计算机将自动还原所有属性
。如果它进入到没有设置给定属性的状态,它将首先搜索祖先的层次结构,以查看该属性是否在那里定义。如果是,则属性将恢复为最近的祖先定义的值。如果没有,它将恢复到它的初始值(即在执行状态中的任何属性分配之前的属性值)。
看看下面的代码:
QStateMachine machine;
machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties);
QState *s1 = new QState();
s1->assignProperty(object, "fooBar", 1.0);//fooBar属性
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的值。
完整代码
//mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QStateMachine>
#include <QDebug>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
QStateMachine _machine;
};
#endif // MAINWINDOW_H
//mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
_machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties);
QState *s1 = new QState();
s1->assignProperty(this, "fooBar", 1.0);
_machine.addState(s1);
_machine.setInitialState(s1);
//现在状态s1有两个子类:s2,和s3,我们选择将s2作为我们首先展示的子类
QState *s2 = new QState(s1);
s2->assignProperty(this, "fooBar", 2.0);
s1->setInitialState(s2);
QState *s3 = new QState(s1);
s2->addTransition(ui->button, SIGNAL(clicked()), s3);
s3->addTransition(ui->button, SIGNAL(clicked()), s2);
s2->assignProperty(ui->label, "text", "In state s2");
s3->assignProperty(ui->label, "text", "In state s3");
QObject::connect(s2, &QState::entered, [=]() {
// 在进入状态时执行的操作
// ...
QVariant fooBarValue = this->property("fooBar");//进入s2状态时打印s2状态下的fooBar值
qDebug() << "Current fooBar value:" << fooBarValue;
});
QObject::connect(s3, &QState::entered, [=]() {
// 在进入状态时执行的操作
// ...
QVariant fooBarValue = this->property("fooBar");//进入s3状态时打印s3状态下的fooBar值
qDebug() << "Current fooBar value:" << fooBarValue;
});
_machine.start();
}
MainWindow::~MainWindow()
{
delete ui;
}
ui文件加:
一个label,
一个button
9.给属性赋予动画
状态机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,则还可以为属性恢复添加动画。
完整代码
//mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QStateMachine>
#include <QDebug>
#include <QPropertyAnimation>
#include <QSignalTransition>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
QStateMachine _machine;
};
#endif // MAINWINDOW_H
//mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
QState *s1 = new QState();
QState *s2 = new QState();
s1->assignProperty(ui->button, "geometry", QRectF(ui->button->geometry().x(),ui->button->geometry().y(), 150, 150));
s2->assignProperty(ui->button, "geometry", QRectF(ui->button->geometry().x(),ui->button->geometry().y(), 400, 400));
QSignalTransition *transition = s1->addTransition(ui->button, &QPushButton::clicked, s2);
transition->addAnimation(new QPropertyAnimation(ui->button, "geometry"));
s1->addTransition(ui->button, SIGNAL(clicked()), s2);
s2->addTransition(ui->button, SIGNAL(clicked()), s1);
s1->assignProperty(ui->label, "text", "In state s1");
s2->assignProperty(ui->label, "text", "In state s2");
_machine.addState(s1);
_machine.addState(s2);
_machine.setInitialState(s1);
_machine.start();
}
MainWindow::~MainWindow()
{
delete ui;
}
ui文件加上
一个label,
一个button
10.检测所有属性都处于某个状态
当使用动画来分配属性
时,状态不再定义当机器处于给定状态时属性的确切值。在动画运行时,属性可以有任何值,具体取决于动画。
在某些情况下,能够检测属性是否实际被分配了状态定义的值是很有用的。
假设我们有以下代码:
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()));
//注:官方这种写法明显是不对的,哪有用qt5的信号,却用qt4的槽?
//果不其然会报错!
//修改:connect(s3, SIGNAL(entered()), messageBox, SLOT(exec()));
s1->addTransition(button, &QPushButton::clicked, s2);
s2->addTransition(s2, &QState::propertiesAssigned, s3);
在这个例子中,当单击按钮时,计算机将输入s2。它会一直保持状态s2,直到geometry属性被设置为QRect(0,0,50,50)。然后它将转换到s3。当输入s3时,将弹出消息框。如果到s2的过渡有一个geometry属性的动画,那么机器将停留在s2,直到动画结束播放。如果没有这样的动画,它将简单地设置属性并立即进入状态s3。
无论哪种方式,当机器处于状态s3时,您都可以保证属性几何已被分配了定义的值。
如果全局恢复策略设置为QStateMachine::RestoreProperties,状态将不会发出propertiesAssigned()信号,直到这些也被执行。
完整代码
//mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QStateMachine>
#include <QDebug>
#include <QPropertyAnimation>
#include <QSignalTransition>
#include <QMessageBox>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
QStateMachine _machine;
};
#endif // MAINWINDOW_H
//mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
QMessageBox *messageBox = new QMessageBox(this);
messageBox->addButton(QMessageBox::Ok);
messageBox->setText("Button geometry has been set!");
messageBox->setIcon(QMessageBox::Information);
QState *s1 = new QState();
s1->assignProperty(ui->button, "geometry",
QRectF(ui->button->geometry().x(),
ui->button->geometry().y(),
ui->button->geometry().width(),
ui->button->geometry().height()));
QState *s2 = new QState();
s2->assignProperty(ui->button, "geometry", QRectF(ui->button->geometry().x(), ui->button->geometry().y(), 150, 150));
connect(s2, SIGNAL(entered()), messageBox, SLOT(exec()));
s1->addTransition(ui->button, &QPushButton::clicked, s2);
s2->addTransition(ui->button, SIGNAL(clicked()), s1);
s1->assignProperty(ui->label, "text", "In state s1");
s2->assignProperty(ui->label, "text", "In state s2");
_machine.addState(s1);
_machine.addState(s2);
_machine.setInitialState(s1);
_machine.start();
}
MainWindow::~MainWindow()
{
delete ui;
}
ui文件加:
一个label
一个button
11.如果状态在动画完成之前退出会发生什么
如果一个状态有属性赋值,并且转换到这个状态的过程中有属性的动画,那么这个状态可能会在属性赋值给状态定义的值之前退出。特别是在状态转换不依赖于propertiesAssigned()信号的情况下,如前一节所述。
状态机API保证状态机分配的属性可以是:
- 具有显式指定给属性的值。
- 以动画形式显示为属性的值。
当一个状态在动画结束之前退出时,状态机的行为取决于转换的目标状态
。
如果目标状态为属性显式指定了值,则不会采取其他操作。属性将被分配目标状态定义的值。
如果目标状态没有给这个属性赋值,有两个选择:
- 默认情况下,这个属性会被赋值给它即将离开的状态(如果动画被允许结束播放,它会被赋值)。[换句话说,属性将保持在当前状态的值。]
- 但是,如果设置了全局恢复策略,则该策略优先级较高,并且该属性将像往常一样恢复。[根据指定的全局恢复策略,属性将像往常一样进行恢复,而不是保留当前状态的值。]
上面的话比较绕,我大概懂了:当涉及到状态机中的属性赋值和属性动画时,需要对转换的目标状态是否显式指定属性值以及全局恢复策略进行考虑。这些因素将决定状态机在状态转换过程中如何处理属性的赋值和恢复。
12.默认的动画(这个不是很懂)
如前所述,你可以为过渡添加动画,以确保目标状态中的属性赋值是动画的。如果您希望特定的动画用于给定的属性,而不管采用哪种过渡,则可以将其作为默认动画添加到状态机。在构造机器时,如果不知道由特定状态分配(或恢复)的属性,这特别有用。
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分配的。
注意,显式设置在过渡上的动画将优先于给定属性的任何默认动画。
完整代码
//mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QStateMachine>
#include <QDebug>
#include <QPropertyAnimation>
#include <QSignalTransition>
#include <QMessageBox>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
Q_PROPERTY(qreal fooBar READ getFooBar WRITE setFooBar) // 属性声明
public:
QState *s1;
QState *s2;
qreal getFooBar() const; // 属性读取方法
void setFooBar(qreal value); // 属性写入方法
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void on_button_clicked();
private:
Ui::MainWindow *ui;
QStateMachine _machine;
qreal _fooBar;
};
#endif // MAINWINDOW_H
//mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
s1 = new QState();
s2 = new QState();
QObject::connect(s1, &QState::entered, [=]() {
// 在进入状态时执行的操作
// ...
QVariant fooBarValue = this->property("fooBar");//进入s1状态时打印s1状态下的fooBar值
qDebug() << "Current fooBar value:" << fooBarValue;
});
QObject::connect(s2, &QState::entered, [=]() {
// 在进入状态时执行的操作
// ...
QVariant fooBarValue = this->property("fooBar");//进入s2状态时打印s2状态下的fooBar值
qDebug() << "Current fooBar value:" << fooBarValue;
});
// 添加属性动画
QPropertyAnimation *animation = new QPropertyAnimation(this, "fooBar");
animation->setStartValue(1.0); // 设置起始值 这个切换动画的起始值为1.0
_machine.addDefaultAnimation(animation);
// 设置属性赋值和转换
//表示s2状态为动画的开始(fooBar为初始值1.0),s2转化之后的状态时动漫的结束(fooBar为2.0)
s2->assignProperty(this, "fooBar", 2.0);//切换时将fooBar属性的值设置为1.0,那么当进入s2状态时,fooBar属性的值为1.0
//s1->addTransition(s2);
s1->addTransition(ui->button, &QPushButton::clicked, s2);
s2->addTransition(ui->button, &QPushButton::clicked, s1);
// 设置状态属性
s1->assignProperty(ui->label, "text", "In state s1");
s2->assignProperty(ui->label, "text", "In state s2");
_machine.addDefaultAnimation(new QPropertyAnimation(this, "fooBar"));
// 添加状态到状态机
_machine.addState(s1);
_machine.addState(s2);
_machine.setInitialState(s1);
_machine.start();
}
MainWindow::~MainWindow()
{
delete ui;
}
// 实现属性 fooBar 的读取和写入方法
qreal MainWindow::getFooBar() const
{
return _fooBar;
}
void MainWindow::setFooBar(qreal value)
{
_fooBar = value;
}
void MainWindow::on_button_clicked()
{
}
ui文件加
一个label
一个button
13. 嵌套状态机
QStateMachine是QState的子类。这允许一个状态机成为另一个机器的子状态。QStateMachine重新实现了QState::onEntry()并调用了QStateMachine::start(),这样当子状态机进入时,它就会自动开始运行。
父状态机将子状态机视为状态机算法中的原子状态。子状态机是独立的;它维护自己的事件队列和配置。特别要注意的是,子计算机的configuration()不是父计算机配置的一部分(只有子计算机本身是)。
子状态机的状态不能指定为父状态机中的转换目标;只有子状态机本身可以。相反,不能将父状态机的状态指定为子状态机中的转换目标。子状态机的finished()信号可用于触发父状态机中的转换。
完结撒花!!!
如果我们想要在qml中实现状态机 还可以参考: The Declarative State Machine Framework.