Qt State Machine Framework

QStateMachine, QState, QFinalState, transitions, hierarchical state machines — modeling device behavior with Qt's state machine framework.

7 min read
53298 chars

What Is a State Machine?

A state machine models an object that can be in exactly one state at a time and transitions between states in response to events. It replaces messy chains of if/else and bool flags with clear, maintainable state logic.

State machines are ideal for:
• Device lifecycle (off → init → running → error → off)
• Protocol parsers  (idle → receiving → validating → processing)
• UI flow          (login → dashboard → settings → logout)
• Traffic lights   (red → green → yellow → red)

Simple 3-State Example

#include <QStateMachine>
#include <QState>
#include <QFinalState>

QStateMachine *machine = new QStateMachine(this);

QState *idle    = new QState(machine);
QState *running = new QState(machine);
QState *error   = new QState(machine);
QFinalState *done = new QFinalState(machine);

machine->setInitialState(idle);

// Transitions
idle->addTransition(this, &Device::startRequested, running);
running->addTransition(this, &Device::errorOccurred, error);
running->addTransition(this, &Device::stopRequested, done);
error->addTransition(this, &Device::resetRequested, idle);

// Entry/exit actions
connect(idle,    &QState::entered, []{ qDebug() << "→ IDLE"; });
connect(running, &QState::entered, []{ qDebug() << "→ RUNNING"; });
connect(error,   &QState::entered, []{ qDebug() << "→ ERROR"; });
connect(done,    &QFinalState::entered, []{ qDebug() << "→ DONE"; });

machine->start();

Transitions on QAbstractTransition

Signal Transition

// Transition when this->signal fires
state->addTransition(this, &MyClass::someSignal, nextState);

Event Transition (keyboard, timer)

#include <QKeyEventTransition>

QKeyEventTransition *spaceKey =
    new QKeyEventTransition(window, QEvent::KeyPress, Qt::Key_Space);
spaceKey->setTargetState(nextState);
currentState->addTransition(spaceKey);

Timed Transition (via QTimer signal)

QTimer *timeout = new QTimer(this);
timeout->setSingleShot(true);

// Enter the waiting state → start timer
connect(waitingState, &QState::entered, [=]{ timeout->start(5000); });

// After 5s → transition to timeout state
waitingState->addTransition(timeout, &QTimer::timeout, timedOutState);

Entry, Exit & Transition Actions

QState *connecting = new QState(machine);

// Entry: start connection
connect(connecting, &QState::entered, [=](){
    statusLabel->setText("Connecting...");
    startConnectionAttempt();
});

// Exit: cleanup
connect(connecting, &QState::exited, [=](){
    statusLabel->setText("");
});

// Assign properties automatically on state entry
QState *activeState = new QState(machine);
activeState->assignProperty(btn, "text", "Disconnect");
activeState->assignProperty(btn, "enabled", true);
activeState->assignProperty(ledLabel, "styleSheet",
    "background-color: #a6e3a1; border-radius: 8px;");

Hierarchical State Machine (HSM)

States can contain child states for complex behavior:

// Parent state
QState *connected = new QState(machine);

// Child states (nested inside 'connected')
QState *idle_c    = new QState(connected);
QState *reading   = new QState(connected);
QState *writing   = new QState(connected);
connected->setInitialState(idle_c);

// Transitions within the parent
idle_c->addTransition(this, &Device::readRequested,  reading);
idle_c->addTransition(this, &Device::writeRequested, writing);
reading->addTransition(this, &Device::done, idle_c);
writing->addTransition(this, &Device::done, idle_c);

// ANY event that goes to disconnected from parent's perspective:
connected->addTransition(this, &Device::disconnected, disconnectedState);

Visual: Device Connection State Machine

                  ┌─────────────────────────────────────────┐
                  │         QStateMachine                   │
                  │                                         │
  ┌──────┐ start  │  ┌────────────┐  connected             │
  │ Idle │────────┼─►│ Connecting ├───────────────────────┐│
  └──────┘        │  └─────┬──────┘                       ││
     ▲            │        │ timeout / error               ││
     │            │        ▼                               ▼│
     │            │  ┌─────────┐            ┌─────────────────────┐
  reset           │  │  Error  │            │     Connected       │
                  │  └─────────┘            │ ┌──────┐  ┌───────┐ │
                  │                         │ │ Idle │⇌ │Reading│ │
                  │                         │ └──────┘  └───────┘ │
                  │                         └─────┬───────────────┘
                  │                               │ disconnected
                  │  ┌────────────┐               │
                  │  │Disconnected│◄──────────────┘
                  │  └────────────┘
                  └─────────────────────────────────────────┘

