在QTableView中显示动画图标的最佳方法是什么?

13

我已经苦恼了一段时间,但似乎找不到正确的方法来实现我的需求。

我想要的是能够使用一个动画图标作为某些项目的装饰(通常用于显示此特定项目的一些处理正在进行)。我有一个自定义表格模型,在QTableView中显示。

我的第一个想法是创建一个自定义委托来负责显示动画。当传递一个用于装饰角色的QMovie时,委托将连接到QMovie以在每次可用新帧时更新显示(请参见下面的代码)。然而,绘图器似乎在委托的paint方法调用后无法保持有效(在调用绘图器的save方法时出错,可能是因为指针不再指向有效的内存)。

另一个解决方案是在每次可用新帧时发出项目的dataChanged信号,但1)这会引起很多不必要的开销,因为数据实际上并没有真正改变;2)在模型级别处理电影看起来并不太干净:应该由显示层(QTableView或委托)负责处理新帧的显示。

有没有人知道在Qt视图中显示动画的干净(最好是有效的)方法?


对于那些感兴趣的人,这是我开发的委托的代码(目前不起作用)。

// 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 * >();
}

1
我在 QxtItemDelegate 中找到了一些相似的东西,它是对 QtItemDelegate 的扩展,可以绘制进度条(等等)。为此,该委托使用的方法与我提出的问题非常相似,但是它存储视图和索引而不是绘图工具;在每个计时器超时时,委托更新所有视图,最好只更新需要更新的项目。 - Luc Touraille
5个回答

9
最佳解决方案是在委托中使用QSvgRendererenter image description here 这种方法非常简单易行,与gif不同,SVG文件大小较小且支持透明度。
    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);
}

这里有一个不错的网站,您可以在其中生成自己的旋转图标并将其导出为SVG格式。


请注意,来自loading.io的大多数(全部?)动画SVG使用<animate>标签,而该标签似乎不受QSvgRenderer支持,而其他一些标签是受支持的(例如<animateColor> [在SVG中已弃用],<animateTransform>)。 Sam Herbert提供了一些受支持的SVG:https://samherbert.net/svg-loaders/。 - ypnos

6
记录一下,最终我使用了在委托的paint方法中调用QAbstractItemView::setIndexWidget的方式,插入一个显示QMovieQLabel到该项中(请参见下面的代码)。
这种解决方案效果很好,将显示问题与模型分离。不过,一个缺点是标签中新帧的显示会导致整个项再次渲染,从而几乎连续调用委托的paint方法...
为了减少这些调用所产生的开销,我试图通过重复使用现有的标签来处理委托中的电影,但是当调整窗口大小时,这会导致奇怪的行为:动画会向右移动,好像两个标签并排放置。
所以,这里提供了一种可能的解决方案,欢迎评论改进方法!
// 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 * >();
}

1
你能提供完整的代码吗?你是如何编写委托的?我之前发布了一个类似的问题,针对QListView,正在寻找解决方案。http://stackoverflow.com/questions/31812979/how-to-show-animated-icon-in-qlistview?noredirect=1#comment51562370_31812979 - zar
@zadane 我的答案中已经提供了委托的完整代码。你只需要通过调用 QAbstractItemView::setItemDelegate 或类似的函数来确保你的 QListView 使用这个委托即可。 - Luc Touraille
谢谢,现在编译通过了,但是没有产生动画。我将动画GIF文件设置为图标,但只有一个帧显示出来,没有动画效果。new QStandardItem(QIcon(":icons/Resources/connecting.gif"), "connecting"); 我需要做其他的事情吗? - zar
@zadane 我编写的MovieDelegate需要一个QMovie而不是一个QIcon。您必须确保当请求使用Qt::DecorationRole角色获取数据时,您的模型返回一个QMovie。您还必须根据自己的要求启动/停止动画(如果您想让动画持续滚动,只需在创建后启动即可)。 - Luc Touraille
你从QStandardItemModel派生了自己的模型类吗?我还没有在那个层面上使用过模型或者data函数,但这是我现在正在研究的东西。 - zar
显示剩余4条评论

4
在我的应用程序中,我有一个典型的旋转圆圈图标来指示表格中一些单元格的等待/处理状态。然而,我最终使用了一种不同于当前接受答案建议的方法,我的方法更简单,而且相对更高效(更新:当时另一个答案被设置为已接受 - 建议使用QAbstractItemView::setIndexWidget)。使用小部件似乎是过度杀伤力的,如果它们太多的话,会破坏性能。在我的解决方案中,所有功能都只在我的模型层(QAbstractItemModel的后代)类中实现。我不需要在视图或委托中进行任何更改。但是,我只在动画一个GIF,所有动画都是同步的。这是我简单方法的当前限制。
用于实现此行为的模型类需要具备以下内容:
- QImage向量 - 我使用QImageReader,它允许我读取所有动画帧,我将它们存储到QVector<QImage>中。 - 以动画GIF的周期性滴答的QTimer - 时间段通过QImageReader::nextImageDelay()获得。 - 当前帧的索引(int)(我假设所有动画单元格的帧都相同 - 它们是同步的;如果您想要不同步,则可以为每个单元格使用一个整数偏移量) - 一些知道哪些单元格应该被动画化并能够将单元格转换为QModelIndex的方法(这取决于您的自定义代码来实现,取决于您的特定需求) - 重写您的模型的QAbstractItemModel::data()部分以响应任何动画单元格(QModelIndex)的Qt::DecorationRole,并返回当前帧作为QImage - 由QTimer::timeout信号触发的槽
关键部分是响应计时器的槽。它必须执行以下操作:
  1. 增加当前帧,例如 m_currentFrame = (m_currentFrame + 1) % m_frameImages.size();

  2. 获取需要进行动画的单元格的索引列表(例如 QModelIndexList getAnimatedIndices();)。getAnimatedIndices() 函数的实现由您自己开发 - 可以使用暴力查询模型中的所有单元格或一些聪明的优化方法...

  3. 为每个需要进行动画的单元格发出 dataChanged() 信号,例如 for (const QModelIndex &idx : getAnimatedIndices()) emit dataChanged(idx, idx, {Qt::DecorationRole});

就是这样。我估计,根据确定哪些索引需要进行动画的函数的复杂程度,整个实现可能会有15到25行左右,无需更改视图或委托,只需更改模型。


你能提供一个这种方法的工作示例吗?我已经找了很多,这是我最信服的一个(唯一让我不信服的部分是不能使用动画svg - 在多屏幕配置和轻量级方面非常好)。 - laurapons

2

一种解决方案是使用带有GIF的QMovie。 我也尝试使用SVG(它很轻便并支持透明度),但是QMovie和QImageReader似乎都不支持动画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();
            }
        }
    }
}

非常不错的解决方案。我已经寻找了很长时间才找到它。谢谢。 - oscarz

0

我编写了一个基于QMovie的解决方案,用于在QListView/QTableView中动画显示单个项目,当它们可见时(使用案例是在聊天程序中的消息中使用动画gif)。该解决方案类似于另一个答案中的QSvgRenderer解决方案,但它使用QMovie,并添加了一个当前可见索引的“映射”,每个都有一个QMovie。请参见提交https://github.com/KDE/ruqola/commit/49015e2aac118fd97b7327a55c19f2e97f37b1c9https://github.com/KDE/ruqola/commit/2b358fb0471f795289f9dc13c256800d73accae4


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接