<Python>PyQt5中在两个线程间传递数据实例记录

前言
在测试利用python调用AI模型API生成图像的程序时候,发现AI模型生成图像有一定的时间,此时UI界面会卡顿,就想到利用多线程来处理,然后就发现多线程之间的数据传递问题,在网络上搜索了相关资料后,一番折腾总算搞清楚了,于是本文作为一个记录,主要是为了以后遇到同样的问题,可以方便参考,当然,如果你有同样的需求,也可以参考本文。

实际需求:最近在使用智谱AI的图像大模型,集成到python中,使用PyQt5作为UI界面,然后就遇到了线程间数据传递的问题。

示例:在主界面添加两个按钮、一个文本框,两个按钮分别表示更新开始和更新复位,文本框实时更新进度值,进度值是利用循环来模拟。

不使用线程
先来看一下不使用子线程的示例:
代码:

import sys
import threading
import time

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

class Test11(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()
        
    def initUI(self):
        
        self.btn0=QPushButton('更新开始',self)
        self.btn0.setGeometry(100,100,80,40)
        self.btn0.clicked.connect(self.countstart_func)
        
        self.btn1=QPushButton('更新复位',self)
        self.btn1.setGeometry(100,160,80,40)
        self.btn1.clicked.connect(self.countstop_func)
        
        self.te1=QTextEdit(self)
        self.te1.setGeometry(200,100,80,20)
        self.te1.setText('0')
        
        self.setWindowTitle('线程数据传递示例')
        self.setGeometry(100,100,400,400)
        self.show()
    def countstart_func(self):
        # thread1=threading.Thread(name='th1',target=self.counter_func)
        # thread1.start()
        self.counter_func()
        
    def counter_func(self):
        """更新进度值"""
        for i in range(100):
            time.sleep(0.05)
            self.te1.setText(str(i))
        
    def countstop_func(self):
        """重置进度值"""
        self.te1.setText('0')
        
if __name__ == '__main__':
    app=QApplication(sys.argv)
    tt=Test11()
    sys.exit(app.exec_())

演示:

通过上面的演示可以发现,点击“更新开始”按钮后,界面就卡住了,然后等待一段时间后,文本框的数值变成99,这期间文本框无法实时更新数值。

那么,我们使用线程来看看:
直接使用线程

import sys
import threading
import time

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

class Test11(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()
        
    def initUI(self):
        
        self.btn0=QPushButton('更新开始',self)
        self.btn0.setGeometry(100,100,80,40)
        self.btn0.clicked.connect(self.countstart_func)
        
        self.btn1=QPushButton('更新复位',self)
        self.btn1.setGeometry(100,160,80,40)
        self.btn1.clicked.connect(self.countstop_func)
        
        self.te1=QTextEdit(self)
        self.te1.setGeometry(200,100,80,20)
        self.te1.setText('0')
        
        self.setWindowTitle('线程数据传递示例')
        self.setGeometry(100,100,400,400)
        self.show()
    def countstart_func(self):
        thread1=threading.Thread(name='th1',target=self.counter_func)
        thread1.start()
        #self.counter_func()
        
    def counter_func(self):
        """更新进度值"""
        for i in range(100):
            time.sleep(0.05)
            self.te1.setText(str(i))
        
    def countstop_func(self):
        """重置进度值"""
        self.te1.setText('0')
        
if __name__ == '__main__':
    app=QApplication(sys.argv)
    tt=Test11()
    sys.exit(app.exec_())

可以看到,上面的代码中,调用循环函数时,新建了一个线程,来运行一下看看:

可以看到,程序运行后,点击“更新开始”按钮,程序直接报错了:

QObject: Cannot create children for a parent that is in a different thread.
(Parent is QTextDocument(0x27bf228b2f0), parent's thread is QThread(0x27bf027dd10), current thread is QThread(0x27bf2ad45e0)

报错的解释:

这个错误信息表明你试图在一个线程中创建一个子对象,而该子对象的父对象位于另一个线程中。在Qt中,对象的父对象和子对象必须在同一个线程中创建和管理,这是为了确保线程安全。

也就是说,直接在新建线程中更新文本框,属于是两个线程间的操作,是不支持的,所以需要采取其他方法。

利用信号、槽在线程间传递数据
既然不能直接在两个线程间操作数据,那就需要间接来对数据进行传递:

import sys
import threading
import time

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

class countcount(QThread):
    data1=pyqtSignal(str)
    def run(self):
        self.counter_func()
        
    def counter_func(self):
        """更新进度值"""
        for i in range(100):
            time.sleep(0.05)
            self.data1.emit(str(i))
        

class Test11(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()
        
    def initUI(self):
        
        self.btn0=QPushButton('更新开始',self)
        self.btn0.setGeometry(100,100,80,40)
        self.btn0.clicked.connect(self.countstart_func)
        
        self.btn1=QPushButton('更新复位',self)
        self.btn1.setGeometry(100,160,80,40)
        self.btn1.clicked.connect(self.countstop_func)
        
        self.te1=QTextEdit(self)
        self.te1.setGeometry(200,100,80,20)
        self.te1.setText('0')
        
        self.setWindowTitle('线程数据传递示例')
        self.setGeometry(100,100,400,400)
        self.show()
        
    def countstart_func(self):
        self.thread1=countcount()
        self.thread1.data1.connect(self.func1)
        self.thread1.start()
        #self.counter_func()
        
    @pyqtSlot(str)
    def func1(self,data):
        
        self.te1.setText(data)
    
        
    def countstop_func(self):
        """重置进度值"""
        self.te1.setText('0')
        
if __name__ == '__main__':
    app=QApplication(sys.argv)
    tt=Test11()
    sys.exit(app.exec_())

可以看到,代码进行较大的改变,首先是新建了一个线程类:

class countcount(QThread):
    data1=pyqtSignal(str)
    def run(self):
        self.counter_func()
        
    def counter_func(self):
        """更新进度值"""
        for i in range(100):
            time.sleep(0.05)
            self.data1.emit(str(i))

在这个countcount类中定义了一个信号data1,被定义为字符类型的数据,用于向外界传递线程内部的数据,此例中,传递的就是循环中间的每一个数值。
另外,要注意到,原本写在主线程程序中的循环函数,被放到了自定义的线程类中,就是counter_func这个函数。

然后在主线程的调用:

  def countstart_func(self):
        self.thread1=countcount()
        self.thread1.data1.connect(self.func1)
        self.thread1.start()
        #self.counter_func()
        
    @pyqtSlot(str)
    def func1(self,data):
        
        self.te1.setText(data)

在主线程中,将之前自定义的线程类countcount实例化为self.thread1,然后将自定义线程中定义的信号data1与主线程的函数self.func1连接起来。
即,每当data1这个信号触发时:

self.data1.emit(str(i))

都会调用self.func1函数,并且将data1的值传递给func1。

@pyqtSlot(str)
    def func1(self,data):
        
        self.te1.setText(data)

在本例中,data1就是循环中的实时值,然后每更新一次,就会调用func1,func1中会将data1的值传到文本框中。
这样就实现了子线程和主线程之间的数据传递。
演示:

完整代码:

import sys
import threading
import time

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

class countcount(QThread):
    data1=pyqtSignal(str)
    def run(self):
        self.counter_func()
        
    def counter_func(self):
        """更新进度值"""
        for i in range(100):
            time.sleep(0.05)
            self.data1.emit(str(i))
        

class Test11(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()
        
    def initUI(self):
        
        self.btn0=QPushButton('更新开始',self)
        self.btn0.setGeometry(100,100,80,40)
        self.btn0.clicked.connect(self.countstart_func)
        
        self.btn1=QPushButton('更新复位',self)
        self.btn1.setGeometry(100,160,80,40)
        self.btn1.clicked.connect(self.countstop_func)
        
        self.te1=QTextEdit(self)
        self.te1.setGeometry(200,100,80,20)
        self.te1.setText('0')
        
        self.setWindowTitle('线程数据传递示例')
        self.setGeometry(100,100,400,400)
        self.show()
        
    def countstart_func(self):
        self.thread1=countcount()
        self.thread1.data1.connect(self.func1)
        self.thread1.start()
        #self.counter_func()
        
    @pyqtSlot(str)
    def func1(self,data):
        
        self.te1.setText(data)
    
        
    def countstop_func(self):
        """重置进度值"""
        self.te1.setText('0')
        
if __name__ == '__main__':
    app=QApplication(sys.argv)
    tt=Test11()
    sys.exit(app.exec_())

注:最后再说明一下,本文主要是为了作一个记录,方便以后遇到类似问题时,可以直接参考,但如果有同样需求或者问题的朋友,你能够通过此文解决一些问题,那也是很好的。

你可能感兴趣的:(python,python,qt,开发语言,pyqt5,多线程)