1、概念理解
(1)扫盲
一个最简单的状态机应该包括状态机(QStateMachine)、状态(QState)和过渡(QAbstractTransition子类)。
状态机就相当于一个容器,过渡就是将某一个状态切换到另一个状态(当然也可以不切换)。
(2)什么时候可以用状态机
说的直白点就是,如果需要大量的if判断,然后判断的结果,下面又要判断走很多分支,但是这种分支状态是有限的,可以慢慢穷举出来,
那么这个时候,就可以画画状态图,然后使用状态机来实现,会更加简单易于扩展和维护。
比如对于一个播放器,或者任务,必定有start、stop、pause、resume这4个命令。那么有哪些状态呢?
按start之前,叫readyState,就绪态;
按了start之后,进入runningState,运行态;
然后按了pause,进入pauseState,暂停态;
然后再按了resume,恢复到running,恢复到运行态;
除了readyState态,我们不能按stop外,其他态都可以按stop。
似乎有点复杂?没关系,我们来看状态转换图,凑合着看吧(^_^):
共有3个状态,就是按钮状态的切换过程,如果不用状态机的话,可能代码就会有各种判断,然后对Start按钮标题,根据状态进行各种修改,最烦的是,会和业务代码缠绕在一起。
那么我们可以使用QState建立上述3个状态,QState可以设置进入该状态时,修改控件的属性,比如修改enabled、text之类,那么在状态切换的时候,控件状态可以得到更新。
那么我们在什么时候调用播放器的start、stop、pause、resume实际操作函数呢?
按钮只有2个,start和stop,至于pause、resume都是start根据状态变化出来的,所以我们也是需要制造4个调用点,这样可以和播放器的实际接口对应,即按钮状态自动处理,
而且2个按钮根据状态自动可以生产出4条执行命令。
另外还有一个问题是,如果我调用播放器接口出现失败,怎么办?很显然,此时按钮的状态时不能切换的,比如点击start后,调用播放器start接口,但是失败了,
那么我的start按钮,还是应该显示start,而不是pause。这个问题也可以得到解决,请看后面的代码吧。
2、动手实现按钮状态机
(1)IPlayer 命令接口类
将播放器的常用4个命令抽象为接口。
/**
* @brief The IPlayer class
* 任务/播放器接口
*/
class IPlayer
{
public:
IPlayer() {}
virtual ~IPlayer() {}
virtual bool start() = 0;
virtual bool pause() = 0;
virtual bool resume() = 0;
virtual bool stop() = 0;
};
(2)PlayStateMachine类
我们定义此类,构造函数传入2个按钮,使用时,直接在外面new出来2个就行,完全不用管PlayStateMachine内部怎么实现的。其实想说明的是,这个类可以用在你的工程中,很少改动,甚至不改。
assignProperty函数的意思是在进入状态时,会修改绑定的对象属性值,我们利用它自动修改按钮的标题和使能。
QSignalTransition* toRunning = new StartCmdProcess(startAction);
toRunning->setTargetState(runningState);
readyState->addTransition(toRunning);
上述代码,是说将toRunning 作为readyState状态切换至runningState状态的过渡类。先后顺序是,出readyState状态,执行过渡类,入runningState状态。
PlayStateMachine类:
/**
* @brief PlayStateMachine::PlayStateMachine
* @param startAction "开始"命令
* @param stopAction "停止"命令
* @param player 任务/播放器接口类
* @param parent 父节点
*/
PlayStateMachine::PlayStateMachine(QAction *startAction,
QAction *stopAction,
IPlayer *player,
QObject *parent)
: QObject(parent)
{
// 保存handler
startAction->setText(tr("Start"));
stopAction->setText(tr("Stop"));
startAction->setData(QVariant::fromValue(player));
stopAction->setData(QVariant::fromValue(player));
// 创建状态机
QStateMachine *machine = new QStateMachine(parent);
/* 创建状态,并配置进入状态时,设置Action属性 */
QState *readyState = new QState(machine); // 创建"就绪状态"
readyState->assignProperty(startAction, "text", tr("Start"));
readyState->assignProperty(stopAction, "enabled", false);
QState *runningState = new QState(machine); // 创建"运行状态"
runningState->assignProperty(startAction, "text", tr("Pause"));
runningState->assignProperty(stopAction, "enabled", true);
QState *pauseState = new QState(machine); // 创建"暂停状态"
pauseState->assignProperty(startAction, "text", tr("Resume"));
pauseState->assignProperty(stopAction, "enabled", true);
/* 创建状态过渡对象,建立源状态与目的状态过渡关联 */
// 点击startAction(start)触发,readyState->runningState转换状态
QSignalTransition* toRunning = new StartCmdProcess(startAction);
toRunning->setTargetState(runningState);
readyState->addTransition(toRunning);
// 点击startAction(pause)触发,runningState->pauseState转换状态
QSignalTransition* toPause = new PauseCmdProcess(startAction);
toPause->setTargetState(pauseState);
runningState->addTransition(toPause);
// 点击stopAction(stop)触发,runningState->readyState转换状态
QSignalTransition* toReady = new StopCmdProcess(stopAction);
toReady->setTargetState(readyState);
runningState->addTransition(toReady);
// 点击startAction(resume)触发,pauseState->runningState转换状态
toRunning = new ResumeCmdProcess(startAction);
toRunning->setTargetState(runningState);
pauseState->addTransition(toRunning);
// 点击stopAction(stop)触发,pauseState->readyState转换状态
toReady = new StopCmdProcess(stopAction);
toReady->setTargetState(readyState);
pauseState->addTransition(toReady);
// 将readyState设置为状态机的初始状态
machine->setInitialState(readyState);
machine->start();
}
(3)状态过渡类
StartCmdProcess类,其他都类似,就讲这个吧。
我们继承自QSignalTransition,这个类是支持使用信号来触发状态切换,构造函数中的意思是sender发送triggered信号,那么会调用这个过渡类,来切换状态。
调用过渡类时,是调用的eventTest(),如果返回true,代表可以切换状态,返回false,表示不切换状态。
我们在eventTest()中调用IPlayer接口,根据接口调用结果来,来决定是否允许状态切换。
/**
* @brief The StartCmdProcess class
* "开始"命令处理类
*/
class StartCmdProcess : public QSignalTransition
{
public:
StartCmdProcess(QAction* sender)
:QSignalTransition(sender, SIGNAL(triggered()))
{}
protected:
bool eventTest(QEvent *event) override
{
if (!QSignalTransition::eventTest(event))
return false;
qDebug() << "StartCmdProcess";
QAction* sender = static_cast<QAction*>(senderObject());
IPlayer* player = sender->data().value<IPlayer*>();
if (player == nullptr)
{
qDebug() << "'player' can not be null";
return false;
}
return player->start();
}
};
3、测试
MainWindow.cpp:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMenuBar>
#include "PlayStateMachine.h"
#include "TaskPlayer.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
QMenu *taskMenu = menuBar()->addMenu(tr("Task"));
QToolBar* taskToolBar = addToolBar(tr("Task"));
QAction* startAction = taskMenu->addAction(tr("Start"));
QAction* stopAction = taskMenu->addAction(tr("Stop"));
taskToolBar->addAction(startAction);
taskToolBar->addAction(stopAction);
player = new TaskPlayer();
// 创建并启动状态机管理器
new PlayStateMachine(startAction, stopAction, player, this);
}
MainWindow::~MainWindow()
{
delete player;
delete ui;
}
TaskPlayer.cpp:
#include "TaskPlayer.h"
#include <QDebug>
TaskPlayer::TaskPlayer()
{
}
TaskPlayer::~TaskPlayer()
{
}
bool TaskPlayer::start()
{
qDebug() << "do start command...";
return true;
}
bool TaskPlayer::pause()
{
qDebug() << "do pause command...";
return true;
}
bool TaskPlayer::resume()
{
qDebug() << "do resume command...";
return true;
}
bool TaskPlayer::stop()
{
qDebug() << "do stop command...";
return true;
}
结果:
这个按钮状态机模块,可以直接在工程中复用,很方便。
若对你有帮助,欢迎点赞、收藏、评论,你的支持就是我的最大动力!!!