Как заставить этот конечный автомат Qt работать? - PullRequest
13 голосов
/ 28 марта 2010

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

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 ]

10 голосов
/ 04 апреля 2010

После прочтения ваших требований, а также ответов и комментариев здесь я думаю, что решение 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;

}

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

3 голосов
/ 31 марта 2010

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

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

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

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

1 голос
/ 18 марта 2013

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

Я снова открыл этот тест, готов использовать его, добавил в .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();
}
1 голос
/ 29 марта 2010

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

1 голос
/ 28 марта 2010

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

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

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

...