如何使这个Qt状态机工作?

时间:2010-03-28 13:57:26

标签: c++ qt state-machine qstatemachine

我有两个可以检查的小部件,以及一个应该包含大于零的值的数字输入字段。每当检查两个小部件,并且数字输入字段包含大于零的值时,应启用一个按钮。我正在努力为这种情况定义一个合适的状态机。到目前为止,我有以下内容:

QStateMachine *machine = new QStateMachine(this);

QState *buttonDisabled = new QState(QState::ParallelStates);
buttonDisabled->assignProperty(ui_->button, "enabled", false);

QState *a = new QState(buttonDisabled);
QState *aUnchecked = new QState(a);
QFinalState *aChecked = new QFinalState(a);
aUnchecked->addTransition(wa, SIGNAL(checked()), aChecked);
a->setInitialState(aUnchecked);

QState *b = new QState(buttonDisabled);
QState *bUnchecked = new QState(b);
QFinalState *bChecked = new QFinalState(b);
employeeUnchecked->addTransition(wb, SIGNAL(checked()), bChecked);
b->setInitialState(bUnchecked);

QState *weight = new QState(buttonDisabled);
QState *weightZero = new QState(weight);
QFinalState *weightGreaterThanZero = new QFinalState(weight);
weightZero->addTransition(this, SIGNAL(validWeight()), weightGreaterThanZero);
weight->setInitialState(weightZero);

QState *buttonEnabled = new QState();
buttonEnabled->assignProperty(ui_->registerButton, "enabled", true);

buttonDisabled->addTransition(buttonDisabled, SIGNAL(finished()), buttonEnabled);
buttonEnabled->addTransition(this, SIGNAL(invalidWeight()), weightZero);

machine->addState(registerButtonDisabled);
machine->addState(registerButtonEnabled);
machine->setInitialState(registerButtonDisabled);
machine->start();

这里的问题是以下过渡:

buttonEnabled->addTransition(this, SIGNAL(invalidWeight()), weightZero);

导致registerButtonDisabled状态中的所有子状态恢复到其初始状态。这是不受欢迎的行为,因为我希望ab状态保持相同状态。

如何确保ab保持同一状态?有没有其他/更好的方法可以使用状态机解决这个问题?


注意即可。有无数(可以说是更好的)方法来解决这个问题。但是,我只对使用状态机的解决方案感兴趣。我认为这样一个简单的用例应该可以使用一个简单的状态机来解决,对吗?

5 个答案:

答案 0 :(得分:10)

在阅读了您的要求以及这里的答案和评论后,我认为merula的解决方案或类似的东西是唯一纯粹的Statemachine解决方案。

正如已经注意到使并行状态触发finished()信号所有禁用的状态必须是最终状态,但这不是他们应该是什么,因为有人可以取消选中其中一个复选框然后你将不得不离开最终状态。你不能这样做,因为FinalState不接受任何转换。使用FinalState退出并行状态也会导致并行状态在重新进入时重新启动。

一种解决方案可能是编写一个只在所有三个状态都处于“良好”状态时才触发的转换,以及一个在没有任何状态时触发的第二个转换。然后将禁用和启用状态添加到已有的并行状态,并将其与上述转换连接。这将使按钮的启用状态与UI片段的所有状态保持同步。它还可以让您离开并行状态并返回一组一致的属性设置。

class AndGateTransition : public QAbstractTransition
{
    Q_OBJECT

public:

    AndGateTransition(QAbstractState* sourceState) : QAbstractTransition(sourceState)
        m_isSet(false), m_triggerOnSet(true), m_triggerOnUnset(false)

    void setTriggerSet(bool val)
    {
        m_triggerSet = val;
    }

    void setTriggerOnUnset(bool val)
    {
        m_triggerOnUnset = val;
    }

