Qt狀態機框架(狀態機就開始異步的運行了,也就是說,它成為了我們應用程序事件循環的一部分了)


     狀態機框架       

       Qt中的狀態機框架為我們提供了很多的API和類,使我們能更容易的在自己的應用程序中集成狀態動畫。這個框架是和Qt的元對象系統機密結合在一起的。比如,各個狀態之間的轉換是通過信號觸發的,狀態可被配置為用來設置QObject對象的屬性以及調用其方法。可以說Qt中的狀態機就是通過Qt自身的事件系統來驅動的。同時,狀態機中的狀態圖是分層次的。一些狀態可以被嵌套到另一些狀態里,當前的狀態機配置是由當前活動的所有狀態組成的。在一個狀態機的有效配置中的所有狀態具有共同的祖先。

       一個簡單的狀態機

       為了闡述Qt狀態機API的核心功能,我們先從一個小的例子說起:這個狀態機只有三個狀態,s1,s2,s3。我們通過一個按鈕的點擊來控制這個狀態機中狀態的轉換;當按鈕被點擊時,就會發生一次狀態轉換,從一個狀態到另一個狀態。初始情況下,狀態機處於s1狀態。這個狀態機所對應的狀態圖如下:

 

下面,我們先來看下怎么通過Qt代碼來實現這個簡單的狀態機。

第一步,我們創建一個狀態機和需要的狀態:


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);
最后,我們啟動狀態機即可:
machine.start();
這樣,我們的狀態機就開始異步的運行了,也就是說,它成為了我們應用程序事件循環的一部分了。這也對應了我們上面說的,Qt的狀態機是通過Qt自身的事件機制來驅動的。
        在狀態轉換時操作QObject對象

        上面所創建的狀態機,作為入門,我們僅僅進行了狀態機中各個狀態之間的見轉換,而未進行其他的工作。其實,我們可以使用QState::assignProperty() 函數當進入某個狀態時讓其去修改某個QObject對象的屬性。例如下面的代碼,當進入各個狀態時,改變QLabel的text屬性,即改變QLabel上顯示的文本內容:


s1->assignProperty(label, "text", "In state s1");
s2->assignProperty(label, "text", "In state s2");
s3->assignProperty(label, "text", "In state s3");
當進入任一狀態時,label的文本都會發生改變。
       除了操作QObject對象的屬性外,我們還能通過狀態的轉換來調用QObject對象的函數。這是通過使用狀態轉換時發出的信號完成的。其中,當進入某個狀態時會發出QState::enterd() 信號,當退出某個狀態時會發出QState::exited() 信號。例如下面的代碼,其實現的功能即為當進入s3狀態時,會調用按鈕的showMaximized() 函數,當退出s3狀態時會調用showMinimized() 函數:


QObject::connect(s3, SIGNAL(entered()), button, SLOT(showMaximized()));
QObject::connect(s3, SIGNAL(exited()), button, SLOT(showMinimized()));
           狀態機的結束
       我們在上面創建的狀態機是永遠不會結束的。為了使一個狀態機在某種條件下結束,我們需要創建一個頂層的final 狀態(QFinalState object) 。當狀態機進入一個頂層的final 狀態時,會發出finished() 信號,然后結束。所以,我們只需要為上面的狀態圖引入一個final 狀態,並把它設置為某個過渡的目標狀態即可。這樣,當狀態機在某種條件下轉換到該狀態時,整個狀態機結束。

       通過狀態分組來共享過渡

       假設我們想讓用戶隨時通過點擊退出按鈕來退出整個應用程序。為了實現這個需求,我們需要創建一個final狀態並使他成為和按鈕的clicked()信號相關聯的那個過渡的目標狀態。一種辦法是我們為狀態s1,s2,s3分別添加一個到final狀態的過渡,但這看上去有點多余,並且不利於將來的擴張。第二種方法就是將狀態s1,s2,s3分成一組。我們通過創建一個新的頂層狀態並使s1,s2,s3成為其孩子來完成。下面是這種方法所對應的狀態轉換圖:

 

       上面的三個狀態被重命名為s11,s12,s13以此來表明它們是s1的孩子。子狀態會隱式的繼承父狀態的過渡。這意味着我們目前可以只添加一個s1到final狀態s2的過渡即可,s11,s12,s13會繼承這個過渡,從而無論在什么狀態均可退出應用程序。並且,將來新添加到s1的新的子狀態也會自動繼承這個過渡。

        而所謂的分組,就是只需在創建狀態時為其指定一個合適的父狀態即可。當然,還需要為這組狀態指定一個初始狀態,即當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);
machine.setInitialState(s1);

