Фреймворк декларативных конечных автоматов
Фреймворк декларативных конечных автоматов предоставляет типы для создания и выполнения графов состояний в QML. Он похож на фреймворк автоматов C++ на основе Statecharts: A visual formalism for complex systems Харела, который также является основой для диаграмм состояний UML. Как и его аналог на C++, фреймворк предоставляет API и модель выполнения на основе State Chart XML (SCXML) для встраивания элементов и семантики автоматов состояний в приложения QML.
Для пользовательских интерфейсов с несколькими визуальными состояниями, независимыми от логического состояния приложения, используйте QML States и Transitions.
Следующие типы QML предоставляются фреймворком для создания управляемых событиями автоматов состояний:
Предоставляет конечное состояние |
|
Тип предоставляет способ возврата к ранее активному подсостоянию |
|
Тип предоставляет переход, основанный на сигнале Qt |
|
Предоставляет универсальное состояние для StateMachine |
|
Предоставляет иерархический конечный автомат |
|
Тип предоставляет переход, основанный на таймере |
Использование QtQuick и QtQml.StateMachine
Предупреждение: Если вы пытаетесь импортировать как QtQuick, так и QtQml.StateMachine в одном файле QML, убедитесь, что импорт QtQml.StateMachine происходит в последнюю очередь. Таким образом, тип State предоставляется фреймворком декларативных конечных автоматов, а не QtQuick:
import QtQuick 2.0
import QtQml.StateMachine 1.0
StateMachine {
State {
// okay, is of type QtQml.StateMachine.State
}
} В качестве альтернативы, вы можете импортировать QtQml.StateMachine в отдельный пространство имён, чтобы избежать неоднозначности с элементом State в QtQuick:
import QtQuick 2.0
import QtQml.StateMachine 1.0 as DSM
DSM.StateMachine {
DSM.State {
// ...
}
} Простой автомат состояний
Чтобы продемонстрировать основные функциональные возможности API автомата состояний, давайте рассмотрим пример: автомат состояний с тремя состояниями, s1, s2 и s3. Автомат состояний управляется одной кнопкой; при нажатии кнопки автомат переходит в другое состояние. Изначально автомат находится в состоянии s1. Ниже приведена диаграмма состояний, демонстрирующая различные состояния в нашем примере.
Следующий фрагмент кода демонстрирует код, необходимый для создания такого автомата состояний.
Button {
anchors.fill: parent
id: button
// change the button label to the active state id
text: s1.active ? "s1" : s2.active ? "s2" : "s3"
}
StateMachine {
id: stateMachine
// set the initial state
initialState: s1
// start the state machine
running: true
State {
id: s1
// create a transition from s1 to s2 when the button is clicked
SignalTransition {
targetState: s2
signal: button.clicked
}
// do something when the state enters/exits
onEntered: console.log("s1 entered")
onExited: console.log("s1 exited")
}
State {
id: s2
// create a transition from s2 to s3 when the button is clicked
SignalTransition {
targetState: s3
signal: button.clicked
}
// do something when the state enters/exits
onEntered: console.log("s2 entered")
onExited: console.log("s2 exited")
}
State {
id: s3
// create a transition from s3 to s1 when the button is clicked
SignalTransition {
targetState: s1
signal: button.clicked
}
// do something when the state enters/exits
onEntered: console.log("s3 entered")
onExited: console.log("s3 exited")
}
} Автомат состояний работает асинхронно, чтобы стать частью цикла обработки событий вашего приложения.
Автоматы состояний, которые завершаются
Автомат состояний, определённый в предыдущем разделе, никогда не завершается. Чтобы автомат состояний мог завершиться, ему необходимо иметь конечное состояние верхнего уровня (FinalState объект). Когда автомат состояний входит в конечное состояние верхнего уровня, автомат испускает сигнал finished и останавливается.
Всё, что вам нужно сделать, чтобы добавить конечное состояние в график, — создать объект FinalState и использовать его в качестве целевого состояния одного или нескольких переходов.
Использование общих переходов
Предположим, что мы хотим, чтобы пользователь мог в любое время выйти из приложения, нажав кнопку "Выход". Для этого нам нужно создать конечное состояние и сделать его целевым состоянием для перехода, связанного с сигналом clicked() кнопки "Выход". Мы могли бы добавить переход для каждого состояния; однако это кажется избыточным, и также нужно будет помнить о добавлении такого перехода для каждого нового состояния в будущем.
Мы можем достичь того же поведения (а именно, нажатие кнопки "Выход" закрывает автомат состояний независимо от того, в каком состоянии находится автомат состояний) путём группирования состояний s1, s2 и s3. Это делается путём создания нового состояния верхнего уровня и размещения трёх исходных состояний в качестве его дочерних состояний. Следующая диаграмма показывает новый автомат состояний.
Три исходных состояния были переименованы в s11, s12 и s13 для отражения того, что они теперь являются дочерними состояниями нового состояния верхнего уровня, s1. Дочерние состояния неявно наследуют переходы родительского состояния. Это означает, что теперь достаточно добавить один переход от s1 к конечному состоянию, s2. Новые состояния, добавленные в s1, автоматически унаследуют этот переход.
Для группирования состояний достаточно указать соответствующего родителя при создании состояния. Вам также нужно указать, какое из дочерних состояний является начальным (то дочернее состояние, в которое автомат состояний должен войти, когда родительское состояние является целью перехода).
Row {
anchors.fill: parent
spacing: 2
Button {
id: button
// change the button label to the active state id
text: s11.active ? "s11" : s12.active ? "s12" : "s13"
}
Button {
id: quitButton
text: "quit"
}
}
StateMachine {
id: stateMachine
// set the initial state
initialState: s1
// start the state machine
running: true
State {
id: s1
// set the initial state
initialState: s11
// create a transition from s1 to s2 when the button is clicked
SignalTransition {
targetState: s2
signal: quitButton.clicked
}
// do something when the state enters/exits
onEntered: console.log("s1 entered")
onExited: console.log("s1 exited")
State {
id: s11
// create a transition from s11 to s12 when the button is clicked
SignalTransition {
targetState: s12
signal: button.clicked
}
// do something when the state enters/exits
onEntered: console.log("s11 entered")
onExited: console.log("s11 exited")
}
State {
id: s12
// create a transition from s12 to s13 when the button is clicked
SignalTransition {
targetState: s13
signal: button.clicked
}
// do something when the state enters/exits
onEntered: console.log("s12 entered")
onExited: console.log("s12 exited")
}
State {
id: s13
// create a transition from s13 to s11 when the button is clicked
SignalTransition {
targetState: s11
signal: button.clicked
}
// do something when the state enters/exits
onEntered: console.log("s13 entered")
onExited: console.log("s13 exited")
}
}
FinalState {
id: s2
onEntered: console.log("s2 entered")
onExited: console.log("s2 exited")
}
onFinished: Qt.quit()
} В этом случае мы хотим, чтобы приложение закрылось при завершении автомата состояний, поэтому сигнал finished() автомата подключается к слоту quit() приложения.
Дочернее состояние может переопределить унаследованный переход. Например, следующий код добавляет переход, который фактически игнорирует нажатие кнопки "Выход", когда автомат состояний находится в состоянии s12.
State {
id: s12
// create a transition from s12 to s13 when the button is clicked
SignalTransition {
targetState: s13
signal: button.clicked
}
// ignore Quit button when we are in state 12
SignalTransition {
targetState: s12
signal: quitButton.clicked
}
// do something when the state enters/exits
onEntered: console.log("s12 entered")
onExited: console.log("s12 exited")
} Переход может иметь любое состояние в качестве своей цели, независимо от того, где находится целевое состояние в иерархии состояний.
Использование состояний истории
Представьте, что мы хотели добавить механизм "прерывания" к примеру, обсуждённому в предыдущем разделе; пользователь должен иметь возможность нажать кнопку, чтобы автомат состояний выполнил некоторую задачу, не связанную с ним, после чего автомат должен продолжить то, что он делал до этого (т. е. вернуться к старому состоянию, которое в этом случае является одним из трёх состояний).
Такое поведение легко моделируется с использованием состояний истории. Состояние истории (HistoryState объект) — псевдосостояние, представляющее дочернее состояние, в котором находилось родительское состояние до его последнего выхода.
Состояние истории создаётся как дочернее состояние состояния, для которого мы хотим записать текущее дочернее состояние; при выполнении автоматом состояний он автоматически записывает текущее (реальное) дочернее состояние при выходе из родительского состояния. Переход к состоянию истории фактически является переходом к дочернему состоянию, которое автомат состояний ранее сохранил; автомат состояний автоматически "перенаправляет" переход к реальному дочернему состоянию.
Следующая диаграмма показывает автомат состояний после добавления механизма прерывания.
Следующий код показывает, как это можно реализовать; в этом примере мы просто выводим сообщение при входе в s3, а затем немедленно возвращаемся к предыдущему дочернему состоянию s1 через состояние истории.
Row {
anchors.fill: parent
spacing: 2
Button {
id: button
// change the button label to the active state id
text: s11.active ? "s11" : s12.active ? "s12" : s13.active ? "s13" : "s3"
}
Button {
id: interruptButton
text: s1.active ? "Interrupt" : "Resume"
}
Button {
id: quitButton
text: "quit"
}
}
StateMachine {
id: stateMachine
// set the initial state
initialState: s1
// start the state machine
running: true
State {
id: s1
// set the initial state
initialState: s11
// create a transition from s1 to s2 when the button is clicked
SignalTransition {
targetState: s2
signal: quitButton.clicked
}
// do something when the state enters/exits
onEntered: console.log("s1 entered")
onExited: console.log("s1 exited")
State {
id: s11
// create a transition from s1 to s2 when the button is clicked
SignalTransition {
targetState: s12
signal: button.clicked
}
// do something when the state enters/exits
onEntered: console.log("s11 entered")
onExited: console.log("s11 exited")
}
State {
id: s12
// create a transition from s2 to s3 when the button is clicked
SignalTransition {
targetState: s13
signal: button.clicked
}
// do something when the state enters/exits
onEntered: console.log("s12 entered")
onExited: console.log("s12 exited")
}
State {
id: s13
// create a transition from s3 to s1 when the button is clicked
SignalTransition {
targetState: s1
signal: button.clicked
}
// do something when the state enters/exits
onEntered: console.log("s13 entered")
onExited: console.log("s13 exited")
}
// create a transition from s1 to s3 when the button is clicked
SignalTransition {
targetState: s3
signal: interruptButton.clicked
}
HistoryState {
id: s1h
}
}
FinalState {
id: s2
onEntered: console.log("s2 entered")
onExited: console.log("s2 exited")
}
State {
id: s3
SignalTransition {
targetState: s1h
signal: interruptButton.clicked
}
// do something when the state enters/exits
onEntered: console.log("s3 entered")
onExited: console.log("s3 exited")
}
onFinished: Qt.quit()
} Использование параллельных состояний
Предположим, что вы хотите смоделировать набор взаимоисключающих свойств автомобиля в одном автомате состояний. Допустим, свойства, которые нас интересуют, — это "Чистый" или "Загрязнённый" и "Двигается" или "Не движется". Это потребовало бы четырёх взаимоисключающих состояний и восьми переходов для представления состояний и свободного перемещения между всеми возможными комбинациями, как показано на следующей диаграмме состояний.
Если бы мы добавили третье свойство (скажем, "Красный" или "Синий"), общее число состояний удвоилось бы до восьми; а если бы мы добавили четвёртое свойство (например, "Закрытый" или "Открытый"), общее число состояний снова удвоилось бы до 16.
Это экспоненциальное увеличение можно уменьшить, используя параллельные состояния, что позволяет линейному росту количества состояний и переходов по мере добавления большего количества свойств. Кроме того, состояния могут быть добавлены или удалены из параллельного состояния без влияния на какие-либо из их братьев. Следующая диаграмма состояний показывает различные параллельные состояния для примера автомобиля.
Для создания группы параллельных состояний установите childMode на QState.ParallelStates.
State {
id: s1
childMode: QState.ParallelStates
State {
id: s11
}
State {
id: s12
}
} Когда вводится группа параллельных состояний, все её дочерние состояния одновременно будут введены. Переходы внутри отдельных дочерних состояний работают нормально. Однако любое из дочерних состояний может принять переход, который выходит из родительского состояния. Когда это происходит, родительское состояние и все его дочерние состояния выходят.
Параллелизм в фреймворке автомата состояний следует интерливированной семантике. Все параллельные операции будут выполнены в одном атомарном шаге обработки событий, поэтому ни одно событие не может прервать параллельные операции. Однако события всё равно будут обрабатываться последовательно, так как сам автомат однопоточный. Например, рассмотрим ситуацию, когда существуют два перехода, которые выходят из одной и той же группы параллельных состояний, и их условия становятся истинными одновременно. В этом случае событие, которое обрабатывается последним из двух, не повлияет.
Выход из составного состояния
Дочернее состояние может быть конечным (объект FinalState); когда вводится конечное дочернее состояние, родительское состояние испускает сигнал State::finished. Следующая диаграмма показывает составное состояние s1, которое выполняет некоторую обработку перед входом в конечное состояние:
Когда конечное состояние s1 вводится, s1 автоматически испускает finished. Мы используем переход по сигналу, чтобы заставить это событие инициировать изменение состояния:
State {
id: s1
SignalTransition {
targetState: s2
signal: s1.finished
}
} Использование конечных состояний в составных состояниях полезно, когда необходимо скрыть внутренние детали составного состояния. Внешний мир должен иметь возможность войти в состояние и получить уведомление о завершении работы состояния, не зная внутренних деталей. Это очень мощный механизм абстракции и инкапсуляции при построении сложных (глубоко вложенных) автоматов состояний. (В приведенном выше примере можно, конечно, создать переход непосредственно из состояния s1 в состояние done, а не полагаться на сигнал finished() s1, но в результате будут видны и зависеть от реализации деталей s1).
Для параллельных групп состояний сигнал State::finished генерируется, когда все дочерние состояния перешли в конечные состояния.
Переходы без цели
Переход не обязательно должен иметь целевое состояние. Переход без целевого состояния может быть инициирован так же, как и любой другой переход; разница в том, что он не вызывает никаких изменений состояния. Это позволяет реагировать на сигнал или событие, когда ваша машина находится в определенном состоянии, не выходя из этого состояния. Например:
Button {
id: button
text: "button"
StateMachine {
id: stateMachine
initialState: s1
running: true
State {
id: s1
SignalTransition {
signal: button.clicked
onTriggered: console.log("button pressed")
}
}
}
} Сообщение «нажатие кнопки» будет отображаться каждый раз при нажатии кнопки, но автомат состояния останется в текущем состоянии (s1). Если целевое состояние было явно установлено в s1, s1 будет выходить и снова входить каждый раз (будут генерироваться сигналы QAbstractState::entered и QAbstractState::exited).
Связанная информация
© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/archives/qt-5.11/qmlstatemachine.html