[Qt-interest] QStateMachine API and design?

Sean Harmer sean.harmer at maps-technology.com
Mon Nov 8 17:22:38 CET 2010


Hi Steve,

On Thursday 04 November 2010 14:03:22 Stephen Kelly wrote: 
> >> My usecase is to create a state machine and expose it to QML so that I
> >> can change the ui state on change of the application state.
> >> Approximately:
> >> 
> >> Item {
> >> 
> >>   State {
> >>   
> >>     when : application.state == "connecting"
> >>     PropertyChanges { target : myText; text : "Please wait" }
> >>   
> >>   }
> >> 
> >> }
> >> 

OK I have now had a chance to have a play around with exposing a C++ state 
machine such that it can be used to control a QML based GUI. I'll write it up 
as an entry on http://developer.qt.nokia.com/wiki too but here is a brief run 
down on how it works along with a tarball of my source code as it stands now. 

I'll carry on expanding it to show a more complex state machine and also use 
some better images for the GUI once I figure out how to easily get them 
imported from Inkscape in a useful fashion.

OK here goes. This example provides a very simple example of a traffic light. 
The GUI is provided entirely by QML and the logic is controlled by a SM in 
C++.


From the world of C++...
========================

Let's cover the C++ state machine side first. We declare a TrafficLight class 
that inherits from QObject. This will be what we later expose to QML. The 
declaration looks like this:

class TrafficLight : public QObject
{
    Q_OBJECT
    Q_PROPERTY( bool powered READ isPowered() WRITE setPowered NOTIFY 
isPoweredChanged )

public:
    explicit TrafficLight( QObject* parent = 0 );

    bool isPowered() const;

public slots:
    void setPowered( bool b );

signals:
    void isPoweredChanged( bool powered );

private:
    QStateMachine* m_machine;
    OffState* m_off;
    OnState* m_on;

    friend class OnState;
    friend class OffState;
};

The TrafficLight class contains the state machine itself and pointers to the 
top-level custom states (on and off). We are only giving the SM two states in 
this first instance for simplicity but I will extend the OnState in a future 
update such that it properly implements a working traffic light.

Note that we expose the on/off states as the "powered" property.

The ctor of TrafficLight looks like this:

TrafficLight::TrafficLight( QObject* parent )
    : QObject( parent ),
      m_machine( new QStateMachine( this ) ),
      m_off( new OffState( this, m_machine ) ),
      m_on( new OnState( this, m_machine ) )
{
    // We need a transition from off to on
    OnTransition* onTrans = new OnTransition( m_off );
    onTrans->setTargetState( m_on );

    // And from on to off
    OffTransition* offTrans = new OffTransition( m_on );
    offTrans->setTargetState( m_off );

    // Start the state machine
    m_machine->setInitialState( m_off );
    m_machine->start();
}

This just creates the states and a pair of transitions between them. We then 
set the initial state and start the SM (well it will start when we enter the 
event loop).

We could equally well have used plain QState objects as opposed to some custom 
states but I thought I would include it so that you can see how an onEntry() 
function might work in practise in this situation. 

Our two custom states inherit from TrafficLightState which contains a pointer 
to the associated TrafficLight object. This is so that we can emit signals 
from the onEntry() functions on behalf of the TrafficLight object. Of course 
we could have done this just as simply with signal-signal connections but if 
the onEntry() function has to do a lot of work it is often more efficient to 
use a pointer directly. This little bit of coupling between the states and the 
parent object is worth it IMHO.

Furthermore, the TrafficLightState inherits from State which uses the 
onEntry()/onExit() functions to add some useful qDebug() messages.

To allow the outside world to flick the power switch, the TrafficLight class 
provides the following function:

void TrafficLight::setPowered( bool b )
{
    if ( b )
    {
        // Post an OnEvent to the SM
        OnEvent* ev = new OnEvent;
        m_machine->postEvent( ev );
    }
    else
    {
        // Post an OffEvent to the SM
        OffEvent* ev = new OffEvent;
        m_machine->postEvent( ev );
    }
}

This simply posts one of two custom events to the SM as needed and than lets 
the SM structure deal with the consequences. Our custom transition classes 
have very simple eventTest() functions something like this:

bool OnTransition::eventTest( QEvent* event )
{
    return event->type() == QEvent::Type( OnEventOffset );
}

