如何使用 long-运行 插槽保持 UI 响应

How to maintain UI responsive with long-running slot

我有一个 python 定义的 worker QObject,它有一个由 QML UI 调用的慢速 work() 插槽(在我的实际 UI,当用户浏览列表时动态地对 FolderListModel 中的每个项目调用该方法,但对于他的示例代码,我只是在 window 完成时调用它作为示例) .

我想 运行 异步 work 以防止 UI 阻塞。我想通过在 QThread 上移动 Worker 实例并在那里调用插槽来做到这一点,但这不起作用,因为 UI 仍然被阻塞等待 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() 以便仅在调用完成后应用其效果?而且,作为奖励,我 doing/understanding 在使用 QThreads 时有什么错误?

解释:

  • 当前执行的"work"方法在哪里?嗯,如果你添加下面的代码,看看你得到了什么:
# ...

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!

如您所见,"work" 方法在主线程中执行,导致它阻塞 GUI。

  • 为什么"work"方法在主线程中执行?一个方法或函数在调用它的上下文中执行,在你的在主线程中执行的 QML 中的情况。

  • 那么你如何在 QObject 所在的线程中执行一个方法? 那么你必须使用 QMetaObject::invokeMethod() 异步执行它(这方法在 PySide2 中是不可能的错误),通过信号的调用,或使用 QTimer::singleShot().


解决方案:

在这些情况下,最好创建一个调用在另一个线程中执行的 function/method 的桥(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)
    }
}