Qt動畫框架

pamxy發表於2013-05-04

轉自:http://blog.csdn.net/fuyajun01/article/details/6106201

狀態機框架提供了建立和執行狀態圖的一些類.這些概念和表示都是基於Harel狀態圖中的一些概念和記法.它也是UML狀態圖表的基礎.狀態機執行的語義是基於狀態圖XML(SCXML).

   狀態圖提供了一種圖形化的方式來對一個系統建模,從而反映它怎麼響應外部觸發.這是通過定義系統可能進入的一些狀態以及系統怎麼從一個狀態轉換到另一個狀態(不同狀態之間轉變)來實現的.事件驅動系統的一個關鍵的特徵(例如Qt應用程式)就是行為通常不僅取決於上次或當前事件,還取決於在它之前的一些事件.用狀態圖,這個資訊非常容易表達.

   狀態機框架提供了一套API以及一種執行模型,可有效地將狀態圖的元素和語義嵌入到Qt應用程式當中.該框架與Qt的元物件系統結合緊密:例如,不同狀態之間的轉變可由訊號觸發且狀態可配置用於設定QObject的屬性和方法.Qt的事件系統用於驅動狀態機.

   狀態機框架中的狀態圖是分層的.狀態可巢狀在另一個狀態內.狀態機的當前配置包含一些當前活躍的狀態.狀態機中的一個有效的配置中的所有狀態都有一個共同的祖先.

狀態機框架中的類

qt提供了這些類來建立事件驅動的狀態機.

QAbstractState

The base class of states of a QStateMachine

QAbstractTransition

The base class of transitions between QAbstractState objects

QEventTransition

QObject-specific transition for Qt events

QFinalState

Final state

QHistoryState

Means of returning to a previously active substate

QKeyEventTransition

Transition for key events

QMouseEventTransition

Transition for mouse events

QSignalTransition

Transition based on a Qt signal

QState

General-purpose state for QStateMachine

QStateMachine

Hierarchical finite state machine

QStateMachine::SignalEvent

Represents a Qt signal event

QStateMachine::WrappedEvent

Holds a clone of an event associated with a QObject

一個簡單的狀態機

為了演示狀態機API的核心功能,讓我們來看一個小例子:一個狀態機有三個狀態s1,s2和s3.狀態機由一個按鈕來控制;當點選按鈕時,狀態機轉換到另一個狀態.剛開始時,狀態機處於狀態s1.該狀態機的狀態圖如下所示:

下面程式碼段顯示了建立一個這樣的狀態機所需的程式碼.首先,我們建立一個狀態機和一些狀態:

QStateMachine machine;

    QState *s1 = new QState();

    QState *s2 = new QState();

    QState *s3 = new QState();

然後,我們使用QState::addTransition()函式建立轉換:

    s1->addTransition(button, SIGNAL(clicked()), s2);

    s2->addTransition(button, SIGNAL(clicked()), s3);

    s3->addTransition(button, SIGNAL(clicked()), s1);

接下來,我們將這些狀態加入狀態機中並設定它的初始狀態:

    machine.addState(s1);

    machine.addState(s2);

    machine.addState(s3);

    machine.setInitialState(s1);

最後,我們啟動狀態機:

狀態是非同步執行的,例如,它成為你的應用程式事件迴圈的一部分.

在狀態入口和出口做有意義的工作

上面的狀態機僅僅從一個狀態轉換到另一個狀態,並沒有執行任何操作.QState::assignProperty()函式可用於當進入某個狀態時設定某個QObject的一個屬性.在下面的程式碼段中,為每個狀態指定了應當賦給QLabel的text屬性的值.

    s1->assignProperty(label, "text", "In state s1");

    s2->assignProperty(label, "text", "In state s2");

    s3->assignProperty(label, "text", "In state s3");

當進入了這些狀態中的任何一個,標籤的值就會相應地改變. 

當進入某個狀態時,就會發出QState::entered()訊號.當離開這個狀態時,就會發出QState::exited()訊號.在下面的程式碼段中,按鈕的showMaximize()槽在進入狀態s3時被呼叫.當退出狀態s3時呼叫showMinimized():

    QObject::connect(s3, SIGNAL(entered()), button, SLOT(showMaximized()));

    QObject::connect(s3, SIGNAL(exited()), button, SLOT(showMinimized()));

自定義的狀態可以重新實現方法QAbstractState::onEntry()QAbstractState::onExit().

完成的狀態機

前面部分定義的狀態機從不完成.為了使一個狀態機能夠完成,它需要擁有一個頂層的最終狀態(QFinalState物件).當狀態機進入一個頂層最終狀態時,該狀態機將會釋放QStateMachine::finished()訊號並停止.

在圖中引入一個最終狀態,所有你需要做的就是建立一個QFinalState物件且使用它作為一個或多個轉換的目標.

通過對狀態進行分組來共享轉換

假設我們想讓使用者能夠通過點選Quit撳鈕在任何時刻能夠退出應用程式.為了完成這個目標,我們需要建立一個最終狀態並將其作為與Quit按鈕的clicked()訊號相關聯的轉換的目標.我們可以從狀態s1,s2,s3中新增一個轉換;但是,這看起來像是多餘的,並且,我們不得不記住從每個將來新加入的狀態新增一個這樣的轉換.

我們可以通過將狀態s1,s2,s3分組取得相同的行為(即點選Quit按鈕將退出狀態機,無論該狀態機處於哪個狀態).這是通過建立一個新的頂層狀態並使三個原先的狀態成為新狀態的孩子.如下圖顯示了新狀態機.

 

三個原先的狀態已經重新命名為s11,s12和s13以反映它們現在已經是新的頂層狀態s1的孩子.孩子狀態隱含地繼承它們的父狀態的轉換.這意味著現在增加一個從狀態s1到最終狀態s2的轉換已經足夠了.新加入s1的狀態也將自動繼承這個轉換.

將狀態分組的所有工作就是當創始狀態時,指定合適的父狀態.你也需要指定哪個子狀態是初始狀態(例如,哪個子狀態將是進入父狀態時應該處於的狀態).

    QState *s1 = new QState();

    QState *s11 = new QState(s1);

    QState *s12 = new QState(s1);

    QState *s13 = new QState(s1);

    s1->setInitialState(s11);

    machine.addState(s1);

    QFinalState*s2 = new QFinalState();

    s1->addTransition(quitButton, SIGNAL(clicked()), s2);

    machine.addState(s2);

 

    QObject::connect(&machine, SIGNAL(finished()), QApplication::instance(), SLOT(quit()));

在本例子中,我們想讓狀態機完成後,應用程式退出,因此狀態機的finished()訊號連線到應用程式的quit()槽.

一個子狀態可以覆蓋一個繼承過來的轉換.例如,如下程式碼新增了一個轉換,它有效地造成了當狀態機處於狀態s12時,Quit按鈕將被忽略.

   s12->addTransition(quitButton, SIGNAL(clicked()), s12);

一個轉換可以將任何狀態作為它的目標,例如,目標狀態不一定要與源狀態處於相同的層次. 

使用歷史狀態來儲存和恢復當前狀態

假設我們要增加一個“中斷”機制到前面提到的例子當中;使用者應該能夠點選一個按鈕使狀態機執行一些不相關的任務,任務完成後狀態機應該能夠恢復到之前執行的任何任務。(例如,返回到舊狀態,在此例子中s11,s12,s13中的一個)。

這樣的行為很容易地使用歷史狀態建模。一個歷史狀態(QHistoryState物件)是一個偽狀態,它代表父狀態最後退出時所處的孩子狀態。

一個歷史狀態建立為某個狀態的孩子,用於為其記錄當前的孩子狀態;當狀態機在執行時檢測到有這樣的一個狀態存在時,它在父狀態退出時自動地記錄當前的孩子狀態。到該歷史狀態的一個轉變實際上是到狀態機之前儲存的子狀態的轉變。狀態機自動地“轉發”到真正孩子狀態的轉變。

下圖顯示了加入了中斷機制後的狀態機。

 

