1

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

2 Answers 2

2

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");
}
Sign up to request clarification or add additional context in comments.

Comments

1

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.

6 Comments

That's most likely it, didn't even consider this!
I gave this a go and updated the example, would you mind having another look? It doesn't quite cut it yet, neither does it work when using a signal instead of an event and I'm fresh out of ideas..
I don't have PyQt5 on hand at the moment. Does event() get called at all? (Can you insert a print statement to check)
Yes, replacing self.cb.call([event.return_value]) with print event.return_value does print the value as expected. The original however causes Python to crash.
Looking at what you based your code on (and mailing list posts from the creator) it looks like you need to wrap the callback when you save it. Eg self.cb = QJSValue(cb) to avoid segfaults. Hopefully this, combined with the thread safety changes stops the segfaults.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.