Spec-Zone.ru › Qt 6.1

Руководство по QML-машинам состояний Qt

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

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

Полный список QML-типов, предоставляемых фреймворком для создания управляемых событиями машин состояний, см. в: QML-типы Qt State Machine

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

Предупреждение: Если вы пытаетесь импортировать как QtQuick, так и QtQml.StateMachine в одном QML-файле, убедитесь, что QtQml.StateMachine импортируется в последнюю очередь. Таким образом, тип State предоставляется фреймворком декларативных машин состояний, а не 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) – это псевдосостояние, которое представляет собой дочернее состояние, в котором находилось родительское состояние до его последнего выхода.

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

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

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

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

  • Типы QML Qt State Machine
  • Обзор Qt State Machine

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

Spec-Zone.ru

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