    addState(QState* state)
    {
        m_states[state] = false;
        connect(m_state, SIGNAL(entered()), this, SLOT(stateActivated());
        connect(m_state, SIGNAL(exited()), this, SLOT(stateDeactivated());
    }

public slots:
    void stateActivated()
    {
        QObject sender = sender();
        if (sender == 0) return;
        m_states[sender] = true;
        checkTrigger();
    }

    void stateDeactivated()
    {
        QObject sender = sender();
        if (sender == 0) return;
        m_states[sender] = false;
        checkTrigger();
    }

    void checkTrigger()
    {
        bool set = true;
        QHashIterator<QObject*, bool> it(m_states)
        while (it.hasNext())
        {
            it.next();
            set = set&&it.value();
            if (! set) break;
        }

        if (m_triggerOnSet && set && !m_isSet)
        {
            m_isSet = set;
            emit (triggered());

        }
        elseif (m_triggerOnUnset && !set && m_isSet)
        {
            m_isSet = set;
            emit (triggered());
        }
    }

pivate:
    QHash<QObject*, bool> m_states;
    bool m_triggerOnSet;
    bool m_triggerOnUnset;
    bool m_isSet;

}

没有编译或甚至测试它,但它应该证明原理

答案 1 :(得分:3)

您上面使用的状态机与您描述的不符。使用最终状态是不正确的,因为输入一个大于零的值后,我没有看到任何阻止用户再次输入零的内容。因此,有效状态不能是最终的。据我所知,您可以从代码中看到用户可以按任何顺序更改窗口小部件的状态。您的状态机必须注意这一点。

我会使用具有四个子状态的状态机(没有有效输入,一个有效输入,两个有效输入,三个有效输入)。你显然没有有效的输入。每个小部件都可以从一个转换为一个转换为一个(两个和三个相同)。输入三个时,所有小部件都有效(启用按钮)。对于所有其他状态,在输入状态时必须禁用该按钮。

我写了一个示例应用。主窗口包含两个QCheckBox,一个QSpinBox和一个QPushButton。主窗口中有信号可以轻松记下状态的转换。当窗口小部件的状态发生变化时会被触发。

MainWindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QtGui>

namespace Ui
{
    class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = 0);
    ~MainWindow();

private:
    Ui::MainWindow *ui;
    bool m_editValid;

    bool isEditValid() const;
    void setEditValid(bool value);

private slots:
    void on_checkBox1_stateChanged(int state);
    void on_checkBox2_stateChanged(int state);
    void on_spinBox_valueChanged (int i);
signals:
    void checkBox1Checked();
    void checkBox1Unchecked();
    void checkBox2Checked();
    void checkBox2Unchecked();
    void editValid();
    void editInvalid();
};

#endif // MAINWINDOW_H

MainWindow.cpp

#include "MainWindow.h"
#include "ui_MainWindow.h"

MainWindow::MainWindow(QWidget *parent)
  : QMainWindow(parent), ui(new Ui::MainWindow), m_editValid(false)
{
  ui->setupUi(this);

  QStateMachine* stateMachine = new QStateMachine(this);
  QState* noneValid = new QState(stateMachine);
  QState* oneValid = new QState(stateMachine);
  QState* twoValid = new QState(stateMachine);
  QState* threeValid = new QState(stateMachine);

  noneValid->addTransition(this, SIGNAL(checkBox1Checked()), oneValid);
  oneValid->addTransition(this, SIGNAL(checkBox1Checked()), twoValid);
  twoValid->addTransition(this, SIGNAL(checkBox1Checked()), threeValid);
  threeValid->addTransition(this, SIGNAL(checkBox1Unchecked()), twoValid);
  twoValid->addTransition(this, SIGNAL(checkBox1Unchecked()), oneValid);
  oneValid->addTransition(this, SIGNAL(checkBox1Unchecked()), noneValid);

  noneValid->addTransition(this, SIGNAL(checkBox2Checked()), oneValid);
  oneValid->addTransition(this, SIGNAL(checkBox2Checked()), twoValid);
  twoValid->addTransition(this, SIGNAL(checkBox2Checked()), threeValid);
  threeValid->addTransition(this, SIGNAL(checkBox2Unchecked()), twoValid);
  twoValid->addTransition(this, SIGNAL(checkBox2Unchecked()), oneValid);
  oneValid->addTransition(this, SIGNAL(checkBox2Unchecked()), noneValid);

  noneValid->addTransition(this, SIGNAL(editValid()), oneValid);
  oneValid->addTransition(this, SIGNAL(editValid()), twoValid);
  twoValid->addTransition(this, SIGNAL(editValid()), threeValid);
  threeValid->addTransition(this, SIGNAL(editInvalid()), twoValid);
  twoValid->addTransition(this, SIGNAL(editInvalid()), oneValid);
  oneValid->addTransition(this, SIGNAL(editInvalid()), noneValid);

  threeValid->assignProperty(ui->pushButton, "enabled", true);
  twoValid->assignProperty(ui->pushButton, "enabled", false);
  oneValid->assignProperty(ui->pushButton, "enabled", false);
  noneValid->assignProperty(ui->pushButton, "enabled", false);

  stateMachine->setInitialState(noneValid);

  stateMachine->start();
}

MainWindow::~MainWindow()
{
  delete ui;
}

bool MainWindow::isEditValid() const
{
  return m_editValid;
}

void MainWindow::setEditValid(bool value)
{
  if (value == m_editValid)
  {
    return;
  }
  m_editValid = value;
  if (value)
  {
    emit editValid();
  } else {
    emit editInvalid();
  }
}

void MainWindow::on_checkBox1_stateChanged(int state)
{
  if (state == Qt::Checked)
  {
    emit checkBox1Checked();
  } else {
    emit checkBox1Unchecked();
  }
}

void MainWindow::on_checkBox2_stateChanged(int state)
{
  if (state == Qt::Checked)
  {
    emit checkBox2Checked();
  } else {
    emit checkBox2Unchecked();
  }
}

void MainWindow::on_spinBox_valueChanged (int i)
{
  setEditValid(i > 0);
}

这应该可以解决问题。正如您自己提到的,有更好的方法来实现这种行为。特别是要跟踪状态之间的所有转换是否容易出错。

答案 2 :(得分:1)

当我必须做这样的事情时,我通常使用信号和插槽。基本上,每个小部件和数字框都会在状态改变时自动发出信号。如果你将每一个链接到一个插槽,检查是否所有3个对象都处于所需的状态,如果它们是,则启用按钮,如果不是,则启用它,那么这应该简化了事情。

有时您还需要在点击按钮后更改按钮状态。

[编辑]:我确定有一些方法可以使用状态机进行此操作,您是否只会在两个盒子都被检查并且您添加了无效重量的情况下还原,或者您还需要还原只检查了一个复选框?如果它是前者,那么您可以设置RestoreProperties状态,允许您恢复到选中的状态。否则有一些方法可以在检查权重有效之前保存状态,还原所有复选框然后恢复状态。

答案 3 :(得分:1)

设置您的体重输入小部件,以便无法输入小于零的体重。那你就不需要invalidWeight()

答案 4 :(得分:1)

修改

我重新打开了这个测试,愿意使用它,添加到.pro

CONFIG += C++11

我发现lambda语法已经改变了...捕获列表不能引用成员变量。这是更正后的代码

auto cs = [/*button, check1, check2, edit, */this](QState *s, QState *t, bool on_off) {
    s->assignProperty(button, "enabled", !on_off);
    s->addTransition(new QSignalTransition(check1, SIGNAL(clicked())));
    s->addTransition(new QSignalTransition(check2, SIGNAL(clicked())));
    s->addTransition(new QSignalTransition(edit, SIGNAL(textChanged(QString))));
    Transition *p = new Transition(this, on_off);
    p->setTargetState(t);
    s->addTransition(p);
};

结束编辑

我使用这个问题作为练习(第一次在QStateMachine上)。解决方案相当紧凑,使用保护转换在“启用/禁用”状态和lambda分解设置之间移动:

#include "mainwindow.h"
#include <QLayout>
#include <QFrame>
#include <QSignalTransition>

struct MainWindow::Transition : QAbstractTransition {
    Transition(MainWindow *main_w, bool on_off) :
        main_w(main_w),
        on_off(on_off)
    {}

