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

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

今回は、ステートマシーンフレームワークの最後です。マウスやキーイベント遷移がどのようなものか、イベント遷移の独自拡張、そして履歴状態や並列状態、遷移とアニメーション効果について説明します。

マウスイベント遷移

前回取り上げたシグナル遷移はステートマシンフレームワークのリファレンスで、コード例を示して説明していますが、マウスイベント遷移とキーイベント遷移についてはシグナル遷移のような説明がありません。そこで、簡単なコードで、まずマウス遷移を説明しましょう。

マウスイベント遷移の説明のためにEventWidgetウィジェットを作りました。このウィジェットは、テキストを表示し、有効化されていれば(enabledプロパティがtrue)テキストを緑色で、無効化されていれば(enabled プロパティがfalse)赤色で描画するようにします。そして、ボタンが押されている間はPressedと表示されるように、ステートマシンフレームワークで制御をするようにしています。動作は図1のようになります。

図1 マウスイベントによる表示の制御
図1 マウスイベントによる表示の制御
リスト1 eventwidget.h
 1: #ifndef EVENTWIDGET_H
 2: #define EVENTWIDGET_H
 3: 
 4: #include <QWidget>
 5: 
 6: class EventWidgetPrivate;
 7: 
 8: class EventWidget : public QWidget
 9: {
10:     Q_OBJECT
11:     Q_DECLARE_PRIVATE( EventWidget )
12:     Q_PROPERTY( QString text READ text WRITE setText )
13: 
14: public:
15:     explicit EventWidget( QWidget* parent = 0 );
16:     ~EventWidget();
17:     QString text() const;
18:     QSize sizeHint() const;
19: 
20: public slots:
21:     void setText( const QString& text );
22: 
23: protected:
24:     void paintEvent( QPaintEvent* event );
25: 
26: private:
27:     EventWidgetPrivate* d_ptr;
28: };
29: 
30: #endif

EventWidgetのヘッダです。今までも見て来たように、ステートマシーンフレームワークでは、ある状態になった時にプロパティを変更する仕組みを持っています。そこで、textプロパティでテキストをアクセスできるようにしておき、状態が変わった時にQState::assignProperty()でテキストを変えられるようにしておきます。

リスト2 eventwidget.cpp
 1: #include <QStateMachine>
 2: #include <QPainter>
 3: #include <QPaintEvent>
 4: #include <QEventTransition>
 5: 
 6: #include "eventwidget.h"
 7: 
 8: class EventWidgetPrivate
 9: {
10: public:
11:     EventWidgetPrivate() {}
12: 
13:     QString text;
14: };
15: 
16: EventWidget::EventWidget( QWidget* parent )
17:     : QWidget( parent ), d_ptr( new EventWidgetPrivate )
18: {
19:     QPalette palette = this->palette();
20:     palette.setColor( QPalette::Window, Qt::gray );
21:     palette.setColor( QPalette::Normal, QPalette::WindowText, Qt::green );
22:     palette.setColor( QPalette::Disabled, QPalette::WindowText, Qt::red );
23:     setPalette( palette );
24:     setAutoFillBackground( true );
25: 

背景色を灰色にするには、パレットのQPalette::Windowロールに色を設定し、setAutoFillBackground( true ) を呼び出して、背景色で塗り潰されるようにします。

有効化と無効化の各状態でのテキストの色は、パレットのQPalette::NormalとQPalette::Disabledの各カラーグループごとに、QPalette::WindowTextロールで指定します。こうしておくとテキスト描画時に、QPainter::drawText()が用いる色が有効化/無効化に合わせてパレットから取り出されて使われます。

26:     setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding );
27: 
28:     QStateMachine* stateMachine = new QStateMachine( this );
29: 
30:     QState* idleState = new QState;
31:     idleState->setObjectName( "idle" );
32:     idleState->assignProperty( this, "text", "Idle" );
33: 
34:     QState* pressedState = new QState;
35:     pressedState->setObjectName( "pressed" );
36:     pressedState->assignProperty( this, "text", "Pressed" );
37: 

