1. 程式人生 > 實用技巧 >《QT Creator快速入門》第十一章(二):動畫框架和狀態機

《QT Creator快速入門》第十一章(二):動畫框架和狀態機

一、動畫框架

Qt中的動畫框架可以在幫助中檢視The Animation Framework關鍵字,主要的類如下圖所示,基類QAbstractAnimation中定義了動畫開始、暫停、停止等方法,它也可以接收時間變化的通知,通過整合它可以建立自定義的動畫類。QPropertyAnimation類用來執行Qt屬性的動畫,如果要對一個值使用動畫可以建立繼承自QObject的類,然後在類中將該值定義為一個屬性。Qt支援的可以進行插值的QVariant型別有int、float、double、QLine、QPoint、QSize、QRect、QColor等,比如可以在QWidget的幫助文件中檢視它支援動畫的屬性。如果要實現複雜的動畫,可以通過動畫組QParallelAnimationGroup和QSequentialAnimationGroup來實現,它們的功能是作為其它動畫類的容器,一個動畫組還可以包含另外的動畫組。動畫框架也被設計為狀態機框架的一部分。

1、使用動畫框架

下面的示例為按鈕部件的geometry屬性建立了動畫,實現了從(0, 0)移動到(25, 250)點,並且其寬高由100*30變換為200*60:

#include <QApplication>
#include <QPushButton>
#include <QPropertyAnimation>

int main(int argc, char** argv)
{
    QApplication app(argc, argv);

    QPushButton button("Animated Button");
    button.show();

    QPropertyAnimation animation(
&button, "geometry"); //給按鈕的geometry屬性建立動畫 animation.setDuration(10000); //動畫持續時間為10秒 animation.setStartValue(QRect(0, 0, 100, 30)); //動畫開始時geometry屬性的值 animation.setEndValue(QRect(250, 250, 200, 60)); //動畫結束時geometry屬性的值 animation.start(); //開始動畫 return app.exec(); }
View Code

呼叫start()方法還可以傳入QAbstractAnimation::DeleteWhenStopped來指定刪除策略(預設為QAbstractAnimation::KeepWhenStopped),當動畫結束後自動銷燬該物件動畫。

可以呼叫setKeyValueAt()方法在動畫中間為屬性設定值,將如下程式碼替換上面setStartValue()和setEndValue()兩行可以實現在8秒時間按鈕由(0,0)移動到(25,250),然後在2秒時間又回到原點並恢復原來的大小:

animation.setKeyValueAt(0, QRect(0, 0, 100, 30));//第一個引數step取值為0.0(開始位置)-1.0(結束位置),第二個引數為屬性的值
animation.setKeyValueAt(0.8, QRect(250, 250, 200, 60));
animation.setKeyValueAt(1, QRect(0, 0, 100, 30));
View Code

使用pause()、resume()、stop()來暫停、恢復、停止動畫。使用setLoopCount()來設定動畫的重複次數,預設為1,即執行一次,0為不執行,-1表示一直反覆持續直到stop()。setDirection()設定方向,QAbstractAnimation::Forward為從開始位置到結束位置(預設),QAbstractAnimation::Backward為從結束到開始。

2、緩和曲線

將前面示例程式程式碼中間修改為以下,執行可以發現實現了按鈕部件像皮球掉落一樣的效果,QEasingCurve中提供了四十多種緩和曲線,還可以自定義緩和曲線,Qt中Animation Framework分類中有一個Easing Curves示例程式,可以演示所有緩和曲線的效果:

animation.setDuration(2000); //動畫持續時間為2秒
animation.setStartValue(QRect(250, 0, 100, 30));
animation.setEndValue(QRect(250, 300, 100, 30));
animation.setEasingCurve(QEasingCurve::OutBounce); //使用QEasingCurve::OutBounce緩和曲線
View Code

3、動畫組

QSequentialAnimationGroup和QParallelAnimationGroup分別提供了序列動畫組和並行動畫組,如下的示例1執行可以看到先執行動畫1,然後才會執行動畫2,示例2則是兩個動畫同時進行。可以將動畫組看做一個獨立的動畫,從而進行暫停、新增到其它動畫組等操作:

#include <QApplication>
#include <QPushButton>
#include <QPropertyAnimation>
#include <QSequentialAnimationGroup>

int main(int argc, char** argv)
{
    QApplication app(argc, argv);

    QPushButton button("Animated Button");
    button.show();

    //按鈕部件的動畫1
    QPropertyAnimation* animation1 = new QPropertyAnimation(&button, "geometry");
    animation1->setDuration(2000);
    animation1->setStartValue(QRect(250, 0, 100, 30));
    animation1->setEndValue(QRect(250, 300, 100, 30));
    animation1->setEasingCurve(QEasingCurve::OutBounce);

    //按鈕部件的動畫2
    QPropertyAnimation* animation2 = new QPropertyAnimation(&button, "geometry");
    animation2->setDuration(1000);
    animation2->setStartValue(QRect(250, 300, 100, 30));
    animation2->setEndValue(QRect(250, 300, 200, 60));

    //序列動畫組
    QSequentialAnimationGroup group;
    group.addAnimation(animation1);
    group.addAnimation(animation2);
    group.start();

    return app.exec();
}
View Code
#include <QApplication>
#include <QPushButton>
#include <QPropertyAnimation>
#include <QParallelAnimationGroup>

int main(int argc, char** argv)
{
    QApplication app(argc, argv);

    QPushButton button1("Animated Button1");
    button1.show();
    QPushButton button2("Animated Button2");
    button2.show();

    //按鈕部件1的動畫
    QPropertyAnimation* animation1 = new QPropertyAnimation(&button1, "geometry");
    animation1->setDuration(2000);
    animation1->setStartValue(QRect(250, 0, 100, 30));
    animation1->setEndValue(QRect(250, 300, 100, 30));
    animation1->setEasingCurve(QEasingCurve::OutBounce);

    //按鈕部件2的動畫
    QPropertyAnimation* animation2 = new QPropertyAnimation(&button2, "geometry");
    animation2->setDuration(2000);
    animation2->setStartValue(QRect(400, 300, 100, 30));
    animation2->setEndValue(QRect(400, 300, 200, 60));

    //序列動畫組
    QParallelAnimationGroup group;
    group.addAnimation(animation1);
    group.addAnimation(animation2);
    group.start();

    return app.exec();
}
View Code

也可以在圖形檢視框架中使用動畫,但因為QGraphicsItem不是繼承自QObject類,所以不能直接來建立動畫,可以使用其子類QGraphicsObject,它繼承自QObject,這個類為需要使用訊號和槽以及屬性的圖形項提供了一個基類。也可以同時繼承QObject和QGraphicsItem來實現自己的圖形項,不過要注意QObject必須是第一個繼承的類,另外也可以繼承自已經是QObject子類的QGraphicsWidget類。如果要使用一個自定義的屬性,那麼要先宣告該屬性,見第七章內容。QGraphicsObject提供了位置pos、透明度opacity、旋轉rotation、縮放scale等屬性,這些都可以用來設定動畫。在Animation Framework分類中有一個Animated Tiles的示例程式。下面這個示例實現了圖形項自動旋轉的動畫效果:

#include <QApplication>
#include <QPropertyAnimation>
#include <QGraphicsScene>
#include <QGraphicsView>
#include "myitem.h"
#include <QGraphicsObject>
#include <QPainter>

class MyItem : public QGraphicsObject
{
public:
    MyItem(QGraphicsItem * parent = 0):QGraphicsObject(parent){}
    QRectF boundingRect()const override
    {
        return QRectF(-10 - 0.5, -10 - 0.5, 20 + 1, 20 + 1);
    }
    void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)override
    {
        painter->drawRect(-10, -10, 20, 20);
    }
};

