如何使用 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)
}
}
我有一个 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)
}
}