通常状態とボタンが押されている状態を用意しています。

38:     QEventTransition* pressTransition = new QEventTransition( this, QEvent::MouseButtonPress, idleState );
39:     pressTransition->setTargetState( pressedState );
40: 
41:     QEventTransition* dblClickTransition = new QEventTransition( this, QEvent::MouseButtonDblClick, idleState );
42:     dblClickTransition->setTargetState( pressedState );
43: 
44:     QEventTransition* releaseTransition = new QEventTransition( this, QEvent::MouseButtonRelease, pressedState );
45:     releaseTransition->setTargetState( idleState );
46: 

本連載の第4回「ステートマシーンフレームワーク」の図1「ステートマシンフレームワークのクラス」で示したように、QEventTransitionクラスは、マウスとキーイベントのベースクラスになっています。その基本機能は、イベントタイプに対して反応する遷移となっていて、コンストラクタで指定した状態の時に、指定したタイプのイベントが発生すると遷移が起きます。たとえばpressTransitionは、このウィジェットidleState状態にある時にイベントタイプがQEvent::MouseButtonPressのイベントが発生すると、pressedState状態への遷移が起きます。

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

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

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

QEventTransitionの代わりにそのサブクラスのQMouseEventTransitionを使うと上記のように書けて、どのボタンで遷移するかも指定できます。この場合には、左ボタンでのみ遷移が起きるようになります。ただし、X11では意図した通りに動きますが、Mac OS Xでは、左ボタンを押したまま右ボタンをクリックしてから左ボタンを離しても左ボタンのリリースイベントが出ないためidleStateに戻らず、意図した動きになりません。

47:     stateMachine->addState( idleState );
48:     stateMachine->addState( pressedState );
49: 
50:     stateMachine->setInitialState( idleState );
51: 
52:     stateMachine->start();
53: }
54: 

ステートマシンの動かし方は今まで通りです。

68: {
69:     Q_D( EventWidget );
70: 
71:     if ( d->text == text )
72:         return;
73: 
74:     d->text = text;
75:     update();
76:     updateGeometry();
77: }
78: 

textプロパティのゲッターとセッターで、テキストが変更されたならば、update()で再描画イベントがスケジュールされるようにし、updateGeometry()でレイアウトマネージャにレイアウトの変更を知らせて、テキストの大きさの変更に追従させます。

79: QSize EventWidget::sizeHint() const
80: {
81:     Q_D( const EventWidget );
82: 
83:     return fontMetrics().boundingRect( d->text ).size();
84: }
85: 

テキストが表示される最小限の大きさをsizeHint()で返すようにして、レイアウトマネージャがちょうどよい大きさでこのウィジェットを配置するようにします。

86: void EventWidget::paintEvent( QPaintEvent* event )
87: {
88:     Q_D( EventWidget );
89:     Q_UNUSED( event );
90: 
91:     QPainter painter( this );
92:     painter.drawText( rect(), Qt::AlignCenter, d->text );
93: }

前述したようにパレットで背景色と状態に合わせた色を設定しているので、drawText()でテキストを描画するだけで、ウィジェットの背景が灰色になり、テキストにも状態に合った色が付きます。

リスト3 main.cpp
 1: #include <QApplication>
 2: #include <QLayout>
 3: 
 4: #include "eventwidget.h"
 5: 
 6: class Harness : public QWidget
 7: {
 8:     Q_OBJECT
 9: 
10: public:
11:     Harness();
12: };
13: 
14: Harness::Harness()
15:     : QWidget( 0 )
16: {
17:     EventWidget* firstWidget = new EventWidget;
18:     EventWidget* secondWidget = new EventWidget;
19:     secondWidget->setEnabled( false );
20: 
21:     QVBoxLayout* topLayout = new QVBoxLayout;
22:     topLayout->addWidget( firstWidget );
23:     topLayout->addWidget( secondWidget );
24: 
25:     setLayout( topLayout );
26: }
27: 
28: int main( int argc, char** argv )
29: {
30:     QApplication app( argc, argv );
31: 
32:     Harness harness;
33:     harness.show();
34: 
35:     return app.exec();
36: }
37: 
38: #include "main.moc"

