Qt state machines: more transition attempts than expected - c++

I have created a simplified example of a state machine transitioning between 2 states on a button click (+ a checkbox showing the active state). I am seeing more calls to the transitions'eventTest method than I expect.
#include <QtStateMachine/QStateMachine>
#include <QtStateMachine/QSignalTransition>
#include <QtWidgets/QApplication>
#include <QtWidgets/QCheckBox>
#include <QtWidgets/QPushButton>
class MySignalTransition : public QSignalTransition
{
public:
MySignalTransition(QString name, QPushButton* button, QCheckBox* checkBox) :
QSignalTransition(button, &QAbstractButton::clicked),
cb(checkBox)
{
setObjectName(name);
}
protected:
bool eventTest(QEvent* e) override {
qDebug() << objectName() << "tested on" << e->type() << '|' << cb->isChecked();
return QSignalTransition::eventTest(e);
}
private:
QCheckBox* cb;
};
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
app.setQuitOnLastWindowClosed(true);
QWidget w;
w.setFixedSize(200, 200);
QPushButton button(QStringLiteral("Click me"), &w);
QCheckBox checkbox(&w);
button.move(10, 80);
checkbox.move(10, 20);
QStateMachine m;
QState s1, s2;
m.addState(&s1);
m.addState(&s2);
m.setInitialState(&s1);
s1.assignProperty(&checkbox, "checked", false);
s2.assignProperty(&checkbox, "checked", true);
//button will be used to make the transition back and forth between states s1 and s2
MySignalTransition t1("Transition 1", &button, &checkbox), t2("Transition 2", &button, &checkbox);
t1.setTargetState(&s2);
t2.setTargetState(&s1);
s1.addTransition(&t1);
s2.addTransition(&t2);
w.show();
m.start();
return app.exec();
}
On top of the checkbox, I placed a qDebug() << ... to watch when Qt attempts to perform transitions.
The first attempt is right when the state machine starts, before I do any click on the button. I guess (but I am not sure) that this is expected.
The real problem happens when I actually do click on the button. I see:
eventTest is called from the starting state (event->type() == QEvent::None)
eventTest is called from the starting state (event->type() == QEvent::StateMachineSignal)
eventTest is called from the target state (event->type() == QEvent::None)
The one call I am most surprised about is point 1.
What causes the state machine to try the transition again when I click on the button with QEvent::None? This is especially surprising to me because it is always a transition the state machine has already tried (either on start or on the previous click).
If I were to create another transition out of s1 and/or s2, should I be worried that transition could have a higher priority than my signal transitions without any way for me to control it?
Edit
To address #Scheff's cat comment below, I changed the transition class to be:
class MySignalTransition : public QSignalTransition
{
public:
MySignalTransition(QString name, QPushButton* button, QCheckBox* checkBox) :
QSignalTransition(button, &QAbstractButton::clicked),
cb(checkBox)
{
setObjectName(name);
}
protected:
bool event(QEvent* e) override {
return false;
}
bool eventTest(QEvent* e) override {
qDebug() << objectName() << "tested on" << e->type() << '|' << cb->isChecked();
return transition;
}
private:
QCheckBox* cb;
public:
inline static bool transition = false;
};
and right after creating the push button, added:
QObject::connect(&button, &QPushButton::clicked, []() { MySignalTransition::transition = true; });
By doing that, the state machine lets me do 1 click before it starts an infinite loop.
When clicking, MySignalTransition::transition is set to true, then eventTest is called.
With a breakpoint, it is easy to see eventTest is never called with event->type() == QEvent::StateMachineSignal.
To me, this is a clear indication there is more to it than just probing.
First, it would need to do the probing with the actual event, not with a dummy event.
Second, it would not do the transition if the result of the probing was correct, I still should see the 2nd call to eventTest.
Last, I thought maybe the state machine tries to execute as many transitions as it can and will then pass the click signal to whatever other state it lands on.However, I am not very satisfied by that explanation because the attempt to execute an automatic transition is already done when entering the state (it is the third call to eventTest) and because I think my click should be applied on the state that is active at the very moment I do it, not on just "whatever other state it lands on".

Related

Handling different gui states

