qt撤销与回退_Qt动画框架
狀態機框架提供了創建和執行狀態圖的一些類.這些概念和表示都是基于Harel狀態圖中的一些概念和記法.它也是UML狀態圖表的基礎.狀態機執行的語義是基于狀態圖XML(SCXML).
狀態圖提供了一種圖形化的方式來對一個系統建模,從而反映它怎么響應外部觸發.這是通過定義系統可能進入的一些狀態以及系統怎么從一個狀態轉換到另一個狀態(不同狀態之間轉變)來實現的.事件驅動系統的一個關鍵的特征(例如Qt應用程序)就是行為通常不僅取決于上次或當前事件,還取決于在它之前的一些事件.用狀態圖,這個信息非常容易表達.
狀態機框架提供了一套API以及一種執行模型,可有效地將狀態圖的元素和語義嵌入到Qt應用程序當中.該框架與Qt的元對象系統結合緊密:例如,不同狀態之間的轉變可由信號觸發且狀態可配置用于設置QObject的屬性和方法.Qt的事件系統用于驅動狀態機.
狀態機框架中的狀態圖是分層的.狀態可嵌套在另一個狀態內.狀態機的當前配置包含一些當前活躍的狀態.狀態機中的一個有效的配置中的所有狀態都有一個共同的祖先.
狀態機框架中的類
qt提供了這些類來創建事件驅動的狀態機.
The
base class of states of a QStateMachine
The
base class of transitions between QAbstractState objects
QObject-specific
transition for Qt events
Final
state
Means
of returning to a previously active substate
Transition
for key events
Transition
for mouse events
Transition
based on a Qt signal
General-purpose
state for QStateMachine
Hierarchical
finite state machine
Represents
a Qt signal event
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();
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);
最后,我們啟動狀態機:
狀態是異步執行的,例如,它成為你的應用程序事件循環的一部分.
在狀態入口和出口做有意義的工作
上面的狀態機僅僅從一個狀態轉換到另一個狀態,并沒有執行任何操作.函數可用于當進入某個狀態時設置某個QObject的一個屬性.在下面的代碼段中,為每個狀態指定了應當賦給QLabel的text屬性的值.
s2->assignProperty(label,
"text", "In state s2");
s3->assignProperty(label,
"text", "In state s3");
當進入了這些狀態中的任何一個,標簽的值就會相應地改變.
當進入某個狀態時,就會發出信號.當離開這個狀態時,就會發出信號.在下面的代碼段中,按鈕的showMaximize()槽在進入狀態s3時被調用.當退出狀態s3時調用showMinimized():
完成的狀態機
前面部分定義的狀態機從不完成.為了使一個狀態機能夠完成,它需要擁有一個頂層的最終狀態(對象).當狀態機進入一個頂層最終狀態時,該狀態機將會釋放信號并停止.
在圖中引入一個最終狀態,所有你需要做的就是創建一個對象且使用它作為一個或多個轉換的目標.
通過對狀態進行分組來共享轉換
假設我們想讓用戶能夠通過點擊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);
在本例子中,我們想讓狀態機完成后,應用程序退出,因此狀態機的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構造函數中。
QState
*s1 = new QState(QState::ParallelStates);
// s11 and
s12 will be entered in parallel
QState
*s11 = new QState(s1);
QState
*s12 = new QState(s1);
當一個并行狀態組進入時,所有的子狀態將會同時進入。每個子狀態里的轉變正常執行。但是,任何一個子狀態可以執行存在于父狀態中的一個轉變。當這發生時,父狀態以及所有的子狀態將退出。
狀態機框架的并行機制遵循如下一種交錯的語義。所有并行操作將以單步,原子地進行,沒有事件可以中斷并行操作。但是,事件仍然會被順序地處理,因為狀態機本身是單線程的。舉個例子:考慮這樣的一個情形,有兩個轉變從相同的狀態組中退出,并且它們的(退出)條件同時變為真。在這種情況下,被處理的事件中的后一個將不會產生任何效果,因為第一個事件已經促使狀態機從并行狀態中退出了。
檢測某個組合狀態已經完成
一個孩子狀態可為最終狀態(一個對象)。當進入最終狀態時,父狀態發出信號。下圖顯示了一個組合狀態s1,在進入最終狀態之前執行一些處理:
當進入s1的最終狀態時,s1會自動地發出finished()。我們使用一個轉變來促使這個事件觸發一個狀態改變:
s1->addTransition(s1,
SIGNAL(finished()), s2);
在組合狀態中使用最終狀態是有用的,當你想隱藏一個組合狀態的內部細節時;例如,位于該組合狀態之外的世界只需能進入到該狀態并在該狀態完成了其工作時獲得通知。在構建復雜的狀態機(深度嵌套)時,這是一種非常強大的抽象和封裝機制。(在以上例子中,當然你可以創建一個直接從s1的done狀態開始的一個轉變,而不依賴s1的finished()信號,但是,會造成s1的實現細節暴露并依賴它。)。
對于并行狀態組,當所有孩子狀態進入了最終狀態時會發出信號。
無目標轉變
一個轉變不需要一個目標狀態。無目標的轉變可與其他轉變一樣的方式被觸發;不同之處在于當無目標轉變被觸發時,它不會造成任何狀態的改變。這可以允許你在當狀態機處于某個特定狀態時,對信號或事件作出響應而不用離開那個狀態。例如:
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會退出并且每次點擊的時候進入(例如,會發出和信號)。
事件,轉變和哨衛
當發送一個自定義的事件到狀態機,你一般也擁有一個或更多個自定義的轉變,這些轉變可以由這種類型的事件觸發。為了創建一個這樣的轉變,你要創建一個子類并重新實現方法,在該方法中,你檢測某個事件是否與你的事件類型匹配(也可以采用其他的判斷規則,如事件對象的屬性)。下面我們定義了自已的事件類型,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(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
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擁有兩個孩子:s2和s3。當進入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,當給定的狀態進入時,該按鈕的幾何屬性可以立即設置。但是,如果我們想讓轉變更為流暢,需要構造一個對象并將其添加到轉變對象中。
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);
當button點擊后,狀態機將轉換到狀態s2,它會設置按鈕的geometry屬性,然后彈出一個信息框來提示用戶geometry已經改變。
在正常情況下,沒有使用動畫時,該操作會以預期地方式執行。但是,如果在狀態s1到s2的轉變中為button的屬性geometry定義了一個動畫,該動畫將在進入s2時啟動,但是,在動畫結束運行之前,geometry屬性并不會到達它定義的值。在這種情況下,在button的geometry屬性實際被設置之前,會彈出一個信息框。
為了確保信息框直到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已經被賦予了定義的值。如果全局恢復策略設置為,該狀態不會發出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賦值。注意,顯式地設置轉變動畫比默認動畫優先級大。
總結
以上是生活随笔為你收集整理的qt撤销与回退_Qt动画框架的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: flask echarts词云可视化_基
- 下一篇: 管道无损检测python_武汉哪里有便携