Введение
Многие части интерфейса и бизнес-логики приложения естественно описываются как конечные автоматы: состояние подключения (отключён/подключается/подключён/ошибка), состояние формы (редактирование/сохранение/сохранено), плеер (воспроизведение/пауза/остановлен). Реализация такой логики через разрозненные булевы флаги и условия быстро становится трудно поддерживаемой. Qt предоставляет встроенный модуль Qt State Machine с классом QStateMachine, который декларативно описывает состояния и переходы между ними. В статье построим конечный автомат для управления состоянием медиаплеера.
Концепция
QState представляет одно состояние автомата, QStateMachine управляет коллекцией состояний и текущим активным состоянием. Переходы между состояниями (QSignalTransition или QEventTransition) триггерятся сигналами объектов — например, переход из состояния «Воспроизведение» в «Пауза» срабатывает по сигналу pauseRequested(). Вход в состояние и выход из него можно связать с произвольными слотами через сигналы entered()/exited() каждого состояния, что позволяет выполнять побочные эффекты (включение/выключение элементов UI, запуск таймеров) автоматически при смене состояния, без ручной проверки условий в каждом месте кода.
Пример кода
#pragma once
#include <QStateMachine>
#include <QState>
#include <QObject>
#include <QDebug>
class MediaPlayerController : public QObject
{
Q_OBJECT
public:
explicit MediaPlayerController(QObject *parent = nullptr) : QObject(parent)
{
setupStateMachine();
}
void start() { m_machine.start(); }
signals:
void playRequested();
void pauseRequested();
void stopRequested();
private:
void setupStateMachine()
{
auto *stoppedState = new QState(&m_machine);
auto *playingState = new QState(&m_machine);
auto *pausedState = new QState(&m_machine);
// Переходы из состояния "Остановлен"
stoppedState->addTransition(this, &MediaPlayerController::playRequested, playingState);
// Переходы из состояния "Воспроизведение"
playingState->addTransition(this, &MediaPlayerController::pauseRequested, pausedState);
playingState->addTransition(this, &MediaPlayerController::stopRequested, stoppedState);
// Переходы из состояния "Пауза"
pausedState->addTransition(this, &MediaPlayerController::playRequested, playingState);
pausedState->addTransition(this, &MediaPlayerController::stopRequested, stoppedState);
// Побочные эффекты при входе в каждое состояние
connect(stoppedState, &QState::entered, []() { qDebug() << "Состояние: Остановлен"; });
connect(playingState, &QState::entered, []() { qDebug() << "Состояние: Воспроизведение"; });
connect(pausedState, &QState::entered, []() { qDebug() << "Состояние: Пауза"; });
m_machine.setInitialState(stoppedState);
}
QStateMachine m_machine;
};
// Использование
#include "mediaplayercontroller.h"
#include <QCoreApplication>
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
MediaPlayerController player;
player.start();
emit player.playRequested(); // Остановлен -> Воспроизведение
emit player.pauseRequested(); // Воспроизведение -> Пауза
emit player.playRequested(); // Пауза -> Воспроизведение
emit player.stopRequested(); // Воспроизведение -> Остановлен
return app.exec();
}
Пояснения к коду
Три состояния (stoppedState, playingState, pausedState) создаются как дети m_machine, что автоматически передаёт владение памятью машине состояний. Переходы добавляются через addTransition(), привязывая конкретный сигнал к целевому состоянию — это явно ограничивает, какие переходы вообще возможны из текущего состояния: например, из stoppedState единственный валидный переход — по сигналу playRequested, и попытка испустить pauseRequested() в этом состоянии просто не будет иметь эффекта, поскольку такого перехода не существует.
Подключение к сигналу entered() каждого состояния демонстрирует, как выполнять побочные эффекты (в реальном приложении — изменение иконки кнопки play/pause, запуск/остановку таймера прогресса) автоматически и централизованно при входе в состояние, вместо разбросанных по коду проверок текущего состояния.
Подводные камни
- Неявное игнорирование сигналов, не имеющих перехода из текущего состояния, без какой-либо ошибки или предупреждения — если ожидался переход, но он не сработал, это легко спутать с багом в логике, тогда как на самом деле переход просто не был объявлен для данного состояния.
- Сложность отладки больших машин состояний с множеством переходов — визуально граф состояний и переходов не виден непосредственно в коде C++ (в отличие от, например, QML StateMachine с более декларативным синтаксисом) и требует либо отдельной документации/диаграммы, либо инструментов визуализации, поддерживающих экспорт состояния
QStateMachine. - Использование
QStateMachineдля очень простых случаев (два-три состояния с однозначными переходами) может быть избыточным усложнением по сравнению с простым enum и switch-оператором — машина состояний оправдывает себя при заметном числе состояний и нетривиальных переходах с побочными эффектами. - Параллельные состояния (
QState::ParallelStates) и история (QHistoryState) — более продвинутые возможности модуля, требующие отдельного изучения; неосторожное смешивание простых и параллельных состояний без понимания их взаимодействия может привести к неожиданному поведению при одновременной активности нескольких веток состояний.