So I want to create the gui interface in Qt/cpp for the server which can be in many different states, and depending on its state the buttons in gui need to be set differently ex:
GUI:
button1 - unchecked and enabled to click
button2 - disabled(grayed out)
button3 - disabled
button3 - disabled
Then after click button1
GUI:
button1 - checked
button2 - enabled to click
button3 - enabled to click
button3 - enabled to click
But for example if server is in different state and you connect via gui the buttons should look like this:
GUI:
button1 - checked
button2 - enabled to click
button3 - disabled to click
button3 - disabled to click
Is there some established pattern/way of handling that intuitively? The biggest problem here is that if the server has a lot of different states that need the buttons to be set in a lot of different configurations. The only thing I can come up with is mapping the state of all buttons to the specific state but well... there's a lot of buttons and a lot of states.
You could try using flags, the idea is that when an event happens and you want the GUI to change you set a flag which in turn is recalled in a loop. Below you can see the general idea and concept.
If you change the state of the flag you will get a different output and it will loop over and over listening for events just write the GUI code for each in the different states.
#include <iostream>
using namespace std;
int getserverstatusfunction() {/*your code to check server status returns 0,1 or 2*/
return 0;
}
UI.button.click(true) { getresult = 1; }; //change the UI state when the button is clicked
int main() {
bool running;
while (running){
int getresult = getserverstatusfunction();
if (getresult == 0)
{
cout << "Draw flag one interface code\n";
}
else if (getresult == 1)
{
cout << "Draw flag two interface code\n";
}
else {
cout << "Draw flag three interface code\n";
}
system("pause");
return 0;
}
I've found the best way to do this is just to have a single slot-method (e.g. UpdateAllButtonStates() that updates all of your buttons and checkboxes, e.g.:
void MyWindow::UpdateAllButtonStates() // Qt-slot method
{
const bool shouldButton1BeEnabled = [...];
button1->setEnabled(shouldButton1BeEnabled);
const bool shouldButton2BeEnabled = [...];
button2->setEnabled(shouldButton2BeEnabled);
[... and so on for all enables and checked/unchecked states in your GUI]
}
... then anytime your program's internal state has changed in any way that might require an update to one or more buttons/checkboxes in the GUI, call this method explicitly, or set up a signal/slot connection that will call it for you.
The advantage of doing it this way is the simplicity -- with this approach, it's trivial to guarantee that your GUI widgets will be in updated to the expected state after any internal-state-change, because there is only one code-path to write and debug. The alternative (trying to come up with the correct transitional behavior for every possible state-change in your program) quickly leads to an intractable amount of complexity, and endless debugging and hair-pulling.
You might think the downside is inefficiency -- after all, we are updating all the buttons even though in any cases, only one of them may have changed -- but Qt's code is smart enough that calling setEnabled(false) on a button that is already disabled is a no-op (likewise calling setEnabled(true) on a button that is already enabled, and so on), so the heavyweight code of redrawing a widget's pixels will only be executed when the widget's state has actually changed.
The logic inside UpdateAllButtonStates() that calculates shouldButton1BeEnabled, etc, does get executed a lot, but it usually ends up being pretty trivial logic so that turns out not to be important. However, if for some reason that logic turns out to be expensive, you have the option of reducing the frequency at which UpdateAllButtonStates() gets executed, by using asynchronous execution and a boolean "dirty-bit", e.g.:
void MyWindow::ScheduleUpdateAllButtonStates() // Qt-slot method
{
if (_dirtyBit == false)
{
_dirtyBit = true;
QTimer::singleShot(0, this, SLOT(UpdateAllButtonStates()));
}
}
void MyWindow::UpdateAllButtonStates()
{
if (_dirtyBit == false) return;
_dirtyBit = false;
// Update button enables/checked states as previously, here
}
... then have all your internal-state-change code call ScheduleUpdateAllButtonStates() rather than calling UpdateAllButtonStates() directly; the advantage is that even if ScheduleUpdateAllButtonStates() gets called 500 times in a row, it will only result in UpdateAllButtonStates() getting called once, during the next iteration of Qt's event loop.
Enabling/disabling button UI logic can be very dirty and difficult to manage and trace. Also, sometimes we want to be in particular state and want to make a minor change like changing state of just one button. Here is an approach. It's generic but you will have to adrop it accordingly with your UI.
#include <iostream>
class UIState
{
protected:
bool btn1;
bool btn2;
bool btn3;
public:
UIState()
{
btn1 = false;
btn2 = false;
btn3 = false;
}
virtual void setBtn1State(bool new_state)
{
btn1 = new_state;
std::cout << btn1 << btn2 << btn3 << std::endl;
};
virtual void setBtn2State(bool new_state)
{
btn2 = new_state;
std::cout << btn1 << btn2 << btn3 << std::endl;
};
virtual void setBtn3State(bool new_state)
{
btn3 = new_state;
std::cout << btn1 << btn2 << btn3 << std::endl;
};
};
class UIStateAllEnabled : public UIState
{
public:
UIStateAllEnabled()
{
btn1 = true;
btn2 = true;
btn3 = true;
std::cout << btn1 << btn2 << btn3 << std::endl;
}
};
class UIStateAllDisabled : public UIState
{
public:
UIStateAllDisabled()
{
btn1 = false;
btn2 = false;
btn3 = false;
std::cout << btn1 << btn2 << btn3 << std::endl;
}
};
class UI
{
UIState * currentState;
public:
UI()
{
currentState = NULL;
}
~UI()
{
if (currentState != NULL)
{
delete currentState;
std::cout << "deleted current state" << std::endl;
}
}
void setState(UIState * new_state)
{
// should also check for if already current state?
UIState * prevState = currentState;
currentState = new_state;
if (prevState != NULL)
{
delete prevState;
std::cout << "deleted previous state" << std::endl;
}
}
void setBtn1State(bool new_state)
{
currentState->setBtn1State(new_state);
};
void setBtn2State(bool new_state)
{
currentState->setBtn2State(new_state);
};
void setBtn3State(bool new_state)
{
currentState->setBtn3State(new_state);
};
};
int main()
{
UI ui;
// enable all buttons
ui.setState(new UIStateAllEnabled);
// Now say you want to change state of a particular button within this state.
ui.setBtn1State(false);
ui.setBtn3State(false);
// switch to a completely new state, disable all buttons
ui.setState(new UIStateAllDisabled);
// customize within that sate
ui.setBtn3State(true);
return 0;
}

slow response on right click contex menu in graphic scene Qt

I have set a large menu in event filter on right click with 45-50 actions
inside and I find that when I right click the response to show the menu is slow
I did try the same code with 5 actions in the menu and the response was fine.
Is there something wrong with this way of coding on a contex menu ?
eventFilter
bool Editor::eventFilter(QObject *o, QEvent *e)
{
Q_UNUSED (o);
QGraphicsSceneMouseEvent *me = (QGraphicsSceneMouseEvent*) e;
switch ((int) e->type()){
case QEvent::GraphicsSceneMousePress:{
switch ((int) me->button()){
case Qt::RightButton:{
QGraphicsItem *item = itemAt(me->scenePos());
showContextMenu(item->scenePos().toPoint());
return true;
}
//more cases here//
}
break;
}
}
return QObject::eventFilter(o, e);
}
showContextMenu
void Editor::showContextMenu(const QPoint &pos)
{
QGraphicsItem *item =itemAt(pos);
// Create main effe menu
effeMenu= new QMenu("Menu");
QString menuStyle(
"QMenu {"
"border:10px };"
//more code here
);
effeMenu->setStyleSheet(menuStyle);
AmpMenu=effeMenu->addMenu(QIcon(":/effectImg/img/effePng/amp.png"),"Amp");
Amp1 =AmpMenu->addAction(QIcon(":/effectImg/img/effePng/amp.png"),"Amp 1");
Amp2 =AmpMenu->addAction(QIcon(":/effectImg/img/effePng/amp.png"),"Amp 2");
CabMenu=effeMenu->addMenu(QIcon(":/effectImg/img/effePng/cab.png"),"Cab");
Cab1 =CabMenu->addAction(QIcon(":/effectImg/img/effePng/cab.png"),"Cab 1");
Cab2 =CabMenu->addAction(QIcon(":/effectImg/img/effePng/cab.png"),"Cab 2");
.
.
.
.
//45 actions more
connect(effeMenu, &QMenu::triggered,this,[this,&item](QAction * k){
menuSelection(k,item);
});
Instead of creating a new QMenu each time you call showContextMenu you could make it a member of the class and build it once. On the other hand it is not necessary to use a signal, you could simply use the exec() method of QMenu:
*.h
class Editor: ...{
...
private:
QMenu effeMenu;
}
*.cpp
Editor::Editor(...){
effeMenu.setTitle("Menu");
QString menuStyle(
"QMenu {"
"border:10px };"
//more code here
);
effeMenu.setStyleSheet(menuStyle);
AmpMenu=effeMenu.addMenu(QIcon(":/effectImg/img/effePng/amp.png"),"Amp");
Amp1 =AmpMenu->addAction(QIcon(":/effectImg/img/effePng/amp.png"),"Amp 1");
Amp2 =AmpMenu->addAction(QIcon(":/effectImg/img/effePng/amp.png"),"Amp 2");
CabMenu=effeMenu.addMenu(QIcon(":/effectImg/img/effePng/cab.png"),"Cab");
Cab1 =CabMenu->addAction(QIcon(":/effectImg/img/effePng/cab.png"),"Cab 1");
Cab2 =CabMenu->addAction(QIcon(":/effectImg/img/effePng/cab.png"),"Cab 2");
...
}
void Editor::showContextMenu(const QPoint &pos){
QGraphicsItem *item =itemAt(pos);
QAction *action = menu.exec(pos);
menuSelection(action, item);
}
There are two things you can do to improve speed:
1 - itemAt(pos) is costly, and you are doing it twice, one in the event, and one in the showContextMenu. From what I could understand from your code you don't need the item in the event, just in the showMenu.
2 - The menu creation that you are doing is expensive: all the actions have pixmaps. this allocs memory for the QPixmap, loads, execute, dumps. Because you told us that you use around 40 actions (and really, that's too much for a menu), this can get costly.
My advice:
Create a class for your menu, create one instance of it, add a setter for the current QGraphicsObject that your menu will work on, and always use that one instance.

QT doesn't properly processes events in loop

I want to repeat the action performed in a SLOT until a qpushbutton is down.So I've done the following connections:
connect(ui->button,SIGNAL(pressed()),this,SLOT(button_hold()));
connect(ui->button,SIGNAL(released()),this,SLOT(button_released()));
and I've implemented the SLOTS in the following way
void My_class::button_hold(){
//CLASS ATTRIBUTE key_is_released , i
QThread::msleep(200);
int wait_lock = 500;
i++; //GOAL OF THE SLOT
QCoreApplication::processEvents(QEventLoop::AllEvents);
while(!key_is_released){
QThread::msleep(wait_lock);
i++;
cout<<i<<endl;
if(wait_lock > 50) wait_lock -= 50;
QCoreApplication::processEvents(QEventLoop::AllEvents);
}
key_is_released = false;
}
void My_class::button_released(){
key_is_released = true;
}
The goal is to repeat the action in the button_hod() Slot (In the example it's to increase i) even more quickly decreasing wait_lock and keeping the button down.The issue is that if i click button two times or more in a quick way the loop never ends,like if processEvents() would not work.The firts msleep is used in order to do not enter the loop (i is increased just once)if the button is just clicked and it is not keep down too long.If i do not click quickly but I keep down the button the method works well.
What am I doing wrong?
Reentering the event loop is a bad idea, and leads to spaghetti code.
Qt provides a wonderful state machine system with UML semantics. It's often best to model the system you're designing as a state machine. Here, there are two states: an idle state, and an active state: the value is incremented periodically while active.
The state machine decouples the Ui particulars (a button) from the core functionality: that of a periodically incrementing value.
Ideally, you would also factor out the state machine and the value to a controller class, and only connect the controller and the Ui from main() or a similar function.
// https://github.com/KubaO/stackoverflown/tree/master/questions/48165864
#include <QtWidgets>
#include <type_traits>
class Ui : public QWidget {
Q_OBJECT
int m_value = -1;
QStateMachine m_machine{this};
QState m_idle{&m_machine}, m_active{&m_machine};
QVBoxLayout m_layout{this};
QPushButton m_button{"Hold Me"};
QLabel m_indicator;
QTimer m_timer;
void setValue(int val) {
if (m_value == val) return;
m_value = val;
m_indicator.setNum(m_value);
}
void step() {
if (m_value < std::numeric_limits<decltype(m_value)>::max())
setValue(m_value + 1);
}
public:
Ui(QWidget * parent = {}) : QWidget(parent) {
m_layout.addWidget(&m_button);
m_layout.addWidget(&m_indicator);
m_machine.setInitialState(&m_idle);
m_idle.addTransition(&m_button, &QPushButton::pressed, &m_active);
m_active.addTransition(&m_button, &QPushButton::released, &m_idle);
m_machine.start();
m_timer.setInterval(200);
connect(&m_timer, &QTimer::timeout, this, &Ui::step);
connect(&m_active, &QState::entered, [this]{
step();
m_timer.start();
});
connect(&m_active, &QState::exited, &m_timer, &QTimer::stop);
setValue(0);
}
};
int main(int argc, char ** argv) {
QApplication app{argc, argv};
Ui ui;
ui.show();
return app.exec();
}
#include "main.moc"
The problem was that clicking fast let say 10 times ,the release slot was called yet 10 times before entering in the loop, so key_is_released would be never updated again to true.Instead the button_hold slot was not yet been called for ten times.So adding a counter, increasing it in the button_hold slot and decreasing it in the release slot and adding a second condition counter>0 besides !key_is_released in the while loop fixes all.(It enters the loop only if not all the release slot have been run)

Yes/No message box using QMessageBox

How do I show a message box with Yes/No buttons in Qt, and how do I check which of them was pressed?
I.e. a message box that looks like this:
You would use QMessageBox::question for that.
Example in a hypothetical widget's slot:
#include <QApplication>
#include <QMessageBox>
#include <QDebug>
// ...
void MyWidget::someSlot() {
QMessageBox::StandardButton reply;
reply = QMessageBox::question(this, "Test", "Quit?",
QMessageBox::Yes|QMessageBox::No);
if (reply == QMessageBox::Yes) {
qDebug() << "Yes was clicked";
QApplication::quit();
} else {
qDebug() << "Yes was *not* clicked";
}
}
Should work on Qt 4 and 5, requires QT += widgets on Qt 5, and CONFIG += console on Win32 to see qDebug() output.
See the StandardButton enum to get a list of buttons you can use; the function returns the button that was clicked. You can set a default button with an extra argument (Qt "chooses a suitable default automatically" if you don't or specify QMessageBox::NoButton).
You can use the QMessage object to create a Message Box then add buttons :
QMessageBox msgBox;
msgBox.setWindowTitle("title");
msgBox.setText("Question");
msgBox.setStandardButtons(QMessageBox::Yes);
msgBox.addButton(QMessageBox::No);
msgBox.setDefaultButton(QMessageBox::No);
if(msgBox.exec() == QMessageBox::Yes){
// do something
}else {
// do something else
}
QT can be as simple as that of Windows. The equivalent code is
if (QMessageBox::Yes == QMessageBox(QMessageBox::Information, "title", "Question", QMessageBox::Yes|QMessageBox::No).exec())
{
}
I'm missing the translation call tr in the answers.
One of the simplest solutions, which allows for later internationalization:
if (QMessageBox::Yes == QMessageBox::question(this,
tr("title"),
tr("Message/Question")))
{
// do stuff
}
It is generally a good Qt habit to put code-level Strings within a tr("Your String") call.
(QMessagebox as above works within any QWidget method)
EDIT:
you can use QMesssageBox outside a QWidget context, see #TobySpeight's answer.
If you're even outside a QObject context, replace tr with qApp->translate("context", "String") - you'll need to #include <QApplication>
QMessageBox includes static methods to quickly ask such questions:
#include <QApplication>
#include <QMessageBox>
int main(int argc, char **argv)
{
QApplication app{argc, argv};
while (QMessageBox::question(nullptr,
qApp->translate("my_app", "Test"),
qApp->translate("my_app", "Are you sure you want to quit?"),
QMessageBox::Yes|QMessageBox::No)
!= QMessageBox::Yes)
// ask again
;
}
If your needs are more complex than provided for by the static methods, you should construct a new QMessageBox object, and call its exec() method to show it in its own event loop and obtain the pressed button identifier. For example, we might want to make "No" be the default answer:
#include <QApplication>
#include <QMessageBox>
int main(int argc, char **argv)
{
QApplication app{argc, argv};
auto question = new QMessageBox(QMessageBox::Question,
qApp->translate("my_app", "Test"),
qApp->translate("my_app", "Are you sure you want to quit?"),
QMessageBox::Yes|QMessageBox::No,
nullptr);
question->setDefaultButton(QMessageBox::No);
while (question->exec() != QMessageBox::Yes)
// ask again
;
}
If you need asynchronous call you should use open and result methods instead of question or exec. Sample code inside a QWidget method:
QMessageBox* const message = new QMessageBox(QMessageBox::Icon::Question, tr("Test"),
tr("Quit?"), QMessageBox::Button::Yes | QMessageBox::Button::No, this);
message->setDefaultButton(QMessageBox::Button::No);
message->open();
connect(message, &QDialog::finished, this, [message] {
message->deleteLater();
if (message->result() == QMessageBox::Button::Yes) {
QApplication::quit();
}
});
It should not be usefull just for a quit dialog but for other confirmation dialogs where parent widget might be destroyed by external events it is the main way to avoid a crash.
Python equivalent code for a QMessageBox which consist of a question in it and Yes and No button. When Yes Button is clicked it will pop up another message box saying yes is clicked and same for No button also. You can push your own code after if block.
button_reply = QMessageBox.question(self,"Test", "Are you sure want to quit??", QMessageBox.Yes,QMessageBox.No,)
if button_reply == QMessageBox.Yes:
QMessageBox.information(self, "Test", "Yes Button Was Clicked")
else :
QMessageBox.information(self, "Test", "No Button Was Clicked")
If you want to make it in python you need check this code in your workbench.
also write like this.
we created a popup box with python.
msgBox = QMessageBox()
msgBox.setText("The document has been modified.")
msgBox.setInformativeText("Do you want to save your changes?")
msgBox.setStandardButtons(QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel)
msgBox.setDefaultButton(QMessageBox.Save)
ret = msgBox.exec_()

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?
After reading your requirements and the answers and comments here I think merula's solution or something similar is the only pure Statemachine solution.
As has been noted to make the Parallel State fire the finished() signal all the disabled states have to be final states, but this is not really what they should be as someone could uncheck one of the checkboxes and then you would have to move away from the final state. You can't do that as FinalState does not accept any transitions. The using the FinalState to exit the parallel state also causes the parallel state to restart when it is reentered.
One solution could be to code up a transition that only triggers when all three states are in the "good" state, and a second one that triggers when any of those is not. Then you add the disabled and enabled states to the parallel state you already have and connect it with the aforementioned transitions. This will keep the enabled state of the button in sync with all the states of your UI pieces. It will also let you leave the parallel state and come back to a consistent set of property settings.
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;
}
Did not compile this or even test it, but it should demonstrate the principle
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.
When I have to do things like this I usually use signals and slots. Basically each of the widgets and the number box will all emit signals automatically when their states change. If you link each of these to a slot that checks if all 3 objects are in the desired state and enables the button if they are or disables it if they aren't, then that should simplify things.
Sometimes you will also need to change the button state once you've clicked it.
[EDIT]: I'm sure there is some way of doing this using state machines, will you only be reverting in the situation that both boxes are checked and you've added an invalid weight or will you also need to revert with only one checkbox checked? If it's the former then you may be able to set up a RestoreProperties state that allows you to revert to the checked box state. Otherwise is there some way you can save the state before checking the weight is valid, revert all checkboxes then restore the state.
Set up your weight input widget so that there is no way a weight less than zero can be entered. Then you don't need invalidWeight()
edit
I reopened this test, willing to use it, added to .pro
CONFIG += C++11
and I discovered that lambda syntax has changed... The capture list cannot reference member variables. Here is the corrected code
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);
};
end edit
I used this question as exercise (first time on QStateMachine). The solution is fairly compact, using a guarded transition to move between 'enabled/disabled' state, and lambda to factorize setup:
#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();
}