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

第7回 ステートマシーンフレームワークの詳細[その3]

この記事を読むのに必要な時間:およそ 13 分

イベント遷移の独自拡張

QMouseEventTransitionを拡張して,ウィジェットのenabledプロパティに応じた動作にしてみましょう。つまり,無効化されていたらマウスイベント遷移が起きないようにしてみます。

リスト4 widgetmouseeventtransition.h

 1: #ifndef WIDGETMOUSEEVENTTRANSITION_H
 2: #define WIDGETMOUSEEVENTTRANSITION_H
 3: 
 4: #include <QMouseEventTransition>
 5: 
 6: class WidgetMouseEventTransition : public QMouseEventTransition
 7: {
 8:     Q_OBJECT
 9: 
10: public:
11:     explicit WidgetMouseEventTransition( QState* sourceState = 0 );
12:     WidgetMouseEventTransition( QObject* object, QEvent::Type type, Qt::MouseButton button, QState* sourceState = 0 );
13: 
14: protected:
15:     bool eventTest( QEvent* event );
16: };
17: 
18: #endif

ヘッダ部です。QMouseEventTransitionのサブクラスとし,eventText()を再実装してenabled プロパティに従うようにしようという方針です。

リスト5 widgetmouseeventtransition.cpp

 1: #include <QWidget>
 2: #include "widgetmouseeventtransition.h"
 3: 
 4: WidgetMouseEventTransition::WidgetMouseEventTransition( QState* sourceState )
 5:     : QMouseEventTransition( sourceState )
 6: {
 7: }
 8: 
 9: WidgetMouseEventTransition::WidgetMouseEventTransition( QObject* object, QEvent::Type type, Qt::MouseButton button, QState* sourceState )
10:     : QMouseEventTransition( object, type, button, sourceState )
11: {
12: }
13: 

コンストラクタはQMouseEventTransitionに合わせて2通り用意しておきましょう。

14: bool WidgetMouseEventTransition::eventTest( QEvent* event )
15: {
16:     if ( !QMouseEventTransition::eventTest( event ) )
17:         return false;
18: 
19:     QWidget* widget = qobject_cast<QWidget*>( eventSource() );
20:     if ( widget && widget->isEnabled() )
21:         return true;
22: 
23:     return false;
24: }

まず,ベースクラスのeventTest()で判定します。そして,eventSource()で得られるオブジェクトは,イベントを発生したオブジェクトなので,それをウィジェットに型変換して,isEnabled()で調べ,有効化されていれば true を返して遷移が起きるようにします。

