【问题标题】:What is the best way to display an animated icon in a QTableView?在 QTableView 中显示动画图标的最佳方式是什么?
【发布时间】:2011-05-20 16:02:54
【问题描述】:

我已经为此苦苦挣扎了一段时间,但似乎找不到正确的方法。

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

我的第一个想法是创建一个负责显示动画的自定义委托。当为装饰角色传递QMovie 时,委托将连接到QMovie,以便在每次有新框架可用时更新显示(参见下面的代码)。但是,在调用委托的paint 方法后,painter 似乎不再有效(调用painter 的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 * >();
}

【问题讨论】:

  • 我在QxtItemDelegate 中发现了一些非常相似的东西,它是QtItemDelegate 的扩展,它可以绘制进度条(除其他外)。为此,该委托使用的方法与我在问题中提出的方法非常相似,但它存储视图和索引而不是画家;在计时器每次超时时,委托更新所有视图,最好只更新需要更新的项目。

标签: c++ qt qtableview qabstracttablemodel


【解决方案1】:

在我的应用程序中,我有一个典型的旋转圆圈图标来指示表格中某些单元格的等待/处理状态。但是我最终使用了一种方法,该方法与当前接受的答案中建议的方法不同,在我看来,我的方法更简单且性能更高(更新:我在将不同的答案设置为已接受时写了这个 - 建议使用QAbstractItemView::setIndexWidget)。使用小部件似乎是一种过度杀伤力,如果它们太多会破坏性能。我的解决方案中的所有功能仅在我的模型层(QAbstractItemModel 的后代)类中实现。我不需要对视图或委托进行任何更改。然而,我只为一个 GIF 制作动画,并且所有动画都是同步的。这是我的简单方法目前的局限性。

用于实现此行为的模型类需要具有以下内容:

  • QImages 的向量 - 我使用 QImageReader,它允许我读取所有动画帧,我将它们存储到 QVector&lt;QImage&gt;

  • a QTimer 与动画 GIF 的周期性滴答作响 - 使用 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 &amp;idx : getAnimatedIndices()) emit dataChanged(idx, idx, {Qt::DecorationRole});

仅此而已。我估计,根据您确定哪些索引动画的函数的复杂性,整个实现可能有 15 到 25 行,无需更改视图或委托,只需更改模型即可。

【讨论】:

    【解决方案2】:

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

    【讨论】:

      【解决方案3】:

      最好的解决方案是在委托中使用QSvgRenderer

      它很容易实现,并且与 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);
      }
      

      Here's 一个不错的网站,您可以在其中生成自己的旋转图标并以 SVG 格式导出。

      【讨论】:

      • 请注意,来自 loading.io 的大多数(全部?)动画 SVG 使用 &lt;animate&gt; 标记,QSvgRenderer 似乎不支持该标记,而其他(例如 &lt;animateColor&gt; [已弃用) SVG],&lt;animateTransform&gt;)。 Sam Herbert 提供了一些支持的 SVG:samherbert.net/svg-loaders
      【解决方案4】:

      一种解决方案是将 QMovie 与 GIF 结合使用。 我也尝试过使用 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();
                  }
              }
          }
      }
      

      【讨论】:

      • 非常不错的解决方案。我一直在寻找很长时间才能找到这个。谢谢
      【解决方案5】:

      为了记录,我最终在我的委托的paint 方法中使用QAbstractItemView::setIndexWidget,在项目中插入一个QLabel,显示QMovie(参见下面的代码)。

      此解决方案效果很好,并将显示问题与模型分开。一个缺点是在标签中显示新框架会导致整个项目再次被渲染,导致几乎连续调用委托的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 * >();
      }
      

      【讨论】:

      • 能否提供完整的代码,委托是怎么写的。我为QListView发布了类似的问题并寻找解决方案。 stackoverflow.com/questions/31812979/…
      • @zadane 我的回答中已经提供了代表的完整代码。您只需要通过调用QAbstractItemView::setItemDelegate 或类似函数来确保您的QListView 使用此委托。
      • 我像你一样创建了相同的类,但我无法实例化委托 movieDelegate = new MovieDelegate( this ); 给出错误 C:\code\svn2\Sync\mainwindow.cpp:72: error: C2664: 'MovieDelegate::MovieDelegate(QAbstractItemView &amp;,QObject *)' : cannot convert parameter 1 from 'MainWindow *const ' to 'QAbstractItemView &amp;' 我的包含视图是 MainWindow
      • @zadane MovieDelegate 构造函数需要一个附加参数,您需要传递将分配给的视图(您的QListView):movieDelegate = new MovieDelegate(*ui.myListView, this),或类似的东西..跨度>
      • thanks 现在可以编译,但它不会产生动画。我将动画 gif 文件设置为图标,只显示一帧,没有动画。 new QStandardItem( QIcon( ":icons/Resources/connecting.gif"), "connecting);我还需要做什么吗?
      猜你喜欢
      • 2014-06-07
      • 2012-01-10
      • 2010-10-19
      • 1970-01-01
      • 2010-09-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多