Как сохранить отзывчивость пользовательского интерфейса с помощью продолжительного слота - PullRequest
2 голосов
/ 02 октября 2019

У меня есть определенный Python рабочий QObject, который имеет медленный слот work(), который вызывается интерфейсом QML (в моем реальном интерфейсе метод вызывается для каждого элемента в FolderListModel динамически, как пользовательпроходит по списку, но для примера кода я просто вызываю его по завершении окна в качестве примера.

Я хотел бы запустить медленный work асинхронно, чтобы предотвратить блокирование пользовательского интерфейса. Я думал сделать это, переместив экземпляр Worker в QThread и вызвав там слот, но это не работает, поскольку пользовательский интерфейс все еще заблокирован в ожидании результата work().

Этокод моей попытки до сих пор:

mcve.qml:

import QtQuick 2.13
import QtQuick.Window 2.13

Window {
    id: window
    visible: true
    width: 800
    height: 600
    title: qsTr("Main Window")

    Component.onCompleted: console.log(worker.work("I'm done!")) // not the actual usage, see note in the question
}

mcve.py:

import sys
from PySide2.QtWidgets import QApplication
from PySide2.QtQml import QQmlApplicationEngine
from PySide2.QtCore import QUrl, QThread, QObject, Slot
from time import sleep

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

  @Slot(str, result=str)
  def work(self, path):
    sleep(5) # do something lengthy
    return path

if __name__ == '__main__':

    app = QApplication(sys.argv)
    engine = QQmlApplicationEngine()

    workerThread = QThread()
    worker = Worker()
    worker.moveToThread(workerThread)
    engine.rootContext().setContextProperty("worker", worker)
    engine.load(QUrl.fromLocalFile('mcve.qml'))
    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec_())

Как мне вызвать work() асинхронно, чтобытолько когда это сделано, его эффекты применяются? И, в качестве бонуса, что я делаю / неправильно понимаю при использовании QThreads?

1 Ответ

3 голосов
/ 02 октября 2019

Объяснение:

  • Где сейчас выполняется метод "работа"? Хорошо, если вы добавите следующий код и проверьте, что получите:
# ...

import threading

class Worker(QObject):
    @Slot(str, result=str)
    def work(self, path):
        print(threading.current_thread())
        sleep(5)  # do something lengthy
        return path

# ...

Вывод:

<_MainThread(MainThread, started 140409078408832)>
qml: I'm done!

Как вы видите, метод "работа" выполняется в главном потоке, заставляя его блокировать графический интерфейс.

  • Почему метод "работа" выполняется в главном потоке? Метод или функция выполняется в контексте, где он вызывается, в вашем случае в QML, который выполняется в главном потоке.

  • Итак, как вы выполняете метод в потоке, в котором находится QObject? Ну, вы должны сделать это асинхронно, используя QMetaObject::invokeMethod() (этот метод невозможен в PySide2 дляошибка), с помощью вызова сигнала или с помощью QTimer::singleShot().


Решение:

В этих случаях лучше создать мост (QObject), который вызывает функцию / метод, выполняемый в другом потоке, и уведомляет об изменениях посредством сигналов.

import sys
from time import sleep
from functools import partial

from PySide2 import QtCore, QtWidgets, QtQml


class Worker(QtCore.QObject):
    resultChaged = QtCore.Signal(str)

    @QtCore.Slot(str)
    def work(self, path):
        sleep(5)  # do something lengthy
        self.resultChaged.emit(path)


class Bridge(QtCore.QObject):
    startSignal = QtCore.Signal(str)
    resultChaged = QtCore.Signal(str, arguments=["result"])

    def __init__(self, obj, parent=None):
        super().__init__(parent)
        self.m_obj = obj
        self.m_obj.resultChaged.connect(self.resultChaged)
        self.startSignal.connect(self.m_obj.work)


if __name__ == "__main__":

    app = QtWidgets.QApplication(sys.argv)
    engine = QtQml.QQmlApplicationEngine()

    workerThread = QtCore.QThread()
    workerThread.start()

    worker = Worker()
    worker.moveToThread(workerThread)

    bridge = Bridge(worker)

    engine.rootContext().setContextProperty("bridge", bridge)
    engine.load(QtCore.QUrl.fromLocalFile("mcve.qml"))
    if not engine.rootObjects():
        sys.exit(-1)

    ret = app.exec_()
    workerThread.quit()
    workerThread.wait()
    sys.exit(ret)
import QtQuick 2.13
import QtQuick.Window 2.13

Window {
    id: window
    visible: true
    width: 800
    height: 600
    title: qsTr("Main Window")

    Component.onCompleted: bridge.startSignal("I'm done!")

    Connections{
        target: bridge
        onResultChaged: console.log(result)
    }
}
...