¿Cuál es la mejor manera de mostrar un icono animado en un QTableView?
-
08-10-2019 - |
Pregunta
He estado luchando con esto por algunas veces, y me parece que no puede encontrar la manera correcta de hacerlo.
Lo que me gustaría es la capacidad de utilizar un icono animado como una decoración para algunos de mis artículos (por lo general para mostrar que algún tipo de procesamiento se está produciendo en este caso particular). Tengo un modelo de tabla personalizada, que puedo mostrar en un QTableView
.
Mi primera idea era crear un delegado a medida que se haría cargo de visualización de la animación. Cuando se transmite un QMovie
para el papel de la decoración, el delegado se conectaría a la QMovie
con el fin de actualizar la pantalla cada vez que un nuevo marco está disponible (ver código de abajo). Sin embargo, el pintor no parece seguir siendo válida después de la llamada al método paint
del delegado (me da un error al llamar método save
del pintor, probablemente debido a que el puntero apunta ya no a memoria válida).
Otra solución sería para emitir la señal de dataChanged
del tema cada vez que un nuevo marco está disponible, pero 1) que induciría muchos sobrecarga innecesaria, ya que los datos no se ha cambiado realmente; 2) no parece muy limpia para manejar la película en el nivel del modelo: debe ser responsabilidad de la capa de presentación (QTableView
o el delegado) para controlar la presentación de nuevos marcos
¿Alguien sabe de una manera limpia (y preferiblemente eficiente) a la animación en pantalla vistas Qt?
Para los interesados, aquí está el código del delegado desarrollé (que no funciona en este momento).
// 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 * >();
}
Solución
La mejor solución es utilizar QSvgRenderer dentro delegado.
Es muy fácil de implementar y, a diferencia del GIF, SVG es ligero y soportes transparencia.
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);
}
Aquí es un buen sitio web donde se puede generar su propio icono hilado y de exportación en SVG.
Otros consejos
Para el registro, que terminé usando QAbstractItemView::setIndexWidget
de dentro del método paint
de mi delegado, para insertar un QLabel
mostrando el QMovie
dentro del artículo (ver código de abajo).
Esta solución funciona bastante bien, y mantener los problemas de visualización separados del modelo. Un inconveniente es que la presentación de un nuevo marco en la etiqueta hace que todo el elemento que pasarán a ser de nuevo, dando lugar a las llamadas casi continuas a paint
método del delegado ...
Para reducir la sobrecarga inccured por estas llamadas, me trató de minimizar el trabajo realizado para la manipulación de las películas en el delegado mediante la reutilización de la etiqueta existente, si es que existe. Sin embargo, esto resulta en un comportamiento extraño cuando cambiar el tamaño de las ventanas:. La animación se desplaza hacia la derecha, como si dos etiquetas se colocan al lado del otro
Así que bueno, aquí es una posible solución, no dude en comentar sobre las formas de mejorarlo!
// 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 * >();
}
En mi solicitud, que tiene un icono típico círculo giratorio para indicar una espera / procesamiento de estado para algunas de las celdas de una tabla. Sin embargo Terminé usando un enfoque, que es diferente de la que se sugiere en la respuesta aceptada actualmente, la mía es mi punto de vista más simple y algo más performante. El uso de widgets parece como una exageración que destruirá el rendimiento si hay demasiados de ellos. Toda la funcionalidad de mi solución sólo se implementa en mi capa del modelo (descendiente de QAbstractItemModel
) clase. No necesito hacer ningún cambio en la vista ni el delegado. Estoy sin embargo, sólo la animación de un GIF y todas las animaciones están sincronizados. Esta es la limitación de la corriente de mi enfoque simple.
La clase de modelo que se utiliza para poner en práctica este necesidades de comportamiento que tienen los siguientes:
-
el vector de
QImage
s - Yo usoQImageReader
, que me permite leer todos los cuadros de animación, que los guarde en unQVector<QImage>
-
a
QTimer
marcando con la periodicidad de la GIF animados -. Se obtiene el período de tiempo usandoQImageReader::nextImageDelay()
-
el índice (int) del marco actual (supongo que la trama es la misma para todas las celdas de animación - que son sincronizados, si quieres no sincronizados, puede utilizar un desplazamiento para cada uno de ellos entero)
-
algún conocimiento de que las células deben ser animados y capacidad de traducir la célula para
QModelIndex
(esto es hasta el código personalizado para implementar esto, dependerá de sus necesidades específicas) -
anulación
QAbstractItemModel::data()
parte de su modelo de responder aQt::DecorationRole
para cualquier celda de animación (QModelIndex
) y devolver el cuadro actual comoQImage
-
una ranura que se activa por la señal de
QTimer::timeout
La parte clave es la ranura que reacciona al temporizador. Se debe hacer esto:
1) Aumentar la trama actual, por ejemplo, m_currentFrame = (m_currentFrame + 1) % m_frameImages.size();
2) Obtener la lista de índices (por ejemplo QModelIndexList getAnimatedIndices();
) de las células que tienen que ser animada. Este código de getAnimatedIndices()
depende de usted para desarrollar - el uso de la fuerza bruta consultar todas las células de su modelo o alguna optimización inteligente ...
3) señal dataChanged()
Emit para cada celda de animación, por ejemplo, for (const QModelIndex &idx : getAnimatedIndices()) emit dataChanged(idx, idx, {Qt::DecorationRole});
Eso es todo. Calculo que en función de la complejidad de sus funciones para determinar qué índices están animados, toda la aplicación puede tener algo como 15 a 25 líneas, sin necesidad de alterar la vista ni delegado, sólo el modelo.
Una solución es utilizar QMovie con el GIF. También probé usando SVG (que es ligero y ofrece soporte para transparencia), pero no parecen tanto QMovie y QImageReader al apoyo de animación 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();
}
}
}
}