Thursday, July 16, 2009

Background animation for Qt-Widgets

Sometimes you want to load data from a specific datasource (e.g. a database or the internet), but let the user work on with your application while it is loading. When doing this, it is essential that the user can see that data is being loaded and immediately notice when loading has finished.


In my situation it is a table view that gets populated with entries and then validates every entry, which can take quite some time. So I wanted to show a "loading" animation in the background of the table, since this is unintrusive, but still easily notable. I created an animation using ajaxload and saved it as Load.gif in my application's directory.


Enabling GIF-support in Qt


The first hurdle you have to take is getting gif-support in your Qt installation. In order to do so, you need to pass -qt-gif to configure and have the qgif library (libqtgif.so or qtgif.dll) in the subdirectory imageformats in the Qt plug-in directory. I'm not getting into more detail here, since it's not the main topic of this post.


Drawing directly on your widget


Next you want to be able to draw directly on the background of your widget. You do this using the paintEvent() and a QPainter. Here's a very minimal example how to do it:


Header:
#include <QWidget>

class TsTestWidget: public QWidget
{
Q_OBJECT
public:
virtual void paintEvent(QPaintEvent *event);
};

Implementation:
#include "Test.h"

#include <QApplication>
#include <QPainter>

void TsTestWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.drawRect(10, 10, 50, 50);
}


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

TsTestWidget test;
test.resize(100, 100);
test.show();

app.exec();
}

Result:


Animating the background


The second step is animating this gif. Qt has the QMovie class for this. QMovie is not a widget, it's just managing the animation's frames and timers and provides you the current frame's pixmap. It's frameChanged(int) signal is emitted whenever the next frame should be displayed, usually using a QLabel. In our case we don't use a QLabel to display it, but directly paint it on the widget as we did in the previous step. The tricky part is that paintEvent() is only called when an external event happend that requires repainting of the widget. So we use the frameChanged(int) signal and repaint the widget every time the current frame has changed. Here's a simple example:


Header:
#include <QWidget>
#include <QMovie>

class TsTestWidget: public QWidget
{
Q_OBJECT
public:
TsTestWidget();
virtual void paintEvent(QPaintEvent *event);
private:
QMovie m_movie;
private slots:
void paintNewFrame(int);
};

Implementation:
#include "Test.h"

#include <QApplication>
#include <QPainter>
#include <QPaintEvent>

TsTestWidget::TsTestWidget():
m_movie(qApp->applicationDirPath() + "/Load.gif")
{
connect(
&m_movie,
SIGNAL(frameChanged(int)),
this,
SLOT(paintNewFrame(int)));
m_movie.start();
}

void TsTestWidget::paintEvent(QPaintEvent *event)
{
// First we extract the current frame
QPixmap currentFrame = m_movie.currentPixmap();

QRect frameRect = currentFrame.rect();

// Only redraw when the frame is in the invalidated area
frameRect.moveCenter(rect().center());
if (frameRect.intersects(event->rect()))
{
QPainter painter(this);
painter.drawPixmap(
frameRect.left(),
frameRect.top(),
currentFrame);
}
}

void TsTestWidget::paintNewFrame(int)
{
repaint();
}

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

TsTestWidget test;
test.resize(100, 100);
test.show();

app.exec();
}

Result:


Item views are special


I first tried doing this on a QTableView and failed, frustrated. There is one tricky thing about QAbstractItemViews: You don't paint on the widgets, but on the viewport(), because they're derived from QAbstractScrollArea. Because I didn't immediately realize this, I always got a confusing error message when creating a QPainter in the paintEvent() using the this-pointer. That's because I need to create the painter for the viewport() widget. After finding this out, it became straightforward implementing the animation in the QTableView, just replacing this with viewport().



More re-usable approach


So here we are, having our very own background animated widget, even supporting item views. I could stop this post right here and leave you alone with implementing it into your favorite widgets. But there' still one thing that bugs me: You have to do this for every widget you want an animated background in. When you're using QListView, QTableView and QTreeView in your application, you have to extend all three classes with this functionality, which is error-prone and, bluntly put, lame.


So here's my solution how you can minimize the code you have to write for each new widget-class you want to integrate it into. We create a helper class called "TsBackgroundAnimation". This class does all the work and thanks to multiple inheritance, you just have to derive a class from your Widget and TsBackgroundAnimation, override paintEvent() and call TsBackgroundAnimation's paintAnimation()-function there. There's a a "workaround" we have to make here. Qt does not support deriving from multiple QObject-decendants, hence TsBackgroundAnimation may not derive from QObject. This means that TsBackgroundAnimation can not have slots and it's impossible for it to connect the frameChanged(int)-event to a slot that repaints the widget. So instead of QMovie a TsMovie class is used that provides a frameChanged()-event without the int parameter. That way TsBackgroundAnimation can connect that signal to the slot repaint() of the widget. This made the final implementation a bit larger, so I did not directly paste the sourcecode here. You can download the complete project from here:


BackgroundAnimation.zip

BackgroundAnimation.tar.bz2

And here's a screenshot of the final result:

3 comments:

  1. Thanks mate,

    your experience helped me a lot to get through.
    I just had to move a couple of methods from TsBackgroundAnimation to TsAnimatedMyWidget and everything worked fine after that.

    see you,
    Kostya

    ReplyDelete
  2. That's just because I need a bit different effect rather than animation at the middle

    ReplyDelete
  3. Great post! Thank you! It was very usefull!

    ReplyDelete