    virtual bool eventTest(QEvent *) {
        bool ok_int, ok_cond =
            main_w->check1->isChecked() &&
            main_w->check2->isChecked() &&
            main_w->edit->text().toInt(&ok_int) > 0 && ok_int;
        if (on_off)
            return ok_cond;
        else
            return !ok_cond;
    }

    virtual void onTransition(QEvent *) {}

    MainWindow *main_w;
    bool on_off;
};

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    QFrame *f = new QFrame(this);
    QVBoxLayout *l = new QVBoxLayout;

    l->addWidget(check1 = new QCheckBox("Ok &1"));
    l->addWidget(check2 = new QCheckBox("Ok &2"));
    l->addWidget(edit = new QLineEdit());

    l->addWidget(button = new QPushButton("Enable &Me"));

    f->setLayout(l);
    setCentralWidget(f);

    QState *s1, *s2;
    sm = new QStateMachine(this);
    sm->addState(s1 = new QState());
    sm->addState(s2 = new QState());
    sm->setInitialState(s1);

    auto cs = [button, check1, check2, edit, this](QState *s, QState *t, bool on_off) {
        s->assignProperty(button, "enabled", !on_off);
        s->addTransition(new QSignalTransition(check1, SIGNAL(clicked())));
        s->addTransition(new QSignalTransition(check2, SIGNAL(clicked())));
        s->addTransition(new QSignalTransition(edit, SIGNAL(textChanged(QString))));
        Transition *tr = new Transition(this, on_off);
        tr->setTargetState(t);
        s->addTransition(tr);
    };
    cs(s1, s2, true);
    cs(s2, s1, false);

    sm->start();
}