Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get this Qt state machine to work?

I have two widgets that can be checked, and a numeric entry field that should contain a value greater than zero. Whenever both widgets have been checked, and the numeric entry field contains a value greater than zero, a button should be enabled. I am struggling with defining a proper state machine for this situation. So far I have the following:

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

The problem here is that the following transition:

buttonEnabled->addTransition(this, SIGNAL(invalidWeight()), weightZero);

causes all the child states in the registerButtonDisabled state to be reverted to their initial state. This is unwanted behaviour, as I want the a and b states to remain in the same state.

How do I ensure that a and b remain in the same state? Is there another / better way this problem can be solved using state machines?


Note. There are a countless (arguably better) ways to solve this problem. However, I am only interested in a solution that uses a state machine. I think such a simple use case should be solvable using a simple state machine, right?

like image 718
Ton van den Heuvel Avatar asked Mar 28 '10 13:03

Ton van den Heuvel


1 Answers

The state machine you used above does not correspond to what you described. Using a final state is not correct because after enter a value greater zero I don't see anything that prevents the user from enter zero again. Therefore the valid states can't be final. As far as I can see from your code the user is allowed to change the state of the widgets in any order. Your state machine has to pay attention to this.

I would use a state machine with four child states (no valid input, one valid input, two valid inputs, three valid inputs). You obviously start with no valid input. Each widget can make a transition from no to one an back (same counts for two and three). When three is entered all widgets are valid (button enabled). For all other states the button has to be disabled when the state is entered.

I wrote a sample app. The main window contains two QCheckBoxes a QSpinBox and a QPushButton. There are signals in the main window the ease write down the transitions of the states. There are fired when the state of the widgets are changed.

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

This should do the trick. As you yourself mentioned already there are better ways to achive this behaviour. Especially keep track of all transistions between the state is error prone.

like image 50
merula Avatar answered Sep 28 '22 16:09

merula