Qt Commercial Support Weekly #15 - Models providing dynamic images in QML

When dealing with a number of different people with different backgrounds every day it is hard for us to know sometimes how much explanation we need to give when providing an answer to a problem.  We don't want to come across as talking down to our audience, but at the same time we want to ensure that we give enough information so that the problem can be resolved and also understood.  Sometimes we will get it wrong, and sometimes we will give too much information, but hopefully it will be appreciated all the same.

 

Note: This blog is best viewed with Mozilla Firefox. Internet Explorer is giving errors on the code snippet. 

 

The reason I am saying this now is because in this weekly I am aiming to cover everyone that may have not have used QML before and also those who have, but have not be aware of this particular problem when providing images via a model. Therefore, they will get something out of the workaround provided.

 

So, this week I am covering the use case where you want to provide images from a model to your view that is in QML,  but allowing for the fact that those images may change later on on the model side.  The QML side of things for this is relatively straightforward and is as follows:

 

import QtQuick 1.1

 

ListView {

    id: view

    width: 200; height: 250

    anchors.fill: parent

    model: myModel

    delegate: Image {

        fillMode: Image.PreserveAspectFit

        source: image

        cache: false

    }

}

 

In short, this is a ListView that has a delegate which shows an image for each entry in the model.  The key things to note here is the source property, which we set to image (this will make sense later on) and the cache property is set to false as we do not want to cache these images as they may change at any point from the model.  So, now our attention turns to the model itself, in this case I have subclassed QAbstractListModel for simplicity's sake:

 

    PixmapModel(QObject *parent = 0) : QAbstractListModel(parent)

    {

        QHash<int, QByteArray> roles;

        roles.insert(Qt::UserRole + 1, "image");

        setRoleNames(roles);

    }

    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const

    {

        if (role != Qt::UserRole + 1)

            return QVariant();

        return QVariant(QString("image://modelPixmaps/row%1").arg(index.row())));

    }

 

These are the main functions in the model. I have omitted the rest for convenience. In the constructor I have added image as a role for the model to use, this enables me to reference the role that stores the image in the QML which I did earlier with the "source: image" line, so now my view knows where to get the image from.  As I said earlier, QML does not take images directly for the Image type, it will only take a url, so since the image schema is reserved for this I have returned a string that will represent my image in an image provider where it will pull the exact image from.  So, the modelPixmaps part is referring to the group of images that the image will belong to and row%1 is the specific image that I am after which should be unique to get that specific image from the provider.  This neatly brings me to how my image provider is implemented:

 

    ModelImageProvider() : QDeclarativeImageProvider(QDeclarativeImageProvider::Pixmap) {}

    QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize)

    {

        int width = 32;

        int height = 32;

 

        if (size)

            *size = QSize(width, height);

        QPixmap pixmap(requestedSize.width() > 0 ? requestedSize.width() : width,

                       requestedSize.height() > 0 ? requestedSize.height() : height);

        pixmap.fill(Qt::red);

        QPainter p(&pixmap);

        p.drawText(0, 10, id);

        p.end();

        return pixmap;

    }

 

My subclass just sets the type of image provided in the constructor.  This indicates that I will be providing pixmaps and you can provide images instead, but you need to reimplement requestImage() for this.  All I do in this case is create a pixmap that is of the requested size and then draw the id inside it.  This ensures I will always get a different pixmap for each id since this text would be different in each one.  This just leaves me to connect them up so that the QDeclarativeView knows about the model and the image provider too.  This is done with the following snippet from the main() function:

 

    QDeclarativeView viewer;

    PixmapModel model;

    QDeclarativeContext *ctxt = viewer.rootContext();

    ctxt->setContextProperty("myModel", &model);

    viewer.engine()->addImageProvider("modelPixmaps", new ModelImageProvider);

 

So, out of the box this will show the images from the model, which so far is what we want to see.  Of course, being a model it is common to want to change the data for a given index and in this case we would want to change the image to be something else.  For example, we might want to change the background of the pixmap dynamically in some form.  However, due to a limitation with QML it will not pull the pixmap from the image provider even if you tell the model that the index has changed because it will check the id to see if that is different and if it has not changed it will not query for a new pixmap.

 

Therefore, in order to force it to cause a request of the pixmap from the image provider so a new one can be provided, you need to ensure the id that comes from the model is unique.  So, by changing the return line in the model's data function to be:

 

  return QVariant(QString("image://modelPixmaps/row%1/%2").arg(index.row()).

arg(QDateTime::currentDateTime().toString("dd-MM-yyyy:hh.mm.ss")));

 

Then, we can force it to always have a unique part to the id which we can just ignore in our image provider.  So, our image provider would then do:

 

  QString realId = id.left(id.indexOf("/"));

 

To get the id that we need to find out what entry in the model the id is referring to.  Now, we have a ListView in QML that will update the image shown for an index when dataChanged() is emitted from the model for that index.

 

Before I close for the week, here is another quick tip for anyone who has problems getting the Qt debugging helper working inside Qt Creator on Linux.  On Linux it depends on gdb to have access to at least Python 2.6, so if you are having problems getting it to be picked up then you may need to build gdb yourself so that it links against Python 2.6 or later, or upgrade your gdb version so that it has a later version of Python with it.  This means for Red Hat Enterprise Linux users that if you are using RHEL 5 then you need to build gdb yourself as the later version of gdb is only offered with RHEL 6.

 

Until next week, happy coding :)


Comments