有効化と無効化の両状態のEventWidgetを表示して動作を確認するコードです。図2のように無効化されていてもマウスイベント遷移が起きます。

図2 無効状態でもマウスイベント遷移をしてしまう
図2 無効状態でもマウスイベント遷移をしてしまう

これはウィジェットのイベントへの振舞いを考えると、直感に反した動作でしょう。QWidgetは無効化されていれば、QWidget::event()でQEvent::MouseButtonPressやQEvent::MouseButtonReleaseに対して対応するイベントハンドラーを呼び出しません。QWidgetのリファレンスにもそのように書かれています。一方、以下のようにQMouseEventTransitionは、QObjectを受け付けるようになっています。

QMouseEventTransition( QObject* object, QEvent::Type type, Qt::MouseButtonbutton, QState* sourceState = 0 )

QObjectである必要はであるでしょう。しかし、マウスイベントはウィジェットと密接に関連しているので、enabledプロパティに従うのが直感にも合った動きです。対象がウィジェットならば、QMouseEventTransitionにenabledプロパティに従ってイベントを扱うようなオプションがあってもよさそうに思えます。

イベント遷移の独自拡張

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: }

遷移とアニメーション効果

ステートマシンフレームワークAPIはアニメーションAPIに関連付けられていて、状態に設定されたプロパティの自動的なアニメーションをします。使い方はとても簡単で、遷移にプロパティアニメーションを設定するだけです。以降のサンプルコード(リスト8)を実行すると、次の動画のようになります。