下面的程式碼顯示了怎麼去實現這種機制;在本例中,我們在進入s3時簡單地顯示一個資訊框,然後通過歷史狀態立即返回到s1之前的孩子狀態中。

   QHistoryState *s1h = new QHistoryState(s1);

 

    QState *s3 = new QState();

    s3->assignProperty(label, "text", "In s3");

    QMessageBox *mbox = new QMessageBox(mainWindow);

    mbox->addButton(QMessageBox::Ok);

    mbox->setText("Interrupted!");

    mbox->setIcon(QMessageBox::Information);

    QObject::connect(s3, SIGNAL(entered()), mbox, SLOT(exec()));

    s3->addTransition(s1h);

    machine.addState(s3);

 

    s1->addTransition(interruptButton, SIGNAL(clicked()), s3);

使用並行狀態以避免狀態的組合爆發

假設你想要在一個狀態機中建立一些相互排斥的屬性。比如說,我們感興趣的屬性是Clean VS Dirty和Moving VS Not moving。需要採用四個互斥的狀態和八個轉變才能描述該狀態機,並能在各個可能的組合中自由的移動。

 

如果我們增加第三個屬性(比如,Red VS Blue),狀態的總數將會翻倍,到8個,且如果我們新增第四個屬性(比如,Enclosed VS Convertible),狀態的總數將再次翻倍到16個。

使用並行狀態,狀態的總數和轉變數會隨著屬性的不斷增加線性地增長,而不是指數地增長。而且,從並行狀態中新增或移除狀態不會影響它們的兄弟狀態。

為了建立一個並行狀態組,傳遞QState::ParallelStates到Qstate建構函式中。

    QState *s1 = new QState(QState::ParallelStates);

    // s11 and s12 will be entered in parallel

    QState *s11 = new QState(s1);

    QState *s12 = new QState(s1);

當一個並行狀態組進入時,所有的子狀態將會同時進入。每個子狀態裡的轉變正常執行。但是,任何一個子狀態可以執行存在於父狀態中的一個轉變。當這發生時,父狀態以及所有的子狀態將退出。

狀態機框架的並行機制遵循如下一種交錯的語義。所有並行操作將以單步,原子地進行,沒有事件可以中斷並行操作。但是,事件仍然會被順序地處理,因為狀態機本身是單執行緒的。舉個例子:考慮這樣的一個情形,有兩個轉變從相同的狀態組中退出,並且它們的(退出)條件同時變為真。在這種情況下,被處理的事件中的後一個將不會產生任何效果,因為第一個事件已經促使狀態機從並行狀態中退出了。

檢測某個組合狀態已經完成

一個孩子狀態可為最終狀態(一個QFinalState物件)。當進入最終狀態時,父狀態發出QState::finished()訊號。下圖顯示了一個組合狀態s1,在進入最終狀態之前執行一些處理:

當進入s1的最終狀態時,s1會自動地發出finished()。我們使用一個轉變來促使這個事件觸發一個狀態改變:

s1->addTransition(s1, SIGNAL(finished()), s2);

在組合狀態中使用最終狀態是有用的,當你想隱藏一個組合狀態的內部細節時;例如,位於該組合狀態之外的世界只需能進入到該狀態並在該狀態完成了其工作時獲得通知。在構建複雜的狀態機(深度巢狀)時,這是一種非常強大的抽象和封裝機制。(在以上例子中,當然你可以建立一個直接從s1的done狀態開始的一個轉變,而不依賴s1的finished()訊號,但是,會造成s1的實現細節暴露並依賴它。)。

對於並行狀態組,當所有孩子狀態進入了最終狀態時會發出QState::finished()訊號。

無目標轉變

一個轉變不需要一個目標狀態。無目標的轉變可與其他轉變一樣的方式被觸發;不同之處在於當無目標轉變被觸發時,它不會造成任何狀態的改變。這可以允許你在當狀態機處於某個特定狀態時,對訊號或事件作出響應而不用離開那個狀態。例如:

QStateMachine machine;

 QState *s1 = new QState(&machine);

 

 QPushButton button;

 QSignalTransition *trans = new QSignalTransition(&button, SIGNAL(clicked()));

 s1->addTransition(trans);

 

 QMessageBox msgBox;

 msgBox.setText("The button was clicked; carry on.");

 QObject::connect(trans, SIGNAL(triggered()), &msgBox, SLOT(exec()));

 

 machine.setInitialState(s1);

該資訊框在每次按鈕被點選時顯示,但是狀態機仍然處於當前狀態(s1)。然而,如果目標狀態顯式地設定為s1,s1會退出並且每次點選的時候進入(例如,會發出QAbstractState::entered()QAbstractState::exited()訊號)。

事件,轉變和哨衛

一個QStateMachine執行在自己的事件迴圈裡,對於訊號轉變(QSignalTransition物件),當它截獲了相應地訊號,QStateMachine會自動地傳送一個QStateMachine::SignalEvent到自身。類似地,對於QObject事件轉變(QEventTransition物件),會傳送一個QStateMachine::WrappedEvent

你可以使用QStateMachine::postEvent()將自己的事件傳送到狀態機。

當傳送一個自定義的事件到狀態機,你一般也擁有一個或更多個自定義的轉變,這些轉變可以由這種型別的事件觸發。為了建立一個這樣的轉變,你要建立一個QAbstractTransition子類並重新實現QAbstractTransition::eventTest()方法,在該方法中,你檢測某個事件是否與你的事件型別匹配(也可以採用其他的判斷規則,如事件物件的屬性)。下面我們定義了自已的事件型別,StringEvent,用於向狀態機中傳送字串:

struct StringEvent : public QEvent

 {

    StringEvent(const QString &val)

    : QEvent(QEvent::Type(QEvent::User+1)),

      value(val) {}

 

    QString value;

 };

接下來,我們定義一個轉變,僅當事件的字串與某個特定的字串(一個哨衛轉變)匹配時才觸發它。

class StringTransition : public QAbstractTransition

 {

 public:

    StringTransition(const QString &value)

        : m_value(value) {}

 

 protected:

    virtual bool eventTest(QEvent *e) const

    {

        if (e->type() != QEvent::Type(QEvent::User+1))// StringEvent

            return false;

        StringEvent *se = static_cast<StringEvent*>(e);

        return (m_value == se->value);

    }

 

    virtual void onTransition(QEvent *) {}

 

 private:

    QString m_value;

 };

eventTest()的過載中,我們首先檢測了事件型別是否是我們想要的型別。如果是的,我們將事件轉換為一個StringEvent並執行字串比較操作。

如下是一個使用了自定義事件和轉變的狀態圖:

 

該狀態圖的實現程式碼如下:

QStateMachine machine;

    QState *s1 = new QState();

    QState *s2 = new QState();

    QFinalState *done = new QFinalState();

 

    StringTransition *t1 = new StringTransition("Hello");

    t1->setTargetState(s2);

    s1->addTransition(t1);

    StringTransition *t2 = new StringTransition("world");

    t2->setTargetState(done);

    s2->addTransition(t2);

 

    machine.addState(s1);

    machine.addState(s2);

    machine.addState(done);

    machine.setInitialState(s1);

一旦狀態機啟動,我們可以將事件傳送給它。

    machine.postEvent(new StringEvent("Hello"));

    machine.postEvent(new StringEvent("world"));

沒有被任何相關的轉變處理的事件將自動由狀態處理。這對於分組狀態和提供這樣的事件的一個預設處理是有用的;例如,如下狀態圖:

 

對於深度巢狀的狀態圖,你可以新增這樣的“回退(fallback)”轉變

使用恢復策略自動地恢復屬性

在一些狀態機中,在精力集中在對狀態中的屬性進行賦值是有用的,而不是當狀態不再活躍時恢復它們。如果你知道當狀態機進入某個狀態時,並且在該狀態下沒有顯式地給屬性一個值,屬性總是應該恢復到它的初始狀態,你可以設定全域性的策略為QStateMachine::RestoreProperties

QStateMachine machine;

 machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties);

