Creating a ThumbnailProxyModel

The future AlbumWidget view will display a grid of thumbnails with the pictures attached to the selected Album. In Chapter 11Dividing Your Project and Ruling Your Code, we designed the gallery-core library to be agnostic of how a picture should be displayed: a Picture class contains only a mUrl field.

In other words, the generation of the thumbnails has to be done in gallery-desktop rather than gallery-core. We already have the PictureModel class that is responsible for retrieving the Picture information, so it would be great to be able to extend its behavior with the thumbnail data.

This is possible in Qt with the use of the QAbstractProxyModel class and its subclasses. The goal of this class is to process data from a base QAbstractItemModel (sorting, filtering, adding data, and so on) and present it to the view by proxying the original model. To take a database analogy, you can view it as a projection over a table.

The QAbstractProxyModel class has two subclasses:

  • The QIdentityProxyModel subclass proxies its source model without any modification (all the indexes match). This class is suitable if you want to transform the data() function.
  • The QSortFilterProxyModel subclass proxies its source model with the ability to sort and filter the passing data.

The former, QIdentityProxyModel, fits our requirements. The only thing we need to do is to extend the data() function with the thumbnail generation content. Create a new class named ThumbnailProxyModel. Here is the ThumbnailProxyModel.h file:

#include <QIdentityProxyModel> 
#include <QHash> 
#include <QPixmap> 
 
class PictureModel; 
 
class ThumbnailProxyModel : public QIdentityProxyModel 
{ 
public: 
    ThumbnailProxyModel(QObject* parent = 0); 
 
    QVariant data(const QModelIndex& index, int role) const override; 
    void setSourceModel(QAbstractItemModel* sourceModel) override; 
    PictureModel* pictureModel() const; 
 
private: 
    void generateThumbnails(const QModelIndex& startIndex, int count); 
    void reloadThumbnails(); 
 
private: 
   QHash<QString, QPixmap*> mThumbnails; 
 
}; 

This class extends QIdentityProxyModel and overrides a couple of functions:

  • The data() function to provide the thumbnail data to the client of ThumbnailProxyModel
  • The setSourceModel() function to register to signals emitted by sourceModel

The remaining custom functions have the following goals:

  • The pictureModel() is a helper function that casts the sourceModel to a PictureModel*
  • The generateThumbnails() function takes care of generating the QPixmap thumbnails for a given set of pictures
  • The reloadThumbnails() is a helper function that clears the stored thumbnails before calling generateThumbnails()

As you might have guessed, the mThumbnails class stores the QPixmap* thumbnails using the filepath for the key.

We now switch to the ThumbnailProxyModel.cpp file and build it from the ground up. Let's focus on generateThumbnails():

const unsigned int THUMBNAIL_SIZE = 350; 
... 
void ThumbnailProxyModel::generateThumbnails( 
                                            const QModelIndex& startIndex, int count) 
{ 
    if (!startIndex.isValid()) { 
        return; 
    } 
 
    const QAbstractItemModel* model = startIndex.model(); 
    int lastIndex = startIndex.row() + count; 
    for(int row = startIndex.row(); row < lastIndex; row++) { 
        QString filepath = model->data(model->index(row, 0),  
                                                   PictureModel::Roles::FilePathRole).toString(); 
        QPixmap pixmap(filepath); 
        auto thumbnail = new QPixmap(pixmap 
                                     .scaled(THUMBNAIL_SIZE, THUMBNAIL_SIZE, 
                                             Qt::KeepAspectRatio, 
                                             Qt::SmoothTransformation)); 
        mThumbnails.insert(filepath, thumbnail); 
    } 
} 

This function generates the thumbnails for a given range indicated by the parameters (startIndex and count). For each picture, we retrieve the filepath from the original model, using model->data(), and we generate a downsized QPixmap that is inserted in the mThumbnails QHash. Note that we arbitrarily set the thumbnail size using const THUMBNAIL_SIZE. The picture is scaled down to this size and respects the aspect ratio of the original picture.

Each time that an album is loaded, we should clear the content of the mThumbnails class and load the new pictures. This work is done by the reloadThumbnails() function:

void ThumbnailProxyModel::reloadThumbnails() 
{ 
    qDeleteAll(mThumbnails); 
    mThumbnails.clear(); 
    generateThumbnails(index(0, 0), rowCount()); 
} 

In this function, we simply clear the content of mThumbnails and call the generateThumbnails() function with parameters indicating that all the thumbnails should be generated. Let's see when these two functions will be used, in setSourceModel():

void ThumbnailProxyModel::setSourceModel(QAbstractItemModel* sourceModel) 
{ 
    QIdentityProxyModel::setSourceModel(sourceModel); 
    if (!sourceModel) { 
        return; 
    } 
 
    connect(sourceModel, &QAbstractItemModel::modelReset,  
                  [this] { 
        reloadThumbnails(); 
    }); 
 
    connect(sourceModel, &QAbstractItemModel::rowsInserted,  
                 [this] (const QModelIndex& parent, int first, int last) { 
        generateThumbnails(index(first, 0), last - first + 1); 
    }); 
} 

When the setSourceModel() function is called, the ThumbnailProxyModel class is configured to know which base model should be proxied. In this function, we register lambdas to two signals emitted by the original model:

  • The modelReset signal is triggered when pictures should be loaded for a given album. In this case, we have to completely reload the thumbnails.
  • The rowsInserted signal is triggered each time new pictures are added. At this point, generateThumbnails should be called to update mThumbnails with these newcomers.

Finally, we have to cover the data() function:

QVariant ThumbnailProxyModel::data(const QModelIndex& index, int role) const 
{ 
    if (role != Qt::DecorationRole) { 
        return QIdentityProxyModel::data(index, role); 
    } 
 
    QString filepath = sourceModel()->data(index,  
                                 PictureModel::Roles::FilePathRole).toString(); 
    return *mThumbnails[filepath]; 
} 

For any role that is not Qt::DecorationRole, the parent class data() is called. In our case, this triggers the data() function from the original model, PictureModel. After that, when data() must return a thumbnail, the filepath of the picture referenced by the index is retrieved and used to return the QPixmap object of mThumbnails. Luckily for us, QPixmap can be implicitly cast to QVariant, so we do not have anything special to do here.

The last function to cover in the ThumbnailProxyModel class is the pictureModel() function:

PictureModel* ThumbnailProxyModel::pictureModel() const 
{ 
    return static_cast<PictureModel*>(sourceModel()); 
} 

Classes that will interact with ThumbnailProxyModel will need to call some functions that are specific to PictureModel to create or delete pictures. This function is a helper to centralize the cast of the sourceModel to PictureModel*.

As a side note, we could have tried to generate thumbnails on-the-fly to avoid a possible initial bottleneck during the album loading (and the call to generateThumbnails()). However, data() is a const function, meaning that it cannot modify the ThumbnailProxyModel instance. This rules out any way of generating a thumbnail in the data() function and storing it in mThumbnails.

As you can see, QIdentityProxyModel, and more generally QAbstractProxyModel, are valuable tools to add behavior to an existing model without breaking it. In our case, this is enforced by design in so far as the PictureModel class is defined in gallery-core rather than gallery-desktop. Modifying PictureModel implies modifying gallery-core and potentially breaking its behavior for other users of the library. This approach lets us keep things cleanly separated.