QObject::connect(&machine, SIGNAL(finished()), QApplication::instance(), SLOT(quit()));
在這個例子中,我們想讓應用程序在狀態機結束時退出,所以我們將狀態機的finished() 信號連接到了應用程序的quit()槽函數上。
        注意,子狀態可以覆蓋從父狀態那里繼承的過渡。例如,下面的代碼通過為s12添加一個新的過渡,導致當狀態機處於s12狀態是,退出按鈕的點擊被忽略。還有,一個過渡可以選擇任何狀態作為其目標狀態,也就是說,一個過渡的目標狀態不需要和他的源狀態在狀態圖上處於同一個層次。

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

        如果我們想給上面的例子添加一個中斷機制,即用戶能通過點擊一個按鈕讓狀態機停下來去做一些其他的工作,之后再返回到它之前停下的地方。這種行為我們就可以通過 歷史狀態 實現。歷史狀態  是一個假想的狀態,它表示了父狀態上次退出時的子狀態。

        歷史狀態通常創建為想要保存的那個狀態的子狀態。這樣,程序運行時,當狀態機檢測到這種狀態的存在時,就會在父狀態退出時自動記錄當前的子狀態。連接到歷史狀態的過渡實際上就是連接到狀態機上次保存的子狀態,狀態機會自動的將過渡前移到正在的子狀態。下面的狀態圖顯示了添加打斷機制后的執行流程:

 

 

下面的代碼展示了具體怎么實現這種功能。在這個例子里,當進入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);
       使用並行狀態來避免過多的狀態組合
       一般情況下,對象的一個屬性對應着兩種狀態,比如汽車的干凈和不干凈,移動和停止。這是4中獨立的狀態,會構成8中不同的狀態轉換。如下:


如果我們繼續添加屬性,比如顏色 紅色和藍色,那么就會變成8中狀態。這是一個指數式的增長,很難想上面一樣把這些狀態放在一起考慮。這時,由於這些屬性都是獨立的,所以我們就可以將這個屬性所構成的狀態轉換看成獨立的,分開實現。可以使用並行狀態來解決這個問題。如下圖所示:

 

創建並行狀態也非常的簡單,只需在創建狀態時將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);
當狀態機進入一個並行狀態組時,所有的子狀態都會同時開始運行,每一個子狀態的過渡都會正常執行。但是,每一個子狀態都有可能退出父狀態,如果這樣,父狀態和它所有的子狀態都會結束。
        在Qt狀態機框架的並行機制里有一個交錯語義。所有的並行操作都是在一個事件處理中獨立的、原子的被執行,所以沒有事件能打斷並行操作。但是,事件仍然是被順序的處理的,因為狀態機本身是單線程的。舉個栗子,如果有兩個過渡退出同一個並行狀態組,並且它們的觸發條件同時被滿足。在這種情況下,第二個被處理的退出事件將沒有任何實際的反應,因為第一個事件已經導致了狀態機從並行狀態中結束。
       檢測組合狀態的結束

       其實子狀態可以是一個final狀態;當進入一個final子狀態時,父狀態會發出finished() 信號。下圖顯示了一個組合狀態s1在做了一系列的處理后進入了一個final狀態:


當s1進入一個final子狀態時,s1會自動發出finished() 信號。我們使用一個 信號過渡 來觸發一個狀態轉換:


s1->addTransition(s1, SIGNAL(finished()), s2);
在組合狀態中使用final狀態對應想隱藏組合狀態的內部細節來說是非常有用的。也就是說,對應外部世界來說,只需要進入這個狀態,然后等待這個狀態的完成信號即可。這對於構建復雜的狀態機來說是一種強有力的的封裝和抽象機制。但是,對應並行狀態組來說,finishe()信號只有在所以的子狀態都進入final狀態時才會發出。
       無目標狀態的過渡

       一個Transition並不是一定要有一個目標狀態,並且,沒有目標狀態的過渡也可以像其他過渡一樣被觸發。但區別是當一個沒有目標狀態的過渡被觸發時,不會導致任何狀態的改變。這運行你在狀態機進入某個狀態時響應一個信號或事件而不必離開那個狀態。例如:

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狀態會結束,然后重新進入該狀態。
            事件和過渡
        狀態機運行在自己的事件循環中。對於信號轉換(QSignalTransition 對象)來說,狀態機會自動給它自己投遞一個QStateMachine::SignalEvent 當它攔截到相應的信號后;同樣,對於QObject事件轉換(QEventTransition 對象)來說,QStateMachine::WrappedEvent會被投遞。當然,你可以使用QStateMachine::postEvent()投遞自己定義的事件給狀態機。

       當向狀態機投遞一個自定義的事件時,你通常還會定義一或多個能被自定義的事件類型觸發的過渡。為了創建這種過渡,可以繼承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
{
Q_OBJECT

public:
StringTransition(const QString &value)
: m_value(value) {}

protected:
virtual bool eventTest(QEvent *e)
{
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"));
另外,沒被任何過渡處理的事件會被狀態機默默的處理掉。
             使用恢復策略自動恢復屬性值

        在使用狀態機時,我們往往將注意力集中在修改對象的屬性值,而不是集中在當狀態退出時怎么恢復它們。如果你知道當狀態機進入某個狀態時,如果未為某個屬性顯示的設置值,那么應該總是將該屬性重置為它的默認值,這時,可以為狀態機設置一個全局的重置策略。


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。當狀態機在s1狀態時,改屬性會被設置為1.0,因為這個狀態顯式的為其設置了值。當狀態機進入s2狀態時,該狀態沒有為fooBar屬性顯式的設置值,所以它會被隱式的重置為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有兩個子狀態:s2和s3。當進入s2狀態時,fooBar屬性會被設置為2.0,因為這個改狀態顯式定義的。當進入s3狀態時,未給該屬性設置值,但是s1狀態為該屬性定義了值1.0,所以,s3會繼承該值,將fooBar設置為1.0。
       為狀態過渡引入動畫

       假設我們有下面的代碼:


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"));
為屬性引入動畫以為着當進入該狀態時,屬性的賦值不會立刻起作用。相反,當進入該狀態時會開發執行該動畫並慢慢的改變屬性的值。以為我們沒有設置動畫的開始值和結束值,動畫會隱式的設置它們。開始值會被設置為動畫開始時的屬性值,結束值會被設置為終止狀態指定的值。
       檢測一個狀態中所有的屬性均被設置完成

       當使用動畫為屬性賦值時,一個狀態不再為屬性定義確切的值,當動畫運行時,屬性可能具有任何值。而在有些情況下,檢測某個屬性是否已經被某個狀態設置完成對我們來說是很重要的。例如下面的代碼:


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);
當按鈕被點擊時,狀態機會進入s2狀態,該狀態會改變按鈕的尺寸,然后彈出一個消息框提示用戶按鈕的尺寸已經被改變了。
正常情況下,也就是沒有使用動畫的情況下,這個動作會如我們期望的所運行。但是,如果我們為s1到s2的轉換添加了動畫,那么當進入s2狀態時會執行該動畫,但是在動畫執行結束之前,按鈕的尺寸不會達到預定義的值。在這種情況下,消息框會在按鈕尺寸實際設置完成之前彈出。

為了確保消息框直到按鈕尺寸變化到指定值時才彈出,我們可以使用狀態的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);
在這個例子中,當按鈕被點擊,狀態機會進入s2。但會保持在s2按鈕的尺寸達到預設的QRect(0, 0, 50, 50)。接着會進入s3狀態。當進入s3狀態時,消息框會彈出。如果到s2的過渡被添加了動畫,那么狀態機會停留在s2直到動畫播放完成。如果沒有添加動畫,就會簡單的設置屬性值然后立即進入s3狀態。無論哪種方式,當狀態機進入s3時,可以確保按鈕的尺寸已經達到了預設值。
        狀態在動畫完成之前退出

        如果一個狀態有屬性賦值,並且到這個狀態的過渡為這個屬性應用了動畫,那么該狀態有可能在屬性被賦予預設值之前退出。這在從不依賴於propertiesAssigned()信號的狀態發出的過渡中更有可能發生。當發生這種情況時,狀態機保證屬性值要么是一個顯式設置的值,要么是狀態結束時動畫運行到的值。

        當一個狀態在動畫結束之前退出,狀態機的行為依賴與過渡的目標狀態。如果目標狀態顯式的設置了該屬性值,那么就不需要進行額外的操作。該屬性會被設置為目標狀態所定義的值。如果目標狀態沒有設置該屬性的值,那么會有兩種可能:默認情況下,該屬性會被設置為正在離開的那個狀態所定義的值。但是,如果設置了全局重置策略,則重置策略優先,該屬性會像往常一樣被重置。

       默認動畫

       正如上文所說,你可以為一個過渡添加動畫從而確保在目標狀態里的屬性賦值時動態的。如果你想為一個屬性應用一個特定的動畫,不論發生的是哪一個過渡,那么你可以把該動畫添加為狀態機的默認動畫。這在創建狀態機之前不知道某個屬性會由哪個狀態所賦值來說至關重要。例如以下代碼:


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設置了。記住,對於給定的屬性 來說,在過渡上顯式設置的動畫優先於默認動畫。
       狀態機的嵌套

       QStateMachine 是QState的子類。這允許一個狀態機是另一個狀態機的孩子。QStateMachine重新實現了QState::onEntry() 並且調用了QStateMachine::start() ,以至於當進入子狀態機時,它會自動開始運行。

       父狀態機會在狀態機算法中將子狀態機看成一個原子狀態。子狀態機是獨立的,它維護自己的事件隊列和相關配置。特別要記住的一點是,子狀態機的configuration() 並不是父狀態機的configuration的一部分。

       子狀態機中的狀態不能被指定為父狀態機中的過渡的目標狀態;反過來也是這樣。不過,子狀態機的finished()信號可以在父狀態機中被用來觸發一個過渡。

        以上就是Qt狀態機框架的基本知識。至於QML中使用的Declarative State Machine Framework,知識點與此類似,大家可以自行研習Qt 幫助文檔The Declarative State Machine Framework 一節。
---------------------
作者:求道玉
來源:CSDN
原文:https://blog.csdn.net/Amnes1a/article/details/62418196
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM