Waiting for a signal inside a QQuickImageProvider

15

I'm creating an application using QML and Qt 5.2 . In it a ListView displays multiple items, each with an image and associated text. The image is built based on data uploaded from a server by HTTP . In a simplified way, I have the following code:

MyProvider::MyProvider() :
    QQuickImageProvider(QQmlImageProviderBase::Image,
                        QQmlImageProviderBase::ForceAsynchronousImageLoading)
{ }

QImage MyProvider::requestImage(const QString& id, QSize* size, const QSize& requestedSize)
{
    // Obter dados JSON que me explicam como montar a imagem
    QNetworkAccessManager manager;
    QNetworkRequest request(QUrl("http://myserver.com/api/imagedata/" + id));
    request.setRawHeader("Accept", "application/json");
    QNetworkReply* reply = manager.get(request);

    // Aguardar pela resposta. Aqui está o problema
    QEventLoop loop;
    QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
    loop.exec();

    // Ler a resposta e montar uma imagem com ela
    QImage img = produceImageFromJsonData(reply->readAll());
    delete reply;

    // Ajustar para o tamanho requisitado
    if (requestedSize.isValid())
        img = img.scaled(requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
    *size = img.size();
    return img;
}

The MyProvider is then registered and used in QML as source of each Image of the list. The problem with this code is that there is a race condition in it. requestImage runs in a thread than the rest of the application. By the time I create a QEventLoop and run it, I'm allowing my thread to receive and process any event that occurs. Because there may be more than one thread running this loop, two events can be sent to the same object at the same time by different threads. I get a crash hard to reproduce (it happened for the first time today, after almost a month of development).

The problem can also be reproduced with this smaller code, showing that the simple existence of a QEventLoop triggers the crash:

MyProvider::MyProvider() :
    QQuickImageProvider(QQmlImageProviderBase::Image,
                        QQmlImageProviderBase::ForceAsynchronousImageLoading)
{ }

QImage MyProvider::requestImage(const QString& id, QSize* size, const QSize& requestedSize)
{
    QEventLoop loop;
    loop.exec();
    return QImage();
}

I found a bugreport dating to Qt 4.7. 1 where the following is said:

  

The problem is that the QEventLoop that is created in the imageprovider causes events to be delivered to the image reader which shares the thread. It receives these events while still processing a previous event. [...] It is not valid to run an event loop in the image provider.

In summary, I can not use QEventLoop in my role. So how can I wait for the QNetworkReply and only return from the function when the response arrives?

    
asked by anonymous 23.01.2014 / 15:12

1 answer

6

I do not know a way to wait for the signal there. Also, it is not possible to make the thread sleep in a loop by checking if the data arrived manually because the QNetworkReply depends on the event loop to be notified of receiving the response.

So I started with a different implementation of the same idea. Instead of creating an image provider and letting the engine take care of everything in place, I chose to do it more manually.

Previous interface:

Image {
  id: avatar
  source: "image://myimageprovider/" + avatarImageId
}

New interface:

Image {
  id: avatar
}

MyImageLoader {
  sourceId: avatarImageId
  target: avatar
}

My goal was to implement the MyImageLoader component so that it would do the desired operation. Here's my implementation:

myimageloader.hpp :

class MyImageLoader : public QObject {
    Q_OBJECT
    Q_PROPERTY(QString sourceId READ sourceId WRITE setSourceId)
    Q_PROPERTY(QObject* target READ target WRITE setTarget)

public:
    MyImageLoader(QObject* parent = 0) : QObject(parent) {}

    QString sourceId() const { return _sourceId; }
    void setSourceId(const QString& sourceId) { _sourceId = sourceId; beginDownload(); }

    QObject* target() const { return _target; }
    void setTarget(QObject* target) {_target = target; setTargetSource(); }

private:
    void beginDownload();
    void endDownload();
    void setTargetSource();
    QString cacheFile();

    QNetworkAccessManager _manager;
    QNetworkReply* _reply = nullptr;
    QString _sourceId;
    QObject* _target = nullptr;
};

myimageloader.cpp :

void MyImageLoader::beginDownload() {
    if (QFile::exists(cacheFile()))
        return setTargetSource();

    delete _reply;
    QNetworkRequest request(QUrl("http://myserver.com/api/imagedata/" + _sourceId));
    request.setRawHeader("Accept", "application/json");
    _reply = _manager.get(request);
    connect(_reply, &QNetworkReply::finished, this, &MyImageLoader::endDownload);
}

void MyImageLoader::endDownload() {
    QImage img = produceImageFromJsonData(_reply->readAll());
    delete _reply;
    img.save(cacheFile());
    setTargetSource();
}

void MyImageLoader::setTargetSource() {
    if (!_target) return;

    QString file = cacheFile();
    _target->setProperty("source", QFile::exists(file) ? "file:///" + file : "");
}

QString MyImageLoader::cacheFile() {
    return QDir::temp().filePath(_sourceId + ".png");
}

The big difference is that I'm not using threads. Everything happens in a single thread (the same one that renders the scene). There is no locking of the interface during charging due to the signals. All of these functions return immediately and do not block.

The downside is that I have to save the image to a file. It's not a problem for me since I planned to cache the data anyway.

This does not answer the question itself, but solves my problem.

    
23.01.2014 / 17:52