Loading images with an ImageProvider

It is now time to display the thumbnails for our freshly persisted album. These thumbnails have to be loaded somehow. Because our application is targeted at mobile devices, we cannot afford to freeze the UI thread while loading thumbnails. We would either hog the CPU or be killed by the OS, neither of which are desirable destinies for gallery-mobile. Qt provides a very handy class to handle the image loading: QQuickImageProvider.

The QQuickImageProvider class provides an interface to load the QPixmap class in your QML code in an asynchronous manner. This class automatically spawns threads to load the QPixmap class and you simply have to implement the function requestPixmap(). There is more to it, QQuickImageProvider caches by default the requested pixmap to avoid hitting the data source too much.

Our thumbnails must be loaded from the PictureModel element, which gives access to the fileUrl of a given Picture. Our implementation of rQQuickImageProvider will have to get the QPixmap class for a row index in PicturelModel. Create a new C++ class named PictureImageProvider, and modify PictureImageProvider.h like this:

#include <QQuickImageProvider> 
 
class PictureModel; 
 
class PictureImageProvider : public QQuickImageProvider 
{ 
public: 
 
    PictureImageProvider(PictureModel* pictureModel); 
 
    QPixmap requestPixmap(const QString& id, QSize* size,  
            const QSize& requestedSize) override; 
 
private: 
    PictureModel* mPictureModel; 
}; 

A pointer to the PictureModel element has to be provided in the constructor to be able to retrieve fileUrl. We override requestPixmap(), which takes an id parameter in its parameters list (the size and requestedSize can be safely ignored for now). This id parameter will be provided in the QML code when we want to load a picture. For a given Image in QML, the PictureImageProvider class will be called like so:

Image { source: "image://pictures/" + index } 

Let's break it down:

  • image: This is the scheme for the URL source of the image. This tells Qt to work with an image provider to load the image.
  • pictures: This is the identifier of the image provider. We will link the PictureImageProvider class and this identifier at the initialization of  QmlEngine in main.cpp.
  • index: This is the ID of the image. Here it is the row index of the picture. This corresponds to the id parameter in requestPixmap().

We already know that we want to display a picture in two modes: thumbnail and full resolution. In both cases, a QQuickImageProvider class will be used. These two modes have a very similar behavior: they will request PictureModel for fileUrl and return the loaded QPixmap.

There is a pattern here. We can easily encapsulate these two modes in PictureImageProvider. The only thing we have to know is when the caller wants a thumbnail or a full resolution QPixmap. This can be easily done by making the id parameter more explicit.

We are going to implement the requestPixmap() function to be able to be called in two ways:

  • images://pictures/<index>/full: Using this syntax to retrieve the full resolution picture
  • images://pictures/<index>/thumbnail: Using this syntax to retrieve the thumbnail version of the picture

If the index value was 0, these two calls would set the ID to 0/full or 0/thumbnail in requestPixmap(). Let's see the implementation in PictureImageProvider.cpp:

#include "PictureModel.h" 
 
PictureImageProvider::PictureImageProvider(PictureModel* pictureModel) : 
    QQuickImageProvider(QQuickImageProvider::Pixmap), 
    mPictureModel(pictureModel) 
{ 
} 
 
QPixmap PictureImageProvider::requestPixmap(const QString& id, QSize* /*size*/, const QSize& /*requestedSize*/) 
{ 
    QStringList query = id.split('/'); 
    if (!mPictureModel || query.size() < 2) { 
        return QPixmap(); 
    } 
 
    int row = query[0].toInt(); 
    QString pictureSize = query[1]; 
 
    QUrl fileUrl = mPictureModel->data(mPictureModel->index(row, 0),       PictureModel::Roles::UrlRole).toUrl(); 
    return ?? // Patience, the mystery will be soon unraveled 
} 

We start by calling the QQuickImageProvider constructor with the QQuickImageProvider::Pixmap parameter to configure QQuickImageProvider to call requestPixmap(). The QQuickImageProvider constructor supports various image types (QImageQPixmapQSGTextureQQuickImageResponse) and each one has its specific requestXXX() function.

In the requestPixmap() function, we start by splitting this ID with the / separator. From here, we retrieve the row values and the desired pictureSize. The fileUrl is loaded by simply calling the mPictureModel::data() function with the right parameters. We used the exact same call in Chapter 12Conquering the Desktop UI.