當該策略設定了後,狀態機會自動地恢復所有的屬性。如果它進入了一個狀態,而某個給定的屬性沒有設定,它會首先尋找祖先的層次結構以檢視該屬性是否已定義。如果是的,該屬性會被恢復到最近祖先定義的值。如果不是,它會被恢復到初始值。

如下程式碼所示:

QStateMachine machine;

    machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties);

 

    QState *s1 = new QState();

    s1->assignProperty(object, "fooBar", 1.0);

    machine.addState(s1);

    machine.setInitialState(s1);

 

    QState *s2 = new QState();

    machine.addState(s2);

比如說,屬性fooBar在狀態機啟動時值為0.0。當機器處於狀態s1,屬性值會為1.0,因為該狀態顯示地設定了該屬性的值。當該機器處於狀態s2,沒有顯式地定義該屬性,因此它會被隱式地恢復為0.0

如果我們使用巢狀的狀態,父狀態為該屬性定義了一個值,所有其後裔並沒有顯式地定義該屬性的值。

    QStateMachine machine;

    machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties);

 

    QState *s1 = new QState();

    s1->assignProperty(object, "fooBar", 1.0);

    machine.addState(s1);

    machine.setInitialState(s1);

 

    QState *s2 = new QState(s1);

    s2->assignProperty(object, "fooBar", 2.0);

    s1->setInitialState(s2);

 

    QState *s3 = new QState(s1);

這裡,s1擁有兩個孩子:s2s3。當進入s2時,屬性fooBar的值為2.0,因為該狀態顯式地定義了該值。當狀態機處於狀態s3時,該狀態沒有定義任何值,但是s1定義了屬性的值為1.0,因此,這就是將被賦給fooBar的值。

動畫屬性賦值

狀態機API與動畫API的連線使得當在狀態中設定動畫屬性時,自動地animating屬性。比如,我們有如下程式碼:

    QState *s1 = new QState();

    QState *s2 = new QState();

 

    s1->assignProperty(button, "geometry", QRectF(0, 0, 50, 50));

    s2->assignProperty(button, "geometry", QRectF(0, 0, 100, 100));

 

    s1->addTransition(button, SIGNAL(clicked()), s2);

這裡,我們定義了使用者介面的兩個狀態,在狀態s1中,button小些,在狀態s2中,button大些。如果我們點選按鈕,從狀態s1轉換到狀態s2,當給定的狀態進入時,該按鈕的幾何屬性可以立即設定。但是,如果我們想讓轉變更為流暢,需要構造一個QPropertyAnimation物件並將其新增到轉變物件中。

    QState *s1 = new QState();

    QState *s2 = new QState();

 

    s1->assignProperty(button, "geometry", QRectF(0, 0, 50, 50));

    s2->assignProperty(button, "geometry", QRectF(0, 0, 100, 100));

 

    QSignalTransition *transition = s1->addTransition(button, SIGNAL(clicked()), s2);

    transition->addAnimation(new QPropertyAnimation(button, "geometry"));

為屬性新增了一個動畫後,屬性的賦值不再當進入狀態時馬上起效。相反地,動畫在狀態進入時開始播放並平滑地使屬性賦值動起來。因為我們沒有設定執行的起始值和結束值,這些將隱式地設定。動畫的起始值將是動畫開始時的當前值。

如果狀態機的全域性恢復策略設定為QStateMachine::RestoreProperties,也可以為恢復屬性新增動畫。

檢測某個狀態下的所有屬性

當動畫用於賦值時,一個狀態不再定義當狀態機進入該狀態時的精確值。當動畫正在執行時,屬性可以擁有任何值,取決於動畫。

在一些情況下,當能檢測到某個屬性被一個狀態定義的實際值時是有用的。

比如,我們有如下程式碼:

    QMessageBox *messageBox = new QMessageBox(mainWindow);

    messageBox->addButton(QMessageBox::Ok);

    messageBox->setText("Button geometry has been set!");

    messageBox->setIcon(QMessageBox::Information);

 

    QState *s1 = new QState();

 

    QState *s2 = new QState();

    s2->assignProperty(button, "geometry", QRectF(0, 0, 50, 50));

    connect(s2, SIGNAL(entered()), messageBox, SLOT(exec()));

 

    s1->addTransition(button, SIGNAL(clicked()), s2);

