Каков наилучший способ отображения анимированного значка в QTableView?

Я уже несколько раз боролся с этим, и я не могу найти правильный способ сделать это.

Я хотел бы использовать возможность использовать анимированный значок в качестве украшения для некоторых моих предметов (как правило, чтобы показать, что для этого конкретного объекта существует некоторая обработка). У меня есть пользовательская модель таблицы, которую я показываю в 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

Лучшее решение - использовать QSvgRenderer внутри делегата.

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

Ответ 2

Для записи я закончил использование 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 * >();
}

Ответ 3

В моем приложении у меня есть типичный значок вращающегося круга, чтобы указать состояние ожидания/обработки для некоторых ячеек в таблице. Однако я в конечном итоге использовал подход, который отличается от предложенного в принятом в настоящее время ответе, мой, на мой взгляд, проще и несколько более эффективен. Использование виджетов кажется излишним, что может привести к снижению производительности, если их слишком много. Все функциональные возможности в моем решении реализованы только в моем слое модели (потомок класса QAbstractItemModel). Мне не нужно вносить никаких изменений в мнение ни делегата. Однако я только анимирую один GIF и все анимации синхронизированы. Это текущее ограничение моего простого подхода.

Класс модели, который используется для реализации этого поведения, должен иметь следующее:

  • вектор QImage - я использую QImageReader, который позволяет мне читать все кадры анимации, я сохраняю их в QVector<QImage>

  • a QTimer тикает с периодичностью анимированного GIF - период времени получается с помощью QImageReader::nextImageDelay().

  • индекс (int) текущего кадра (я полагаю, что кадр одинаков для всех анимированных ячеек - они синхронизированы; если вы хотите не синхронизироваться, то вы можете использовать целочисленное смещение для каждой из них)

  • некоторые знания о том, какие ячейки должны быть анимированы, и возможность перевода ячейки в QModelIndex (это зависит от вашего пользовательского кода для реализации этого, зависит от ваших конкретных потребностей)

  • переопределить QAbstractItemModel::data() часть вашей модели, чтобы ответить на Qt::DecorationRole для любой анимированной ячейки (QModelIndex) и вернуть текущий кадр как 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 строк, без необходимости изменять представление или делегировать, только модель.

Ответ 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();
            }
        }
    }
}