動画1 遷移へのアニメーションづけ
リスト8
 1: #include <QApplication>
 2: #include <QStateMachine>
 3: #include <QSignalTransition>
 4: #include <QPropertyAnimation>
 5: #include <QPushButton>
 6: #include <QLayout>
 7: 
 8: int main( int argc, char** argv )
 9: {
10:     QApplication app( argc, argv );
11: 
12:     QWidget* drawWidget = new QWidget;
13:     drawWidget->setFixedSize( 400, 200 );
14: 
15:     QWidget* leftWidget = new QWidget( drawWidget );
16:     QPalette palette = leftWidget->palette();
17:     palette.setColor( QPalette::Window, Qt::green );
18:     leftWidget->setPalette( palette );
19:     leftWidget->setAutoFillBackground( true );
20:     leftWidget->setGeometry( 90, 90, 20, 20 );
21: 
22:     QWidget* rightWidget = new QWidget( drawWidget );
23:     palette = leftWidget->palette();
24:     palette.setColor( QPalette::Window, Qt::red );
25:     rightWidget->setPalette( palette );
26:     rightWidget->setAutoFillBackground( true );
27:     rightWidget->setGeometry( 290, 90, 20, 20 );
28: 

drawWidgetの上に、緑色のleftWidgetと赤色のrightWidgetを置いています。置いた両ウィジェットは、大きさを変えたいのでレイアウトマネージャには管理させません。

29:     QPushButton* leftAnimateButton = new QPushButton( "Animate" );
30:     QPushButton* rightAnimateButton = new QPushButton( "Animate" );
31: 
32:     QHBoxLayout* buttonLayout = new QHBoxLayout;
33:     buttonLayout->addStretch();
34:     buttonLayout->addWidget( leftAnimateButton );
35:     buttonLayout->addStretch();
36:     buttonLayout->addWidget( rightAnimateButton );
37:     buttonLayout->addStretch();
38: 
39:     QVBoxLayout* topLayout = new QVBoxLayout;
40:     topLayout->addWidget( drawWidget );
41:     topLayout->addLayout( buttonLayout );
42: 

操作用のボタンを作り、全体をレイアウトしています。

43:     QWidget topWidget;
44:     topWidget.setLayout( topLayout );
45: 
46:     QStateMachine* stateMachine = new QStateMachine( &topWidget );
47:     
48:     QState* parallelState = new QState(QState::ParallelStates, stateMachine );
49:     QState* leftStateGroup = new QState( parallelState );
50:     QState* rightStateGroup = new QState( parallelState );
51: 

緑と赤の両ウィジェットを並行して操作するために並列状態を使っています。

52:     QState* leftWidgetStateSmall = new QState( leftStateGroup );
53:     QState* leftWidgetStateLarge = new QState( leftStateGroup );
54:     leftWidgetStateSmall->assignProperty( leftWidget, "geometry", QRectF( 90, 90, 20, 20 ) );
55:     leftWidgetStateLarge->assignProperty( leftWidget, "geometry", QRectF( 10, 10, 180, 180 ) );
56:     leftStateGroup->setInitialState( leftWidgetStateSmall );
57: 

小さいときの状態と大きいときの状態を用意し、geometry プロパティでそれぞれの状態での大きさを設定しています。

58:     QSignalTransition* leftMaximizeTransition = leftWidgetStateSmall->addTransition( leftAnimateButton, SIGNAL(clicked()), leftWidgetStateLarge );
59:     QPropertyAnimation* animation = new QPropertyAnimation( leftWidget, "geometry" );
60:     animation->setDuration( 4 * 1000 );
61:     leftMaximizeTransition->addAnimation( animation );
62:     QSignalTransition* leftMinimizeTransition = leftWidgetStateLarge->addTransition( leftAnimateButton, SIGNAL(clicked()), leftWidgetStateSmall );
63:     animation = new QPropertyAnimation( leftWidget, "geometry" );
64:     animation->setDuration( 4 * 1000 );
65:     leftMinimizeTransition->addAnimation( animation );
66: 

ボタンをクリックすると緑のウィジェットが大きくなったり小さくなったりするようなアニメーションを作成し、QSignalTransition::addAnimation()で設定するだけで、遷移が起きるとアニメーションが行われます。

67:     QState* rightWidgetStateSmall = new QState( rightStateGroup );
68:     QState* rightWidgetStateLarge = new QState( rightStateGroup );
69:     rightWidgetStateSmall->assignProperty( rightWidget, "geometry", QRectF( 90 + 200, 90, 20, 20 ) );
70:     rightWidgetStateLarge->assignProperty( rightWidget, "geometry", QRectF( 10 + 200, 10, 180, 180 ) );
71:     rightStateGroup->setInitialState( rightWidgetStateSmall );
72: 
73:     QSignalTransition* rightMaximizeTransition = rightWidgetStateSmall->addTransition( rightAnimateButton, SIGNAL(clicked()), rightWidgetStateLarge );
74:     animation = new QPropertyAnimation( rightWidget, "geometry" );
75:     animation->setDuration( 4 * 1000 );
76: 
77:     rightMaximizeTransition->addAnimation( animation );
78:     QSignalTransition* rightMinimizeTransition = rightWidgetStateLarge->addTransition( rightAnimateButton, SIGNAL(clicked()), rightWidgetStateSmall );
79:     animation = new QPropertyAnimation( rightWidget, "geometry" );
80:     animation->setDuration( 4 * 1000 );
81:     rightMinimizeTransition->addAnimation( animation );
82: 

右の赤いウィジェットについて、緑のウィジェットと同様にアニメーションをするようにしています。

83:     stateMachine->setInitialState( parallelState );
84:     stateMachine->start();
85: 
86:     topWidget.show();
87: 
88:     return app.exec();
89: }

おわりに

次回は、この連載の最終回でイフェクトについて説明します。そして、しばらく期間を置いて、Qt 4.7リリース時にその概要と、この連載で説明した機能をベースにした宣言的ユーザインターフェースQt Quickの連載を予定しています。

おすすめ記事

記事・ニュース一覧