0

I have a c++ Q_INVOKABLE function that starts an asynchronous operation (image downloading). I want to pass a QML callback to the function for when the operation finishes.

I know that it could be done with QJSValue, but this way the callback is not called in the GUI thread so when I try to update some QML element, it prints an error and crashes the application: Updates can only be scheduled from GUI thread or from QQuickItem::updatePaintNode(). Also, that question is 5 years old and maybe a better way has been introduced since then.

This is my QML code:

Rectangle {
    // ...

    Loader {
        id: loader
        source: "[loading animation]";
    }

    Component {
        id: imageView
        Image {
            id: image
            // ...
        }
    }


    Component.onCompleted: {
        ImageLoader.start_loading(post_id, function () {
            loader.sourceComponent = imageView;
        });
    }

And the c++ code:

void ImageLoader::start_loading(const QString & id, QJSValue on_finished)
{
    ImageLoaderRunnable * runnable_raw = new ImageLoaderRunnable(id, this->m_cache);

    connect(runnable_raw, &ImageLoaderRunnable::finished, [on_finished]() mutable {
        if (on_finished.isCallable()) {
            on_finished.call();
        }
    });

    runnable_pool.start(runnable_raw);
}

Also, this method is unsafe since the QML object can be destroyed before the callback is called.

splaytreez
  • 552
  • 5
  • 13
  • 1
    Why don't you use a signal? Signals are perfect for this. Also see https://doc.qt.io/qt-5/qtqml-cppintegration-interactqmlfromcpp.html#invoking-qml-methods – ypnos Apr 18 '21 at 17:56
  • @ypnos the components that call the method are created dynamically in a ListView so I can't refer to some particular object or function. Also, I'm trying to follow the MVM principle and have the c++ model provide an interface for qml and not vice versa. – splaytreez Apr 18 '21 at 18:05
  • So a signal offered by your c++ object wouldn't fit your design? – ypnos Apr 18 '21 at 19:24
  • @ypnos , I simplified it for that post. Essentially, the ``ImageLoader`` class can start loading an image multiple times and at the same time (for different QML elements that call the function) so I can't just create a finished() signal in that class (how would I know which one of the callers it refers to?). I also have another idea but it's hacky and would take longer to explain (and I don't like it anyway). – splaytreez Apr 18 '21 at 19:33
  • 1
    Well, the caller could provide an identifier which is then given back in the signal. Not great but straightforward. – ypnos Apr 18 '21 at 19:35
  • @ypnos That might work though I think there must be a prettier solution, I'll try this one for now, thanks! – splaytreez Apr 18 '21 at 19:43
  • @ypnos So I tried that and it didn't work. When I connect QML function to the c++ slot (``ImageLoader.finished.connect(on_finished))`` - this is in QML) it works fine unless I refer to objects outside of the function scope (so I still can't refer to the ``loader``, for example). What's strange is that the function can refer to the ``loader`` when called inside QML, but not when called by connecting to the signal. – splaytreez Apr 19 '21 at 07:47
  • Now that's strange. Does it mean Qt expects QML to only connect to signals coming from GUI thread? Unfortunatly I am not of much help here because my knowledge of QML is quite limited. – ypnos Apr 19 '21 at 09:13

2 Answers2

1

I figured it out. I just needed a way to call the slot in the GUI thread. When I pass the receiver object to the connect() method, it runs the slot in the receiver's thread. Thus, I can pass either the qml engine object or the qml component that calls the function to the connect() method and it will work.

A:

void ImageLoader::start_loading(const QString & id, QJSValue on_finished)
{
    ImageLoaderRunnable * runnable_raw = new ImageLoaderRunnable(id, this->m_cache);

    // m_engine -- pointer to QQmlApplicationEngine.
    connect(runnable_raw, &ImageLoaderRunnable::finished, m_engine, [on_finished]() mutable {
        if (on_finished.isCallable()) {
            on_finished.call();
        }
    });

    runnable_pool.start(runnable_raw);
}

B:

// c++
void ImageLoader::start_loading(const QString & id, QObject * receiver, QJSValue on_finished)
{
    ImageLoaderRunnable * runnable_raw = new ImageLoaderRunnable(id, this->m_cache);

    connect(runnable_raw, &ImageLoaderRunnable::finished, receiver, [on_finished]() mutable {
        if (on_finished.isCallable()) {
            on_finished.call();
        }
    });

    runnable_pool.start(runnable_raw);
}

// qml
Component.onCompleted: {
    ImageLoader.start_loading(post_id, this, function () {
        loader.sourceComponent = imageView;
    });
}
splaytreez
  • 552
  • 5
  • 13
0

You need to emit a signal from your C++ model, and handle the signal in the QML.

You really need to give more detail why that doesn't work for your use case.

If you have a ListView then you might need to define a role for whatever you need to pass to your view and emit dataChanged for the index of image that was loaded to update the view.

pooya13
  • 2,060
  • 2
  • 23
  • 29
  • I want to make an interface for the qml side of the application. Should have said that it's not only ``ListView`` that needs to use these functions. Say, when I open an image, another qml module may request to load it. I'm going to go with ypnos's suggestion for now as it's the closest to what a I intended. – splaytreez Apr 18 '21 at 20:17