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

13

我有两个小部件可以勾选,并且一个数字输入字段应该包含大于零的值。只有当这两个小部件都被勾选并且数字输入字段包含大于零的值时,按钮才应该启用。我正在为这种情况定义一个合适的状态机而感到困难。到目前为止,我有以下内容:

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个回答

10

阅读您的要求以及在这里得到的答案和评论后,我认为merula的解决方案或类似的方案是唯一的纯状态机解决方案。

正如已经指出的那样,为了使并行状态触发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;

}

我没有编译或测试过这个代码,但它应该能够展示原理。


似乎编写自定义转换确实是解决这个问题的方法。我有点失望,因为对于一个看似微不足道的用例,需要如此复杂的实现 :( - Ton van den Heuvel
Qt文档中关于QAbstractTransition::triggered信号的说明如下:“注意:这是一个私有信号。它可以用于信号连接,但不能由用户发射。”也许有一种方法可以定期调用eventTest? - Phidelux

4
你上面使用的状态机与你所描述的不相符。使用最终状态是不正确的,因为在输入大于零的值后,我没有看到任何阻止用户再次输入零的东西。因此,有效状态不能是最终状态。从你的代码中可以看出,用户可以以任何顺序更改窗口小部件的状态。你的状态机必须注意这一点。
我将使用一个具有四个子状态的状态机(无有效输入,一个有效输入,两个有效输入,三个有效输入)。显然,你从无效输入开始。每个窗口小部件都可以从无到一个并返回(同样适用于两个和三个)。当输入三个时,所有窗口小部件都是有效的(按钮启用)。对于所有其他状态,在进入状态时必须禁用按钮。
我写了一个示例应用程序。主窗口包含两个QCheckBoxes、一个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);
}

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


您提到如果子状态之一进入最终状态,则父状态将保留。但是,这并不正确,因为我使用的是并行父状态。只有在所有子状态都进入最终状态时,并行父状态才会进入其最终状态。根据Qt文档:“对于并行状态组,当所有子状态进入最终状态时,将发出QState :: finished()信号。” - Ton van den Heuvel
我曾经想过一个类似于你的解决方案,但是它并不可靠。假设注册按钮已启用,用户输入两次无效号码后,用户必须再次输入两个有效号码才能再次启用该按钮。 - Ton van den Heuvel
Ton,感谢你澄清最终状态和并行状态之间的关系。我之前不知道这一点。但即使达到了最终状态,也没有回到无效状态的方法。历史状态也无济于事,因为它们保留了父状态离开时的状态。不能保证用户会以与离开时相同的方式重新进入无效状态。 我建议修复你在第二条评论中提到的错误(请参见上面的代码),并确保旋转框信号仅在真实状态更改时触发。 - merula

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();
}

1

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

有时候,你还需要在点击按钮后改变按钮的状态。

[编辑]:我确定有一种使用状态机来做这件事的方法,你只会在两个复选框都被选中并且你添加了无效的重量时才进行还原,还是你还需要在只有一个复选框被选中时进行还原?如果是前者,那么你可以设置一个RestoreProperties状态,允许你还原到选中复选框的状态。否则,你是否可以在检查重量是否有效之前保存状态、还原所有复选框,然后恢复状态。


我非常希望看到一个使用状态机的解决方案。我无法相信这样一个相对简单的用例不能方便地使用状态机来解决。如果没有办法使用状态机,我可能会使用你提出的解决方案。 - Ton van den Heuvel
回答你的问题,你是正确的,假设只有一个复选框被选中时,我需要还原。换句话说,如果启用按钮的前提条件之一不再满足,我想要还原。状态机框架确实支持历史状态(QHistoryState),但我不知道如何让它们为我的问题工作。 - Ton van den Heuvel
是否可以设置一些故障状态集合,以便状态机在失败时能够恢复到适当的状态。类似于信号映射器根据信号来源连接到插槽的方式。我怀疑当面对大量故障状态时,这样的解决方案可能会很快变得非常烦琐。 - Amos

1

设置您的重量输入小部件,以便无法输入小于零的重量。然后您就不需要使用invalidWeight()函数了。


我在这里使用了一个简化的例子。在我的应用程序中,我有一个数字输入键盘,用户必须输入一个值。只有在用户输入了一个值的情况下,示例中的按钮才应启用。再次强调,我不是真正关心替代解决方案,我只关心使用状态机的解决方案。 - Ton van den Heuvel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接