Qt ステートマシン QML ガイド

QtステートマシンQML APIは、QMLでステートグラフを作成・実行するための型を提供します。これは Harel のStatecharts に基づく C++ State Machine フレームワークと似ています:複雑なシステムのための視覚的なフォーマリズムであり、UMLのステートダイアグラムの基礎でもあります。C++ counterpart と同様、このフレームワークはステートチャート XML (SCXML)に基づいた API と実行モデルを提供し、ステートチャートの要素とセマンティクスを QML アプリケーションに組み込みます。

アプリケーションの論理的な状態とは別に、複数の視覚的な状態を持つユーザーインターフェースには、QMLのStates and Transitionsの利用を検討してください。

イベント駆動型のステートマシンを作成するためにフレームワークが提供するQML型の全リストは、こちらを参照してください:Qt State Machine QML Types

QtQuickとQtQml.StateMachineの両方のインポートを使う」を参照してください。

警告 1つのQMLファイルで、QtQuickQtQml. StateMachine の両方をインポートする場合は、 QtQml.StateMachine 最後にインポートしてください。こうすることで、ステートタイプは宣言型ステートマシン・フレームワークによって提供され、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のコア機能を示すために、例を見てみよう:s1s2s3 の3つの状態を持つステートマシンである。ボタンがクリックされると、マシンは別の状態に遷移する。最初、ステートマシンは状態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 オブジェクトを作成し、それを1つ以上のトランジションのターゲットとして使用することです。

トランジションの共有

ユーザーがいつでもQuitボタンをクリックしてアプリケーションを終了できるようにしたいとします。これを実現するには、最終状態を作成し、それをQuitボタンのclicked()シグナルに関連するトランジションのターゲットにする必要があります。状態ごとにトランジションを追加することもできるが、これは冗長に思えるし、将来追加される新しい状態ごとにトランジションを追加しなければならない。

状態s1s2s3 をグループ化することで、同じ動作(ステートマシンがどの状態にあるかにかかわらず、Quitボタンをクリックするとステートマシンが終了する)を実現できる。これは、新しいトップレベル状態を作成し、元の3つの状態を新しい状態の子にすることで実現します。次の図は、新しいステートマシンを示しています。

元の3つの状態は、s11s12s13 という名前に変更され、新しいトップレベル状態であるs1 の子状態になりました。子状態は親状態の遷移を暗黙的に継承する。つまり、s1 から最終状態であるs2 への遷移を1つ追加するだけでよくなりました。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 のときに、Quit ボタンを無視するトランジションを追加しています。

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

並列ステートの使用

車の互いに排他的なプロパティのセットを、1つのステートマシンでモデル化したいとします。興味のある特性は、「きれい」対「汚い」、「動いている」対「動いていない」だとしよう。状態を表すには4つの互いに排他的な状態と8つの遷移が必要で、以下の状態図に示すように、すべての可能な組み合わせの間を自由に移動できる。

もし3つ目の特性(例えば赤対青)を追加すれば、状態の総数は2倍の8つになり、4つ目の特性(例えば密閉対転換可能)を追加すれば、状態の総数はまた2倍の16になる。

この指数関数的な増加は、並列状態を使用することで抑えることができ、プロパティを追加するにつれて状態と遷移の数を線形に増やすことができる。さらに、状態をパラレル・ステートに追加したり、パラレル・ステートから削除したりしても、その兄弟ステートには影響を与えない。以下のステートチャートは、車の例におけるさまざまなパラレルステートを示しています。

パラレルステートグループを作成するには、childModeをQState.ParallelStatesに設定します。

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

パラレルステートグループに入ると、すべての子ステートが同時に入ります。個々の子状態内の遷移は正常に動作する。しかし、子状態のどれかが親状態を終了する遷移を取ることがある。この場合、親状態とそのすべての子状態が終了する。

ステートマシン・フレームワークの並列処理は、インターリーブ・セマンティクスに従う。すべての並列処理は、イベント処理の単一アトミックステップで実行されるため、イベントが並列処理を中断することはない。しかし、マシン自体がシングルスレッドであるため、イベントは依然として順次処理される。例えば、同じ並列状態グループから出る2つの遷移があり、それらの条件が同時に真になる状況を考えてみましょう。この場合、2つのうち最後に処理されたイベントは何の影響も及ぼしません。

複合状態の終了

子状態を最終状態(FinalState オブジェクト)にすることができます。最終子状態に入ると、親状態はState::finished シグナルを発信します。次の図は、最終状態に入る前に何らかの処理を行う複合状態s1

s1 の最終ステートに入ると、s1 は自動的にfinished を発する。シグナル遷移を使用して、このイベントを状態変更のトリガーにする:

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

複合ステートで最終ステートを使うのは、複合ステートの内部の詳細を隠したいときに便利である。外部は、内部の詳細を知らなくても、ステートに入り、ステートが処理を完了したときに通知を受け取ることができるはずだ。これは、複雑な(深くネストされた)ステートマシンを構築する際に、非常に強力な抽象化とカプセル化のメカニズムである。(上記の例では、s1 の finished() シグナルに依存するのではなく、s1done ステートから直接トランジションを作成することももちろん可能だが、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")
            }
        }
    }
}

ボタンがクリックされるたびに "button pressed "メッセージが表示されるが、ステートマシンは現在の状態(s1)のままである。ターゲット状態が明示的にs1に設定された場合、s1は毎回終了し、再入力される(QAbstractState::enteredQAbstractState::exited シグナルが発せられる)。

本ドキュメントに含まれる文書の著作権は、それぞれの所有者に帰属します 本書で提供されるドキュメントは、Free Software Foundationによって発行されたGNU Free Documentation License version 1.3の条項の下でライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。