4.6がやってきた-Qt最新事情2010

第5回ステートマシーンフレームワークの詳細

前回はステートマシーンフレームワークの経緯を説明し、詳しいことは省いて「アニメーションフレームワーク」の説明の最後の「パラレルアニメーションによるアニメーションの並行実行」のサンプルプログラムにアニメーションフレームワークを適用した例を示しました。今回は、ステートマシーンフレームワークの個々の機能を少し詳しく見てみましょう。

ステートマシンフレームワークの使用手順

ステートマシンフレームワークの基本的な使用手順は次のようになります。

①状態遷移機械の生成

QStateMachineのインスタンスを生成します。

②状態の生成

QStateのインスタンスが状態を表し、コンストラクタにはQStateMachineまたはQStateのインスタンスを指定します。通常の状態では、QStateMachineを渡してQState のインスタンスを生成します。複合状態や並列状態を生成する場合には、QState のインスタンスを渡します。

③遷移の生成

オブジェクトのシグナル送信によって引き起される遷移の場合には、QSignalTransitionのインスタンスを生成し、マウスとキーによる場合には、QMouseEventTransitionやQKeyEventTransitionのインスタンスを生成して、遷移を生成し、QState::addTransition() で、状態に遷移を設定します。

以下のような、簡単にトランジションを設定できるメソッドも用意されています。

QState::addTransition( QObject* sender, const char* signal, QAbstractState* target )

QAbstractTransition::addAnimation()で必要に応じてアニメーションを付加します。

④初期状態の設定

QStateMachine::setInitialState()で、初期状態となるQStateのインスタンスを設定します。複合状態や並列状態の場合には、入れ子になった各状態に対して、QState::setInitialState()で初期状態を設定します。

⑤状態遷移機械の起動

QStateMachineのインスタンスに対してstart() を呼び出します。

基本的なステートマシンフレームワークの適用例

ステートマシンフレームワークのサンプルプログラムがQtに付属しています。まず、基本的なTwo-way Button Exampleで、前述のステートマシンフレームワークの使用手順を確認しましょう。

このサンプルプログラムを動かすと、図1のようにクリックするたびにボタンのテキストがOffとOnに切り替わります。

図1 ⁠Two-way Button Example⁠の動作
図1 “Two-way Button Example”の動作

状態遷移図は図2のようになります。前回のサンプルプログラムの状態遷移図と本質的には同じです。

図2 ⁠Two-way Button Example⁠の状態遷移
図2 “Two-way Button Example”の状態遷移

サンプルプログラムを見ていきましょう。

