I've been struggling with this for some times now, and I can't seem to find the right way to do this.
What I would like is the ability to use an animated icon as a decoration for some of my items (typically to show that some processing is occuring for this particular item). I have a custom table model, that I display in a QTableView
.
My first idea was to create a custom delegate that would take care of displaying the animation. When passed a QMovie
for the decoration role, the delegate would connect to the QMovie
in order to update the display every time a new frame is available (see code below). However, the painter does not seem to remain valid after the call to the delegate's paint
method (I get an error when calling the painter's save
method, probably because the pointer no longer points to valid memory).
Another solution would be to emit the dataChanged
signal of the item every time a new frame is available, but 1) that would induce many unnecessary overhead, since the data is not really changed; 2) it does not seem really clean to handle the movie at the model level: it should be the responsibility of the display tier (QTableView
or the delegate) to handle the display of new frames.
Does anyone know a clean (and preferably efficient) way to display animation in Qt views?
For those interested, here is the code of the delegate I developped (which does not work at the moment).
// Class that paints movie frames every time they change, using the painter
// and style options provided
class MoviePainter : public QObject
{
Q_OBJECT
public: // member functions
MoviePainter( QMovie * movie,
QPainter * painter,
const QStyleOptionViewItem & option );
public slots:
void paint( ) const;
private: // member variables
QMovie 开发者_运维知识库 * movie_;
QPainter * painter_;
QStyleOptionViewItem option_;
};
MoviePainter::MoviePainter( QMovie * movie,
QPainter * painter,
const QStyleOptionViewItem & option )
: movie_( movie ), painter_( painter ), option_( option )
{
connect( movie, SIGNAL( frameChanged( int ) ),
this, SLOT( paint( ) ) );
}
void MoviePainter::paint( ) const
{
const QPixmap & pixmap = movie_->currentPixmap();
painter_->save();
painter_->drawPixmap( option_.rect, pixmap );
painter_->restore();
}
//-------------------------------------------------
//Custom delegate for handling animated decorations.
class MovieDelegate : public QStyledItemDelegate
{
Q_OBJECT
public: // member functions
MovieDelegate( QObject * parent = 0 );
~MovieDelegate( );
void paint( QPainter * painter,
const QStyleOptionViewItem & option,
const QModelIndex & index ) const;
private: // member functions
QMovie * qVariantToPointerToQMovie( const QVariant & variant ) const;
private: // member variables
mutable std::map< QModelIndex, detail::MoviePainter * > map_;
};
MovieDelegate::MovieDelegate( QObject * parent )
: QStyledItemDelegate( parent )
{
}
MovieDelegate::~MovieDelegate( )
{
typedef std::map< QModelIndex, detail::MoviePainter * > mapType;
mapType::iterator it = map_.begin();
const mapType::iterator end = map_.end();
for ( ; it != end ; ++it )
{
delete it->second;
}
}
void MovieDelegate::paint( QPainter * painter,
const QStyleOptionViewItem & option,
const QModelIndex & index ) const
{
QStyledItemDelegate::paint( painter, option, index );
const QVariant & data = index.data( Qt::DecorationRole );
QMovie * movie = qVariantToPointerToQMovie( data );
// Search index in map
typedef std::map< QModelIndex, detail::MoviePainter * > mapType;
mapType::iterator it = map_.find( index );
// if the variant is not a movie
if ( ! movie )
{
// remove index from the map (if needed)
if ( it != map_.end() )
{
delete it->second;
map_.erase( it );
}
return;
}
// create new painter for the given index (if needed)
if ( it == map_.end() )
{
map_.insert( mapType::value_type(
index, new detail::MoviePainter( movie, painter, option ) ) );
}
}
QMovie * MovieDelegate::qVariantToPointerToQMovie( const QVariant & variant ) const
{
if ( ! variant.canConvert< QMovie * >() ) return NULL;
return variant.value< QMovie * >();
}
The best solution is to use QSvgRenderer within delegate.
It's very easy to implement and unlike gif, SVG is lightweight and supports transparency.
TableViewDelegate::TableViewDelegate(TableView* view, QObject* parent)
: QStyledItemDelegate(parent), m_view(view)
{
svg_renderer = new QSvgRenderer(QString{ ":/res/img/spinning_icon.svg" }, m_view);
connect(svg_renderer, &QSvgRenderer::repaintNeeded,
[this] {
m_view->viewport()->update();
});
}
void TableViewDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& index) const
{
QStyleOptionViewItem opt{ option };
initStyleOption(&opt, index);
if (index.column() == 0) {
if (condition)
{
// transform bounds, otherwise fills the whole cell
auto bounds = opt.rect;
bounds.setWidth(28);
bounds.moveTo(opt.rect.center().x() - bounds.width() / 2,
opt.rect.center().y() - bounds.height() / 2);
svg_renderer->render(painter, bounds);
}
}
QStyledItemDelegate::paint(painter, opt, index);
}
Here's a nice website where you can generate your own spinning icon and export in SVG.
For the record, I ended up using QAbstractItemView::setIndexWidget
from inside the paint
method of my delegate, to insert a QLabel
displaying the QMovie
inside the item (see code below).
This solution works quite nicely, and keep the display issues separated from the model. One drawback is that the display of a new frame in the label causes the entire item to be rendered again, resulting in almost continuous calls to the delegate's paint
method...
To reduce the overhead inccured by these calls, I tried to minimize the work done for handling movies in the delegate by reusing the existing label if there is one. However, this results in weird behavior when resizing the windows: the animation gets shifted to the right, as if two labels were positioned side by side.
So well, here is a possible solution, feel free to comment on ways to improve it!
// Declaration
#ifndef MOVIEDELEGATE_HPP
#define MOVIEDELEGATE_HPP
#include <QtCore/QModelIndex>
#include <QtGui/QStyledItemDelegate>
class QAbstractItemView;
class QMovie;
class MovieDelegate : public QStyledItemDelegate
{
Q_OBJECT
public: // member functions
MovieDelegate( QAbstractItemView & view, QObject * parent = NULL );
void paint( QPainter * painter,
const QStyleOptionViewItem & option,
const QModelIndex & index ) const;
private: // member functions
QMovie * qVariantToPointerToQMovie( const QVariant & variant ) const;
private: // member variables
mutable QAbstractItemView & view_;
};
#endif // MOVIEDELEGATE_HPP
// Definition
#include "movieDelegate.hpp"
#include <QtCore/QVariant>
#include <QtGui/QAbstractItemView>
#include <QtGui/QLabel>
#include <QtGui/QMovie>
Q_DECLARE_METATYPE( QMovie * )
//---------------------------------------------------------
// Public member functions
//---------------------------------------------------------
MovieDelegate::MovieDelegate( QAbstractItemView & view, QObject * parent )
: QStyledItemDelegate( parent ), view_( view )
{
}
void MovieDelegate::paint( QPainter * painter,
const QStyleOptionViewItem & option,
const QModelIndex & index ) const
{
QStyledItemDelegate::paint( painter, option, index );
const QVariant & data = index.data( Qt::DecorationRole );
QMovie * movie = qVariantToPointerToQMovie( data );
if ( ! movie )
{
view_.setIndexWidget( index, NULL );
}
else
{
QObject * indexWidget = view_.indexWidget( index );
QLabel * movieLabel = qobject_cast< QLabel * >( indexWidget );
if ( movieLabel )
{
// Reuse existing label
if ( movieLabel->movie() != movie )
{
movieLabel->setMovie( movie );
}
}
else
{
// Create new label;
movieLabel = new QLabel;
movieLabel->setMovie( movie );
view_.setIndexWidget( index, movieLabel );
}
}
}
//---------------------------------------------------------
// Private member functions
//---------------------------------------------------------
QMovie * MovieDelegate::qVariantToPointerToQMovie( const QVariant & variant ) const
{
if ( ! variant.canConvert< QMovie * >() ) return NULL;
return variant.value< QMovie * >();
}
In my application, I have a typical spinning circle icon to indicate a waiting/processing state for some of the cells in a table. However I ended up using an approach, which is different from the one suggested in the currently accepted answer, mine is in my view simpler and somewhat more performant (UPDATE: I wrote this when a different answer was set as accepted - the one suggesting using QAbstractItemView::setIndexWidget
). Using widgets seems as an overkill which will destroy the performance if there are too many of them. All the functionality in my solution is only implemented in my model layer (descendant of QAbstractItemModel
) class. I do not need to make any changes in the view nor the delegate. I am however only animating one GIF and all animations are synchronized. This is the current limitation of my simple approach.
The model class which is used to implement this behavior needs to have the following:
the vector of
QImage
s - I useQImageReader
, which allows me read all animation frames, I store them into aQVector<QImage>
a
QTimer
ticking with the periodicity of the animated GIF - the time period is obtained usingQImageReader::nextImageDelay()
.the index (int) of the current frame (I suppose the frame is the same for all animated cells - they are synchronized; if you want unsynchronized then you can use an integer offset for each of them)
some knowledge of which cells should be animated and ability to translate the cell to
QModelIndex
(this is up to your custom code to implement this, depend on your specific needs)override
QAbstractItemModel::data()
part of your model to respond toQt::DecorationRole
for any animated cell (QModelIndex
) and return the current frame as aQImage
a slot which is triggered by the
QTimer::timeout
signal
The key part is the slot which reacts to the timer. It must do this:
Increase the current frame, e.g.
m_currentFrame = (m_currentFrame + 1) % m_frameImages.size();
Get the list of indices (e.g.
QModelIndexList getAnimatedIndices();
) of the cells which have to be animated. This code ofgetAnimatedIndices()
is up to you to develop - use brute force querying all cells in your model or some clever optimization...emit
dataChanged()
signal for each animated cell, e.g.for (const QModelIndex &idx : getAnimatedIndices()) emit dataChanged(idx, idx, {Qt::DecorationRole});
Thats all. I estimate that depending on the complexity of your functions for determining which indices are animated, the whole implementation can have something like 15 to 25 lines, without need to alter the view nor delegate, just the model.
One solution is to use QMovie with GIF. I also tried using SVG (it's lightweight and offers support for transparency), but both QMovie and QImageReader don't seem to support animated SVG.
Model::Model(QObject* parent) : QFileSystemModel(parent)
{
movie = new QMovie{ ":/resources/img/loading.gif" };
movie->setCacheMode(QMovie::CacheAll);
movie->start();
connect(movie, &QMovie::frameChanged,
[this] {
dataChanged(index(0, 0), index(rowCount(), 0),
QVector<int>{QFileSystemModel::FileIconRole});
});
}
QVariant Model::data(const QModelIndex& index, int role) const
{
case QFileSystemModel::FileIconRole:
{
if (index.column() == 0) {
auto const path = QString{ index.data(QFileSystemModel::FilePathRole).toString() };
if (path.isBeingLoaded()){
return movie->currentImage();
}
}
}
}
I wrote a QMovie-based solution to animate individual items in a QListView/QTableView, when they are visible (the use case was animated gifs in messages, in a chat program). The solution is similar to the QSvgRenderer solution in another answer, but it uses QMovie and it adds a "map" of currently visible indexes with a QMovie (each). See commits https://github.com/KDE/ruqola/commit/49015e2aac118fd97b7327a55c19f2e97f37b1c9 and https://github.com/KDE/ruqola/commit/2b358fb0471f795289f9dc13c256800d73accae4.
精彩评论