Lab 1 — Traffic Light State Machine

class TrafficLight : public QWidget {
    Q_OBJECT
public:
    TrafficLight(QWidget *p = nullptr) : QWidget(p) {
        setFixedSize(100, 280);

        auto *machine = new QStateMachine(this);

        QState *red    = new QState(machine);
        QState *green  = new QState(machine);
        QState *yellow = new QState(machine);
        machine->setInitialState(red);

        // Assign colors via properties
        red->assignProperty(this, "activeColor", "red");
        green->assignProperty(this, "activeColor", "green");
        yellow->assignProperty(this, "activeColor", "yellow");

        // Timed transitions
        auto timedTransition = [&](QState *from, QState *to, int ms) {
            QTimer *t = new QTimer(this);
            t->setSingleShot(true);
            connect(from, &QState::entered, [t, ms]{ t->start(ms); });
            from->addTransition(t, &QTimer::timeout, to);
        };

        timedTransition(red,    green,  3000);  // red → green after 3s
        timedTransition(green,  yellow, 2000);  // green → yellow after 2s
        timedTransition(yellow, red,    1000);  // yellow → red after 1s

        machine->start();
    }

    Q_PROPERTY(QString activeColor READ activeColor WRITE setActiveColor NOTIFY colorChanged)
    QString activeColor() const { return m_color; }
    void setActiveColor(const QString &c) {
        m_color = c; emit colorChanged(); update();
    }

signals:
    void colorChanged();

protected:
    void paintEvent(QPaintEvent *) override {
        QPainter p(this);
        p.setRenderHint(QPainter::Antialiasing);
        p.fillRect(rect(), QColor("#1e1e2e"));

        struct Light { QString color; QColor on; QColor off; int y; };
        const QList<Light> lights = {
            {"red",    QColor("#f38ba8"), QColor("#3d1a21"), 20 },
            {"yellow", QColor("#f9e2af"), QColor("#3d3518"), 100},
            {"green",  QColor("#a6e3a1"), QColor("#1a3d27"), 180}
        };
        for (const auto &l : lights) {
            p.setBrush(m_color == l.color ? l.on : l.off);
            p.setPen(Qt::NoPen);
            p.drawEllipse(10, l.y, 80, 80);
        }
    }

private:
    QString m_color = "red";
};

Lab 2 — Device Connection Manager

class DeviceConnectionManager : public QObject {
    Q_OBJECT
    Q_PROPERTY(QString stateName READ stateName NOTIFY stateChanged)

public:
    DeviceConnectionManager(QObject *p = nullptr) : QObject(p) {
        m_machine = new QStateMachine(this);

        auto *disconnected = new QState(m_machine);
        auto *connecting   = new QState(m_machine);
        auto *connected    = new QState(m_machine);
        auto *error        = new QState(m_machine);

        disconnected->assignProperty(this, "stateName", "Disconnected");
        connecting  ->assignProperty(this, "stateName", "Connecting");
        connected   ->assignProperty(this, "stateName", "Connected");
        error       ->assignProperty(this, "stateName", "Error");

        // Transitions
        disconnected->addTransition(this, &DeviceConnectionManager::connectRequested,
                                    connecting);
        connecting  ->addTransition(this, &DeviceConnectionManager::connectSucceeded,
                                    connected);
        connecting  ->addTransition(this, &DeviceConnectionManager::connectFailed,
                                    error);
        connected   ->addTransition(this, &DeviceConnectionManager::disconnectRequested,
                                    disconnected);
        error       ->addTransition(this, &DeviceConnectionManager::retryRequested,
                                    connecting);

        // Timeout in connecting state
        QTimer *connTimeout = new QTimer(this);
        connTimeout->setSingleShot(true);
        connect(connecting, &QState::entered, [=]{ connTimeout->start(5000); });
        connect(connecting, &QState::exited,  [=]{ connTimeout->stop(); });
        connecting->addTransition(connTimeout, &QTimer::timeout, error);

        // Actions
        connect(connecting, &QState::entered,  this, &DeviceConnectionManager::doConnect);
        connect(connected,  &QState::entered,  this, &DeviceConnectionManager::startHeartbeat);
        connect(connected,  &QState::exited,   this, &DeviceConnectionManager::stopHeartbeat);

        m_machine->setInitialState(disconnected);
        m_machine->start();
    }

