天天看點

qt 狀态機架構初探

狀态機架構       

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

       一個簡單的狀态機

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

qt 狀态機架構初探

下面,我們先來看下怎麼通過Qt代碼來實作這個簡單的狀态機。

第一步,我們建立一個狀态機和需要的狀态:

 QStateMachine machine;

QState *s1 = new QState();

QState *s2 = new QState();

QState *s3 = new QState();

第二步,我們使用QState::addTransition() 函數為這些狀态之間添加過渡:

  1.  s1->addTransition(button, SIGNAL(clicked()), s2);
  2.  s2->addTransition(button, SIGNAL(clicked()), s3);
  3.  s3->addTransition(button, SIGNAL(clicked()), s1);

第三步,将上面建立的三個狀态添加到狀态機進行管理,并為我們的狀态機設定一個初始狀态:

  1.  machine.addState(s1);
  2.  machine.addState(s2);
  3.  machine.addState(s3);
  4.  machine.setInitialState(s1);

最後,我們啟動狀态機即可:

machine.start();      

這樣,我們的狀态機就開始異步的運作了,也就是說,它成為了我們應用程式事件循環的一部分了。這也對應了我們上面說的,Qt的狀态機是通過Qt自身的事件機制來驅動的。

        在狀态轉換時操作QObject對象

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

  1.  s1->assignProperty(label, "text", "In state s1");
  2.  s2->assignProperty(label, "text", "In state s2");
  3.  s3->assignProperty(label, "text", "In state s3");

當進入任一狀态時,label的文本都會發生改變。

       除了操作QObject對象的屬性外,我們還能通過狀态的轉換來調用QObject對象的函數。這是通過使用狀态轉換時發出的信号完成的。其中,當進入某個狀态時會發出QState::enterd() 信号,當退出某個狀态時會發出QState::exited() 信号。例如下面的代碼,其實作的功能即為當進入s3狀态時,會調用按鈕的showMaximized() 函數,當退出s3狀态時會調用showMinimized() 函數:

  1.  QObject::connect(s3, SIGNAL(entered()), button, SLOT(showMaximized()));
  2.  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成為其孩子來完成。下面是這種方法所對應的狀态轉換圖:

qt 狀态機架構初探

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

        而所謂的分組,就是隻需在建立狀态時為其指定一個合适的父狀态即可。當然,還需要為這組狀态指定一個初始狀态,即當s1是某個過渡的目标狀态時,狀态機應該進入哪個子狀态。簡單的實作代碼如下:

  1.  QState *s1 = new QState();
  2.  QState *s11 = new QState(s1);
  3.  QState *s12 = new QState(s1);
  4.  QState *s13 = new QState(s1);
  5.  s1->setInitialState(s11);
  6.  QFinalState *s2 = new QFinalState();
  7.  s1->addTransition(quitButton, SIGNAL(clicked()), s2);
  8.  QObject::connect(&machine, SIGNAL(finished()), QApplication::instance(), SLOT(quit()));

在這個例子中,我們想讓應用程式在狀态機結束時退出,是以我們将狀态機的finished() 信号連接配接到了應用程式的quit()槽函數上。

        注意,子狀态可以覆寫從父狀态那裡繼承的過渡。例如,下面的代碼通過為s12添加一個新的過渡,導緻當狀态機處于s12狀态是,退出按鈕的點選被忽略。還有,一個過渡可以選擇任何狀态作為其目标狀态,也就是說,一個過渡的目标狀态不需要和他的源狀态在狀态圖上處于同一個層次。

        使用曆史 曆史狀态儲存和恢複目前狀态

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

        曆史狀态通常建立為想要儲存的那個狀态的子狀态。這樣,程式運作時,當狀态機檢測到這種狀态的存在時,就會在父狀态退出時自動記錄目前的子狀态。連接配接到曆史狀态的過渡實際上就是連接配接到狀态機上次儲存的子狀态,狀态機會自動的将過渡前移到正在的子狀态。下面的狀态圖顯示了添加打斷機制後的執行流程:

qt 狀态機架構初探

下面的代碼展示了具體怎麼實作這種功能。在這個例子裡,當進入s3時我們隻是簡單的顯示一個消息框,然後就立刻通過曆史狀态再傳回到s1。

  1.  QHistoryState *s1h = new QHistoryState(s1);
  2.  QState *s3 = new QState();
  3.  s3->assignProperty(label, "text", "In s3");
  4.  QMessageBox *mbox = new QMessageBox(mainWindow);
  5.  mbox->addButton(QMessageBox::Ok);
  6.  mbox->setText("Interrupted!");
  7.  mbox->setIcon(QMessageBox::Information);
  8.  QObject::connect(s3, SIGNAL(entered()), mbox, SLOT(exec()));
  9.  s3->addTransition(s1h);
  10.  s1->addTransition(interruptButton, SIGNAL(clicked()), s3);

       使用并行狀态來避免過多的狀态組合

       一般情況下,對象的一個屬性對應着兩種狀态,比如汽車的幹淨和不幹淨,移動和停止。這是4中獨立的狀态,會構成8中不同的狀态轉換。如下:

