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中,必须在主线程中来创建子对象。

你可能感兴趣的:(Python,PyQt5,Python)