Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. General and Desktop
  4. QStateMachine: How To Implement Logical OR Between States
Qt 6.11 is out! See what's new in the release blog

QStateMachine: How To Implement Logical OR Between States

Scheduled Pinned Locked Moved Unsolved General and Desktop
1 Posts 1 Posters 516 Views
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • KenAppleby 0K Offline
    KenAppleby 0K Offline
    KenAppleby 0
    wrote on last edited by
    #1

    The application I am working on can enter a Paused state for any one of several reasons. In the example I have included here I use just two reasons, called AppPause and LevelPause

    I am using QStateMachine from Qt6.4

    The logic is that the application will be paused if either AppPause or LevelPause are true, or both. The application will resume only when neither are true.

    I struggled to find a simple way of representing this in a state chart. My attempt is shown in the diagram below.

    qstatemachine-or.jpg

    My first question is a general one: is there a simpler way of implementing this relationship between such states in a state chart, bearing in mind that it has to be realised using QStateMachine? It seems to be a lot of complexity for what is really just a logical OR.

    Secondly, using QStateMachine there is a problem with retaining history of the two compound states AppPause and LevelPause.

    If the starting states are AppPaused and LevelPaused, then two consequetive transitions AppResume then LevelResume are made, the following occurs:

    On AppResume: AppPaused is exited, AppUnpaused is entered.,
    On LevelResume: LevelPaused is exited then AppPause exits, then Paused exits. The state LevelUnpaused is not entered.
    This means that when a subsequent AppPause occurs the LevelPause state returns to LevelPaused not LevelUnpaused. This requires extra transitions to put the LevelPause compound state back to where it should be.

    Complete code for a QTest application that demonstrates this is below:

    #include <QTest>
    
    #include <QSignalSpy>
    #include <QStateMachine>
    #include <QState>
    #include <QHistoryState>
    
    class State : public QState
    {
        Q_OBJECT
    public:
        State(QString name, QState * parent);
        State(QString name, QState::ChildMode mode, QState * parent);
    
        void onEntry(QEvent * e) override;
        void onExit(QEvent * e) override;
    
        const QString& name() const;
    
        QString mName;
        int mEntryCount{ 0 };
    };
    
    State::State(QString name, QState * parent)
      : QState{ parent },
        mName{ name }
    {
    }
    
    State::State(QString name, QState::ChildMode mode, QState * parent)
      : QState{ mode, parent },
        mName{ name }
    {
    }
    
    void State::onEntry(QEvent * e)
    {
        QState::onEntry(e);
        qDebug() << "entered" << mName;
        mEntryCount++;
    }
    
    void State::onExit(QEvent * e)
    {
        QState::onExit(e);
        qDebug() << "exited" << mName;
        mEntryCount--;
    }
    
    
    class TestQStateMachine : public QObject
    {
        Q_OBJECT
    
    public:
    
    signals:
    
        void appPause();
        void appResume();
        void levelPause();
        void levelResume();
    
    private slots:
        void initTestCase();
        void testTransitions();
        void cleanupTestCase();
    
    private:
        QStateMachine mStateMachine;
    
        State * mPauseState{ nullptr };
        State * mLevelUnpausedState{ nullptr };
    };
    
    
    void TestQStateMachine::initTestCase()
    {
        State * app{ new State{ "app", &mStateMachine } };
        State * pause{ new State{ "pause", QState::ParallelStates, &mStateMachine } };
        State * appPause{ new State{ "app pause", pause } };
        State * levelPause{ new State{ "level pause", pause } };
        State * appPaused{ new State{ "app paused", appPause } };
        State * appUnpaused{ new State{ "app unpaused", appPause } };
        State * levelPaused{ new State{ "level paused", levelPause } };
        State * levelUnpaused{ new State{ "level unpaused", levelPause } };
    
        QHistoryState * aph{ new QHistoryState{ QHistoryState::HistoryType::ShallowHistory, appPause } };
        QHistoryState * lph{ new QHistoryState{ QHistoryState::HistoryType::ShallowHistory, levelPause } };
        aph->setDefaultState(appPaused);
        lph->setDefaultState(levelPaused);
    
        mPauseState = pause;
        mLevelUnpausedState = levelUnpaused;
    
        State * error{ new State{ "error", &mStateMachine } };
        mStateMachine.setErrorState(error);
    
        appPause->setInitialState(appPaused);
        levelPause->setInitialState(levelPaused);
    
        mStateMachine.setInitialState(pause);
    
        app->addTransition(this, &TestQStateMachine::appPause, appPaused);
        app->addTransition(this, &TestQStateMachine::appPause, lph);
        app->addTransition(this, &TestQStateMachine::levelPause, levelPaused);
        app->addTransition(this, &TestQStateMachine::levelPause, aph);
    
        appPaused->addTransition(this, &TestQStateMachine::appResume, appUnpaused);
        appUnpaused->addTransition(this, &TestQStateMachine::appPause, appPaused);
    
        levelPaused->addTransition(this, &TestQStateMachine::levelResume, levelUnpaused);
        levelUnpaused->addTransition(this, &TestQStateMachine::levelPause, levelPaused);
    
        appUnpaused->addTransition(this, &TestQStateMachine::levelResume, app);
        levelUnpaused->addTransition(this, &TestQStateMachine::appResume, app);
    
        mStateMachine.start();
        QSignalSpy spy(&mStateMachine, &QStateMachine::started);
        spy.wait();
    }
    
    void TestQStateMachine::testTransitions()
    {
        emit appResume(); // app paused => app unpaused
    
        emit levelResume(); // level paused exits but level unpaused is not entered, level pause is exited instead, then pause => app ...
    
        QCOMPARE(mPauseState->mEntryCount, 0);
    
        emit appPause(); // ... which means that here the level pause history state is level paused, not level unpaused
    
        QCOMPARE(mPauseState->mEntryCount, 1);
        QCOMPARE(mLevelUnpausedState->mEntryCount, 1); // This fails.
    }
    
    void TestQStateMachine::cleanupTestCase()
    {
    }
    
    
    QTEST_MAIN(TestQStateMachine)
    #include "qstatemachinetest.moc"
    

    CMakeLists.txt:

    cmake_minimum_required(VERSION 3.23)
    
    project(qstatemachinetest LANGUAGES CXX)
    
    set(CMAKE_AUTOUIC ON)
    set(CMAKE_AUTOMOC ON)
    set(CMAKE_AUTORCC ON)
    
    set(CMAKE_CXX_STANDARD 20)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)
    
    find_package(Qt6 REQUIRED COMPONENTS Core StateMachine Test)
    
    add_executable(qstatemachinetest
      qstatemachinetest.cpp
    )
    target_link_libraries(qstatemachinetest PRIVATE Qt6::Core Qt6::StateMachine Qt6::Test)
    
    set_target_properties(qstatemachinetest PROPERTIES
        WIN32_EXECUTABLE OFF
        MACOSX_BUNDLE ON
    )
    
    1 Reply Last reply
    0

    • Login

    • Login or register to search.
    • First post
      Last post
    0
    • Categories
    • Recent
    • Tags
    • Popular
    • Users
    • Groups
    • Search
    • Get Qt Extensions
    • Unsolved