PyQt子线程处理业务事件的问题解决
作者:阮靓仔
在PyQt中是不推荐使用UI主线程来处理耗时操作的,会造成窗口组件阻塞。耗时操作一般放在子线程中。子线程处理完成后,可能需要更新窗口组件,但是PyQt不推荐使用子线程来更新主线程(也不是不能更新),这就用到了信号槽机制来更新主线程。
- 在QObject的一个子类中创建一个信号(
PyQt5.QtCore.pyqtSignal
)属性 - 将这个信号属性和其他类中的函数绑定,绑定的这个函数叫做整个信号的槽函数。一个信号可以和多个槽函数绑定。
- 该信号发出时,就会调用对应的槽函数
可能会有疑问,槽函数被执行时所在的线程和发送信号的线程是不是同一个?
需要注意,信号一定义在QObject或其子类中。调用该属性的emit方法发出信号后,和该信号绑定的槽函数都将要被调用,但是调用的线程并不一定是发送信号的这个线程,这和PyQt中的线程亲和性(Thread Affinity)
有关。
线程亲和性(Thread Affinity)
在 PyQt 中,一个对象可以被移动到不同的线程中,但一个对象在同一时刻只能属于一个线程。这是因为 Qt 使用线程亲和性(Thread Affinity)的概念来管理对象所属的线程。
每个 Qt 对象都与一个特定的线程相关联,即它的线程亲和性。对象的线程亲和性决定了该对象的槽函数是在哪个线程中执行。默认情况下,对象在创建时会与创建它的线程相关联,但可以使用 moveToThread 方法将对象移动到另一个线程中。
错误示例:
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton,QFileDialog from PyQt5.QtCore import QThread,pyqtSignal,QObject import sys, threading class MyWindow(QMainWindow): def __init__(self, parent=None): super(MyWindow, self).__init__(parent) self.button = QPushButton('Hi') self.button.clicked.connect(self.on_click) self.setCentralWidget(self.button) def on_click(self): print("on_click",threading.current_thread().name) self.thread = MyThread(self) self.thread.start() def set_text(self,file_name): print("setText",threading.current_thread().name) self.button.setText(file_name) class MyThread(QThread): def __init__(self,mv:QMainWindow) -> None: super().__init__(None) self.mv = mv def run(self): print('run',threading.current_thread().name) QThread.sleep(5) self.mv.set_text("Hello World") if __name__ == '__main__': app = QApplication([]) window = MyWindow() window.show() sys.exit(app.exec_())
输出结果:
on_click MainThread
run Dummy-1
setText Dummy-1 //子线程更新UI,不推荐
使用信号槽机制
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton,QFileDialog from PyQt5.QtCore import QThread,pyqtSignal,QObject import sys, threading class MyWindow(QMainWindow): def __init__(self, parent=None): super(MyWindow, self).__init__(parent) self.button = QPushButton('Hi') self.button.clicked.connect(self.on_click) self.setCentralWidget(self.button) def on_click(self): print("on_click",threading.current_thread().name) self.thread = MyThread(self) self.thread.pyqtSignal.connect(self.set_text) self.thread.start() def set_text(self,file_name): print("setText",threading.current_thread().name) self.button.setText(file_name) class MyThread(QThread): pyqtSignal = pyqtSignal(str) def __init__(self,mv:QMainWindow) -> None: super().__init__(None) self.mv = mv def run(self): print('run',threading.current_thread().name) QThread.sleep(5) self.pyqtSignal.emit("Hello World") if __name__ == '__main__': app = QApplication([]) window = MyWindow() window.show() sys.exit(app.exec_())
输出结果:
on_click MainThread
run Dummy-1
setText MainThread //更新UI时,执行的线程为主线程
setText槽函数为什么会被主函数执行,就是因为线程亲和性,槽函数所在对象和MainThread绑定,当然会被主线程所执行。
但是这种将事务直接写在run,PyQt5是不推荐的,正确写法如下
创建一个类集成QObject,来做业务的处理。并将这个对象和新创建的线程通过moveToThread绑定,作为这个对象的亲和线程。将QThread的started信号和这个业务事件绑定。线程启动,发送started信号,业务对象开始处理业务,完成之后发送信号给主线程槽函数。
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton,QFileDialog from PyQt5.QtCore import QThread,pyqtSignal,QObject import sys, threading class MyWindow(QMainWindow): def __init__(self, parent=None): super(MyWindow, self).__init__(parent) self.button = QPushButton('Hi') self.button.clicked.connect(self.on_click) self.setCentralWidget(self.button) def on_click(self): print("on_click",threading.current_thread().name) self.thread = QThread() self.myHander = MyHandler() self.myHander.moveToThread(self.thread) self.myHander.pyqtSignal.connect(self.set_text) self.thread.started.connect(self.myHander.handle) self.thread.start() def set_text(self,file_name): print("setText",threading.current_thread().name) self.button.setText(file_name) class MyHandler(QObject): pyqtSignal = pyqtSignal(str) def handle(self): print('handle',threading.current_thread().name) self.pyqtSignal.emit("Hello World") if __name__ == '__main__': app = QApplication([]) window = MyWindow() window.show() sys.exit(app.exec_())
子线程中调用QFileDialog
如果在子线程中调用了QFileDialog窗口选择文件,QFileDialog窗口出现后几秒后程序会崩溃,代码如下
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton,QFileDialog from PyQt5.QtCore import QThread,pyqtSignal,QObject import sys, threading class MyWindow(QMainWindow): def __init__(self, parent=None): super(MyWindow, self).__init__(parent) self.button = QPushButton('Hi') self.button.clicked.connect(self.on_click) self.setCentralWidget(self.button) def on_click(self): print("on_click",threading.current_thread().name) self.thread = MyThread(self) self.thread.pyqtSignal.connect(self.set_text) self.thread.start() def set_text(self,file_name): print("setText",threading.current_thread().name) self.button.setText(file_name) class MyThread(QThread): pyqtSignal = pyqtSignal(str) def __init__(self,mv:QMainWindow) -> None: super().__init__(None) self.mv = mv def run(self): print('run',threading.current_thread().name) file_name = QFileDialog.getOpenFileName(self.mv, '选择文件', './', 'Excel files(*.xlsx , *.xls)') print(file_name) self.pyqtSignal.emit("Hello World") if __name__ == '__main__': app = QApplication([]) window = MyWindow() window.show() sys.exit(app.exec_())
输出结果:
on_click MainThread
run Dummy-1
QObject::setParent: Cannot set parent, new parent is in a different thread
CoCreateInstance failed (操作成功完成。)
QObject: Cannot create children for a parent that is in a different thread.
(Parent is QApplication(0x21fb451d190), parent's thread is QThread(0x21fb443b430), current thread is MyThread(0x21fb8788df0)
CoCreateInstance failed (操作成功完成。)
QObject::startTimer: Timers cannot be started from another thread
问题原因
PyQt中,必须在主线程中来创建子对象。
到此这篇关于PyQt子线程处理业务事件的问题解决的文章就介绍到这了,更多相关PyQt子线程内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!