Spec-Zone.ru › Qt 5.13

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

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

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

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

class="annotated">

FinalState

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

HistoryState

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

SignalTransition

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

State

Предоставляет универсальное состояние для StateMachine

StateMachine

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

TimeoutTransition

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

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

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

import QtQuick 2.13
import QtQml.StateMachine 1.13

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

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

import QtQuick 2.13
import QtQml.StateMachine 1.13 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).

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

  • 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.13/qmlstatemachine.html

Spec-Zone.ru

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