リスト1 Two-way Button Example
 1: #include <QtGui>
 2: 
 3: int main(int argc, char **argv)
 4: {
 5:     QApplication app(argc, argv);
 6:     QPushButton button;
 7:     QStateMachine machine;

「状態遷移機械の生成」です。

 9:     QState *off = new QState();
10:     off->assignProperty(&button, "text", "Off");
11:     off->setObjectName("off");
12: 
13:     QState *on = new QState();
14:     on>setObjectName("on");
15:     on->assignProperty(&button, "text", "On");

「状態の生成」です。onとoffの2つの状態を用意し、各状態でのプロパティを設定しています。このような簡単なものでは不要ですが、オブジェクト名をsetObjectName()で付けるようにしておくと、複雑な状態遷移を使ったときにデバッグがしやすくなります。

17:     off->addTransition(&button, SIGNAL(clicked()), on);
18:     on->addTransition(&button, SIGNAL(clicked()), off);

状態に、addTransition()で遷移を設定しています。簡易メソッドを使っているので、明示的にQSignalTransitionのインスタンスを生成していませんが、内部で生成されます。QStateには以下の3つのaddTransition()メソッドが用意されています。

void addTransition(QAbstractTransition* transition)

QSignalTransition、QMouseEventTransitionやQKeyEventTransitionのインスタンスを生成してから設定します。リスト1の17~18行は、以下のように書き換えられます。

QSignalTransition *fromOfftoOnTransition = new QSignalTransition(&button, SIGNAL(clicked()), off);
fromOfftoOnTransition->setTargetState(on);
QSignalTransition *fromOntoOffTransition = new QSignalTransition(&button, SIGNAL(clicked()), on);
fromOntoOffTransition->setTargetState(off);

QSignalTransitionのインスタンスの所有者は、offまたはonのQStateインスタンスになることに注意しましょう。

QSignalTransition *addTransition(QObject *sender, const char *signal, QAbstractState *target)

このサンプルプログラムで使われているメソッドです。signalにはQObject::connect()と同様に、マクロSIGNAL()によって正規化したシグナルのシグネチャ文字列を渡します。

QAbstractTransition *addTransition(QAbstractState *target)

無条件遷移で、遷移のためのトリガーはなく、元状態からtargetに指定した状態にすぐに遷移します。

20:     machine.addState(off);
21:     machine.addState(on);

「状態の生成」の変形で20~21を削除し、9行目と13行目を以下のようにしても同じ状態遷移機械となり、同じ動作をします。

 9:    QState *off = new QState(&machine);
        
13:   QState *on = new QState(&machine);
23:     machine.setInitialState(off);

「初期状態の設定」です。起動直後の状態がoffになります。

24:     machine.start();

「状態遷移機械の起動」です。状態遷移機械がイベントループで開始されるようにスケジューリングされます。

26:     button.resize(100, 50);
27:     button.show();
28:     return app.exec();
29: }

複合状態を持つステートマシンフレームワークの適用例

次に、Traffic Light Exampleで複合状態を持つ場合について説明します。リファレンスにコードの説明がありますが、状態遷移機械をどうして使うかということも含めて、より詳しく追ってみましょう。

Traffic Lightでは簡略化した信号機の動作を状態遷移機械で表現していて、実際に動作させると図3のように変化します。

図3 Traffic Lightの動作
図3 Traffic Lightの動作

信号機の動作仕様を文章で書き下すと、以下のようになります。

  • ① 信号機には3つの電球があり、赤、黄、青の3色である。
  • ② 各電球は、ある一時点で一色のみが表示される。
  • ③ 各電球は、点灯して一定時間経つと消える。
  • ④ 電球の点灯時間は、赤と青は 3 秒、黄は 1 秒である。
  • ⑤ ある電球が点灯し、その点灯時間が経つと、別の電球が点灯する。点灯する順番は、赤、黄、青、黄、赤…と繰り返される。
  • ⑥ 最初に点灯する電球は赤である。

この仕様に従った信号機の動作をするプログラムを作成するのですが、普通の文章で書かれた事柄がプログラムで満たされているかを確かめようとすると、どうしても間違いや漏れが出やすくなります。この例位の記述ならば間違えることはあり得ませんが、⁠赤、黄、青、黄、赤…と繰返される」というような曖昧な表現の解釈も問題になり得ます。

文章よりもはっきりと形式化して、仕様を確実に表現でき、プログラミング上でも仕様に添っているかを検証できるようにするために、ステートマシンフレームワークを使うとどうなるかを見てみましょう。

各電球の状態遷移図

Traffic Lightには2つ状態遷移図が描かれています。最初の図は、各電球の動作仕様を状態遷移機械で表現したものです。図を説明すると次のようになります。図と照らし合わせながら確かめてみてください。

1つ目の図LightStateは、1つの電球の点灯状態を表す図です図4⁠。点灯状態は2つの状態があって、点いているか消えているかです。点灯して一定時間経つと消えるという動作をします。

図4 各電球の状態遷移図
図4 各電球の状態遷移図

