Как заставить этот конечный автомат Qt работать?

У меня есть два виджета, которые можно проверить, и числовое поле ввода, которое должно содержать значение больше нуля. Всякий раз, когда оба виджета были проверены, и числовое поле ввода содержит значение больше нуля, кнопка должна быть включена. Я борюсь с определением надлежащего конечного автомата для этой ситуации. Пока у меня есть следующее:

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 состояние должно быть возвращено в исходное состояние. Это нежелательное поведение, так как я хочу a а также b состояния, чтобы остаться в том же состоянии.

Как я могу гарантировать, что a а также b оставаться в том же состоянии? Есть ли другой / лучший способ решить эту проблему с помощью конечных автоматов?


Примечание Существует бесчисленное (возможно, лучшее) решение этой проблемы. Однако меня интересует только решение, использующее конечный автомат. Я думаю, что такой простой вариант использования должен быть решен с помощью простого конечного автомата, верно?

5 ответов

Решение

После ознакомления с вашими требованиями, ответами и комментариями, я думаю, что решение merula или что-то подобное - единственное чистое решение Statemachine.

Как было отмечено, чтобы заставить Параллельное государство уволить finished() сигнализировать, что все отключенные состояния должны быть конечными состояниями, но это не совсем то, чем они должны быть, поскольку кто-то может снять один из флажков, и тогда вам придется отойти от конечного состояния. Вы не можете сделать это, так как FinalState не принимает никаких переходов. Использование FinalState для выхода из параллельного состояния также приводит к перезапуску параллельного состояния при повторном входе.

Одним из решений может быть кодирование перехода, который срабатывает только тогда, когда все три состояния находятся в "хорошем" состоянии, и второе, которое запускается, когда какое-либо из них не является. Затем вы добавляете отключенные и включенные состояния в уже имеющееся параллельное состояние и соединяете его с вышеупомянутыми переходами. Это позволит синхронизировать состояние кнопки со всеми состояниями элементов пользовательского интерфейса. Это также позволит вам выйти из параллельного состояния и вернуться к согласованному набору параметров свойств.

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;

}

Не компилировать это или даже проверить это, но это должно продемонстрировать принцип

Конечный автомат, который вы использовали выше, не соответствует описанному вами. Использование конечного состояния некорректно, потому что после ввода значения больше нуля я не вижу ничего, что мешало бы пользователю снова ввести ноль. Поэтому действительные состояния не могут быть окончательными. Насколько я вижу из вашего кода, пользователю разрешено изменять состояние виджетов в любом порядке. Ваш конечный автомат должен обратить на это внимание.

Я бы использовал конечный автомат с четырьмя дочерними состояниями (без действительного ввода, одного действительного ввода, двух допустимых входов, трех допустимых входов). Вы, очевидно, начинаете без правильного ввода. Каждый виджет может сделать переход от одного к одному назад (то же самое считается для двух и трех). Когда введено три, все виджеты действительны (кнопка включена). Для всех остальных состояний кнопка должна быть отключена при входе в состояние.

Я написал пример приложения. Главное окно содержит два 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);
}

Это должно сделать свое дело. Как вы уже упоминали, есть лучшие способы добиться такого поведения. Особенно отслеживать все переходы между состояниями подвержены ошибкам.

Настройте свой виджет для ввода веса так, чтобы нельзя было ввести вес меньше нуля. Тогда вам не нужно invalidWeight()

Когда мне приходится делать такие вещи, я обычно использую сигналы и слоты. По сути, каждый из виджетов и числовое поле будут автоматически излучать сигналы при изменении их состояний. Если вы свяжете каждый из них со слотом, который проверяет, все ли 3 объекта находятся в желаемом состоянии, и включает кнопку, если они есть, или отключает ее, если это не так, то это должно упростить вещи.

Иногда вам также нужно изменить состояние кнопки, как только вы нажали на нее.

[РЕДАКТИРОВАТЬ]: Я уверен, что есть какой-то способ сделать это с помощью конечных автоматов, вы будете возвращаться только в том случае, если оба флажка установлены, и вы добавили неверный вес, или вам также нужно будет вернуться только с одним флажок отмечен? Если это первое, то вы можете установить состояние RestoreProperties, которое позволит вам вернуться к состоянию флажка. В противном случае можно каким-то образом сохранить состояние перед проверкой правильности веса, отменить все флажки и восстановить состояние.

редактировать

Я снова открыл этот тест, готов использовать его, добавил в.pro

CONFIG += C++11

и я обнаружил, что синтаксис лямбда изменился... Список захвата не может ссылаться на переменные-члены. Вот исправленный код

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). Решение достаточно компактное, с использованием защищенного перехода для перехода между состоянием "включено / отключено" и лямбда-коэффициентом для настройки:

#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();
}
Другие вопросы по тегам