qt 狀态機架構初探

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

qt 狀态機架構初探

建立并行狀态也非常的簡單,隻需在建立狀态時将QState::ParallelStates 傳給QState的構造函數即可。如下:

  1.  QState *s1 = new QState(QState::ParallelStates);
  2.  // s11 and s12 will be entered in parallel

當狀态機進入一個并行狀态組時,所有的子狀态都會同時開始運作,每一個子狀态的過渡都會正常執行。但是,每一個子狀态都有可能退出父狀态,如果這樣,父狀态和它所有的子狀态都會結束。

        在Qt狀态機架構的并行機制裡有一個交錯語義。所有的并行操作都是在一個事件進行中獨立的、原子的被執行,是以沒有事件能打斷并行操作。但是,事件仍然是被順序的處理的,因為狀态機本身是單線程的。舉個栗子,如果有兩個過渡退出同一個并行狀态組,并且它們的觸發條件同時被滿足。在這種情況下,第二個被處理的退出事件将沒有任何實際的反應,因為第一個事件已經導緻了狀态機從并行狀态中結束。

       檢測組合狀态的結束

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

qt 狀态機架構初探

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

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

在組合狀态中使用final狀态對應想隐藏組合狀态的内部細節來說是非常有用的。也就是說,對應外部世界來說,隻需要進入這個狀态,然後等待這個狀态的完成信号即可。這對于建構複雜的狀态機來說是一種強有力的的封裝和抽象機制。 但是,對應并行狀态組來說,finishe()信号隻有在是以的子狀态都進入final狀态時才會發出。

       無目标狀态的過渡

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

  1.  QState *s1 = new QState(&machine);
  2.  QPushButton button;
  3.  QSignalTransition *trans = new QSignalTransition(&button, SIGNAL(clicked()));
  4.  s1->addTransition(trans);
  5.  QMessageBox msgBox;
  6.  msgBox.setText("The button was clicked; carry on.");
  7.  QObject::connect(trans, SIGNAL(triggered()), &msgBox, SLOT(exec()));

在上面的例子中,消息框在每次按鈕點選時都會顯示出來,但是狀态機會始終停留在s1狀态。但是如果顯示的把狀态機的狀态設定為s1,s1狀态會結束,然後重新進入該狀态。

            事件和過渡

        狀态機運作在自己的事件循環中。對于信号轉換(QSignalTransition 對象)來說,狀态機會自動給它自己投遞一個QStateMachine::SignalEvent 當它攔截到相應的信号後;同樣,對于QObject事件轉換(QEventTransition 對象)來說,QStateMachine::WrappedEvent會被投遞。當然,你可以使用QStateMachine::postEvent()投遞自己定義的事件給狀态機。

       當向狀态機投遞一個自定義的事件時,你通常還會定義一或多個能被自定義的事件類型觸發的過渡。為了建立這種過渡,可以繼承QAbstractTransition 并且實作eventTest() 方法,在這個方法中判斷目前事件是否比對你的事件類型。下面是一個自定義的事件類型,StringEvent,用于向狀态機投遞字元串:

  1.  struct StringEvent : public QEvent
  2.  {
  3.  StringEvent(const QString &val)
  4.  : QEvent(QEvent::Type(QEvent::User+1)),
  5.  value(val) {}
  6.  QString value;
  7.  };

接下來,我們再定義一個過渡,僅僅當事件的字元串比對特定的字元串時才觸發該過渡:

  1.  class StringTransition : public QAbstractTransition
  2.  Q_OBJECT
  3.  public:
  4.  StringTransition(const QString &value)
  5.  : m_value(value) {}
  6.  protected:
  7.  virtual bool eventTest(QEvent *e)
  8.  if (e->type() != QEvent::Type(QEvent::User+1)) // StringEvent
  9.  return false;
  10.  StringEvent *se = static_cast<StringEvent*>(e);
  11.  return (m_value == se->value);
  12.  }
  13.  virtual void onTransition(QEvent *) {}
  14.  private:
  15.  QString m_value;

在重新實作的eventTest() 函數中,我們首先檢查接收到的事件是否是我們想要的,如果是,就把它轉換成StringEvent并且進行字元串的比較。

下面的狀态圖使用了自定義的事件和過渡:

qt 狀态機架構初探

下面,我們就實作這個狀态圖,使用我們剛才定義的事件和過渡:

  1.  QState *s2 = new QState();
  2.  QFinalState *done = new QFinalState();
  3.  StringTransition *t1 = new StringTransition("Hello");
  4.  t1->setTargetState(s2);
  5.  s1->addTransition(t1);
  6.  StringTransition *t2 = new StringTransition("world");
  7.  t2->setTargetState(done);
  8.  s2->addTransition(t2);
  9.  machine.addState(done);

一旦我們啟動了狀态機,就可以向它投遞我們自定義的事件了:

  1.  machine.postEvent(new StringEvent("Hello"));
  2.  machine.postEvent(new StringEvent("world"));