so that the transition only fires when the correct type of event is received.

Finally, when the on/off states are entered we execute an onEntry() function 
that emits the TrafficLight::isPoweredChanged(bool) signal. For e.g.:

void OnState::onEntry( QEvent* event )
{
    TrafficLightState::onEntry( event );
    emit m_trafficLight->isPoweredChanged( true );
}

So now we have declared a simple object with a contained state machine that 
implements the on/off logic of a traffic light (or anything else that can be 
turned on and off at this stage).

In our main() function we expose this new TrafficLight type to the world of 
QML as follows:

// Expose the TrafficLight C++ type to QML space as TrafficLightLogic
qmlRegisterType<TrafficLight>( "SMExample", 1, 0, "TrafficLightLogic" );

So now in QML we are able to access the controlling logical object as the 
TrafficLightLogic QML type. Now let's see how to use it.


...to the world of QML!
=======================

For this first simple example I am just using the built-in QML rectangle type 
as opposed to any fancy graphical components. I do have some assets prepared 
for a later update but I am not a graphical designer ;-)

Starting at the bottom, I have made a simple Light QML type as follows:

import Qt 4.7

Rectangle {
    id: light
    anchors.horizontalCenter: parent.horizontalCenter
    width: 150
    height: 150
    property color baseColor
    color: baseColor

    states: [
        State {
            name: "off"
            when: !logic.powered // This is the declarative way of hooking up 
to the state machine
            PropertyChanges {
                target: light;
                color: Qt.darker( light.baseColor )
            }
        }
    ]

    transitions: [
        Transition {
            ColorAnimation { properties: "color"; duration: 300 }
        }
    ]
}

As you can see this is simply a rectangle with a custom baseColor property and 
two states: the default state and an "off" state. The off state simply sets 
the colour of the rectangle to a darker shade of the base color. Whereas the 
default state uses the baseColor property directly. I've also included a 
simple transition to make it slightly nicer when the Light's state changes.

The key line in the above is this:

when: !logic.powered

"OK", you say. "What is this logic object?" For this we need to go up a level 
in our type hierarchy. The above Light object is only meant to be used within 
the next type, TrafficLight. This is defined as:

import Qt 4.7
import SMExample 1.0

// A background rectangle for the traffic light housing
Rectangle {
    id: trafficLight
    property alias powered: logic.powered

    width: 180
    height: 520
    color: "gray"

    // This is the qml view of the C++ TrafficLight object which contains
    // the state machine controlling the logical operation of the traffic
    // light
    TrafficLightLogic {
        id: logic
    }

    // Lay out the lights in a column
    Column {
        anchors.horizontalCenter : parent.horizontalCenter
        anchors.verticalCenter : parent.verticalCenter
        spacing: 20

        Light {
            id: redLight
            baseColor: "red"
        }
        Light {
            id: amberLight
            baseColor: "orange"
        }
        Light {
            id: greenLight
            baseColor: "green"
        }
    }
}

Here we simply have a 3 lights (red, amber, green) layed out in a column in a 
rectangle representing the body of the traffic light. Such artistic 
imagination :-)

In addition, we declare a TrafficLightLogic object which if you recall maps 
onto the C++ TrafficLight class. This is given the id: logic. We then also 
expose the "powered" property of the TrafficLightLogic object to the outside 
world via an aliased property:

property alias powered: logic.powered

There is no need to set the initial value of the powered property on 
TrafficLightLogic as this is taken care of by the call to m_machine-
>setInitialState( m_off ) in the ctor of the TrafficLight C++ class.

Now we are ready to look at our simple main.qml file to get the entire GUI up 
and running. Without further ado here it is:

import Qt 4.7

Rectangle {
    id: screen
    width: 200
    height: 600

    TrafficLight {
        id: myTrafficLight
        anchors.horizontalCenter: screen.horizontalCenter
    }

    Button {
        id: onButton
        anchors.bottom: parent.bottom
        anchors.left: parent.left
        text: "Power On"
        onClicked: myTrafficLight.powered = true
    }

    Button {
        id: offButton
        anchors.bottom: parent.bottom
        anchors.right: parent.right
        text: "Power Off"
        onClicked: myTrafficLight.powered = false
    }
}