Great, we know which fileUrl should be loaded and what the desired dimension is. However, we have one last thing to handle. Because we manipulate a row and not a database ID, we will have the same request URL for two different pictures, which are in different albums. Remember that PictureModel loads a list of pictures for a given Album.

We should picture (pun intended) the situation. For an album called Holidays, the request URL will be images://pictures/0/thumbnail to load the first picture. It will be the same URL for another album Pets, which will load the first picture with images://pictures/0/thumbnail. As we said earlier, QQuickImageProvider automatically generates a cache which will avoid subsequent calls to requestPixmap() for the same URL. Thus, we will always serve the same picture, no matter which album is selected.

This constraint forces us to disable the cache in PictureImageProvider and to roll out our own cache. This is an interesting thing to do; here is a possible implementation:

// In PictureImageProvider.h 
 
#include <QQuickImageProvider> 
#include <QCache> 
 
... 
public: 
    static const QSize THUMBNAIL_SIZE; 
 
    QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override; 
 
    QPixmap* pictureFromCache(const QString& filepath, const QString& pictureSize); 
 
private: 
    PictureModel* mPictureModel; 
    QCache<QString, QPixmap> mPicturesCache; 
}; 
 
// In PictureImageProvider.cpp 
const QString PICTURE_SIZE_FULL = "full"; 
const QString PICTURE_SIZE_THUMBNAIL = "thumbnail"; 
const QSize PictureImageProvider::THUMBNAIL_SIZE = QSize(350, 350); 
 
QPixmap PictureImageProvider::requestPixmap(const QString& id, QSize* /*size*/, const QSize& /*requestedSize*/) 
{ 
    ... 
    return *pictureFromCache(fileUrl.toLocalFile(), pictureSize); 
} 
 
QPixmap* PictureImageProvider::pictureFromCache(const QString& filepath, const QString& pictureSize) 
{ 
    QString key = QStringList{ pictureSize, filepath } 
                    .join("-"); 
 
        QPixmap* cachePicture = nullptr; 
    if (!mPicturesCache.contains(pictureSize)) { 
        QPixmap originalPicture(filepath); 
        if (pictureSize == PICTURE_SIZE_THUMBNAIL) { 
            cachePicture = new QPixmap(originalPicture 
                                  .scaled(THUMBNAIL_SIZE, 
                                          Qt::KeepAspectRatio, 
                                          Qt::SmoothTransformation)); 
        } else if (pictureSize == PICTURE_SIZE_FULL) { 
            cachePicture = new QPixmap(originalPicture); 
        } 
        mPicturesCache.insert(key, cachePicture); 
    } else { 
        cachePicture = mPicturesCache[pictureSize]; 
    } 
 
    return cachePicture; 
} 

This new pictureFromCache() function aims to store the generated QPixmap in mPicturesCache and return the proper QPixmap. The mPicturesCache class relies on a QCache; this class lets us store data in a key/value fashion with the possibility to assign a cost for each entry. This cost should roughly map the memory cost of the object (by default, cost = 1). When QCache is instantiated, it is initialized with a maxCost value (by default 100). When the cost of the sum of all objects' exceeds the maxCostQCache starts deleting objects to make room for the new objects, starting with the less recently accessed objects.

In the pictureFromCache() function, we first generate a key composed of the fileUrl and the pictureSize before trying to retrieve the QPixmap from the cache. If it is not present, the proper QPixmap (scaled to THUMBNAIL_SIZE macro if needed) will be generated and stored inside the cache. The mPicturesCache class becomes the owner of this QPixmap.

The last step to complete the PictureImageProvider class is to make it available in the QML context. This is done in main.cpp:

#include "AlbumModel.h" 
#include "PictureModel.h" 
#include "PictureImageProvider.h" 
 
int main(int argc, char *argv[]) 
{ 
    QGuiApplication app(argc, argv); 
    ... 
 
    QQmlContext* context = engine.rootContext(); 
    context->setContextProperty("thumbnailSize", PictureImageProvider::THUMBNAIL_SIZE.width()); 
    context->setContextProperty("albumModel", &albumModel); 
    context->setContextProperty("pictureModel", &pictureModel); 
 
    engine.addImageProvider("pictures", new 
                            PictureImageProvider(&pictureModel)); 
    ... 
} 

The PictureImageProvider class is added to the QML engine with engine.addImageProvider(). The first argument will be the provider identifier in QML. Note that the engine takes ownership of the passed PictureImageProvider. One last thing, the thumbnailSize parameter is also passed to engine, it will constrain the thumbnails to be displayed with the specified size in the QML code.