int main(int argc, char** argv)
{
    QApplication app(argc, argv);

    QGraphicsScene scene;
    scene.setSceneRect(-200, -150, 400, 300);
    MyItem* item = new MyItem;
    scene.addItem(item);
    QGraphicsView view;
    view.setScene(&scene);
    view.show();

    //為圖形項的rotation屬性建立動畫
    QPropertyAnimation animation(item, "rotation");
    animation.setDuration(10000); //動畫持續時間為10秒
    animation.setStartValue(0); //動畫開始時rotation屬性的值
    animation.setEndValue(360); //動畫結束時rotation屬性的值
    animation.start(); //開始動畫

    return app.exec();
}
View Code

二、狀態機框架

1、狀態機、狀態、訊號

狀態機框架與Qt的元物件系統是緊密結合的,例如Qt的事件系統用來驅動狀態機,狀態機中狀態間的切換可以由訊號來觸發。關於狀態機可以參考The State Machine Framework關鍵字。如下的示例中,狀態機被一個按鈕控制,包含3個狀態s1、s2、s3,s1為初始狀態,當單擊按鈕時狀態機切換到另一個狀態並設定了新的geometry屬性值,可以看到單擊按鈕的話按鈕會在三個位置輪流切換:

#include <QApplication>
#include <QPushButton>
#include <QState>
#include <QStateMachine>

int main(int argc, char**argv)
{
    QApplication app(argc, argv);

    QPushButton button("State Machine");

    //建立狀態機和三個狀態,將三個狀態新增到狀態機中
    QStateMachine machine;
    QState* s1 = new QState(&machine);
    QState* s2 = new QState(&machine);
    QState* s3 = new QState();
    machine.addState(s3);
    //使用按鈕部件的單擊訊號來完成3個狀態的切換
    s1->addTransition(&button, SIGNAL(clicked()), s2);
    s2->addTransition(&button, SIGNAL(clicked()), s3);
    s3->addTransition(&button, SIGNAL(clicked()), s1);
    //設定進入指定狀態時設定按鈕部件的geometry屬性值
    s1->assignProperty(&button, "geometry", QRect(100, 100, 100, 50));
    s2->assignProperty(&button, "geometry", QRect(300, 100, 100, 50));
    s3->assignProperty(&button, "geometry", QRect(200, 200, 100, 50));
    //設定狀態機的初始狀態並啟動狀態機
    machine.setInitialState(s1);
    machine.start();

    button.show();

    return app.exec();
}
View Code

當狀態機進入一個狀態時會發射QState::entered()訊號,退出一個狀態時會發射QState::exited()訊號,比如新增如下程式碼後當第二次點選按鈕後按鈕就最小化了:

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

如果想切換到一個狀態後狀態機就停止,那麼可以設定這個狀態為QFinalState型別,等切換到該狀態時狀態機就會發射finished()訊號並停止。

2、在狀態機中使用動畫

將上面示例中的三個addTransition()方法語句替換為以下程式碼並新增標頭檔案<QPropertyAnimation>、<QSignalTransition>,可以看到,點選按鈕後按鈕會平滑的移動到另一個位置:

    QSignalTransition* transition1 = s1->addTransition(&button, SIGNAL(clicked()), s2);
    QSignalTransition* transition2 = s2->addTransition(&button, SIGNAL(clicked()), s3);
    QSignalTransition* transition3 = s3->addTransition(&button, SIGNAL(clicked()), s1);
    QPropertyAnimation* animation = new QPropertyAnimation(&button, "geometry"); 
    transition1->addAnimation(animation);
    transition2->addAnimation(animation);
    transition3->addAnimation(animation);
View Code

如果想使所有的狀態切換都使用一個動畫那麼可以在狀態機中使用預設動畫,所以也可以將三個addTransition()方法語句換成以下一個方法語句:

machine.addDefaultAnimation(animation);