    QString stateName() const { return m_stateName; }

public slots:
    void setStateName(const QString &n) {
        if (m_stateName != n) { m_stateName = n; emit stateChanged(n); }
    }
    void connect_()  { emit connectRequested(); }
    void disconnect_() { emit disconnectRequested(); }

signals:
    void connectRequested();
    void connectSucceeded();
    void connectFailed();
    void disconnectRequested();
    void retryRequested();
    void stateChanged(const QString &state);

private slots:
    void doConnect() {
        qDebug() << "Attempting connection...";
        QTimer::singleShot(800, this, [this](){
            if (rand() % 3 != 0)  // simulate 66% success
                emit connectSucceeded();
            else
                emit connectFailed();
        });
    }
    void startHeartbeat() { qDebug() << "Heartbeat started"; }
    void stopHeartbeat()  { qDebug() << "Heartbeat stopped"; }

    QStateMachine *m_machine;
    QString        m_stateName;
};

Interview Questions

Q1: What is the difference between a state machine and a chain of if/else?

A state machine makes state transitions explicit and self-documenting. It prevents invalid transitions (e.g., you can’t go from “idle” to “error” directly). It handles concurrent states and hierarchical nesting cleanly. if/else chains become unmaintainable as states grow.

Q2: What are hierarchical states and when do you use them?

Hierarchical states (HSM) allow a state to contain child states. Transitions defined on a parent state apply to all children — so you don’t duplicate error-handling transitions on every sub-state. Use HSM when many sub-states share common transitions (e.g., “any state → error on disconnect”).

Q3: How does assignProperty work?

When a state is entered, Qt automatically sets the specified property on the target object. This is cleaner than connecting entered signals to individual setters. Changes are reverted when the state exits (if you set up the machine to restore properties).

Q4: What is QFinalState and when is it needed?

QFinalState marks the end of a state machine. When the machine enters a final state, it emits finished(). Useful for sub-machines embedded in parent states — the parent state automatically exits when the sub-machine finishes.

Q5: Can a Qt state machine run in a worker thread?

Yes. QStateMachine has its own event processing and can run in any thread. However, all state transitions and property assignments happen in the machine’s thread. Signal/slot connections across threads use the usual Qt::QueuedConnection rules.


Application: Vending Machine

class VendingMachine : public QObject {
    Q_OBJECT
public:
    VendingMachine(QObject *p = nullptr) : QObject(p) {
        m_machine = new QStateMachine(this);

        auto *idle      = new QState(m_machine);
        auto *selecting = new QState(m_machine);
        auto *paying    = new QState(m_machine);
        auto *dispensing= new QState(m_machine);

        m_machine->setInitialState(idle);

        idle     ->addTransition(this, &VendingMachine::itemSelected,  selecting);
        selecting->addTransition(this, &VendingMachine::paymentInserted, paying);
        selecting->addTransition(this, &VendingMachine::cancelled,      idle);
        paying   ->addTransition(this, &VendingMachine::paymentAccepted, dispensing);
        paying   ->addTransition(this, &VendingMachine::paymentRejected, selecting);
        dispensing->addTransition(this, &VendingMachine::dispensingDone, idle);

        connect(idle,      &QState::entered, [=]{ qDebug() << "[IDLE] Please select an item"; });
        connect(selecting, &QState::entered, [=]{ qDebug() << "[SELECT] Insert payment for:" << m_item; });
        connect(paying,    &QState::entered, [=]{ qDebug() << "[PAY] Processing..."; });
        connect(dispensing,&QState::entered, [=]{
            qDebug() << "[DISPENSE] Dispensing:" << m_item;
            QTimer::singleShot(1000, this, [this]{ emit dispensingDone(); });
        });

        m_machine->start();
    }

    void selectItem(const QString &item) {
        m_item = item;
        emit itemSelected();
    }
    void insertPayment(float amount) {
        m_paid = amount;
        emit paymentInserted();
        QTimer::singleShot(500, this, [this]{
            if (m_paid >= m_price) emit paymentAccepted();
            else                   emit paymentRejected();
        });
    }

signals:
    void itemSelected();
    void paymentInserted();
    void paymentAccepted();
    void paymentRejected();
    void cancelled();
    void dispensingDone();

private:
    QStateMachine *m_machine;
    QString        m_item;
    float          m_paid  = 0.0f;
    float          m_price = 2.50f;
};

// Usage
VendingMachine vm;
vm.selectItem("Cola");
vm.insertPayment(3.00f);

References

ResourceLink
Qt Docs: State Machine Frameworkhttps://doc.qt.io/qt-6/statemachine-api.html
Qt Docs: QStateMachinehttps://doc.qt.io/qt-6/qstatemachine.html
Qt Docs: QStatehttps://doc.qt.io/qt-6/qstate.html
SCXML State Chartshttps://doc.qt.io/qt-6/qtscxml-index.html

Next tutorial → Qt Animation Framework