一点一滴成长

导航

《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

posted on 2020-09-14 11:11  整鬼专家  阅读(699)  评论(0编辑  收藏  举报