Making Qt-Widgets State-Driven

Utilizing SCXML state machines to make the behavior of Qt applications more explicit

Frameworks like Qt are built with the Model-View-Controller architecture in mind. This architecture aims to make the code of complex user interfaces more modular and, therefore, easier to write and maintain. As the name suggests, it splits the functionality of user interfaces into 3 distinct components.

The model component represents the underlying data that the user interacts with when operating the interface. This can be an interface for a database, a file, a web service, or other data sources. It can be modified or updated by the controller or the view components and also informs the other components of any changes.

The view component, on the other hand, defines how the data is presented to the user and how the user can interact with it. It could be a graphical interface, or it could be a textual or acoustic representation. If the data of the model component changes it can notify the view. The view can also modify the model when the user interacts with it.

Gluing everything together, the controller component acts as the logical link between the view and the model. Not every part of the model is directly represented by the view and not every aspect of the view is part of the model. Because of this disconnect, the controller fills in the gaps and represents all the implicit data and interactions that are not part of the model-view-connection.

Both model and view components can be structured quite easily since they represent well-defined concepts. A view is like a collection of all the parts that the user interacts with and a model is just a data structure that can be modified and notifies other components when something changes.  The controller, however, is hard to define and acts more like a pile of scrap that doesn't really belong to either the view or the model components.

For this reason, Qt simplifies the MVC pattern and merges view- and controller components. Instead of dealing with the ambiguity of the controller definition. It allows the developer to implement the interface between the user and the data, in any way they want. This model/view pattern puts the responsibility of structuring the code into the hands of developers and results in a huge variance of unique implementations and architectures.

Using State Machines to Define Behavior

The MVC architecture is mainly concerned with structuring your code and is less about the actual implementation details. The behavior and the state of the interface are implicitly defined by the connections between the model, view, and controller components. This implicit definition makes the code harder to understand, test, and maintain. In this post, I want to explore the usage of state machines to make that behavior more explicit. Let's start with a simple example implementation:

The program in the picture above is a simple CSV editor. Using the File menu, you can open and save CSV files. By double-clicking any table entry, you can edit its contents. The Add Row button adds a row to the bottom of the table while the Remove Selected button deletes the currently selected row.

If we were to implement this using Qts Model/View architecture, the view component would be comprised of the buttons, the table view, the menu bar, etc... These components can be statically defined using Qts ui-files and result in auto-generated C++ code. The model component is just a simple table that is internally stored in a CSV file (Qt offers that component as well). The portion of the View component, that you have to self-implement, are the dynamically generated UI components like buttons that only appear during runtime and the actual behavior of all components (usually implemented using Qts signals and slots).

This class diagram shows the code structure of the CSV editor. The MainWindow is the core component that needs to be implemented here. The ui property of that class contains the statically defined UI components.  These two represent the "view" in Qts Model/View architecture. The actual behavior that is a result of user interactions will be handled using the signals and slots of MainWindow. They update the CSVModel which represents the model part of the architecture. To understand the complexity of this behavior, let's have a look at a state machine, representing it:

I have defined the behavior in the form of two parallel state machines (the green ones).  The DialogWindowState on the left side, defines the behavior of all the dialog windows. It starts in the NoFileOpenedState where there is just an empty main window. When the file-open action in the File menu bar is clicked, a file-selection dialog will be opened and the state machine transitions to the FileOpenDialogState. From there, you can either transition back if no valid file was selected or go to the FileModificationState. In this state the CSV file was loaded, is visible in the table view, and can be edited. Once there is a request to close the window, a save changes dialog will appear if the data was modified. After the changes are saved or discarded, we arrive in the MainWindowClosedState which marks the end of execution.

In parallel to all of this, there is also the RowSelectionState machine on the right side which keeps track of the currently selected, added, and removed rows. In order to select rows, there must be a selection model (which means a file was opened). If that is the case, we are in the ExistingSelectionModelState where we can select and deselect rows, remove selected ones or add rows to the end of the table. This is just one of many ways to describe the behavior of such a program but it makes clear how much information wasn't actually addressed in the Model/View architecture.

