Spec-Zone.ru › Qt 5.15

Фреймворк декларативных конечных автоматов

Фреймворк декларативных конечных автоматов предоставляет типы для создания и выполнения графов состояний в QML. Он похож на фреймворк конечных автоматов C++ на основе Statecharts: A visual formalism for complex systems Харела, который также является основой для диаграмм состояний UML. Как и его аналог на C++, фреймворк предоставляет API и модель выполнения, основанную на State Chart XML (SCXML), для встраивания элементов и семантики автоматов состояний в приложения QML.

Для пользовательских интерфейсов с несколькими визуальными состояниями, независимыми от логического состояния приложения, используйте QML состояния и переходы.

Эти следующие типы QML предоставляются фреймворком для создания управляемых событиями конечных автоматов:

class="annotated">

КонечноеСостояние

Предоставляет конечное состояние

СостояниеИстории

Тип предоставляет способ возвращения к ранее активному подсостоянию

ПереходПоСигналу

Тип предоставляет переход, основанный на сигнале Qt

Состояние

Предоставляет общее состояние для конечного автомата

КонечныйАвтомат

Предоставляет иерархический конечный автомат

ПереходПоТаймеру

Тип предоставляет переход, основанный на таймере

Использование QtQuick и QtQml.StateMachine импортов

Предупреждение: Если вы пытаетесь импортировать как QtQuick, так и QtQml.КонечныйАвтомат в одном файле QML, убедитесь, что вы импортируете QtQml.КонечныйАвтомат в последнюю очередь. Таким образом, тип Состояние предоставляется фреймворком декларативных конечных автоматов, а не QtQuick:

import QtQuick 2.15
import QtQml.StateMachine 1.15

StateMachine {
    State {
        // okay, is of type QtQml.StateMachine.State
    }
}

В качестве альтернативы, вы можете импортировать QtQml.КонечныйАвтомат в отдельный пространство имён, чтобы избежать неоднозначностей с элементом Состояние QtQuick:

import QtQuick 2.15
import QtQml.StateMachine 1.15 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")
        }
    }

Конечный автомат работает асинхронно, становясь частью цикла событий вашего приложения.

Конечные автоматы, которые завершаются

Конечный автомат, определённый в предыдущем разделе, никогда не завершается. Для того, чтобы конечный автомат мог завершиться, ему необходимо иметь верхнеуровневое конечное состояние (КонечноеСостояние). Когда конечный автомат входит в верхнеуровневое конечное состояние, автомат излучает сигнал finished и останавливается.

Всё, что вам нужно сделать, чтобы добавить конечное состояние в граф, это создать объект КонечноеСостояние и использовать его в качестве назначения одного или нескольких переходов.

Общий доступ к переходам

Предположим, мы хотели, чтобы пользователь мог в любое время выйти из приложения, нажав кнопку «Выход». Для достижения этой цели нам нужно создать конечное состояние и сделать его целевым для перехода, связанного со сигналом 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")
            }

Переход может иметь в качестве целевого состояния любое состояние, независимо от того, где находится целевое состояние в иерархии состояний.

Использование состояний истории

Представьте, что мы хотели добавить механизм «прерывания» к примеру, рассмотренному в предыдущем разделе; пользователь должен иметь возможность нажать кнопку, чтобы заставить конечный автомат выполнить некоторую не связанную задачу, после чего конечный автомат должен возобновить выполнение предыдущей задачи (т. е. вернуться к предыдущему состоянию, которое в данном случае является одним из трёх состояний).

Такое поведение легко моделируется с помощью состояний истории. Состояние истории (СостояниеИстории) — это псевдосостояние, которое представляет дочернее состояние, в котором находилось родительское состояние до его последнего выхода.

Состояние истории создаётся как дочернее состояние того состояния, для которого мы хотим записать текущее дочернее состояние; когда конечный автомат обнаруживает такое состояние во время выполнения, он автоматически записывает текущее (реальное) дочернее состояние при выходе из родительского состояния. Переход в состояние истории фактически является переходом в дочернее состояние, которое конечный автомат ранее сохранил; конечный автомат автоматически «перенаправляет» переход на реальное дочернее состояние.

На следующей диаграмме показан конечный автомат после добавления механизма прерывания.

Следующий код показывает, как это можно реализовать; в этом примере мы просто отображаем диалоговое окно с сообщением при входе в 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::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).

Связанная информация

  • Qt QML State Machine QML Типы
  • Фреймворк автомата состояний

© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/qt-5.15/qmlstatemachine.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API