在PyQt中是不推荐使用UI主线程来处理耗时操作的,会造成窗口组件阻塞。耗时操作一般放在子线程中。子线程处理完成后,可能需要更新窗口组件,但是PyQt不推荐使用子线程来更新主线程(也不是不能更新),这就用到了信号槽机制来更新主线程。
- 在QObject的一个子类中创建一个信号(
PyQt5.QtCore.pyqtSignal
)属性 - 将这个信号属性和其他类中的函数绑定,绑定的这个函数叫做整个信号的槽函数。一个信号可以和多个槽函数绑定。
- 该信号发出时,就会调用对应的槽函数
可能会有疑问,槽函数被执行时所在的线程和发送信号的线程是不是同一个?
需要注意,信号一定义在QObject或其子类中。调用该属性的emit方法发出信号后,和该信号绑定的槽函数都将要被调用,但是调用的线程并不一定是发送信号的这个线程,这和PyQt中的
线程亲和性(Thread Affinity)
有关。
线程亲和性(Thread Affinity)
在 PyQt 中,一个对象可以被移动到不同的线程中,但一个对象在同一时刻只能属于一个线程。这是因为 Qt 使用线程亲和性(Thread Affinity)的概念来管理对象所属的线程。
每个 Qt 对象都与一个特定的线程相关联,即它的线程亲和性。对象的线程亲和性决定了该对象的槽函数是在哪个线程中执行。默认情况下,对象在创建时会与创建它的线程相关联,但可以使用 moveToThread 方法将对象移动到另一个线程中。
错误示例:
py
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,不推荐
使用信号槽机制
python
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_())
输出结果:
python
on_click MainThread
run Dummy-1
setText MainThread //更新UI时,执行的线程为主线程
setText槽函数为什么会被主函数执行,就是因为线程亲和性,槽函数所在对象和MainThread绑定,当然会被主线程所执行。
但是这种将事务直接写在run,PyQt5是不推荐的,正确写法如下
创建一个类集成QObject,来做业务的处理。并将这个对象和新创建的线程通过moveToThread绑定,作为这个对象的亲和线程。将QThread的started信号和这个业务事件绑定。线程启动,发送started信号,业务对象开始处理业务,完成之后发送信号给主线程槽函数。
python
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窗口出现后几秒后程序会崩溃,代码如下
python
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中,必须在主线程中来创建子对象。