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

第4回ステートマシーンフレームワーク

はじめに

Qt 4.6で導入されたQtのステートマシーンフレームワークは、状態遷移による実行モデルをQtアプリケーションフレームワークで利用できるようにしています。Qtのメタオブジェクトシステムと密接に結びつけられて実装されいて、状態間の遷移はQtのシグナルやキーイベント、マウスイベントなどのイベントによって引き起されます。さらに、Qtのプロパティ(筆者が以前書いた特集記事「Qtの基本プログラミング~オブジェクトモデル」参照)を使って状態の規定もできます。

Qtのステートマシンフレームワークは、ハレルの状態遷移図を元とし、その実行動作はSCXML(State Chart XML)に基づいています。現時点で正式にリリースされているのは、今回説明するステートマシンフレームワークですが、この他の実装がQt LabsのProjects/xml/scxmlで試みられています。このプロジェクトでは、以下の3つの実装がなされています。

ステートチャートコンパイラ
SCXML記述をQtステートマシンフレームワークのC++コードに変換するコンパイラ(scc、State Chart Compiler)。
QtScript用(QScxmlを利用)
WebKitベースのブラウザ用
Qt Labs BlogsのページではQt Webkit, Arora, Safari, Chromeなどを使って、実際にデモを動かすことができます。
動画1 WebKitブラウザ用デモの模様

そしてさらに、こられの3実装で用いられるSCXMLをグラフィカルな状態遷移図として表示するQt Creatorプラグインの計画やこの連載の第1回「速報!Qt 4.6新機能とロードマップ」で触れたDeclarative UIのQMLとの併用も試みられています。

今回紹介するアニメーションプログラムのソースリストはこちらからダウンロードしてください。
examples.zip

ステートマシンフレームワークのクラス

図1は、ステートマシンフレームワークに用意されているクラスです。主要なクラスの機能概要は表1のようになります。

図1 ステートマシンフレームワークのクラス
図1 ステートマシンフレームワークのクラス
表1
クラス名機能概要
QStateMachineひとつの階層型有限状態機械。
QState状態機械中の状態で、初期状態のプロパティinitialStateや並列状態を区別するプロパティchildModeがあります。
QFinalState状態機械中の終了状態。
QHistoryState前の活性化された副状態への復帰に用いる。
QEventTransitionQtのイベント対するQObjectに特化した遷移で、QObjectのインタンスとイベントを束ねたオブジェクトです。
QKeyEventTransitionキーイベントによって引き起される遷移。
QMouseEventTransitionマウスイベントによって引き起される遷移。
QSignalTransitionQtのシグナルによって引き起される遷移。

アニメーションの書き換え

詳しい説明は次回に回し、前回の「アニメーションフレームワーク」の説明の最後の「パラレルアニメーションによるアニメーションの並行実行」のサンプルプログラムにアニメーションフレームワークを適用して書き換えてみましょう。

図2は、適用した状態遷移図です。

図2 サンプルプログラムanimatedtransitionの状態遷移図
図2 サンプルプログラムanimatedtransitionの状態遷移図

startState状態は、アニメーションの開始待ち状態を表します。図2の左の黒丸からの矢印は、この状態が初期状態であることを示しています。この状態をプロパティで以下のように規定しています。

  • ボタンのテキストに "Start" をセットする。
  • 3つのボタンの位置を (startX, y) にセットし、位置を左端にする。

