2010-03-28 25 views
12

Tengo dos widgets que se pueden verificar y un campo de entrada numérico que debe contener un valor mayor que cero. Siempre que se hayan verificado ambos widgets y el campo de entrada numérica contenga un valor mayor que cero, se debe habilitar un botón. Estoy luchando con la definición de una máquina de estado adecuada para esta situación. Hasta ahora tengo el siguiente:¿Cómo hacer funcionar esta máquina de estado 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(); 

El problema aquí es que la transición siguiente:

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

hace que todos los estados del niño en el estado registerButtonDisabled a revertir a su estado inicial. Este es un comportamiento no deseado, ya que quiero que los estados a y b permanezcan en el mismo estado.

¿Cómo me aseguro de que a y b permanezcan en el mismo estado? ¿Hay alguna otra forma mejor de resolver este problema utilizando máquinas de estado?


Nota. Hay innumerables (posiblemente mejores) formas de resolver este problema. Sin embargo, solo estoy interesado en una solución que utiliza una máquina de estado. Creo que un caso de uso tan simple debería poder resolverse usando una máquina de estado simple, ¿verdad?

Respuesta

10

Después de leer sus requisitos y las respuestas y comentarios aquí, creo que la solución de merula o algo similar es la única solución pura de Statemachine.

Como se ha señalado para que el Estado paralelo se disparó la señal finished() todos los estados con discapacidad tienen que ser estados finales, pero esto no es realmente lo que deberían ser como alguien podría desactivar una de las casillas de verificación y luego se tendría que alejarse del estado final. No puede hacer eso ya que FinalState no acepta ninguna transición. El uso de FinalState para salir del estado paralelo también hace que el estado paralelo se reinicie cuando se vuelve a ingresar.

Una solución podría ser codificar una transición que solo se dispara cuando los tres estados están en el estado "bueno", y una segunda que se dispara cuando ninguno de ellos es. Luego agrega los estados deshabilitados y habilitados al estado paralelo que ya tiene y lo conecta con las transiciones mencionadas anteriormente. Esto mantendrá el estado habilitado del botón sincronizado con todos los estados de sus piezas de UI. También le permitirá salir del estado paralelo y volver a un conjunto constante de configuraciones de propiedad.

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; 

} 

no recogió este o incluso probarlo, pero debe demostrar el principio

+0

