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
enteredsignals 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?
QFinalStatemarks the end of a state machine. When the machine enters a final state, it emitsfinished(). 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.
QStateMachinehas 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 usualQt::QueuedConnectionrules.
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
| Resource | Link |
|---|---|
| Qt Docs: State Machine Framework | https://doc.qt.io/qt-6/statemachine-api.html |
| Qt Docs: QStateMachine | https://doc.qt.io/qt-6/qstatemachine.html |
| Qt Docs: QState | https://doc.qt.io/qt-6/qstate.html |
| SCXML State Charts | https://doc.qt.io/qt-6/qtscxml-index.html |
Next tutorial → Qt Animation Framework