Qt – анимация на графической сцене

В этой теме 2 ответа, 2 участника, последнее обновление  Васильев Владимир Сергеевич 6 мес. назад.

  • Автор
    Сообщения
  • #3185

    При разработке приложений, и особенно игр, часто требуется анимировать изменение свойств, например координат или угла поворота объекта. Это нужно если в вашей игре могут летать астероиды, пули или двигаться юниты. В библиотеке Qt для этого используется класс QPropertyAnimation – он позволяет плавно , но при работе с графической сценой применяется QGraphicsItemAnimation, т.к. QGraphicsItem не является наследником QObject.

    В этой заметке я описал как вы можете сделать анимацию перемещения объекта на графической сцене. Мы создадим наследника класса QGraphicsView, добавим на него графическую сцену и один QGraphicsEllipseItem, изображающий круг. Определим метод mousePressEvent, для обработки кликов мышью. При щелчке левой кнопкой выполним плавное перемещение объекта в новую точку, а при щелчке правой – просто изменим позицию элемента (очень резкое перемещение). Ниже приведено видео работы программы:

    qgraphicsitemanimation_example

    #include <QGraphicsView>
    #include <QGraphicsScene>
    #include <QGraphicsEllipseItem>
    
    class View : public QGraphicsView {
        Q_OBJECT
    public:
        explicit View(QWidget *parent = 0);
    protected slots:
        void mousePressEvent(QMouseEvent *event);
        void onAnimationFinished();
    private:
        const int AnimationPeriodMS = 1000;
        const int ActorSize = 10;
    
        QGraphicsScene m_scene;
        QGraphicsEllipseItem m_actor;
    };

    Константами задаются время перемещения объекта по сцене (скорость будет зависеть от расстояния) и размер элемента. Обратите внимание на слот onAnimationFinished() – он нужен чтобы корректно освободить память после анимации.

    В конструкторе создадим сцену и перемещаемый элемент, установим для вида сцену, добавим на сцену элемент:

    #include "view.h"
    #include <QMouseEvent>
    #include <QTimeLine>
    #include <QGraphicsItemAnimation>
    
    View::View(QWidget *parent)
        : QGraphicsView(parent),
          m_scene(),
          m_actor(0, 0, ActorSize, ActorSize) {
        setScene(&m_scene);
        m_scene.addItem(&m_actor);
        m_actor.setBrush(QBrush(QColor(255, 128, 128)));
        m_scene.setSceneRect(-100, -100, 200, 200);
        setSceneRect(-100, -100, 100, 100);
    }

    При обработки события мыши нас интересуют только правая и левая кнопки. При событии правой кнопки мы просто перемещаем элемент в заданную позицию. Заметьте, что QMouseEvent содержит позицию для QGraphicsView, а перемещаем мы элемент по сцене, поэтому нам требуется преобразовать координаты вида в координаты сцены методом mapToScene:

    void View::mousePressEvent(QMouseEvent *event) {
        const QPointF eventPos = mapToScene(event->pos()) - QPoint(ActorSize, ActorSize)/2;
        if (event->button() == Qt::LeftButton) {
            QTimeLine *timer = new QTimeLine(AnimationPeriodMS, this);
            QGraphicsItemAnimation *animation = new QGraphicsItemAnimation(timer);
    
            connect(timer, SIGNAL(finished()), SLOT(onAnimationFinished()));
    
            animation->setItem(&m_actor);
            animation->setTimeLine(timer);
            animation->setPosAt(1.0, eventPos);
    
            timer->start();
        }
        else if (event->button() == Qt::RightButton) {
            m_actor.setPos(eventPos);
        }
    }

    При событии от левой кнопки мыши выполняется плавное перемещение, которое будет длиться AnimationPeriodMS миллисекунд – для этого нужно создать QTimeLine и задать в нем соответствующие параметры. Затем создадим объект анимации – QGraphicsItemAnimation, обратите внимание, что в качестве родительского для него мы указываем таймер. Анимации устанавливается элемент, таймер и конечная точка (точки анимации нумеруются от нуля до единицы). Сигнал окончания работы таймера связывается со слотом onAnimationFinished для освобождения памяти (ведь память под таймер и анимацию была выделена динамически и это нельзя сделать на стеке, т.к. объекты существуют дольше, чем выполняется текущая функция).

    В слоте onAnimationFinished методом sender() мы получим отправителя сигнала – это таймер. Вызовем для него деструтор, но не сразу, а более безопасно – методом deleteLater (объект будет разрушен когда очередь входящих сигналов у него станет пустой, т.е. сначала он выполнит все, что от него хотели):

    void View::onAnimationFinished() {
        sender()->deleteLater();
    }

    Мы освободили память из под QTimeLine, а из под объекта анимации – нет, но она будет освобождена автоматически – именно для этого мы установили у нее таймер в качестве родительского класса.

  • #3854

    Desmond
    Участник

    Отличная статья, очень помогло.
    У меня пару вопросов. Почему Вы не разместили актера и сцену в динамической памяти(через new)? Так удобнее или безопаснее?
    И ещё, почему задаются размеры сцены и вида разные, и почему у них числа отрицательные? Можно было бы, наверное просто 0, 0, 400, 400. Так тоже работает.

    Кстати, про фичу с deleteLater и sender только узнал. Хорошая штука.
    Я пока в Qt относительно недавно, как и в самоом C++, так что, просто любопытствую с вопросами)

    И да, чуть не забыл. Этот класс устарел. Есть что-то новее для таких задач, как анимация перемещения объектов?

    • #3855

      По порядку:

      Почему Вы не разместили актера и сцену в динамической памяти(через new)? Так удобнее или безопаснее?

      Очень советую вам почитать книги Маерса, ну и просто подумать о том что такое динамическая память перед тем, как использовать. Во-первых (основное) трудно гарантировать корректное освобождение памяти. Во-вторых это связано с издержками. Частично проблему можно решить умными указателями (статья про unique_ptr), но зачем создавать самому себе проблемы?

      И ещё, почему задаются размеры сцены и вида разные, и почему у них числа отрицательные? Можно было бы, наверное просто 0, 0, 400, 400. Так тоже работает.

      Координаты могут быть любыми. Размер сцены вообще может меняться (без вашего ведома) во время работы программы если объект выходит за ее пределы. Мне просто удобнее когда после запуска ноль находится по центру экрана.

      По поводу устаревшего класса – спасибо, я не заметил как QGraphicsItemAnimation стал “не рекомендованным к использованию”. Я посмотрел – вроде как QPropertyAnimation стал более “мощным”.

Для ответа в этой теме необходимо авторизоваться.