Parece que la codificación de una transición personalizada es de hecho el camino a seguir aquí. Estoy un poco decepcionado de que para un caso de uso aparentemente trivial se requiera una implementación tan elaborada :( –

1

Cuando tengo que hacer cosas como esta usualmente uso señales y ranuras. Básicamente, cada uno de los widgets y el cuadro de números emitirán señales automáticamente cuando cambien sus estados. Si vincula cada uno de estos a un espacio que comprueba si los 3 objetos están en el estado deseado y habilita el botón si lo están o lo desactiva si no lo están, entonces eso debería simplificar las cosas.

A veces también necesitará cambiar el estado del botón una vez que lo haya hecho clic.

[EDITAR]: estoy seguro de que hay alguna forma de hacerlo con máquinas de estado, solo se revertirá en caso de que se verifiquen ambas casillas y haya agregado un peso no válido o también necesitará revertir con solo una casilla de verificación marcada? Si es el primero, entonces puede configurar un estado RestoreProperties que le permita volver al estado de casilla marcada. De lo contrario, hay alguna manera de guardar el estado antes de verificar que el peso sea válido, revertir todas las casillas de verificación y restaurar el estado.

+0

Me gustaría ver una solución usando una máquina de estado. No puedo creer que un caso de uso relativamente simple no se pueda resolver de manera conveniente con una máquina de estado. En caso de que no sea posible una solución que utilice máquinas de estado, probablemente utilice la solución que usted propone. –

+0

Para responder a su pregunta, tiene razón en suponer que debo revertir en caso de que solo esté marcada una de las casillas de verificación. En otras palabras, quiero revertir en caso de que una de las condiciones previas que habilita el botón deje de estar satisfecha. El marco de la máquina de estado admite estados históricos ('QHistoryState'), pero no veo cómo puedo hacer que funcionen para mi problema. –

+0

¿Es posible establecer algún tipo de conjunto de estados de falla, de modo que State Machine pueda revertir al estado apropiado en caso de falla?De forma similar al Signal Mapper que se conecta a una ranura dependiendo de dónde se originó la señal. Sospecho que una solución de este tipo podría ser muy tediosa muy rápidamente cuando se enfrentan a muchos estados de falla. – Amos

1

Configure su widget de entrada de peso para que no haya forma de ingresar un peso inferior a cero. Entonces no necesita invalidWeight()

+0

Estaba usando un ejemplo simplificado aquí. En mi aplicación, tengo un teclado numérico de entrada donde el usuario tiene que ingresar un valor. Solo en el caso de que el usuario haya ingresado un valor, el botón en el ejemplo debe estar habilitado. Nuevamente, no estoy realmente interesado en soluciones alternativas, estoy interesado en una solución que use máquinas de estado. –

3

La máquina de estado que utilizó anteriormente no corresponde a lo que describió. Usar un estado final no es correcto porque después de ingresar un valor mayor a cero no veo nada que impida que el usuario ingrese cero nuevamente. Por lo tanto, los estados válidos no pueden ser finales. Por lo que puedo ver en tu código, el usuario puede cambiar el estado de los widgets en cualquier orden. Su máquina de estado tiene que prestar atención a esto.

Utilizaría una máquina de estado con cuatro estados secundarios (sin entrada válida, una entrada válida, dos entradas válidas, tres entradas válidas). Obviamente, comienza sin una entrada válida. Cada artilugio puede hacer una transición de no a uno y atrás (lo mismo cuenta para dos y tres). Cuando se ingresan tres, todos los widgets son válidos (botón habilitado). Para todos los demás estados, el botón debe deshabilitarse cuando se ingresa el estado.

Escribí una aplicación de muestra. La ventana principal contiene dos QCheckBoxes, un QSpinBox y un QPushButton. Hay señales en la ventana principal, la facilidad de escribir las transiciones de los estados.Se disparan cuando se cambia el estado de los widgets.

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

Esto debería hacer el truco. Como usted mismo ya mencionó, hay mejores formas de lograr este comportamiento. Especialmente, haga un seguimiento de todas las transiciones entre el estado propenso a errores.

+0

Usted declara que el estado padre se deja en caso de que uno de los estados secundarios entre en un estado final. Aunque esto no es correcto ya que utilizo un estado parental paralelo. Un estado padre paralelo solo ingresa su estado final en caso de que todos los estados secundarios hayan ingresado a su estado final. De la documentación de Qt: "Para grupos de estado paralelo, la señal QState :: finished() se emite cuando todos los estados secundarios han ingresado estados finales". –

+0

Pensé en una solución similar a la tuya. Sin embargo, no es sonido. Suponiendo que el botón de registro está habilitado, y el usuario ingresa un número inválido dos veces, el usuario tiene que ingresar un número válido dos veces nuevamente para habilitar el botón una vez más. –

+0

Ton, gracias por aclarar la relación de los estados finales y los estados paralelos. No estaba enterado de esto. Pero incluso si se alcanza el estado final, no hay forma de volver al estado inválido. Los estados históricos no ayudan ya que conservan los estados como estaban cuando se dejó el estado padre. No hay garantía de que el usuario vuelva a ingresar el estado no válido de la misma manera que lo dejó. Propongo que corrija el error que menciona en su segundo comentario (vea el código anterior) y que la señal del cuadro de giro solo se active en los cambios de estado verdadero. – merula

1

edición

Reabrí esta prueba, dispuestos a utilizarlo, sumado a .pro

CONFIG += C++11 

y descubrí que la sintaxis lambda ha cambiado ... La lista de captura no puede hacer referencia a las variables de los miembros. Aquí está el código corregido

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

final de edición

utilicé esta cuestión como el ejercicio (por primera vez en QStateMachine). La solución es bastante compacta, usa una transición protegida para moverse entre el estado 'habilitado/deshabilitado' y la configuración lambda para factorizar:

#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(); 
} 
Cuestiones relacionadas