В справке Qt довольно подробно расписано использование конечного автомата QStateMachine в случае с последовательно меняющимися состояниями. Однако реализация автомата с распараллеливанием не имеет примеров и описывается лишь диаграммой, да парой абзацев текста на английском языке, не сильно вносящих ясности.
Как реализовать параллельные состояния и переходы между ними в QStateMachine? Отдельно хотелось бы увидеть организацию переходов посредством использования событий вместо сигналов, поскольку вторые не во всех случаях удобно использовать.
Ответ
Реализация параллельных состояний в Qt State Machine Framework в отличие от последовательных имеет одну существенную особенность. Но для полноты картины начнём именно с последовательных. Создадим машину и пару состояний.
QStateMachine *machine = new QStateMachine();
QState *state1 = new QState(machine);
QState *state2 = new QState(machine);
Если для нашей машины предполагается, что она, при наличии определённых условий (скажем, при прохождении двух ранее объявленных состояний), должна завершить свою работу, то удобно добавить в неё состояние завершения работы.
QFinalState *final_state = new QFinalState(machine);
Использование QFinalState удобно тем, что при входе в него контекста выполнения этот тип состояния автоматически отправляет своему родителю сигнал finished(). Если родителем является объект QStateMachine, то машина будет остановлена, если же это объект QState, то уже тот в свою очередь отправит такой же сигнал вверх по цепочке. Эта особенность в полной мере используется и при параллельном подключении состояний, что будет рассмотрено ниже.
К слову сказать, во всех примерах Qt-справки машина имеет неопределённую длительность жизни, тогда как на практике часто встречаются задачи, которые, с одной стороны требуют асинхронного выполнения (например, отправка и обработка сетевых запросов), а с другой - возникают по мере необходимости. Нет никакого интереса содержать в памяти объект машины со всеми состояниями, если, скажем, надо что-то выполнять регулярно, но не постоянно.
Для таких случаев при объявлении машины можно просто подключить соответствующие сигналы и объект машины ликвидирует сам себя и все дочерние объекты, как только завершит работу.
connect(machine, &QStateMachine::stopped, machine, &QStateMachine::deleteLater);
connect(machine, &QStateMachine::finished, machine, &QStateMachine::deleteLater);
Для переключения состояний обычно используют сигналы от внешних по отношению к машине объектов.
state1->addTransition(external_obj1, SIGNAL(customSignal()), state2);
state2->addTransition(external_obj2, SIGNAL(customSignal()), final_state);
Однако этот вариант не подходит, если таковых объектов не имеется. В этом случае можно добавить в класс, в котором существует объект машины, собственный произвольный сигнал. Например:
class MyClass : public QObject {
Q_OBJECT
signals:
void customSignal();
public:
MyObject(QObject *parent = Q_NULLPTR)
: QObject(parent) {}
public slots:
void run();
};
void MyClass::run() {
QStateMachine *machine = new QStateMachine(this);
connect(machine, &QStateMachine::stopped
, machine, &QStateMachine::deleteLater);
connect(machine, &QStateMachine::finished
, machine, &QStateMachine::deleteLater);
QState *state1 = new QState(machine);
connect(state1, &QState::entered, [this]() {
// Код при входе в первое состояние ...
emit customSignal();
});
QState *state2 = new QState(machine);
connect(state2, &QState::entered, [this]() {
// Код при входе во второе состояние ...
emit customSignal();
});
QFinalState *final_state = new QFinalState(machine);
connect(final_state, &QFinalState::entered, [this]() {
// Здесь, например, можно уведомить
// пользователя о завершении работы.
});
// Подключаем переходы.
state1->addTransition(this, SIGNAL(customSignal()), state2);
state2->addTransition(this, SIGNAL(customSignal()), final_state);
// Указываем точку входа и активируем машину.
machine->setInitialState(state1);
machine->start();
}
Однако такой подход приводит к проблемам при необходимости расширения типов реакции машины на каждое из состояний.
Предположим, что теперь требуется учитывать успешно или неуспешно завершились state1 и state2. Конечно, можно добавить ещё несколько сигналов в MyClass, но что за "ёрш" в итоге получится, если и количество состояний будет расти? Очевидно, что в данной ситуации требуется знать не только результат выполнения кода при выходе из каждого состояния, но и, собственно, что это было за состояние из списка имеющихся. Помочь может использование событий и переходов (transition) по событиям.
Для начала класс произвольного события stateevent.h
#include
class QState;
class StateEvent : public QEvent {
public:
enum Result {
RESULT_NULL,
RESULT_SUCCEED,
RESULT_FAILED
};
static const QEvent::Type &_event_type;
StateEvent(QState *state, Result result);
QState *state() const {return _state;}
StateEvent::Result result() const {return _result;}
private:
QState *_state;
Result _result;
};
... и stateevent.cpp
#include "stateevent.h"
const QEvent::Type &StateEvent::_event_type
= static_cast
StateEvent::StateEvent(QState *state, Result result)
: QEvent(_event_type), _state(state), _result(result) {}
А теперь и класс перехода:
#include
#include "stateevent.h"
class StateTransition : public QAbstractTransition {
Q_OBJECT
public:
StateTransition(QState *source_state = Q_NULLPTR)
: QAbstractTransition(source_state)
, _result(StateEvent::RESULT_NULL) {}
StateEvent::Result eventResult() const {return _result;}
void setEventResult(StateEvent::Result result) {_result = result;}
protected:
virtual bool eventTest(QEvent *event) {
if(event->type() == StateEvent::_event_type) {
StateEvent *state_event = static_cast
return false;
}
virtual void onTransition(QEvent *event) {Q_UNUSED(event);}
private:
StateEvent::Result _result;
};
Метод eventTest(QEvent*) проверяет, соответствует ли объект состояния (QState) и результат выполнения (StateEvent::Result) объекту перехода (StateTransition). Если истина, то машина переключится в новое состояние, которое назначено при выполнении перечисленных условий.
Теперь код активации машины меняется на следующий (произвольные сигналы нам больше не нужны):
class MyClass : public QObject {
Q_OBJECT
public:
MyObject(QObject *parent = Q_NULLPTR)
: QObject(parent) {}
public slots:
void run();
};
void MyClass::run() {
QStateMachine *machine = new QStateMachine(this);
connect(machine, &QStateMachine::stopped
, machine, &QStateMachine::deleteLater);
connect(machine, &QStateMachine::finished
, machine, &QStateMachine::deleteLater);
QState *state1 = new QState(machine);
connect(state1, &QState::entered, [this,state1]() {
// Код при входе в первое состояние ...
bool result = ... ;
if(result == true) {
state1->machine()
->postEvent(new StateEvent(state1
, StateEvent::RESULT_SUCCEED));
} else {
state1->machine()
->postEvent(new StateEvent(state1
, StateEvent::RESULT_FAILED));
}
});
QState *state2 = new QState(machine);
connect(state2, &QState::entered, [this,state2]() {
// Код при входе во второе состояние ...
bool result = ... ;
if(result == true) {
state2->machine()
->postEvent(new StateEvent(state2
, StateEvent::RESULT_SUCCEED));
} else {
state2->machine()
->postEvent(new StateEvent(state2
, StateEvent::RESULT_FAILED));
}
});
QFinalState *final_state = new QFinalState(machine);
connect(final_state, &QFinalState::entered, [this]() {
// Здесь, например, можно уведомить
// пользователя о завершении работы.
});
// Подключаем переходы.
{
StateTransition *success_transition = new StateTransition();
success_transition->setEventResult(StateEvent::RESULT_SUCCEED);
success_transition->setTargetState(state2);
StateTransition *fail_transition = new StateTransition();
fail_transition->setEventResult(StateEvent::RESULT_FAILED);
fail_transition->setTargetState(final_state);
state1->addTransition(success_transition);
state1->addTransition(fail_transition);
}
{
StateTransition *success_transition = new StateTransition();
success_transition->setEventResult(StateEvent::RESULT_SUCCEED);
success_transition->setTargetState(final_state);
StateTransition *fail_transition = new StateTransition();
fail_transition->setEventResult(StateEvent::RESULT_FAILED);
fail_transition->setTargetState(final_state);
state2->addTransition(success_transition);
state2->addTransition(fail_transition);
}
// Указываем точку входа и активируем машину.
machine->setInitialState(state1);
machine->start();
}
С помощью продемонстрированного механизма можно свободно "плодить" различные типы результата работы для состояний, что в свою очередь позволит конечному автомату переключать эти самые состояния в той последовательности и по тем правилам, что заранее были для него определены.
При отправке события о завершении работы каждого из состояний, например:
state1->machine()
->postEvent(new StateEvent(state1
, StateEvent::RESULT_SUCCEED));
... в StateEvent вносится и сохраняется указатель на объект состояния, из которого должен быть осуществлён переход. При этом метод QStateMachine::postEvent(QEvent*) отправит вновь созданное событие всем объектам перехода, которые были зарегистрированы на этапе создания конечного автомата, а те уже проверят самостоятельно, соответствует ли кому-нибудь из них это событие.
При последовательном подключении состояний сохранять объект состояния (QState), из которого должен быть осуществлён переход, необязательно, поскольку в этом режиме работы в конечном автомате (QStateMachine) текущим может быть только одно состояние. Достаточно отправлять лишь сведения о результате выполнения. Но всё меняется, если состояния подключаются параллельно: одно событие переключит сразу нескольких, что во многих случаях приведёт к непредсказуемому результату. Сохранение же указателя на источник перехода позволит избежать обозначенной проблемы.
Осталось поменять код из примера выше для параллельного подключения состояний.
void MyClass::run() {
QStateMachine *machine = new QStateMachine(QState::ParallelStates, this);
connect(machine, &QStateMachine::stopped
, machine, &QStateMachine::deleteLater);
connect(machine, &QStateMachine::finished
, machine, &QStateMachine::deleteLater);
for(int i = 0; i < 2; ++i) {
QState *parent_state = new QState(machine);
QState *state = new QState(parent_state);
connect(state, &QState::entered, [this,state]() {
// Код при входе в **i-ое** состояние ...
bool result = ... ;
if(result == true) {
state->machine()
->postEvent(new StateEvent(state
, StateEvent::RESULT_SUCCEED));
} else {
state->machine()
->postEvent(new StateEvent(state
, StateEvent::RESULT_FAILED));
}
});
QFinalState *final_state = new QFinalState(parent_state);
StateTransition *success_transition = new StateTransition();
success_transition->setEventResult(StateEvent::RESULT_SUCCEED);
success_transition->setTargetState(final_state);
StateTransition *fail_transition = new StateTransition();
fail_transition->setEventResult(StateEvent::RESULT_FAILED);
fail_transition->setTargetState(final_state);
state->addTransition(success_transition);
state->addTransition(fail_transition);
}
// Активируем машину (указывать точку входа нужды нет).
machine->start();
}
Поскольку QStateMachine является наследником QState мы имеем возможность объявить посредством установки флага QState::ParallelStates, что дочерние состояния машины должны быть подключены в параллельном режиме. Это указание действует только на тех из них, для которых объект конечного автомата является непосредственным родителем.
Таким образом в теле цикла в параллельный режим войдут только копии parent_state, тогда как уже их дочерние состояния останутся по умолчанию - подключенными последовательно.
Разумеется, что именно такое построение как в примере вовсе необязательно и объекты QState (в т.ч. QStateMachine) можно компоновать с любым количеством иерархических уровней, однако последний, самый низкий уровень должен быть представлен в виде последовательного подключения как минимум одного QState и QFinalState, поскольку только тогда "наверх" по иерархии будет отправлен сигнал о завершении работы.
Да, можно обойтись даже и одним QFinalState, но тогда необходимо иметь в виду, что сигнал finished() будет отправлен прежде, чем будет выполнен вход в состояние QFinalState, а значит код, ассоциированный с ним, будет также выполнен позднее. Для многих задач это бывает неприемлемо.
Каждый из parent_state в примере будет выполняться параллельно по отношению к другому, и может закончить свою работу раньше, либо позднее других. Однако при этом QStateMachine не остановится, несмотря на то, что от одного из выполнившихся parent_state к нему придёт сигнал finished(). Лишь только тогда, когда все parent_state завершат свою работу, только тогда конечный автомат завершит и свою, отправив finished(), но уже от себя.
QStateMachine чрезвычайно мощный и гибкий инструмент, и совершенно напрасно многие считают его избыточным, либо подходящим лишь для подключения анимации. Этот фреймворк содержится в модуле QtCore и может быть весьма полезен для реализации практически любых задач, требующих, прежде всего, асинхронного выполнения.
Комментариев нет:
Отправить комментарий