[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