LightState状態は、内部に状態遷移機械を抱えていて、点灯状態の制御、つまり点いてから一定時間経つと消えるという動作を制御しています。内部状態遷移機械が表現しているのは、以下の3つです。

  • ①点灯中を表すtiming状態を持つ。
  • ②黒丸が指している先は、LightStateの最初の状態。つまりLightStateという状態に遷移すると、内部状態 timing に遷移し、電球が点いて、タイマーが起動されます。
  • ③タイムアウトすると電球が消えて、終了状態になります。丸で囲まれた黒丸が終了状態を表します。

図4を使うと文章で書いた仕様の③が満たされていることが確認できます。

信号機の状態遷移図

2つ目の図は、信号機の状態遷移機械です図5⁠。どの電球が点いているかを各色のLightState 状態を用いて、以下のように表現しています。

LightState意味付け
redGoingYellow赤の点灯制御のための状態
yellowGoingGreen次の点灯が青の場合の黄の点灯制御のための状態
greenGoingYellow緑の点灯制御のための状態
yellowGoingRed次の点灯が赤の場合の黄の点灯制御のための状態
図5 信号機の状態遷移図
図5 信号機の状態遷移図

この状態遷移機械が表現しているのは以下の6つです。

  • ①黒丸が指している先のredGoingYellowが最初の状態。つまり、最初に赤が点灯する。
  • ②redGoingYellow.finished、つまり、redGoingYellow状態の内部状態が終了状態になると yellowGoingGreen 状態に遷移する。
  • ③yellowGoingGreen状態の内部状態が終了状態になるとgreenGoingYellow状態に遷移する。
  • ④greenGoingYellow状態の内部状態が終了状態になるとyellowGoingRed状態に遷移する。
  • ⑤yelloowGoingRed状態の内部状態が終了状態になるとredGoingYellow状態に遷移する。つまり、最初の状態に戻る。
  • ⑥終了状態はなく、永久に前述の動作を繰返す。

図5を使うと、文章で書いた仕様の残りの項目が満たされていることが確認できます[1]⁠。

このようにしてステートマシンで形式化した仕様を機械的にコードに書き換えることができれば、仕様を満たしたプログラムの作成が容易になり、コードの検証も機械的になって確実さが増します。保守性も向上するでしょう。あるいは、状態遷移機械の図を描くようなエディタから、プログラムのコード生成をするような仕組みがあれば、機械的な作業も効率化され得るでしょう。形式化されるとその正しさを自動的にチェックすることもできる可能性があります。たとえば、状態遷移機械の記述が不十分で、無限ループに陥る可能性があることもチェックできるでしょう。こういったことが、状態遷移機械をプログラム開発に適用しようという動機です。

ここまでの説明を念頭にして、サンプルプログラムを見てみます。