在上面示例程式碼的三條assignProperty()方法語句後新增以下程式碼,可以看到在第一次點選按鈕後且按鈕移動到QRect(300, 100, 100, 50)動畫效果之前就會彈出訊息提示對話方塊:

    QMessageBox* messageBox = new QMessageBox();
    messageBox->addButton(QMessageBox::Ok);
    messageBox->setText("Button geometry has been set!");
    messageBox->setIcon(QMessageBox::Information);
    QObject::connect(s2, SIGNAL(entered()), messageBox, SLOT(exec()));
View Code

如果我們想要提示框在geometry屬性獲得指定的值之後彈出來,即動畫效果移動到QRect(300, 100, 100, 50)之後彈出來,那麼可以使用狀態的propertiesAssigned訊號,它是在屬性被分配到最終的值時被髮射的,將上面的connect()方法中的entered訊號修改成propertiesAssigned即可:

QObject::connect(s2, SIGNAL(propertiesAssigned()), messageBox, SLOT(exec())); 

如果一個狀態在動畫結束前退出了,那麼狀態機的行為會依賴於切換的目標狀態。

3、為狀態分組來共享切換

狀態還可以加入另一個狀態來作為子狀態,子狀態會繼承父狀態的切換,比如將上面的示例修改為下面的程式碼,將三個狀態加入到一個狀態s,設定s的狀態切換為點選quitButton時候切換到QFinalState狀態,這時候三個子狀態會繼承這個切換(新的狀態機如下圖所示),當點選quitButton狀態機就會切換到QFinalState狀態從而收到finished訊號,這裡使用connect()設定狀態機的finished訊號的槽方法為呼叫qApp的quit()來退出程式:

    ......
    QState* s = new QState(&machine);
    QState* s1 = new QState(s);
    QState* s2 = new QState(s);
    QState* s3 = new QState(s);
    s->setInitialState(s1);
    
    QFinalState* sFinal = new QFinalState(&machine);
    s->addTransition(&quitButton, SIGNAL(clicked()), sFinal);
    QObject::connect(&machine, SIGNAL(finished()), qApp, SLOT(quit()));
    ......
    machine.setInitialState(s);
    machine.start();
    ......
View Code

子狀態也可以覆蓋繼承的狀態,比如要使在s2狀態時忽略quitButton,可以新增如下的程式碼:

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

切換的目標狀態可以是任意狀態,比如目標狀態可以和源狀態不在狀態層次結構的同一層中。

4、使用歷史狀態來儲存或恢復當前狀態

歷史狀態是一個偽狀態,它應該建立為一個狀態的子狀態,它代表了當父狀態退出的時候所在的那個子狀態,比如以下程式碼添加了一箇中斷按鈕和訊息提示框和一個歷史狀態,中斷按鈕點選則切換到狀態s4,而進入狀態s4後顯示一個訊息提示框再切換到儲存的歷史狀態,這樣就回到了中斷按鈕點選前的狀態(s1或s2或s3)。在這裡如果我們不使用歷史狀態的話,點選中斷按鈕後進入狀態s4而沒有回到原來的狀態,所以再點選按鈕則不會出現原來的狀態切換動畫效果。

    ......
    QPushButton interruptButton("interrupt");
    interruptButton.show();
    QMessageBox box;
    box.addButton(QMessageBox::Ok);
    box.setText("Interrupted!");
    box.setIcon(QMessageBox::Information);

    //歷史狀態加入狀態s,用來記錄s狀態退出時的子狀態
    QHistoryState* sh = new QHistoryState(s);
    //中斷按鈕點選的時候切換到狀態s4,進入狀態s4則顯示訊息提示框,再由狀態s4切換到記錄的歷史狀態
    QState* s4 = new QState(&machine);
    QObject::connect(s4, SIGNAL(entered()), &box, SLOT(exec()));
    s->addTransition(&interruptButton, SIGNAL(clicked()), s4);
    s4->addTransition(sh); //設定切換到狀態s4執行後再切換到狀態sh?


    return app.exec();
View Code