Фреймворк декларативных конечных автоматов
Фреймворк декларативных конечных автоматов предоставляет типы для создания и выполнения графов состояний в QML. Он похож на фреймворк конечных автоматов на C++ на основе Statecharts: A visual formalism for complex systems Харела, который также является основой для диаграмм состояний UML. Как и его аналог на C++, фреймворк предоставляет API и модель выполнения на основе State Chart XML (SCXML) для встраивания элементов и семантики автоматов состояний в приложения QML.
Для пользовательских интерфейсов с несколькими визуальными состояниями, независимыми от логического состояния приложения, рассмотрите использование QML состояний и переходов.
Эти типы QML предоставляются фреймворком для создания управляемых событиями конечных автоматов:
Обеспечивает конечное состояние |
|
Тип предоставляет способ возвращения к ранее активному подсостоянию |
|
Тип предоставляет переход, основанный на сигнале Qt |
|
Предоставляет общее состояние для StateMachine |
|
Предоставляет иерархический конечный автомат |
|
Тип предоставляет переход, основанный на таймере |
Использование QtQuick и QtQml.StateMachine импортов
Предупреждение: Если вы пытаетесь импортировать как QtQuick, так и QtQml.StateMachine в одном файле QML, убедитесь, что импорт QtQml.StateMachine происходит последним. Таким образом, тип State предоставляется фреймворком декларативных конечных автоматов, а не QtQuick:
import QtQuick 2.12
import QtQml.StateMachine 1.12
StateMachine {
State {
// okay, is of type QtQml.StateMachine.State
}
} В качестве альтернативы, вы можете импортировать QtQml.StateMachine в отдельный пространство имен, чтобы избежать неоднозначности с элементом State QtQuick:
import QtQuick 2.12
import QtQml.StateMachine 1.12 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 объект) — это псевдосостояние, которое представляет дочернее состояние, в котором родительское состояние находилось до последнего выхода из него.
Состояние истории создается как дочернее состояние состояния, для которого мы хотим сохранить текущее дочернее состояние; при обнаружении такого состояния в runtime конечный автомат автоматически сохраняет текущее (реальное) дочернее состояние при выходе из родительского состояния. Переход в состояние истории фактически является переходом в дочернее состояние, которое конечный автомат ранее сохранил; конечный автомат автоматически «перенаправляет» переход в реальное дочернее состояние.
Следующая диаграмма отображает конечный автомат после добавления механизма прерывания.
Следующий код демонстрирует, как это можно реализовать; в этом примере мы просто отображаем окно сообщения при входе в 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/qt-5.12/qmlstatemachine.html