Marcus Ottosson
Marcus Ottosson

Reputation: 3301

Asynchronous call from QML to Python with callback

From QML, I'd like to:

  1. Call a Python slot.
  2. Pass along a callback.
  3. Have that callback be run once the slot is complete.

I've tried this:

  1. Register a context-property (Service)
  2. Call Service.request("data", function (response) { console.log(response) }
  3. In Python, the function is received as a QtQml.QJSValue
  4. The function is called in a separate thread, after some expensive operation

However, the function only has an effect sometimes, and most of the time not at all or crashes the Python interpreter. If I remove the call to time.sleep(1), it is more likely to produce results.

Any ideas?

Here's a non-working implementation of the above

main.qml

import QtQuick 2.3

import "application.js" as App


Rectangle {
    id: appWindow
    width: 200
    height: 200
    Component.onCompleted: App.onLoad()
}

main.py

import sys
import time

import threading

from PyQt5 import QtCore, QtGui, QtQml, QtQuick


class Service(QtCore.QObject):
    def __init__(self, parent=None):
        super(Service, self).__init__(parent)

    @QtCore.pyqtSlot(str, str, QtCore.QVariant, QtQml.QJSValue)
    def request(self, verb, endpoint, data, cb):
        """Expensive call"""
        print verb, endpoint, data

        self.cb = cb

        def thread():
            time.sleep(1)
            event = QtCore.QEvent(1000)
            event.return_value = "expensive result"
            QtGui.QGuiApplication.postEvent(self, event)

        worker = threading.Thread(target=thread)
        worker.daemon = False
        worker.start()

        self.worker = worker

    def event(self, event):
        if event.type() == 1000:
            self.cb.call([event.return_value])

        return super(Service, self).event(event)


app = QtGui.QGuiApplication(sys.argv)
view = QtQuick.QQuickView()
context = view.rootContext()

service = Service()
context.setContextProperty("Service", service)

view.setSource(QtCore.QUrl("main.qml"))
view.show()
app.exec_()

application.js

"use strict";
/*global print, Service*/


function onLoad() {
    Service.request("POST", "/endpoint", {"data": "value"}, function (reply) {
        print(reply);
        print(reply);
        print(reply);
    });

    print("request() was made");
}

The implementation is adapted from here
https://github.com/ben-github/PyQt5-QML-CallbackFunction

Best,
Marcus

Upvotes: 1

Views: 2014

Answers (2)

Marcus Ottosson
Marcus Ottosson

Reputation: 3301

I found an alternative approach that also works.

The differences are:

  1. Python registers a new type, instead of settings a context property
  2. Instead of calling Javascript from Python, Python emits a signal

This seems cleaner to me, as Javascript never has to enter Python.

main.qml

import QtQuick 2.0

import "application.js" as App


Rectangle {
    id: appWindow
    width: 200
    height: 200
    Component.onCompleted: App.onLoad()
}

main.py

import sys
import time
import threading

from PyQt5 import QtCore, QtGui, QtQml, QtQuick


class MockHTTPRequest(QtCore.QObject):
    requested = QtCore.pyqtSignal(QtCore.QVariant)

    @QtCore.pyqtSlot(str, str, QtCore.QVariant)
    def request(self, verb, endpoint, data):
        """Expensive call"""
        print verb, endpoint, data

        def thread():
            time.sleep(1)
            self.requested.emit("expensive result")

        threading.Thread(target=thread).start()

app = QtGui.QGuiApplication(sys.argv)
view = QtQuick.QQuickView()
context = view.rootContext()

QtQml.qmlRegisterType(MockHTTPRequest, 'Service', 1, 0, 'MockHTTPRequest')

view.setSource(QtCore.QUrl("main.qml"))
view.show()
app.exec_()

application.js

"use strict";
/*global print, Service, Qt, appWindow*/


function MockHTTPRequest() {
    return Qt.createQmlObject("import Service 1.0; MockHTTPRequest {}",
                              appWindow, "MockHTTPRequest");
}

function onLoad() {
    var xhr = new MockHTTPRequest();
    xhr.requested.connect(function (reply) {
        print(reply);
    });

    xhr.request("POST", "/endpoint", {"data": "value"});

    print("request() was made");
}

Upvotes: 2

three_pineapples
three_pineapples

Reputation: 11869

There is no indication from the documentation that QJSValue is thread safe. This page indicates the classes that are re-entrant or thread safe are marked as such in the documentation. However, there is no mention of the word thread on the page for QJSValue.

As such, I would suggest you make sure that your callback is only called from the main thread. Obviously, you are still going to want to put your long running task in a thread, so I would suggest using something like QCoreApplication.postEvent() to send an event from your Python thread to the main thread, which will then call your callback function.

Note: I've wrapped calls to QCoreApplication.postEvent for PyQt4 here. If you need help understanding how to use the QCoreApplication.postEvent method, you can probably adapt it to work with PyQt5 as well.

Upvotes: 1

Related Questions