Руководство по QML Qt State Machine
QML API Qt State Machine предоставляют типы для создания и выполнения диаграмм состояний в QML. Он похож на фреймворк State Machine на C++ на основе Statecharts: A visual formalism for complex systems Харела, который также является основой для диаграмм состояний UML. Как и его аналог на C++, фреймворк предоставляет API и модель выполнения на основе State Chart XML (SCXML) для встраивания элементов и семантики диаграмм состояний в приложения QML.
Для пользовательских интерфейсов с несколькими визуальными состояниями, независимыми от логического состояния приложения, рассмотрите использование QML States и Transitions.
Полный список типов QML, предоставляемых фреймворком для создания управляемых событиями автоматов состояний, см.: Типы QML Qt State Machine
Использование QtQuick и QtQml.StateMachine импортов
Предупреждение: Если вы пытаетесь импортировать как QtQuick, так и QtQml.StateMachine в одном файле QML, убедитесь, что вы импортируете QtQml.StateMachine в последнюю очередь. Таким образом, тип State предоставляется декларативным фреймворком State Machine, а не QtQuick:
import QtQuick
import QtQml.StateMachine
StateMachine {
State {
// okay, is of type QtQml.StateMachine.State
}
} В качестве альтернативы вы можете импортировать QtQml.StateMachine в отдельный пространство имен, чтобы избежать неоднозначности с элементом State QtQuick:
import QtQuick
import QtQml.StateMachine as DSM
DSM.StateMachine {
DSM.State {
// ...
}
} Простой автомат состояний
Чтобы продемонстрировать основную функциональность API State Machine, рассмотрим пример: автомат состояний с тремя состояниями, 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 object) — это псевдосостояние, которое представляет дочернее состояние, в котором находилось родительское состояние до последнего выхода.
Состояние истории создается как дочернее состояние того состояния, для которого мы хотим сохранить текущее дочернее состояние; когда автомат состояний обнаруживает такое состояние во время выполнения, он автоматически сохраняет текущее (реальное) дочернее состояние при выходе из родительского состояния. Переход в состояние истории фактически является переходом в дочернее состояние, которое автомат состояний ранее сохранил; автомат состояний автоматически «перенаправляет» переход в реальное дочернее состояние.
Следующая диаграмма показывает автомат состояний после добавления механизма прерывания.
Следующий код показывает, как это можно реализовать; в этом примере мы просто отображаем диалоговое окно сообщения при входе в 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
}
} При входе в группу параллельных состояний все её дочерние состояния будут одновременно активированы. Переходы внутри отдельных дочерних состояний работают в обычном режиме. Однако любое из дочерних состояний может выполнить переход, который выводит из родительского состояния. В этом случае родительское состояние и все его дочерние состояния выходят.
Параллелизм в фреймворке State Machine следует интерливированной семантике. Все параллельные операции будут выполняться в одном атомарном шаге обработки событий, поэтому ни одно событие не может прервать параллельные операции. Однако события всё равно будут обрабатываться последовательно, так как сама машина является однопоточной. Например, рассмотрим ситуацию, когда существуют два перехода, которые выходят из одной и той же группы параллельных состояний, и их условия становятся истинными одновременно. В этом случае событие, обработанное последним из двух, не окажет никакого эффекта.
Выход из составного состояния
Дочернее состояние может быть конечным (объект 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-6.2/qmlstatemachine-qml-guide.html