kzsnyk
kzsnyk

Reputation: 2211

Controlling QML UI from a C++ QStateMachine

I am prototyping an application and i would like to control QML UI transitions from a QStateMachine on the C++ side of the app. To make things easier, we can say that the QML UI consists in several pages which contains buttons that should trigger a transition from one page to another.

// main.qml
Window {

    // ..
        StackLayout {
            id: layout
            anchors.fill: parent
            currentIndex: uiController.currentPage // binding with the C++ side

            Page0 {
                id: page0
            }
            Page1 {
                id: page1
            }
            Page2 {
                id: page2
            }
            Page3 {
                id: page3
            }
        }
    // ..
    }
}

Now each Page has a Button so that the user can to go to another page:

// example of Page0.qml 
Page {
    id: root

    // ..    

    Button {
        text: "Page 1"
        width: 100
        height: 100
        anchors.top: text.bottom
        anchors.horizontalCenter: text.horizontalCenter
        anchors.horizontalCenterOffset: 10
        onClicked: {
            console.log("Button clicked")
            backend.msg = "Button clicked !"
            uiController.buttonClicked = 1; // binding with the C++ side
        }
    }
    // ..      
}

On the C++ side i have a controller that internally use a statemachine to control the transitions :

class UIController : public QObject
{
    Q_OBJECT
    Q_PROPERTY(int buttonClicked READ buttonClicked WRITE setButtonClicked NOTIFY buttonClickedChanged)
    Q_PROPERTY(int currentPage READ currentPage WRITE setCurrentPage NOTIFY currentPageChanged)

public:
    // ..

private:
    QStateMachine m_machine;
    int m_buttonClicked;
    int m_currentPage;
};

Now the important part is the set up of the QStateMachine :

UIController::UIController()
    : m_buttonClicked(0)
{   
    QState *page1 = new QState();
    QState *page2 = new QState();
    QState *page3 = new QState();
    QState *page4 = new QState();
    // ButtonTransition rely on a ButtonEvent 
    ButtonTransition *tr1 = new ButtonTransition(1);
    ButtonTransition *tr2 = new ButtonTransition(2);
    ButtonTransition *tr3 = new ButtonTransition(3);
    ButtonTransition *tr4 = new ButtonTransition(4);

    // the current page is a state property
    page1->assignProperty(this, "currentPage", 0);
    page2->assignProperty(this, "currentPage", 1);
    page3->assignProperty(this, "currentPage", 2);
    page4->assignProperty(this, "currentPage", 3);

    tr1->setTargetState(page2);
    tr2->setTargetState(page3);
    tr3->setTargetState(page4);
    tr4->setTargetState(page1);

    page1->addTransition(tr1);
    page2->addTransition(tr2);
    page3->addTransition(tr3);
    page4->addTransition(tr4);

    m_machine.addState(page1);
    m_machine.addState(page2);
    m_machine.addState(page3);
    m_machine.addState(page4);
    m_machine.setInitialState(page1);

    m_machine.start();
}

And finally for the transitions to occurs :

/* this setter function is called everytime the QML side change the
   buttonClicked property of the UiController */
void UIController::setButtonClicked(int button)
{
    if (m_buttonClicked != button) {
        m_buttonClicked = button;
        m_machine.postEvent(new ButtonEvent(button));
        emit buttonClickedChanged();
    }
}

It actually works but i am asking if there are better ways to do that : i think this approach is a bit "clumsy".

Especially is it possible to bind the state machine transition directly to QML signals ? (as for QSignalTransition)

Thank you.

Upvotes: 0

Views: 981

Answers (2)

pixelgrease
pixelgrease

Reputation: 2118

Instead of QStateMachine, use the declarative state machine framework in QML projects. It is concise, readable and has features not available in QStateMachine such as the TimeoutTransition.

Your sample QML code needs few changes to use the declarative state machine.

Add a top-level signal to your "Page" components:

// Page1.qml
import QtQuick 2.0
import QtQuick.Controls 2.12

Page {
    id: root

    signal clicked()                      // <- new signal

    // ..

    Button {
        text: "Page 1"
        width: 100; height: 100
        onClicked: {
            console.log("Button clicked")
            root.clicked()                // emit new signal
        }
    }
    // ..
}

And here's the entire main.qml with the declarative state machine:

// main.qml
import QtQuick 2.12
import QtQuick.Controls 2.5
import QtQuick.Layouts 1.12
import QtQml.StateMachine 1.12 as DSM

ApplicationWindow {
    visible: true
    width: 640; height: 480

    StackLayout {
        id: layout
        anchors.fill: parent

        Page0 { id: page0 }
        Page1 { id: page1 }
        Page2 { id: page2 }
        Page3 { id: page3 }
    }

    DSM.StateMachine {
        initialState: p0
        running: true

        DSM.State {
            id: p0
            onEntered: layout.currentIndex = 0
            DSM.SignalTransition { targetState: p1; signal: page0.clicked }
        }
        DSM.State {
            id: p1
            onEntered: layout.currentIndex = 1
            DSM.SignalTransition { targetState: p2; signal: page1.clicked }
        }
        DSM.State {
            id: p2
            onEntered: layout.currentIndex = 2
            DSM.SignalTransition { targetState: p3; signal: page2.clicked }
        }
        DSM.State {
            id: p3
            onEntered: layout.currentIndex = 3
            DSM.SignalTransition { targetState: p0; signal: page3.clicked }
        }
    }
}

Upvotes: 1

user268396
user268396

Reputation: 12006

Especially is it possible to bind the state machine transition directly to QML signals ?

Yes. You can connect the entered() signal from any sub-state to e.g. buttonClickedChanged().

Upvotes: 2

Related Questions