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

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

今回はシグナル遷移がどのようなものかを詳しく説明し、シグナル遷移の独自拡張の方法について説明します。

シグナル遷移

シグナル遷移とは図1のように表すことができます。図で「遷移前の状態」と書かれているのが現在の状態のときに、あるオブジェクトで特定のシグナルが発生すると引き起されるように定義された遷移です。

図1 シグナル遷移(QSignalTransition)
図1 シグナル遷移(QSignalTransition)

シグナル遷移を表すためにはQSignalTransitionのインスタンスが用いられ、遷移のために表1に挙げるようなプロパティがあります。

表1 シグナル遷移のプロパティ
プロパティデータ型説明
senderObjectQObject*シグナル発生源となるオブジェクト。
signalQByteArray遷移を引き起すシグナル。
sourceStateQState* const遷移前の状態。読込み専用プロパティ。内部的には親オブジェクトとなる。
targetStateQAbstractState*遷移後の状態。
targetStatesQList<QAbstractState*>並列状態のための遷移後の状態リスト。

最初の4つのプロパティを図1と見比べてみると、これらのプロパティでシグナル遷移が定まっているのがわかります。

シグナル遷移の内部動作

ステートマシンフレームワーク内でのシグナル遷移の処理概要は以下のようになっています。

  1. シグナル遷移の対象となっているオブジェクトでシグナルが発生する。
  2. QStateMachineはイベント型がQEvent::StateMachineSignalのイベントQStateMachine::SignalEventを発生させ、QSignalTransitionインスタンスに送る。
  3. QSignalTransition::eventTest()でイベントを受け取り、遷移対象のシグナルかを判定する。戻り値がtrueならば状態が遷移し、falseならば遷移しない。

前回の説明でも触れたように、ステートマシンフレームワークでは、Qtのイベントループを介して状態遷移機械がイベントループで開始されるようにスケジューリングされます。したがって、ステートマシンフレームワークを用いるには、イベントループを起こす必要があります。

オブジェクトツリー構成

オブジェクトのメモリ管理のために、オブジェクトツリーは、どのようになっているかを把握しておきましょう。前表のように、QSignalTransition オブジェクトの親オブジェクトは、sourceState() が返す QState オブジェクトになっています。QState オブジェクトの親オブジェクトは、QStateMachine オブジェクトになるので、QStateMachine オブジェクトを頂点とするオブジェクトツリーができていることになります。

シグナル遷移の設定パターン

前回の「基本的なステートマシンフレームワークの適用例」で、シグナル遷移の設定パターンをいくつか説明しました。他の設定パターンを含めてまとめ、それぞれの設定でどのようなことがされているか見てみましょう。

 1: #include <QApplication>
 2: #include <QPushButton>
 3: #include <QStateMachine>
 4: #include <QSignalTransition>
 5: 
 6: int main( int argc, char** argv )
 7: {
 8:     QApplication app( argc, argv );
 9: 
10:     QPushButton button;
11: 
12:     QStateMachine machine;
13:     QState* beforeState = new QState( &machine );
14:     beforeState->assignProperty( &button, "text", "Before" );
15:     QState* afterState = new QState( &machine );
16:     afterState->assignProperty( &button, "text", "After" );
17: 
18:     beforeState->addTransition( &button, SIGNAL(clicked()), afterState );
19:     
20:     button.show();
21: 
22:     machine.setInitialState( beforeState );
23:     machine.start();
24: 
25:     return app.exec();
26: }

図2のようにボタンをクリックすると、初期状態から別の状態に1回だけ遷移するコードです。

図2 1回だけ遷移するパターンの例
図2 1回だけ遷移するパターンの例
18:     beforeState->addTransition( &button, SIGNAL(clicked()), afterState );

前回にも出て来たように、最初の設定パターンは最も簡潔です。このaddTransition()では、内部でQSignalTransition のインスタンスを生成してからQState::addTransition( QAbstractTransition* transition ) でシグナル遷移を設定しています。 QSignalTransitionのインスタンスが返されますが、ここでは戻り値を無視しています。

