Асинхронный вызов из QML в Python с обратным вызовом

Из QML я хотел бы:

  1. Вызов слота Python.
  2. Передайте обратный вызов.
  3. Запустите этот обратный вызов после заполнения слота.

Я пробовал это:

  1. Зарегистрируйте свойство контекста (Service)
  2. Звоните Service.request("data", function (response) { console.log(response) }
  3. В Python функция принимается как QtQml.QJSValue
  4. Функция вызывается в отдельном потоке после какой-то дорогостоящей операции

Однако эта функция оказывает эффект только иногда, а в большинстве случаев вообще не действует или приводит к сбою интерпретатора Python. Если я уберу вызов time.sleep(1), это, скорее всего, даст результаты.

Есть идеи?

Вот нерабочая реализация вышеизложенного

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");
}

Реализация адаптирована отсюда
https://github.com/ben-github/PyQt5-QML-CallbackFunction

С наилучшими пожеланиями,
Маркус


person Marcus Ottosson    schedule 22.11.2014    source источник


Ответы (2)


В документации нет указаний на то, что QJSValue является потокобезопасным. Эта страница указывает классы, которые являются повторно входящими или потокобезопасными. отмечены как таковые в документации. Однако на странице для QJSValue слово "нить" не упоминается. .

Таким образом, я бы посоветовал вам убедиться, что ваш обратный вызов вызывается только из основного потока. Очевидно, что вы все равно захотите поместить свою длительную задачу в поток, поэтому я бы предложил использовать что-то вроде QCoreApplication.postEvent() для отправки события из вашего потока Python в основной поток, который затем вызовет вашу функцию обратного вызова.

Примечание. Я упаковал вызовы QCoreApplication.postEvent для PyQt4 здесь< /а>. Если вам нужна помощь в понимании того, как использовать метод QCoreApplication.postEvent, возможно, вы сможете адаптировать его и для работы с PyQt5.

person three_pineapples    schedule 23.11.2014
comment
Скорее всего так, даже не подумал об этом! - person Marcus Ottosson; 23.11.2014
comment
Я попробовал это и обновил пример, не могли бы вы взглянуть еще раз? Это еще не совсем работает, и это не работает при использовании сигнала вместо события, и у меня нет идей. - person Marcus Ottosson; 23.11.2014
comment
На данный момент у меня нет под рукой PyQt5. event() вообще звонят? (Можете ли вы вставить оператор печати для проверки) - person three_pineapples; 23.11.2014
comment
Да, замена self.cb.call([event.return_value]) на print event.return_value действительно печатает значение, как и ожидалось. Однако оригинал вызывает сбой Python. - person Marcus Ottosson; 23.11.2014
comment
Глядя на то, на чем вы основывали свой код (и сообщения в списке рассылки от создателя), похоже, что вам нужно обернуть обратный вызов при его сохранении. Например, self.cb = QJSValue(cb), чтобы избежать segfaults. Надеюсь, это, в сочетании с изменениями безопасности потоков, остановит segfaults. - person three_pineapples; 23.11.2014
comment
Ведь это работает! Я заметил это в коде, на котором основывал свой, но удалил его, так как решил, что он лишний. Спасибо! - person Marcus Ottosson; 23.11.2014

Я нашел альтернативный подход, который также работает.

Различия:

  1. Python регистрирует новый тип вместо настройки свойства контекста
  2. Вместо того, чтобы вызывать Javascript из Python, Python выдает сигнал

Мне это кажется чище, так как Javascript никогда не должен входить в 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");
}
person Marcus Ottosson    schedule 23.11.2014