Как лучше всего отобразить анимированный значок в 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 * >();
}

person Luc Touraille    schedule 05.12.2010    source источник
comment
Я нашел нечто похожее в QxtItemDelegate, расширении к QtItemDelegate, которое позволяет рисовать индикаторы выполнения ( между прочим). Для этого этот делегат использует подход, очень похожий на тот, который был предложен в моем вопросе, но он хранит представления и индексы вместо художников; при каждом тайм-ауте таймера делегат обновляет все представления, предпочтительно только те элементы, которые нуждаются в обновлении.   -  person Luc Touraille    schedule 09.12.2010


Ответы (5)


Лучшее решение — использовать 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.

person Soyal7    schedule 29.09.2019
comment
Обратите внимание, что большинство (все?) анимированных SVG от loading.io используют тег <animate>, который, похоже, не поддерживается QSvgRenderer, в то время как другие поддерживаются (например, <animateColor> [устарело в SVG], <animateTransform>). Некоторые поддерживаемые SVG предоставлены Сэмом Гербертом: samherbert.net/svg-loaders - person ypnos; 07.04.2020

Для справки, в итоге я использовал 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 * >();
}
person Luc Touraille    schedule 09.12.2010
comment
Можете ли вы предоставить полный код, как вы написали делегат. Я разместил аналогичный вопрос для QListViewи искал решение. stackoverflow .com/questions/31812979/ - person zar; 05.08.2015
comment
@zadane Полный код делегата уже указан в моем ответе. Вам просто нужно убедиться, что ваш QListView использует этот делегат, вызвав QAbstractItemView::setItemDelegate или аналогичную функцию. - person Luc Touraille; 05.08.2015
comment
Я создаю тот же класс, что и вы, но я не могу создать экземпляр делегата movieDelegate = new MovieDelegate( this );, выдает ошибку C:\code\svn2\Sync\mainwindow.cpp:72: error: C2664: 'MovieDelegate::MovieDelegate(QAbstractItemView &,QObject *)' : cannot convert parameter 1 from 'MainWindow *const ' to 'QAbstractItemView &' Мое содержащее представление - MainWindow - person zar; 05.08.2015
comment
@zadane Конструктор MovieDelegate принимает дополнительный параметр, вам нужно передать представление, которому он будет назначен (ваш QListView): movieDelegate = new MovieDelegate(*ui.myListView, this) или что-то в этом роде.. - person Luc Touraille; 05.08.2015
comment
спасибо, теперь компилируется, но не создает анимацию. Я устанавливаю анимированный gif-файл в качестве значка, и появляется только один кадр, без анимации. new QStandardItem( QIcon( ":icons/Resources/connecting.gif"), "connecting); нужно ли мне что-то еще делать? - person zar; 05.08.2015
comment
Внутри функции делегата paint() управление переходит в if ( ! movie ) { view_.setIndexWidget(index, NULL); } так что кажется, что фильм вообще не запускается. - person zar; 05.08.2015
comment
@zadane MovieDelegate, которое я написал, ожидает QMovie, а не QIcon. Вы должны убедиться, что ваша модель возвращает QMovie при запросе данных с ролью Qt::DecorationRole. Вы также должны убедиться, что фильм запускается/останавливается в соответствии с вашими требованиями (если вы хотите, чтобы анимация продолжалась, вы можете просто запустить ее после ее создания). - person Luc Touraille; 08.08.2015
comment
@zadane Вот несколько фрагментов кода моей модели. Я создал фильмы в конструкторе: movies_.push_back(new QMovie(":/images/ajax-loader.gif")),. Затем в функции data я вернул соответствующий фильм: case Qt::DecorationRole: return QVariant::fromValue(movies_[idx]);. Чтобы это работало, вы должны зарегистрировать QMovie в системе метатипов Qt: Q_DECLARE_METATYPE(QMovie *). - person Luc Touraille; 08.08.2015
comment
вы получили свой собственный класс модели от QStandardItemModel? Я не использовал модель на этом уровне или функцию data, но сейчас я изучаю это. - person zar; 12.08.2015

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

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

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

  • 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 строк без необходимости изменять представление или делегировать, только модель.

person V.K.    schedule 25.03.2019

Одним из решений является использование 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();
            }
        }
    }
}
person Soyal7    schedule 14.09.2019
comment
очень достойное решение. Я долго искал, чтобы найти это. Спасибо - person oscarz; 29.12.2020

Я написал решение на основе QMovie для анимации отдельных элементов в QListView/QTableView, когда они видны (вариант использования — анимированные gif-файлы в сообщениях, в программе чата). Решение похоже на решение QSvgRenderer в другом ответе, но оно использует QMovie и добавляет «карту» видимых в данный момент индексов с QMovie (каждым). См. коммиты https://github.com/KDE/ruqola/commit/49015e2aac118fd97b797b>c515c197b2e иhttps://github.com/KDE/ruqola/commit/2b358fb0471f795289f9dc13c2568040d73ac2568040d73ace.ca>

person David Faure    schedule 01.03.2020