As you can see we simply make a rectangle as our screen and then declare a 
TrafficLight object. All we do with it is give it an id and position it. No 
more is needed.

The Button type is just a very simple pushbutton type so I won't explain it 
here. We just make two buttons: "Power On" and "Power Off". I have implemented 
the onClicked signal handlers for the buttons to set the powered property of 
the TrafficLight object as appropriate.

And that is it! A little more explanation is warranted though as the code path 
is non-trivial.

When the application is executed the state machine takes care of putting the 
system into the off state. If the user then clicks on the "Power On" button 
the following takes place:

1). The onButton.onClicked signal handler is executed. This sets the powered 
property of the TrafficLight object to true.

2). This property is aliased to the powered property of the TrafficLightLogic 
object declared in TrafficLight.qml. 

3). The TrafficLightLogic object is a type exported from C++, namely the 
TrafficLight class. So this gets routed to a call to TrafficLight::setPowered( 
true ).

4). This call results in a OnEvent being posted to the SM's event queue.

5). When the SM processes this event, the structure of the SM causes the 
OnTransition::eventTest() function to be called. This allows the transition to 
happen.

6). The SM enters the m_on state which means that the OnState::onEntry() 
function gets called. This emits the TrafficLight::isPoweredChanged( bool ) 
signal.

7). The signal emission is used by the QML machinery to update any bindings 
that are dependent upon this property, namely the Light objects' state 
handler. Recall that "when: !logic.powered" we saw earlier.

8). This causes the Lights' transition to come into effect which takes care of 
animating the GUI changes that correspond to the logical change in state 
inside the C++ SM.


Summary
=======

Although the above may seem scary at first glance, when you get down to it 
things are actually quite simple and elegant IMHO. The SM controls the logic 
and we expose this to the QML side which then declares a "logic controller" 
object and uses it's properties to update the necessary elements of the GUI.

I know that this example is a little contrived in that traffic lights do not 
normally have an on/off state (unless you include a connection to the power 
grid in your SM model). However, this example could very easily be extended to 
more realistically model a working traffic light system.

For example, you could add more child states to the OnState class (for the UK 
system):

* Stopped (red)
* Starting (red + amber)
* Started (green)
* Stopping (amber)

I see a coule of options on how we could map from this conceptual model to the 
QML GUI.

a). The light state changes could be exposed from TrafficLight by adding 
boolean  properties for the red, amber, and green lights along with suitable 
NOTIFY signals. These new properties could then be used in custom RedLight, 
AmberLight, and GreenLight qml objects in a similar way as demonstrated here. 
The NOTIFY signals for the red, amber, green lights would be emitted in the 
onEntry() functions of the corresponding custom states. For e.g.:

void StartingState::onEntry( QEvent* e )
{
    TrafficLightState::onEntry( e ); // Show qDebug() output
    emit m_trafficLight->redLightChanged( true );
    emit m_trafficLight->amberLightChanged( true );
}

With the above approach the SM is closely coupled to the UK system of traffic 
lights as the exact light sequence is controlled by the C++ SM. Another 
approach is to use:

b). Instead of having properties for the red, green, amber lights on the 
TrafficLight object we instead have properties that map onto the conceptual 
states. For example a "starting" property. With this approach the SM can be 
reused for any traffic light GUI since the GUI is then responsible for mapping 
the conceptual state onto a specific combination of lights.

Which of the above options one would go for would be dependent upon the use 
case or requirements.

Coupling the above with some improved graphical assets would yield a nice 
little traffic light simulator.

Anyway, this post has turned into a rather lengthy one but I hope it helps to 
show one possible way of separating the GUI from the backend logic when making 
use of a C++ SM and a QML GUI. 

The advantage of doing this is that the state machines possible with pure QML 
seem quite limited. From what I can tell so far pure QML does not support 
hierarchical states, guarded transitions, history states etc. Also I think it 
is nicer to have the logic contained in the C++ side for a number of reasons.

HTH,

Sean
-------------- next part --------------
A non-text attachment was scrubbed...
Name: trafficlight.tar.gz
Type: application/x-compressed-tar
Size: 14717 bytes
Desc: not available
Url : http://lists.qt-project.org/pipermail/qt-interest-old/attachments/20101108/03f488b9/attachment.bin 


More information about the Qt-interest-old mailing list