Spec-Zone.ru › Qt 5.6

Фреймворк Декларативных Сетевых Машин

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

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

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

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

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

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

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

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

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

Состояние

Предоставляет общее состояние для StateMachine

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).

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

  • Типы QML автомата состояний (декларативный)
  • Фреймворк автомата состояний

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

Spec-Zone.ru

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