QState* afterState2 = new QState( &machine );
afterState2->assignProperty( &button, "text", "After 2" );

QSignalTransition* transition = beforeState->addTransition( &button, SIGNAL(clicked()), afterState );
transition = beforeState->addTransition( &button, SIGNAL(pressed()), afterState2 );

もし上記のようなコードを書いたとすると、pressed()シグナルで遷移が起き、clicked()シグナルの方では遷移は起きません。同様に、共にclicked()シグナルにすると、最初に呼出した方で遷移が起きます。つまり、遷移条件に無矛盾性を与えるのは、コードの書き手に委ねられます。

次に、上記の18行目を他の設定パターンで書き換えてみましょう。

QSignalTransition* signalTransition = new QSignalTransition( &button, SIGNAL(clicked()), beforeState );
signalTransition->setTargetState( afterState );

この設定パターンも前回示しました。このようにQSignalTransition のインスタンスを生成すると、遷移前の状態beforeState を親オブジェクトとするQSignalTransition オブジェクトができます。遷移後の状態をQSignalTransition::setTargetState()で設定すれば、元の18行目と同じ結果となります。

QSignalTransition* signalTransition = new QSignalTransition( beforeState );
signalTransition->setSenderObject( &button );
signalTransition->setSignal( SIGNAL(clicked()) );
signalTransition->setTargetState( afterState );

別の設定パターンです。遷移前の状態のみを指定してQSignalTransitionのインスタンスを生成した場合には、シグナルを発生させるオブジェクト、シグナル、遷移後の状態を設定します。QSignalTransitionのインスタンス生成で指定するbeforeState が親オブジェクトになります。

QSignalTransition* signalTransition = new QSignalTransition;
signalTransition->setSenderObject( &button );
signalTransition->setSignal( SIGNAL(clicked()) );
signalTransition->setTargetState( afterState );
beforeState->addTransition( signalTransition );

最後の設定パターンです。パラメータを指定せずにQSignalTransitionのインスタンスを生成すると、親オブジェクトは 0、つまりトップレベルのオブジェクトになります。2~4行は、前の設定パターンと同一です。最後の行では、QState::addTransition( QAbstractTransition* transition )で、遷移前の状態にシグナル遷移を設定しています。この呼出しで、QSignalTransition のインスタンスの親オブジェクトがbeforeStateオブジェクトに設定されます。このaddTransition()は状態に遷移を追加するメソッドですが、もし同一の遷移に対して繰り返し呼し出しを行っても、二重に登録されることはありません。

無条件遷移

シグナル遷移の設定パターンではありませんが、18行目を以下のように書き換えると無条件で遷移が起きます。

beforeState->addTransition( afterState );

初期状態にしているbeforeStateの状態になると、すぐにafterState状態になります。無条件遷移は経過的な状態が必要な場合に用いられます。

無条件遷移でも内部的に以下のクラスの遷移オブジェクトが生成されます。

class UnconditionalTransition : public QAbstractTransition
{
public:
    UnconditionalTransition(QAbstractState *target)
        : QAbstractTransition()
    { setTargetState(target); }
protected:
    void onTransition(QEvent *) {}
    bool eventTest(QEvent *) { return true; }
};

そして、QState::addTransition( QAbstractTransition* transition )が呼び出されています。ステートマシンフレームワークでは、内部的に遷移を励起させるためのイベントを発生させています。上記の無条件遷移クラスは、eventTest() で常にtrueを返すようにしているので、そのイベントが来ると遷移が起こります。

ここ迄のいろいろな設定パターンからわかるように、どのような場合でも遷移オブジェクトが使われます。

シグナル遷移の独自拡張

無条件遷移でUnconditionalTransitionクラスと同じようにして、独自の判定を行うシグナル遷移を作成する方法を説明します。

QPushButtonがチェックされ、valueプロパティが100以上の整数値の場合に遷移するようなシグナル遷移を作成してみます。動作は図3のようになります。