リスト2 Two-way Button Example
 1: #include <QtGui>
 2: 
 3: class LightWidget : public QWidget
 4: {
 5:     Q_OBJECT
 6:     Q_PROPERTY(bool on READ isOn WRITE setOn)

電球にonプロパティを用意し、このプロパティをステートマシンフレームワークの機能で操作するようにしています。

 7: public:
 8:     LightWidget(const QColor &color, QWidget *parent = 0)
 9:         : QWidget(parent), m_color(color), m_on(false) {}
10: 
11:     bool isOn() const
12:         { return m_on; }
13:     void setOn(bool on)
14:     {
15:         if (on == m_on)
16:             return;
17:         m_on = on;
18:         update();
19:     }

onプロパティのゲッターとセッターのうちセッターでは電球のオン/オフが設定されるので、update()を呼び出して、paintEvent で描画されるようにしています。

21: public slots:
22:     void turnOff() { setOn(false); }
23:     void turnOn() { setOn(true); }

遷移が起きたときに呼び出すためのスロットです。

25: protected:
26:     virtual void paintEvent(QPaintEvent *)
27:     {
28:         if (!m_on)
29:             return;
30:         QPainter painter(this);
31:         painter.setRenderHint(QPainter::Antialiasing);
32:         painter.setBrush(m_color);
33:         painter.drawEllipse(0, 0, width(), height());
34:     }

電球の点灯状態に合わせて、色の付いた円を描画しています。オフの場合には、何もしなければ、LightWidgetがTrafficLightWidgetの上に乗るようにするので、TrafficLightWidgetの背景色の黒の描画されるので、電球が消えたように描画されます[2]⁠。

36: private:
37:     QColor m_color;
38:     bool m_on;
39: };
40: 
41: class TrafficLightWidget : public QWidget
42: {
43: public:
44:     TrafficLightWidget(QWidget *parent = 0)
45:         : QWidget(parent)
46:     {
47:         QVBoxLayout *vbox = new QVBoxLayout(this);
48:         m_red = new LightWidget(Qt::red);
49:         vbox->addWidget(m_red);
50:         m_yellow = new LightWidget(Qt::yellow);
51:         vbox->addWidget(m_yellow);
52:         m_green = new LightWidget(Qt::green);
53:         vbox->addWidget(m_green);
54:         QPalette pal = palette();
55:         pal.setColor(QPalette::Background, Qt::black);
56:         setPalette(pal);
57:         setAutoFillBackground(true);
58:     }

三色の電球を配置し、背景色を黒に設定しています。

60:     LightWidget *redLight() const
61:         { return m_red; }
62:     LightWidget *yellowLight() const
63:         { return m_yellow; }
64:     LightWidget *greenLight() const
65:         { return m_green; }

各電球のインスタンスへの参照アクセッサーです。

67: private:
68:     LightWidget *m_red;
69:     LightWidget *m_yellow;
70:     LightWidget *m_green;
71: };
72: 
73: QState *createLightState(LightWidget *light, int duration, QState *parent = 0)

各電球の内部状態遷移機械を持つ状態を生成するためのメソッドです。

74: {
75:     QState *lightState = new QState(parent);
76:     QTimer *timer = new QTimer(lightState);
77:     timer->setInterval(duration);
78:     timer->setSingleShot(true);
79:     QState *timing = new QState(lightState);

lightStateは生成する状態で、lightStateを指定して生成したtimingが内部状態遷移機械となります。

80:     QObject::connect(timing, SIGNAL(entered()), light, SLOT(turnOn()));
81:     QObject::connect(timing, SIGNAL(entered()), timer, SLOT(start()));
82:     QObject::connect(timing, SIGNAL(exited()), light, SLOT(turnOff()));

シグナルentered()と exited() はQAbstractStateクラスで定義されていて、状態に入ったときと状態から出たときに送信されます。これらのシグナルをLightWidgetやQTimerと接続することで、遷移の発生対して動作を決められます[3]⁠。

83:     QFinalState *done = new QFinalState(lightState);
84:     timing->addTransition(timer, SIGNAL(timeout()), done);

終了状態を生成し、タイマがタイムアウトで終了状態に遷移するようにしています[4]⁠。

85:     lightState->setInitialState(timing);

内部状態遷移機械の初期状態を設定が必要なことに注意しましょう。

 86:     return lightState;
 87: }
 88:
 89: class TrafficLight : public QWidget
 90: {
 91: public:
 92:     TrafficLight(QWidget *parent = 0)
 93:         : QWidget(parent)
 94:     {
 95:         QVBoxLayout *vbox = new QVBoxLayout(this);
 96:         TrafficLightWidget *widget = new TrafficLightWidget();
 97:         vbox->addWidget(widget);
 98:         vbox->setMargin(0);
 99: 
100:         QStateMachine *machine = new QStateMachine(this);
101:         QState *redGoingYellow = createLightState(widget->redLight(), 3000);
102:         redGoingYellow->setObjectName("redGoingYellow");
103:         QState *yellowGoingGreen = createLightState(widget->yellowLight(), 1000);
104:         yellowGoingGreen->setObjectName("yellowGoingGreen");
105:         redGoingYellow->addTransition(redGoingYellow, SIGNAL(finished()), yellowGoingGreen);
106:         QState *greenGoingYellow = createLightState(widget->greenLight(), 3000);
107:         greenGoingYellow->setObjectName("greenGoingYellow");
108:         yellowGoingGreen->addTransition(yellowGoingGreen, SIGNAL(finished()), greenGoingYellow);
109:         QState *yellowGoingRed = createLightState(widget->yellowLight(), 1000);
110:         yellowGoingRed->setObjectName("yellowGoingRed");
111:         greenGoingYellow->addTransition(greenGoingYellow, SIGNAL(finished()), yellowGoingRed);
112:         yellowGoingRed->addTransition(yellowGoingRed, SIGNAL(finished()), redGoingYellow);
113: 
114:         machine->addState(redGoingYellow);
115:         machine->addState(yellowGoingGreen);
116:         machine->addState(greenGoingYellow);
117:         machine>addState(yellowGoingRed);
118:         machine->setInitialState(redGoingYellow);
119:         machine->start();

今までに出てきたものと同じようにして、2つ目の状態遷移図(図5)に沿って各色の電球の点灯状態の変化を実装しています。※4で触れたように、子状態の終了のシグナルfinished()で遷移するようにしています。

120:     }
121: };
122: 
123: int main(int argc, char **argv)
124: {
125:     QApplication app(argc, argv);
126: 
127:     TrafficLight widget;
128:     widget.resize(110, 300);
129:     widget.show();
130: 
131:     return app.exec();
132: }
133: 
134: #include "main.moc"