endState状態は、アニメーションの完了状態を表します。状態機械の終了状態ではなく、アニメーションの終わりを示します(正確には、アニメーションが始まるとendStateに遷移しています⁠⁠。この状態をプロパティで以下のように規定しています。

  • ボタンのテキストに"Reset"をセットする。
  • 3つのボタンの位置を (startX, y) にセットし、位置を左端にする。

movingBallTransition遷移は、startState状態にあるときにボタンがシグナルclicked()を送信することで引き起される遷移です。この遷移に、並列アニメーションを設定して、startState 状態でのボタンのクリックで、アニメーションが始まるようにします。

resetTransition遷移は、endState状態にあるときにボタンがシグナルclicked()を送信することで引き起されます。アニメーションの実行中には、endState状態になっているので、ボタンをクリックすれば、中断されてstartState 状態に戻って、アニメーションの開始待ち状態となります。

前回の説明で補足したように、書き換えると3つのボールを左端に戻すのを初期状態としてわかりやすく記述できます。

これを実行させた模様は次の動画のようになります。

動画2 書き換えたアニメーション

では、書き換えたサンプルコードの要点を変更箇所を中心に説明します。

リスト1 書き換えサンプル
 1: #include <QApplication>
 2: #include <QGraphicsView>
 3: #include <QGraphicsScene>
 4: #include <QGraphicsPixmapItem>
 5: #include <QPropertyAnimation>
 6: #include <QSequentialAnimationGroup>
 7: #include <QParallelAnimationGroup>
 8: #include <QLayout>
 9: #include <QPushButton>
10: #include <QState>
11: #include <QStateMachine>
12: #include <QSignalTransition>

10~12行で、ステートマシンフレームワークのシグナルトランジションを使用するために必要なヘッダファイルをインクルードしています。

52: Harness::Harness()
53:     : QWidget( 0 ), d_ptr( new HarnessPrivate )
54: {
55:     Q_D( Harness );
56: 
57:     QGraphicsScene* graphicsScene = new QGraphicsScene( 0, 0, d->movingAreaSize.width(), d->movingAreaSize.height() );
58:     MovableGraphicsPixmapItem* yellowBallItem = createBallItem( ":/images/YellowGlassBall.png" );
59:     QSize ballSize = yellowBallItem->pixmap().size();
60:     int ballSpacingY = ( d->movingAreaSize.height() - ballSize.height() * 3 ) / 4;
61:     int yellowBallY = ballSpacingY;
62:     yellowBallItem->setPos( d->horizontalOffset, yellowBallY );
63:     graphicsScene->addItem( yellowBallItem );
64: 
65:     MovableGraphicsPixmapItem* greenBallItem = createBallItem( ":/images/GreenGlassBall.png" );
66:     int greenBallY = yellowBallY + ballSize.height() + ballSpacingY;
67:     greenBallItem->setPos( d->horizontalOffset, greenBallY );
68:     graphicsScene->addItem( greenBallItem );
69: 
70:     MovableGraphicsPixmapItem* redBallItem = createBallItem( ":/images/RedGlassBall.png" );
71:     int redBallY = greenBallY + ballSize.height() + ballSpacingY;
72:     redBallItem->setPos( d->horizontalOffset, redBallY );
73:     graphicsScene->addItem( redBallItem );
74: 
75:     QGraphicsView* graphicsView = new QGraphicsView();
76:     graphicsView->setScene( graphicsScene );
77: 
78:     QPushButton* animateButton = new QPushButton( "Start" );

初期状態に入ると、プロパティによってボタンのテキストがセットされるので、new QPushButton()としても良いのですが、レイアウト時にサイズが決められるようにすることでフリッカーを避けられるので、あらかじめセットするようにします。

 80:     QHBoxLayout* buttonLayout = new QHBoxLayout;
 81:     buttonLayout->addStretch();
 82:     buttonLayout->addWidget( animateButton );
 83: 
 84:     QVBoxLayout* topLayout = new QVBoxLayout;
 85:     topLayout->addWidget( graphicsView );
 86:     topLayout->addLayout( buttonLayout );
 87: 
 88:     setLayout( topLayout );
 89:     setFixedSize( sizeHint() );
 90: 
 91:     int movableBallStartX = d->horizontalOffset;
 92:     int movableBallEndX = d->movingAreaSize.width() - ballSize.width() - d->horizontalOffset;
 93: 
 94:     QPropertyAnimation* yellowBallAnimation = createBallAnimation( yellowBallItem, 20 * 1000, movableBallStartX, movableBallEndX, yellowBallY );
 95:     QPropertyAnimation* greenBallAnimation = createBallAnimation( greenBallItem, 10 * 1000, movableBallStartX, movableBallEndX, greenBallY );
 96:     QPropertyAnimation* redBallAnimation = createBallAnimation( redBallItem, 10 * 1000, movableBallStartX, movableBallEndX, redBallY );
 97: 
 98:     d->ballAnimationGroup = new QParallelAnimationGroup( this );
 99:     d->ballAnimationGroup->addAnimation( yellowBallAnimation );
100:     QSequentialAnimationGroup* sequentialAnimation = new QSequentialAnimationGroup;
101:     sequentialAnimation->addAnimation( greenBallAnimation );
102:     sequentialAnimation->addAnimation( redBallAnimation );
103:     d->ballAnimationGroup->addAnimation( sequentialAnimation );
104: 
105:     QStateMachine* stateMachine = new QStateMachine( this );

状態遷移機械のインスタンスで、以降でこれに対して状態や遷移を定義します。

107:     QState* startState = new QState( stateMachine );
108:     startState->assignProperty( yellowBallItem, "pos", yellowBallAnimation->startValue() );
109:     startState->assignProperty( greenBallItem, "pos", greenBallAnimation->startValue() );
110:     startState->assignProperty( redBallItem, "pos", redBallAnimation->startValue() );
111:     startState->assignProperty( animateButton, "text", "Start" );
112:     stateMachine->setInitialState( startState );

アニメーションの開始待ちを表すstartState状態のインスタンスです。メソッドassignProperty()でこの状態になったときに、オブジェクト(QObjectのサブクラスのオブジェクト)のposプロパティに設定する値を指定しています。設定値は、ボールが左端にあるときの位置です。setInitialState()で、状態遷移機械stateMachine の初期状態の設定をします。初期状態は必ず指定する必要があり、忘れた場合には実行時に警告メッセージがコンソールに表示されます。

114:     QState* endState = new QState( stateMachine );
115:     endState->assignProperty( yellowBallItem, "pos", yellowBallAnimation->endValue() );
116:     endState->assignProperty( greenBallItem, "pos", greenBallAnimation->endValue() );
117:     endState->assignProperty( redBallItem, "pos", redBallAnimation->endValue() );
118:     endState->assignProperty( animateButton, "text", "Reset" );

アニメーションの完了状態を表すendState状態のインスタンスで、ボールが右端にあるときの位置を指定しています。このように、どの状態のときにどのようなプロパティを宣言的に規定することで、どのようになっていて欲しいかを明示的に表記できるのがステートマシンフレームワークの利点のひとつです。

120:     QSignalTransition* movingBallTransition = startState->addTransition( animateButton, SIGNAL( clicked() ), endState );
121:     movingBallTransition->addAnimation( d->ballAnimationGroup );

startState状態にあるときに、ボタンがシグナルclicked()を送信するとendState状態に遷移するシグナルトランジションのインスタンスです。addAnimation()でアニメーションをセットするとボタンのクリックで、アニメーションが始まります。クリックの瞬間に指定した状態に移るのであって、アニメーションの完了によって状態が移るのではないことに注意が必要です。

122:     QSignalTransition* resetTransition = endState->addTransition( animateButton, SIGNAL( clicked() ), startState );
123:     Q_UNUSED( resetTransition );

逆に、endState状態にあるときにボタンがシグナルclicked()を送信すると、startState状態に戻るシグナルトランジションのインスタンスです。endState状態は、見かけ上はアニメーションの動作中か、3つのボールがすべて右端にあるときの状態です。

125:     stateMachine->start();
126: }

状態遷移機械によって動作が規定されてるので、ボタンのクリックで呼出されるHarness::startAnimation()スロットは不要です。スロットでボールを左端に戻す必要もなく、アニメーションを開始させるような実行コードは明示的には書きません。こういったことは、状態遷移機械stateMachineで宣言的に表明しているからです。

おわりに

Qtのステートマシンフレームワークの概要とその実際の適用例について説明しました。簡単な例ですが、状態遷移機械で動作を規定すると検証し易いコードにもなることがわかります。次回は、各クラスの詳細や独自のシグナルトランジションやイベントトランジションの作成方法について説明する予定です。

おすすめ記事

記事・ニュース一覧