【问题标题】:How to get this Qt state machine to work?如何让这个 Qt 状态机工作?
【发布时间】:2014-07-26 22:18:47
【问题描述】:

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

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 保持相同状态?有没有另一种/更好的方法可以使用状态机来解决这个问题?


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

【问题讨论】:

    标签: c++ qt state-machine qstatemachine


    【解决方案1】:

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

    正如已经注意到的那样,使 Parallel State 触发 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;
    
    }
    

    这个没有编译,也没有测试过,但是应该演示一下原理

    【讨论】:

    • 似乎编写自定义转换确实是这里的方法。我有点失望,对于一个看似微不足道的用例,需要如此精细的实现:(
    • QAbstractTransition::triggered 信号的 Qt 文档指出:“注意:这是一个私有信号。它可以用于信号连接,但不能由用户发出。”也许有一种方法可以定期调用 eventTest?
    【解决方案2】:

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

    我将使用具有四个子状态(无有效输入、一个有效输入、两个有效输入、三个有效输入)的状态机。您显然从没有有效输入开始。每个小部件都可以从无到一个后退(两个和三个相同的计数)。当输入三个时,所有小部件都有效(启用按钮)。对于所有其他状态,在进入状态时必须禁用按钮。

    我编写了一个示例应用程序。主窗口包含两个 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,感谢您阐明最终状态和并行状态的关系。我没有意识到这一点。但即使达到最终状态,也无法回到无效状态。历史状态没有帮助,因为它们保留了离开父状态时的状态。无法保证用户会以与离开时相同的方式重新进入无效状态。我建议修复您在第二条评论中提到的错误(请参阅上面的代码)并确保旋转框信号仅在真实状态更改时触发。
    【解决方案3】:

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

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

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

    【讨论】:

    • 我真的很想看看使用状态机的解决方案。我不敢相信这样一个相对简单的用例不能用状态机方便地解决。如果无法使用状态机的解决方案,我可能会使用您提出的解决方案。
    • 要回答您的问题,您可以假设我需要还原以防仅选中一个复选框。换句话说,如果不再满足启用按钮的先决条件之一,我想恢复。状态机框架确实支持历史状态 (QHistoryState),但我不知道如何让它们解决我的问题。
    • 是否可以设置某种故障状态集,以便状态机可以在故障时恢复到适当的状态。与信号映射器类似,它根据信号的来源连接到一个插槽。我怀疑这样的解决方案在遇到许多故障状态时会很快变得非常烦人。
    【解决方案4】:

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

    【讨论】:

    • 我在这里使用了一个简化的例子。在我的应用程序中,我有一个数字输入键盘,用户必须在其中输入一个值。只有在用户输入了值的情况下,才应启用示例中的按钮。同样,我对替代解决方案并不真正感兴趣,我对使用状态机的解决方案感兴趣。
    【解决方案5】:

    编辑

    我重新打开了这个测试,愿意用,添加到.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();
    }
    

    【讨论】:

      猜你喜欢
      • 2013-04-15
      • 2020-08-16
      • 2015-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-12-13
      相关资源
      最近更新 更多