button點選後,狀態機將轉換到狀態s2,它會設定按鈕的geometry屬性,然後彈出一個資訊框來提示使用者geometry已經改變。

在正常情況下,沒有使用動畫時,該操作會以預期地方式執行。但是,如果在狀態s1s2的轉變中為button的屬性geometry定義了一個動畫,該動畫將在進入s2時啟動,但是,在動畫結束執行之前,geometry屬性並不會到達它定義的值。在這種情況下,在buttongeometry屬性實際被設定之前,會彈出一個資訊框。

為了確保資訊框直到geometry達到它的最終值的時候才彈出,我們可以使用狀態的propertiesAssigned()訊號,當屬性被賦予最終的值時,就會發出propertiesAssigned()訊號。

QMessageBox *messageBox = new QMessageBox(mainWindow);

    messageBox->addButton(QMessageBox::Ok);

    messageBox->setText("Button geometry has been set!");

    messageBox->setIcon(QMessageBox::Information);

 

    QState *s1 = new QState();

 

    QState *s2 = new QState();

    s2->assignProperty(button, "geometry", QRectF(0, 0, 50, 50));

 

    QState *s3 = new QState();

    connect(s3, SIGNAL(entered()), messageBox, SLOT(exec()));

 

    s1->addTransition(button, SIGNAL(clicked()), s2);

    s2->addTransition(s2, SIGNAL(propertiesAssigned()), s3);

在該例子中,當button點選時,狀態機進入s2,當仍然處於狀態s2直到屬性geometry被設定為QRect(0, 0, 50, 50)。然後,它會轉變到s3。當進入s3時,資訊框會彈出。如果轉變到s2有一個geometry屬性的動畫,那麼狀態機將會處於s2中直到動畫完成。如果沒有這樣的動畫,它會設定該屬性並立即進入狀態s3。

不管什麼方式,當狀態機處於狀態s3,可以保證屬性geometry已經被賦予了定義的值。如果全域性恢復策略設定為QStateMachine::RestoreProperties,該狀態不會發出propertiesAssigned()訊號,直到這些也被執行了。

在動畫完成之前某個狀態退出了會發生什麼

如果一個狀態有屬性被賦值並且狀態的轉變過程中為該屬性設定了動畫,狀態有可能在動畫完成之前退出。這是可能發生的,特別當從狀態的轉變出來的一些轉變不依賴於propertiesAssigned()訊號。

狀態機API保證一個被狀態機賦值的屬性:

——擁有顯式賦給該屬性的一個值

——是當前正被漸進到一個顯式地賦予給該屬性的值。

當一個狀態在動畫完成之前退出時,狀態機的行為取決於轉變的目標狀態。如果目標狀態顯式地為屬性賦予了一個值,不會採用另外的動作。屬性將被賦予由目標狀態定義的值。

如果目標狀態沒有賦予屬性任何值,有兩種選擇:預設的,屬性會被賦予它離開時的狀態的值。但是,如果設定了全域性恢復策略,優先採取這種選擇,屬性會像平常一樣被恢復。

預設動畫

正如早前所描述的一樣,你可以新增動畫到轉變中以確保目標狀態的屬性賦值會被漸變。如果你想為某個給定的屬性使用一個特定的動畫而不管採用什麼轉變,你可以新增它作為狀態機的一個預設的動畫。

QState *s1 = new QState();

 QState *s2 = new QState();

 

 s2->assignProperty(object, "fooBar", 2.0);

 s1->addTransition(s2);

 

 QStateMachine machine;

 machine.setInitialState(s1);

 machine.addDefaultAnimation(new QPropertyAnimation(object, "fooBar"));

當狀態機處於狀態s2,狀態機會為屬性fooBar播放預設的動畫,因為該屬性由s2賦值。注意,顯式地設定轉變動畫比預設動畫優先順序大。


相關文章