1、概念理解

(1)扫盲

一个最简单的状态机应该包括状态机(QStateMachine)、状态(QState)和过渡(QAbstractTransition子类)。

状态机就相当于一个容器,过渡就是将某一个状态切换到另一个状态(当然也可以不切换)。


(2)什么时候可以用状态机

说的直白点就是,如果需要大量的if判断,然后判断的结果,下面又要判断走很多分支,但是这种分支状态是有限的,可以慢慢穷举出来,

那么这个时候,就可以画画状态图,然后使用状态机来实现,会更加简单易于扩展和维护。



比如对于一个播放器,或者任务,必定有start、stop、pause、resume这4个命令。那么有哪些状态呢?

按start之前,叫readyState,就绪态;

按了start之后,进入runningState,运行态;

然后按了pause,进入pauseState,暂停态;

然后再按了resume,恢复到running,恢复到运行态;

除了readyState态,我们不能按stop外,其他态都可以按stop。

似乎有点复杂?没关系,我们来看状态转换图,凑合着看吧(^_^):

Android 状态机 transitionTO 状态机切换_状态机

共有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;
}

结果:

Android 状态机 transitionTO 状态机切换_构造函数_02


这个按钮状态机模块,可以直接在工程中复用,很方便。

 


若对你有帮助,欢迎点赞、收藏、评论,你的支持就是我的最大动力!!!