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

发布于 2024-10-06 03:58:18 字数 3820 浏览 0 评论 0原文

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

我想要的是能够使用动画图标作为我的某些项目的装饰(通常是为了表明该特定项目正在进行某些处理)。我有一个自定义表格模型,显示在 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 * >();
}

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

自在安然 2024-10-13 03:58:18

最好的解决方案是在委托中使用 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);
}

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

The best solution is to use QSvgRenderer within delegate.

enter image description here

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.

最后的乘客 2024-10-13 03:58:18

根据记录,我最终使用了 QAbstractItemView::setIndexWidget从我的委托的 paint 方法内部插入一个 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 * >();
}

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 * >();
}
冷…雨湿花 2024-10-13 03:58:18

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

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

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

  • 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. emit dataChanged()每个动画单元格的信号,例如 for (const QModelIndex &idx : getAnimatedIndices()) 发出 dataChanged(idx, idx, {Qt::DecorationRole});

就这些。我估计,根据用于确定哪些索引进行动画处理的函数的复杂性,整个实现可能有 15 到 25 行,不需要更改视图或委托,只需更改模型。

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 QImages - I use QImageReader, which allows me read all animation frames, I store them into a QVector<QImage>

  • a QTimer ticking with the periodicity of the animated GIF - the time period is obtained using QImageReader::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 to Qt::DecorationRole for any animated cell (QModelIndex) and return the current frame as a QImage

  • 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:

  1. Increase the current frame, e.g. m_currentFrame = (m_currentFrame + 1) % m_frameImages.size();

  2. Get the list of indices (e.g. QModelIndexList getAnimatedIndices();) of the cells which have to be animated. This code of getAnimatedIndices() is up to you to develop - use brute force querying all cells in your model or some clever optimization...

  3. 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.

冷心人i 2024-10-13 03:58:18

一种解决方案是将 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();
            }
        }
    }
}

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();
            }
        }
    }
}
烟─花易冷 2024-10-13 03:58:18

我编写了一个基于 QMovie 的解决方案,用于在 QListView/QTableView 中的各个项目可见时对它们进行动画处理(用例是消息中的动画 GIF、聊天程序中的 GIF)。该解决方案类似于另一个答案中的 QSvgRenderer 解决方案,但它使用 QMovie 并添加当前可见索引的“地图”和 QMovie(每个)。查看提交 https://github.com/KDE/ruqola/commit/49015e2aac118fd97b7327a55c19f2e97 f37b1c9https://github.com/KDE/ruqola/commit/2b358fb0471f795289f9dc13c256800d73accae4

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文