另外,沒被任何過渡處理的事件會被狀态機默默的處理掉。

             使用恢複政策自動恢複屬性值

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

  1.  machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties);

當設定了這個重置政策,狀态機會自動的重置所有的屬性。當狀态機進入一個狀态時,若某個屬性未被設定,它會首先查找它的父級,看是否在那裡定義了該屬性。如果有,就将該屬性重置為其最近的父級所定義的值。如果沒有,就将它重置為其初始值。例如:

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

我們假定當狀态機啟動時,fooBar屬性值為0。當狀态機在s1狀态時,改屬性會被設定為1.0,因為這個狀态顯式的為其設定了值。當狀态機進入s2狀态時,該狀态沒有為fooBar屬性顯式的設定值,是以它會被隐式的重置為0.

如果我們使用嵌套的狀态,父狀态為某個屬性定義的值會被所有未給該屬性顯式指派的子孫後代繼承。例如:

  1.  QState *s2 = new QState(s1);
  2.  s2->assignProperty(object, "fooBar", 2.0);
  3.  s1->setInitialState(s2);
  4.  QState *s3 = new QState(s1);

在這個例子中,s1有兩個子狀态:s2和s3。當進入s2狀态時,fooBar屬性會被設定為2.0,因為這個改狀态顯式定義的。當進入s3狀态時,未給該屬性設定值,但是s1狀态為該屬性定義了值1.0,是以,s3會繼承該值,将fooBar設定為1.0。

       為狀态過渡引入動畫

       假設我們有下面的代碼:

  1.  s1->assignProperty(button, "geometry", QRectF(0, 0, 50, 50));
  2.  s2->assignProperty(button, "geometry", QRectF(0, 0, 100, 100));

這裡我們定義了一個使用者界面的兩種狀态。在s1狀态時button是比較小的,在s2狀态時,button變的更大。如果我們點選按鈕觸發s1到s2的過渡,那麼按鈕的尺寸會立刻改變。如果我們想讓這個過渡更平滑,需要做的僅僅是為過渡添加一個屬性動畫QPropertyAnimation。代碼如下:

  1.  QSignalTransition *transition = s1->addTransition(button, SIGNAL(clicked()), s2);
  2.  transition->addAnimation(new QPropertyAnimation(button, "geometry"));

為屬性引入動畫以為着當進入該狀态時,屬性的指派不會立刻起作用。相反,當進入該狀态時會開發執行該動畫并慢慢的改變屬性的值。以為我們沒有設定動畫的開始值和結束值,動畫會隐式的設定它們。開始值會被設定為動畫開始時的屬性值,結束值會被設定為終止狀态指定的值。

       檢測一個狀态中所有的屬性均被設定完成

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

  1.  QMessageBox *messageBox = new QMessageBox(mainWindow);
  2.  messageBox->addButton(QMessageBox::Ok);
  3.  messageBox->setText("Button geometry has been set!");
  4.  messageBox->setIcon(QMessageBox::Information);
  5.  s2->assignProperty(button, "geometry", QRectF(0, 0, 50, 50));
  6.  connect(s2, SIGNAL(entered()), messageBox, SLOT(exec()));

當按鈕被點選時,狀态機會進入s2狀态,該狀态會改變按鈕的尺寸,然後彈出一個消息框提示使用者按鈕的尺寸已經被改變了。

正常情況下,也就是沒有使用動畫的情況下,這個動作會如我們期望的所運作。但是,如果我們為s1到s2的轉換添加了動畫,那麼當進入s2狀态時會執行該動畫,但是在動畫執行結束之前,按鈕的尺寸不會達到預定義的值。在這種情況下,消息框會在按鈕尺寸實際設定完成之前彈出。

為了確定消息框直到按鈕尺寸變化到指定值時才彈出,我們可以使用狀态的propertiesAssigned() 信号。該信号會在屬性達到最終值時被發出。如下面代碼所示:

  1.  connect(s3, SIGNAL(entered()), messageBox, SLOT(exec()));
  2.  s2->addTransition(s2, SIGNAL(propertiesAssigned()), s3);

在這個例子中,當按鈕被點選,狀态機會進入s2。但會保持在s2按鈕的尺寸達到預設的QRect(0, 0, 50, 50)。接着會進入s3狀态。當進入s3狀态時,消息框會彈出。如果到s2的過渡被添加了動畫,那麼狀态機會停留在s2直到動畫播放完成。如果沒有添加動畫,就會簡單的設定屬性值然後立即進入s3狀态。無論哪種方式,當狀态機進入s3時,可以確定按鈕的尺寸已經達到了預設值。

        狀态在動畫完成之前退出

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

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

       預設動畫

       正如上文所說,你可以為一個過渡添加動畫進而確定在目标狀态裡的屬性指派時動态的。如果你想為一個屬性應用一個特定的動畫,不論發生的是哪一個過渡,那麼你可以把該動畫添加為狀态機的預設動畫。這在建立狀态機之前不知道某個屬性會由哪個狀态所指派來說至關重要。例如以下代碼:

  1.  s1->addTransition(s2);
  2.  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,知識點與此類似

繼續閱讀