Qt状态机QML指南

Qt状态机QML API提供用于在QML中创建和执行状态图的类型。它与基于Harel的

对于具有多个视觉状态的用户界面,这些状态独立于应用程序的逻辑状态,请考虑使用QML状态和转换。

要查看框架提供的用于创建事件驱动的状态机的QML类型完整列表,请参阅: Qt状态机QML类型

同时使用QtQuick和QtQml.StateMachine导入

警告:如果您尝试在一个单个QML文件中导入QtQuick

import QtQuick
import QtQml.StateMachine

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

或者,您可以导入QtQml.StateMachine到单独的命名空间中,以避免与QtQuickState项目混淆

import QtQuick
import QtQml.StateMachine as DSM

DSM.StateMachine {
    DSM.State {
        // ...
    }
}

一个简单的状态机

为了展示状态机API的核心功能,让我们看看一个示例:一个有三个状态

以下代码片段显示了创建此类状态机所需的代码。

    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")
        }
    }

状态机异步运行,成为您应用程序事件循环的一部分。

完成的状态机

上一节中定义的状态机永远都不会完成。为了使状态机能够完成,它需要一个顶层

在图中引入最终状态,只需创建一个

共享转换

假设我们希望用户能够通过点击“退出”按钮在任何时候退出应用程序。为了实现这一点,我们需要创建一个最终状态,并将其作为与“退出”按钮的clicked()信号关联的转换的目标。我们可以为每个状态添加转换;然而,这看起来有些多余,并且还必须记住在将来添加的每个新状态中添加此类转换。

我们可以通过将状态分组(即s1s2s3)来实现相同的行为(即无论状态机处于何种状态,点击“退出”按钮都会退出状态机)。这是通过创建一个新的顶级状态并将三个原始状态作为该新状态的子级来完成的。下面的图显示了新的状态机。

三个原始状态已重命名为s11s12s13,以反映它们现在是新顶级状态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")
            }

转换可以具有任何状态作为其目标,而不考虑目标状态在状态层次结构中的位置。

使用历史状态

假设我们想要在上一节讨论的示例中添加“中断”机制;用户应能够点击一个按钮,使状态机执行一些与当前任务无关的任务,然后状态机应恢复之前执行的任务(即回到旧状态,在这种情况下是三个状态之一)。

此类行为可以通过使用history states(历史状态)来轻松建模。历史状态(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
    }
}

在组合状态中使用最终状态非常有用,当您想隐藏组合状态的内部细节时。外部世界应该能够进入该状态并在完成工作后收到通知,而无需了解内部细节。这是构建复杂(深度嵌套)状态机时一个非常强大和封装的抽象机制。(在上述示例中,您当然可以直接从 s1done 状态创建一个转换,而不是依赖于 s1 的 finished() 信号,但这会导致对 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::enteredQAbstractState::exited信号)。

© 2024 Qt公司有限公司。本文档中的文档贡献权属于各自的拥有者。本文档受人机自由文档许可证版本1.3的许可,该许可证由自由软件基金会发布。Qt及其标志是芬兰及其它国家的Qt公司商标。所有其他商标均为其各自拥有者的财产。