#include "widgetmouseeventtransition.h"
...
EventWidget::EventWidget( QWidget* parent )
	: QWidget( parent ), d_ptr( new EventWidgetPrivate )
{
    ...
    WidgetMouseEventTransition* pressTransition = new WidgetMouseEventTransition( this, QEvent::MouseButtonPress, Qt::LeftButton, idleState );
    pressTransition->setTargetState( pressedState );

    WidgetMouseEventTransition* dblClickTransition = new WidgetMouseEventTransition( this, QEvent::MouseButtonDblClick, Qt::LeftButton, idleState );
    dblClickTransition->setTargetState( pressedState );

    WidgetMouseEventTransition* releaseTransition = new WidgetMouseEventTransition( this, QEvent::MouseButtonRelease, Qt::LeftButton, pressedState );
    releaseTransition->setTargetState( idleState );

拡張したWidgetMouseEventTransitionを使う部分は上記のように置き換えます。これで,ウィジェットが無効化されている場合には,マウスイベント遷移が起きないようにできます。

キーイベント遷移

キーイベント遷移は,QKeyEventTransitionクラスを使い,キー値を指定するコンストラクタがあるなど,ほとんどマウスイベント遷移と似た構成です。また,Qtのデモやサンプルコードに使用例があるので,それらを引用するにとどめます。

Sub-Attaq Demo
URL:http://doc.trolltech.com/4.6/demos-sub-attaq.html
Rogue Example
URL:http://doc.trolltech.com/4.6/statemachine-rogue.html
Pad Navigator Exmaple
URL:http://doc.trolltech.com/4.6/graphicsview-padnavigator.html

Sub-AttaqとRogueには,リファレンスに説明がありますが,Pad Navigatorの方はまだ説明が書かれていず,ソースコードが引用されているだけです。

履歴状態

第4回「ステートマシーンフレームワーク」では複合状態について説明しました。履歴状態と並列状態についても簡単な動くコードで確認してみましょう。

ステートマシンフレームワークのリファレンスの履歴状態の説明で使われているコード断片は,履歴状態のコードの書き方としては合っています。しかし,このコードでは状態がs3 になったときにラベルに "In s3" と表示はできません。少し書き直して動くようにしたコードで履歴状態を説明してみます。

リスト6 historystate.cpp

 1: #include <QApplication>
 2: #include <QPushButton>
 3: #include <QLabel>
 4: #include <QLayout>
 5: #include <QStateMachine>
 6: #include <QState>
 7: #include <QHistoryState>
 8: #include <QFinalState>
 9: 
10: int main( int argc, char** argv )
11: {
12:     QApplication app( argc, argv );
13: 
14:     QLabel* stateLabel = new QLabel;
15:     QPushButton* stateChangeButton = new QPushButton( "Change" );
16:     QPushButton* interruptButton = new QPushButton( "Interrupt" );
17:     QPushButton* quitButton = new QPushButton( "Quit" );
18: 

状態を順に変えるためのボタン,履歴状態に入るためのボタン,終了のためのボタンを用意します。

19:     QVBoxLayout* topLayout = new QVBoxLayout;
20:     topLayout->addWidget( stateLabel );
21:     topLayout->addWidget( stateChangeButton );
22:     topLayout->addWidget( interruptButton );
23:     topLayout->addWidget( quitButton );
24: 
25:     QWidget topWidget;
26:     topWidget.setLayout( topLayout );
27: 
28:     QStateMachine* stateMachine = new QStateMachine( &topWidget );
29:     QState* s1 = new QState( stateMachine );
30:     QState* s11 = new QState( s1 );
31:     QState* s12 = new QState( s1 );
32:     QState* s13 = new QState( s1 );
33:     QFinalState* finalState = new QFinalState( stateMachine );
34:     s1->setInitialState( s11 );
35: 

履歴状態を使うには,複合状態を用意して,その複合状態内に子状態を作ります。

36:     s11->assignProperty( stateLabel, "text", "In s11" );
37:     s12->assignProperty( stateLabel, "text", "In s12" );
38:     s13->assignProperty( stateLabel, "text", "In s13" );
39:     
40:     s11->addTransition( stateChangeButton, SIGNAL(clicked()), s12 );
41:     s12->addTransition( stateChangeButton, SIGNAL(clicked()), s13 );
42:     s13->addTransition( stateChangeButton, SIGNAL(clicked()), s11 );
43: 

ボタンstateChangeButtonをクリックするとs11,s12,s13,s11,…と順に複合状態s1内の状態が遷移するようにしています。図 history-state.png 状態の変化⁠⁠ の A のときに Change ボタンをクリックし B へ変わる動きです。

図3 状態の変化

図3 状態の変化

44:     s1->addTransition( quitButton, SIGNAL(clicked()), finalState );
45:     app.connect( stateMachine, SIGNAL(finished()), SLOT(quit()) );
46: 

複合状態s1に居るときにボタンquitButtonをクリックすると終了状態finalStateに遷移するようにします。終了状態になると状態遷移機械stateMachineはfinished()シグナルを送信するので,それをQApplicationのインスタンスappのquit()スロットに接続してプログラムが終了するようにしています。

47:     QHistoryState* s1h = new QHistoryState( s1 );
48: 
49:     QState* s3 = new QState( stateMachine );
50:     s3->assignProperty( stateLabel, "text", "In s3" );
51:     s3->assignProperty( interruptButton, "text", "Restart" );
52:     s1->assignProperty( interruptButton, "text", "Interrupt" );
53:     s3->addTransition( interruptButton, SIGNAL(clicked()), s1h );
54:     s1->addTransition( interruptButton, SIGNAL(clicked()), s3 );
55: 

履歴状態の作成です。記憶しておきたい子状態 s11,s12,s13を持つ複合状態s1を親とするように履歴状態s1hを作成します。最初にinterruptButtonをクリックすると状態s3に遷移します。この遷移をするときに,s11,s12,s13 の内のどの子状態だったかが記憶されます。図3のBからCへ変わる動きです。Cのときには状態s3にいるので,ChangeやQuitをクリックしても何も起きないことに注目しましょう。

s3から履歴状態s1hに遷移するようにしているのが要点で,この遷移によってもう一度interruptButtonをクリックすると複合状態s1に遷移し,子状態も復帰します。図3のDへ変わる動きで,ラベルstateLabelの表示がIn s12に戻っていることで,このように動作しているのがわかります。

56:     stateMachine->setInitialState( s1 );
57:     stateMachine->start();
58: 
59:     topWidget.show();
60: 
61:     return app.exec();
62: }
63: 

並列状態

ステートマシンフレームワークのリファレンスUsing Parallel States to Avoid a Combinatorial Explosion of Statesに説明されているように,並列状態を考えると,複雑な遷移を構成的にして遷移の爆発を抑え,記述をわかりやすくします。ここでは,Ping Pong States Exampleを用いて,並列状態のコードの書き方を説明します。

図4は以下のことを図示しています。

  1. 状態遷移機械が開始されるとpingerとpongerの2つの状態に同時に入る。
  2. pinger状態に入るとpingイベントを状態遷移機械にポストする。
  3. pinger状態はpongイベントを受取るとpingイベントを状態遷移機械にポストする。
  4. ponger状態はpingイベントを受取るとpoイベントを状態遷移機械にポストする。

図4 Ping Pong Statesの状態遷移図

図4 Ping Pong Statesの状態遷移図

終了条件がないので,強制的にプログラムを中断しなければ処理はずっと続きます。

リスト7

 1: #include <QtCore>
 2: #include <stdio.h>
 3: 
 4: class PingEvent : public QEvent
 5: {
 6: public:
 7:     PingEvent() : QEvent(QEvent::Type(QEvent::User+2))
 8:     {}
 9: };
10: 
11: class PongEvent : public QEvent
12: {
13: public:
14:     PongEvent() : QEvent(QEvent::Type(QEvent::User+3))
15:     {}
16: };
17: 

ステートマシンフレームワークでは,ウェジェットなどと同じように,ユーザイベントも扱えるようになっています。ここでは,PingEvent とPongEvent をイベントタイプのみを指定して新たに作成しています。

18: class Pinger : public QState
19: {
20: public:
21:     Pinger(QState *parent)
22:         : QState(parent) {}
23: 
24: protected:
25:     virtual void onEntry(QEvent *)
26:     {
27:         machine()->postEvent(new PingEvent());
28:         fprintf(stdout, "ping?\n");
29:     }
30: };
31: 

最初にpinger状態に入ったときにpongEventイベントを状態遷移機械にポストするために,QStateのサブクラスPingerを作成し,onEntry()でイベントをポストするようにしています。pinger状態はずっと続くので,onEntry()は最初の1回のみ呼出されます。

32: class PongTransition : public QAbstractTransition
33: {
34: public:
35:     PongTransition() {}
36: 
37: protected:
38:     virtual bool eventTest(QEvent *e) {
39:         return (e-&gt;type() == QEvent::User+3);
40:     }
41:     virtual void onTransition(QEvent *)
42:     {
43:         machine()-&gt;postDelayedEvent(new PingEvent(), 500);
44:         fprintf(stdout, &quot;ping?\n&quot;);
45:     }
46: };
47: 

状態pingerに設定する遷移です。イベントタイプがQEvent::User+3のPongEventを受け取ると遷移し,pingEventをポストします。

48: class PingTransition : public QAbstractTransition
49: {
50: public:
51:     PingTransition() {}
52: 
53: protected:
54:     virtual bool eventTest(QEvent *e) {
55:         return (e->type() == QEvent::User+2);
56:     }
57:     virtual void onTransition(QEvent *)
58:     {
59:         machine()->postDelayedEvent(new PongEvent(), 500);
60:         fprintf(stdout, "pong!\n");
61:     }
62: };
63: 

こちらは,状態pongerに設定する遷移です。イベントタイプはQEvent::User+2なので,PingEventを受け取ると遷移し,pongEventをポストします。

64: int main(int argc, char **argv)
65: {
66:     QCoreApplication app(argc, argv);
67: 
68:     QStateMachine machine;
69:     QState *group = new QState(QState::ParallelStates);
70:     group->setObjectName("group");
71: 

並列状態を作るには,まず親状態の子状態のモード(ChildMode)をQState::ParallelStatesとして作成します。

72:     Pinger *pinger = new Pinger(group);
73:     pinger->setObjectName("pinger");
74:     pinger->addTransition(new PongTransition());
75: 
76:     QState *ponger = new QState(group);
77:     ponger->setObjectName("ponger");
78:     ponger->addTransition(new PingTransition());
79: 

この状態groupを親状態にして子状態を作成すれば,子状態が並列化されます。このサンプルコードでのユーザイベントによる処理の流れは以下のようになります。

  1. pinger状態に最初に入ったときに,PingerEventがポストされる。
  2. 状態遷移機械は,各状態の遷移を調べて eventTest() を呼び,遷移するものを見つけようとする。
  3. ponger状態に設定されたPingTransition遷移がPingerEventで遷移するので,PingerEvent::onTransition() が呼び出されてPongEventがポストされる。
  4. 2.と同じ。
  5. pinger状態に設定された PongTransition 遷移がPongerEventで遷移するので,PongerEvent::onTransition()が呼び出されてPingEventがポストされる。
  6. 2.に戻る。
80:     machine.addState(group);
81:     machine.setInitialState(group);
82:     machine.start();
83: 
84:     return app.exec();
85: }

著者プロフィール

杉田研治(すぎたけんじ)

1955年生まれ。東京都出身。株式会社SRAに勤務。プログラマ。

仕事のほとんどをMac OS XとKubuntu KDE 4でQtと供に過ごす。