Spec-Zone.ru › Qt 5.14

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

Фреймворк декларативных конечных автоматов предоставляет типы для создания и выполнения графов состояний в 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.14
import QtQml.StateMachine 1.14

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

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

import QtQuick 2.14
import QtQml.StateMachine 1.14 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 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
    }
}

При входе в группу параллельных состояний все ее дочерние состояния будут одновременно введены. Переходы внутри отдельных дочерних состояний работают нормально. Однако любое из дочерних состояний может выполнить переход, который выводит из родительского состояния. Когда это происходит, родительское состояние и все его дочерние состояния выходят.

Параллелизм в фреймворке конечных автоматов следует семантике переключения. Все параллельные операции будут выполняться в одном атомарном шаге обработки событий, поэтому ни одно событие не может прервать параллельные операции. Однако события все равно будут обрабатываться последовательно, так как сам автомат является однопоточным. Например, рассмотрите ситуацию, когда есть два перехода, выходящие из одной и той же группы параллельных состояний, и их условия становятся истинными одновременно. В этом случае событие, которое обрабатывается последним из двух, не окажет никакого влияния.

Выход из составного состояния

Дочернее состояние может быть конечным (объект FinalState); когда вводится конечное дочернее состояние, родительское состояние излучает сигнал State::finished. На следующей диаграмме показано составное состояние s1, которое выполняет некоторую обработку перед входом в конечное состояние:

Когда вводится конечное состояние s1, s1 автоматически излучит finished. Мы используем переход с сигналом, чтобы заставить это событие инициировать изменение состояния:

State {
    id: s1
    SignalTransition {
        targetState: s2
        signal: s1.finished
    }
}

Использование конечных состояний в составных состояниях полезно, когда необходимо скрыть внутренние детали составного состояния. Внешний мир должен иметь возможность войти в состояние и получить уведомление о завершении работы состояния, не имея необходимости знать внутренние детали. Это очень мощный механизм абстракции и инкапсуляции при построении сложных (глубоко вложенных) автоматов состояний. (В приведенном выше примере, конечно, можно создать переход непосредственно из s1 's 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.14/qmlstatemachine.html

Spec-Zone.ru

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