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恢复到它们的初始状态。这是不受欢迎的行为,因为我希望aandb状态保持在同一状态。

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


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

4

5 回答 5

10

在阅读了您的要求以及此处的答案和评论后,我认为 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;

}

没有编译这个甚至没有测试它,但它应该演示原理

于 2010-04-04T12:21:34.820 回答
4

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

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

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

主窗口.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

主窗口.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);
}

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

于 2010-03-31T10:25:36.687 回答
1

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

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

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

于 2010-03-28T14:40:39.607 回答
1

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

于 2010-03-29T10:06:33.007 回答
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();
}
于 2013-03-17T20:57:13.850 回答