This state machine was created using Qt's SCXML framework, which offers a way of visually editing state machines and integrating them using signals and slots so that direct interactions with other Qt components are possible. The open specification of the SCXML standard allows for integration with other frameworks, which means that state machines can not only communicate via signals and slots but we can also execute custom C++ code within them using implementations like the QScxmlCppDataModel. To integrate this into the example, the architecture would change to something like this:

The actual behavior was previously handled using the signals and slots of MainWindow class. With this updated implementation, the behavior (the green component) can be separated from the view components and is more explicitly defined using the HiddenState::Interface. It acts as an interface between the Model/View architecture and the QScxmlStateMachine. Every signal that was generated by the Model/View architecture is sent to the state machine, using the interface slots. The state machine acts on those signals, transitions between states, and uses the QScxmlCppDataModel to do more complex computations internally. While doing that, it sends the appropriate signals back to Qt, using the signals of that Interface.

Code Example

With a rough understanding of the whole structure, let's dive into a small code example. All the code of the CSV editor can be downloaded below but in this example, I will focus on just the file selection dialog part of it. As mentioned earlier: in the File menu, you can click on an open action that is connected to the state machine interface (the hiddenState).

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
    , hiddenState(new HiddenState(this))
{
    // Setup UI
    ui->setupUi(this);
    ...

    // MainWindow -> HiddenState
    connect(ui->actionOpen, &QAction::triggered, hiddenState, &HiddenState::onOpenAction);
    ...

    // Start state machine
    hiddenState->startStateMachine();
}
    

This interface turns the signal into an event for the state machine (sm).

HiddenState::Interface::Interface(QObject* parent)
    : QObject(parent)
    , dm(new DataModel(this))
    , sm(new MainWindowStateMachine(this))
{
    sm->setDataModel(dm);
    sm->init();
}

void HiddenState::Interface::onOpenAction() {
    sm->submitEvent("OpenActionSlot");
}
    

As can be seen in the state machine picture above, the OpenActionSlot event causes a transition to the FileOpenDialogState. In the actual SCXML file, this behavior looks something like this:

<scxml>
    <parallel id="MainWindowStates">
        <state id="DialogWindowState">
            <state id="NoFileOpenedState">
                <transition event="OpenActionSlot" target="FileOpenDialogState"/>
            </state>
            <state id="FileOpenDialogState">
                <onentry>
                    <send event="FileOpenDialogSignal"/>
                </onentry>
            </state>
        </state>
        ...
    </parallel>
</scxml>
    

The XML file here is just another representation of the state machine from the picture above. One thing you can only see in this file, however, is the signal that will be sent when the FileOpenDialogState is entered. This FileOpenDialogSignal will be sent back to Qt and forwarded by the Interface.

HiddenState::Interface::Interface(QObject* parent)
    ...
{
    sm->connectToEvent("FileOpenDialogSignal", this, &Interface::openFileDialog);
    ...
}
    

The forwarded signal then results in opening the file selection dialog:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    ...
    , openFileDialog(new QFileDialog(this))
{
    ...
    // HiddenState -> MainWindow
    connect(hiddenState, &HiddenState::openFileDialog, openFileDialog, qOverload<>(&QFileDialog::open));
    ...
}
    

This implementation allows us to describe all the implicit behavior using a state machine and have one interface class that translates all interactions between Qt and that state machine.

Conclusion

What did we actually achieve with this approach? As mentioned earlier, Qt puts the responsibility of structuring the code into the hands of developers since there is no enforced distinction between view and controller. The strict separation using the state machine and the interface class creates such a structure and, at the same time, enables a more explicit description behavior.

Since the state machine makes the behavior more explicit, there are other advantages as well. The QtScxmlMonitor project, for example, can display the current states and transitions of a running QtScxml state machine. Aside from that visualization, you can also use introspection to determine, which states are currently active or which events were triggered which can improve unit testing.

The actual example implementation of the CSV editor shows, how all of this can be implemented and is downloadable using the link below.