Skip to main content
 首页 » 编程设计

python之如何通过长时间运行的插槽保持 UI 响应

2024年08月13日40lidabo

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

我想异步运行缓慢的 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() 以便仅在它完成时应用其效果?而且,作为奖励,我在使用 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。

  • 为什么“工作”方法在主线程中执行?方法或函数在调用它的上下文中执行,在您的 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) 
    } 
}