在使用pyqt开发界面时,遇到了一种情况,就是在点击按钮之后,响应函数中会启动一个循环,该循环会一直执行,然后就造成界面无响应,如下所示,由于我是在Linux下运行的,所以界面直接显示成灰色(windows应该显示“无响应”):
这是因为对于pyqt来说,界面线程是主线程,如果我们在主线程函数里面调用了一个耗时比较久的循环,可能就会造成主界面线程卡死在循环中,从而造成无法操作主界面或者主界面卡顿、卡死。
所以这种情况下必须使用多线程的方式来解决,即在主界面线程中在启动一个新的子线程,利用该子线程处理比较耗时的操作,然后通过signal-slot机制将子线程的数据反馈到主界面线程中,而且在子线程中不能操作界面。这就是所说的:UI只用来操作UI,子线程只用来处理数据,就是将UI的操作与耗时数据的处理进行分开处理。
在pyqt中,可以通过QThread建立一个线程,
下面介绍 QThread 的第一种用法:新建一个类 RunThread 继承自 QThread,然后在 RunThread 类中重写 run() 函数,在 run() 函数中进行耗时数据的处理。下面是它的用法:
#!/usr/bin/python
# coding:UTF-8
from PyQt5 import QtWidgets, QtCore
import sys
from PyQt5.QtCore import *
import time
# 继承QThread
class Runthread(QtCore.QThread):
# 通过类成员对象定义信号对象
_signal = pyqtSignal(str)
def __init__(self):
super(Runthread, self).__init__()
def __del__(self):
self.wait()
def run(self):
for i in range(100):
time.sleep(0.1)
self._signal.emit(str(i)) # 注意这里与_signal = pyqtSignal(str)中的类型相同
class Example(QtWidgets.QWidget):
def __init__(self):
super(Example, self).__init__()
# 按钮初始化
self.button = QtWidgets.QPushButton('开始', self)
self.button.move(120, 80)
self.button.clicked.connect(self.start_login) # 绑定多线程触发事件
# 进度条设置
self.pbar = QtWidgets.QProgressBar(self)
self.pbar.setGeometry(50, 50, 210, 25)
self.pbar.setValue(0)
# 窗口初始化
self.setGeometry(300, 300, 300, 200)
self.show()
self.thread = None # 初始化线程
def start_login(self):
# 创建线程
self.thread = Runthread()
# 连接信号
self.thread._signal.connect(self.call_backlog) # 进程连接回传到GUI的事件
# 开始线程
self.thread.start()
def call_backlog(self, msg):
self.pbar.setValue(int(msg)) # 将线程的参数传入进度条
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
myshow = Example()
myshow.show()
sys.exit(app.exec_())
上面的代码建立一个界面,界面中只包含了一个进度条和一个按钮,点击按钮之后,进度条开始运行。
在上面的代码中,新建了一个 RunThread 类,该类继承自 QThread 类,在 RunThread 中重写了 run() 函数,并将耗时处理放在了 run() 函数中,点击按钮之后,触发 start_login() 函数,在start_login() 中,先创建了 RunThread 线程类的对象,然后将该类中的 _signal 信号与 Example 类中的 call_back() 函数建立连接,这样,就可以在run()函数运行时,将运行时的数据传递(异步,因为信号的传递与触发有一定的延时)到主机面 Example 类中并进行显示,如下所示:
在pyqt中多线程的使用还有另外一种方式:RunThread 类继承自 QObject,而非继承自 QThread。这种方式使用起来比第一种要复杂,但是这种方法将数据的处理与线程的创建与启动分开进行处理,在某些场景下,采用这种方式会比较方便。
下面是第二种方式的代码:
#!/usr/bin/python
# coding:UTF-8
from PyQt5 import QtWidgets, QtCore
import sys
from PyQt5.QtCore import *
import time
# 继承 QObject
class Runthread(QtCore.QObject):
# 通过类成员对象定义信号对象
signal = pyqtSignal(str)
def __init__(self):
super(Runthread, self).__init__()
self.flag = True
def __del__(self):
print ">>> __del__"
def run(self):
i = 0
while self.flag:
time.sleep(1)
if i <= 100:
self.signal.emit(str(i)) # 注意这里与_signal = pyqtSignal(str)中的类型相同
i += 1
print ">>> run end: "
class Example(QtWidgets.QWidget):
# 通过类成员对象定义信号对象
_startThread = pyqtSignal()
def __init__(self):
super(Example, self).__init__()
# 按钮初始化
self.button_start = QtWidgets.QPushButton('开始', self)
self.button_stop = QtWidgets.QPushButton('停止', self)
self.button_start.move(60, 80)
self.button_stop.move(160, 80)
self.button_start.clicked.connect(self.start) # 绑定多线程触发事件
self.button_stop.clicked.connect(self.stop) # 绑定多线程触发事件
# 进度条设置
self.pbar = QtWidgets.QProgressBar(self)
self.pbar.setGeometry(50, 50, 210, 25)
self.pbar.setValue(0)
# 窗口初始化
self.setGeometry(300, 300, 300, 200)
self.show()
self.myT = Runthread() # 创建线程对象
self.thread = QThread(self) # 初始化QThread子线程
# 把自定义线程加入到QThread子线程中
self.myT.moveToThread(self.thread)
self._startThread.connect(self.myT.run) # 只能通过信号-槽启动线程处理函数
self.myT.signal.connect(self.call_backlog)
def start(self):
if self.thread.isRunning(): # 如果该线程正在运行,则不再重新启动
return
# 先启动QThread子线程
self.myT.flag = True
self.thread.start()
# 发送信号,启动线程处理函数
# 不能直接调用,否则会导致线程处理函数和主线程是在同一个线程,同样操作不了主界面
self._startThread.emit()
def stop(self):
if not self.thread.isRunning(): # 如果该线程已经结束,则不再重新关闭
return
self.myT.flag = False
self.stop_thread()
def call_backlog(self, msg):
self.pbar.setValue(int(msg)) # 将线程的参数传入进度条
def stop_thread(self):
print ">>> stop_thread... "
if not self.thread.isRunning():
return
self.thread.quit() # 退出
self.thread.wait() # 回收资源
print ">>> stop_thread end... "
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
myshow = Example()
myshow.show()
sys.exit(app.exec_())
该界面包含了一个进度条、一个开始按钮、一个停止按钮。当点击“开始”按钮之后,进度条会开始运行;当点击“停止”按钮时,进度条会停止运行,如下所示:
其中 RunThread 类是线程处理函数类,该类继承自 QObject,然后通过 moveToThread 函数将该线程处理函数类添加进一个线程中。
在使用这种方式时需要注意一下几点:
在QT中,查看connect函数原型:
当然,connect有多重函数重载形式,以上只是其中的一种。其中的第五个参数type指明了signal-slot的连接方式,Qt::ConnectionType有一些几种类型:
前面三种是比较常用的,其中QueuedConnection方式是用在上面多线程的情况下。
QueuedConnection:槽函数所在线程和接收所在线程是一样的;
DirectConnection: 槽函数所在线程和发送者所在线程是一样的;
不过大多数情况下,调用connect是使用默认参数就可以了,当使用默认参数AutoConnection时:
在多线程情况下,默认使用QueuedConnection;
在单线程下,默认使用DirectConnection;
同理,在pyqt中也一样,pyqt中connect函数原型:
使用方式也一样,直接使用默认连接方式就可以了。
其实pyqt和qt差别不大,就只有语言上的差别,使用方式还都是一样的,我一般都是先查qt上资料然后在套用到pyqt上。