図3 独自判定をするシグナル遷移の例
図3 独自判定をするシグナル遷移の例
 1: #include <QApplication>
 2: #include <QPushButton>
 3: #include <QStateMachine>
 4: #include <QSignalTransition>
 5: 
 6: class ClickedTransition : public QSignalTransition
 7: { 

独自のシグナル遷移を作成するには、まず、QSignalTransitionを継承します。

 8: public:
 9:     explicit ClickedTransition( QState* sourceState = 0 )
10:         : QSignalTransition( 0, SIGNAL(clicked()), sourceState ) {}
11: 
12:     explicit ClickedTransition( QPushButton* button, QState* sourceState = 0 )
13:         : QSignalTransition( button, SIGNAL(clicked()), sourceState ) {}
14: 

clicked(bool) シグナルに限定して、継承元のQSignalTransitionを初期化するコンストラクタを用意します。

15: protected:
16:     bool eventTest( QEvent* event ) {
17:         if ( !QSignalTransition::eventTest( event ) )
18:             return false;
19:         QStateMachine::SignalEvent *signalEvent = static_cast<:signalevent>( event );
20:         return signalEvent->arguments().at(0).toBool() 
21:             && signalEvent->sender()->property( "value" ).toInt() > 99;
22:     }

メソッドeventTest()には、シグナル情報をラップしたQStateMachine::SignalEventが渡ってきます。シグナル情報は表2のようになっています。

表2 シグナル遷移におけるシグナル情報
シグナル情報説明
QList<QVariant> arguments() constシグナルの引数。arguments().at(0)で最初の引数、at(1)で2番目の引数が取り出せる。
QObject* sender () constシグナルを送信したオブジェクト。
int signalIndex () constシグナルのインデックス。

シグナルclicked(bool)を用いているので、次のようにするとシグナルの1番目のbool型の引数値を取出せます。

signalEvent->arguments().at(0).toBool()

さらに詳しくシグナルの送信元オブジェクトを参照するには、以下のようにします。

QPushButton* pushButton = qobject_cast<QPushButton*>( signalEvent->sender() );

さらにメタオブジェクト情報を辿れば、どのようなシグナルが渡されて来たかもわかります。

QMetaMethod metaMethod = pushButton->metaObject()->method( signalEvent->signalIndex() );

たとえばmetaMethod.signature()で、シグナルのシグニチャが"clicked(bool)"とわかります。

23: };
24: 
25: int main( int argc, char** argv )
26: {
27:     QApplication app( argc, argv );
28: 
29:     QPushButton button;
30:     button.setCheckable( true );
31:     button.setChecked( true );

QPushButtonのチェックを有効にして、チェック状態にしておきます。

32:     button.setProperty( "value", 100 );

ダイナミックプロパティvalueに整数値100を設定して、シグナル遷移後にボタンのテキストが変わるようにします。この値を100未満にすると、2度目のクリックでボタンのテキストが変わらなくなります。

33: 
34:     QStateMachine machine;
35:     QState* beforeState = new QState( &machine );
36:     beforeState->assignProperty( &button, "text", "Before" );
37:     QState* afterState = new QState( &machine );
38:     afterState->assignProperty( &button, "text", "After" );
39: 
40:     ClickedTransition* clickedTransition = new ClickedTransition( &button, beforeState );
41:     clickedTransition->setTargetState( afterState );
42:     

状態を2つ用意して、独自のシグナル遷移ClickedTransitionを使っています。

43:     button.show();
44: 
45:     machine.setInitialState( beforeState );
46:     machine.start();
47: 
48:     return app.exec();
49: }

再実装したQSignalTransition::eventTest()でいろいろな判定をして、遷移条件を独自に作ることができます。しかし、できるだけ独自のものは使わないようにした方が見通しがよくなることに注意してください。

おわりに

次回以降では、マウスとキーイベント遷移、イベント遷移の独自拡張、履歴状態、並列状態、遷移とアニメーション効果について説明して行く予定です。

おすすめ記事

記事・ニュース一覧