ステートマシンフレームワークを適用したサンプルコード

Qtには、以下のようなステートマシンフレームワークを用いたサンプルコードがいろいろ付属しています。

State Machine Examples
  • Event Transitions
  • Factorial States
  • Ping Pong States
  • Rogue
  • Traffic Light
  • Two-way Button
Animation Framework Examples
  • Animated Tiles
  • Application Chooser
  • Move Blocks
  • States
  • Stick man
Sub-Attaq
ほとんどはGUIでの動作をするものですが、ステートマシーンフレームワークは、Qtの非GUI機能の基本モジュールQtCoreに含まれていることからわかるように、GUIを持たないプログラムにも適用でき、Factorial Statesと Ping Pong StatesはGUI機能を使わずに書かれています。

ステートマシーンフレームワーク適用の注意

経験的に、ステートマシーンフレームワークの適用にはいくつか注意が必要です。

①低動作レベルな場合には適用しない。

ステートマシンフレームワークは、独自のイベントキューを持ち、Qtのイベントループと協調して動作します。つまり、Qt のイベントシステムとシグナル/スロットと統合されたフレームワークのため、低動作レベルの記述には適しているとは言えません。パフォーマンスが影響するような部分に対して、状態遷移の仕組みが必要な場合には、低動作レベルに適した他のステートマシンフレームワークを併用することもできるでしょう。

②Qtアプリケーションの全動作をステートマシーンフレームワークだけで実装ししない。

適宜いろいろな手段で状態遷移を記述するべきで、たとえば、メタオブジェクトシステムとうまく統合されているからという理由だけで状態へのプロパティの設定だけで記述しようとすると、かえってわかりにくく複雑になりがちです。

③状態遷移図または状態遷移表を最初に記述する

いきなりステートマシンフレームワークを使ってコードを記述すると、どうしてもやり直しが多くなり、見通しもよくはならないことが多いです。状態遷移図または状態遷移表を最初に記述して、どのような仕様かをはっきり決めてから取り掛かるようにした方が良いと思います。

おわりに

次回はステートマシンフレームワークの最後として、今回予定していて触れられなかったシグナルトとイベントの独自拡張、シグナル遷移とマウスとキーイベント遷移、履歴状態と並列状態、遷移へのアニメーション効果の付け方について説明する予定です。

おすすめ記事

記事・ニュース一覧