本页内容

Qt State Machine QML 指南

Qt State Machine QML API 提供了用于在 QML 中创建和执行状态图的类型。这类似于基于哈雷尔(Harel)的《状态图:复杂系统的可视化形式化表示》(Statecharts: A visual formalism for complex systems)一书所提出的 C++ 状态机框架,该框架也是 UML 状态图的基础。 与C++ counterpart 类似,该框架提供基于状态图XML(SCXML)的API和执行模型,用于将状态图的元素和语义嵌入到QML应用程序中。

对于具有多种视觉状态且与应用程序逻辑状态无关的用户界面,建议使用 QML 状态和转换。

有关该框架为创建事件驱动的状态机提供的完整 QML 类型列表,请参阅: Qt State Machine QML Types

同时使用 QtQuick 和 QtQml.StateMachine 导入

警告:如果您试图 在同一个 QML 文件中同时导入QtQuick 和QtQml.StateMachine,请务必将QtQml.StateMachine 放在最后导入。这样,“State”类型将由声明式状态机框架提供,而非由QtQuick 提供:

import QtQuick
import QtQml.StateMachine

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

或者,您可以将QtQml 中的StateMachine导入到一个单独的命名空间中,以避免与QtQuick 中的State项产生歧义:

import QtQuick
import QtQml.StateMachine as DSM

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

一个简单的状态机

为了演示状态机 API 的核心功能,让我们来看一个示例:一个包含三个状态(s1 、s2 和s3 )的状态机。该状态机由一个按钮控制;当点击该按钮时,状态机将过渡到另一个状态。初始时,状态机处于s1 状态。以下是展示本示例中不同状态的状态图。

三个状态 s1、s2、s3 在 button.clicked 事件中循环切换

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

    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,这些子状态在按下按钮时循环切换

三个原始状态已分别重命名为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 对象)是一种伪状态,它表示父状态上次退出时所处的子状态。

历史状态是作为我们希望记录其当前子状态的该状态的子状态而创建的;当状态机在运行时检测到此类状态的存在时,它会在父状态退出时自动记录当前(真实)的子状态。 向历史状态的转换实际上是向状态机先前已保存的子状态的转换;状态机会自动将该转换“转发”至真正的子状态。

下图展示了添加中断机制后的状态机。

状态 s1 包含子状态和历史状态;按下中断按钮后状态转换至 s3,完成后将返回 s1 的历史状态

以下代码展示了其实现方式;在此示例中,当进入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 个。

这种指数级增长可以通过使用并行状态来减少,这使得随着属性数量的增加,状态和转换的数量仅呈线性增长。此外,可以在并行状态中添加或移除状态,而不会影响其任何同级状态。下图展示了汽车示例中的不同并行状态。

并行状态 s1,包含区域 s11(干净/脏)和 s12(移动/不移动)

要创建并行状态组,请将 childMode 设置为QState.ParallelStates。

State {
    id: s1
    childMode: QState.ParallelStates
    State {
        id: s11
    }
    State {
        id: s12
    }
}

当进入一个并行状态组时,其所有子状态将同时被进入。各个子状态内的状态转换将正常进行。但是,任何一个子状态都可能发生导致退出父状态的状态转换。一旦发生这种情况,父状态及其所有子状态都将被退出。

状态机框架中的并行性遵循交错语义。所有并行操作都将在事件处理的单一步骤中以原子方式执行,因此没有任何事件可以中断并行操作。不过,由于状态机本身是单线程的,事件仍将按顺序处理。 例如,假设存在两个退出同一并行状态组的转换,且它们的条件同时成立。在这种情况下,两个事件中最后被处理的那个将不会产生任何影响。

退出复合状态

子状态可以是最终状态(FinalState 对象);当进入最终子状态时,父状态会发出State::finished 信号。下图展示了一个复合状态s1 ,该状态在进入最终状态前会进行一些处理:

初始状态 s1,处理子状态过渡到最终状态,触发向 s2 的转换

当进入s1 的最终状态时,s1 会自动发出finished 信号。我们使用信号转换来使该事件触发状态变化:

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

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

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.