19 多线程
《PyQt5快速开发与实战》学习笔记。
一般情况下,应用程序都是单线程运行的,但是对于 GUI 程序来说,单线程有时候满足不了需求。比如,如果需要执行一个特别耗时的操作,在执行过程中整个程序就会卡顿,这时候用户可能以为程序出错,就把程序关闭了;或者 Windows 系统也认为程序运行出错,自动关闭了程序。要解决这种问题就涉及多线程的知识。一般来说,多线程技术涉及三种方法,其中一种是使用计时器模块 QTimer;一种是使用多线程模块 QThread;还有一种是使用事件处理的功能。
19.1 QTimer
如果要在应用程序中周期性地进行某项操作,比如周期性地检测主机的 CPU 值,则需要用到 QTimer(定时器),QTimer 类提供了重复的和单次的定时器。要使用定时器,需要先创建一个 QTimer 实例,将其 timeout
信号连接到相应的 slot,并调用 start()
。然后,定时器会以恒定的间隔发出 timeout
信号。当窗口控件收到 timeout
信号后,它就会停止这个定时器。这是在图形用户界面中实现复杂工作的一个典型方法,随着技术的进步,多线程在越来越多的平台上被使用,最终 QTimer 对象会被线程所替代。
from PyQt5.QtWidgets import QWidget, QPushButton, QApplication, QListWidget, QGridLayout, QLabel
from PyQt5.QtCore import QTimer, QDateTime
import sys
class WinForm(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("QTimer demo")
self.listFile = QListWidget()
self.label = QLabel('显示当前时间')
self.startBtn = QPushButton('开始')
self.endBtn = QPushButton('结束')
layout = QGridLayout(self)
# 初始化一个定时器
self.timer = QTimer(self)
# showTime()方法
self.timer.timeout.connect(self.showTime)
layout.addWidget(self.label, 0, 0, 1, 2)
layout.addWidget(self.startBtn, 1, 0)
layout.addWidget(self.endBtn, 1, 1)
self.startBtn.clicked.connect(self.startTimer)
self.endBtn.clicked.connect(self.endTimer)
self.setLayout(layout)
def showTime(self):
# 获取系统现在的时间
time = QDateTime.currentDateTime()
# 设置系统时间显示格式
timeDisplay = time.toString("yyyy-MM-dd hh:mm:ss dddd")
# 在标签上显示时间
self.label.setText(timeDisplay)
def startTimer(self):
# 设置计时间隔并启动
self.timer.start(1000)
self.startBtn.setEnabled(False)
self.endBtn.setEnabled(True)
def endTimer(self):
self.timer.stop()
self.startBtn.setEnabled(True)
self.endBtn.setEnabled(False)
if __name__ == "__main__":
app = QApplication(sys.argv)
form = WinForm()
form.show()
sys.exit(app.exec_())
效果如下:
演示弹出一个窗口,然后这个窗口在 10 秒后消失。其完整代码如下:
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
if __name__ == '__main__':
app = QApplication(sys.argv)
label = QLabel(
"<font color=red size=128><b>Hello PyQT,窗口会在10秒后消失!</b></font>")
label.setWindowFlags(Qt.SplashScreen | Qt.FramelessWindowHint)
label.show()
# 设置10s后自动退出
QTimer.singleShot(10000, app.quit)
sys.exit(app.exec_())
效果如下:
弹出的窗口将在10秒后消失,模仿程序的启动画面。将弹出的窗口设置为无边框。
19.2 QThread
QThread 是 Qt 线程类中最核心的底层类。由于 PyQt5 的跨平台特性,QThread 要隐藏所有与平台相关的代码。要使用 QThread 开始一个线程,可以创建它的一个子类,然后覆盖其 QThread.run()
函数。
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
class MainWidget(QWidget):
def __init__(self, parent=None):
super(MainWidget, self).__init__(parent)
self.setWindowTitle("QThread 例子")
self.thread = Worker()
self.listFile = QListWidget()
self.btnStart = QPushButton('开始')
layout = QGridLayout(self)
layout.addWidget(self.listFile, 0, 0, 1, 2)
layout.addWidget(self.btnStart, 1, 1)
self.btnStart.clicked.connect(self.slotStart)
self.thread.sinOut.connect(self.slotAdd)
def slotAdd(self, file_inf):
self.listFile.addItem(file_inf)
def slotStart(self):
self.btnStart.setEnabled(False)
self.thread.start()
class Worker(QThread):
sinOut = pyqtSignal(str)
def __init__(self, parent=None):
super(Worker, self).__init__(parent)
self.working = True
self.num = 0
def __del__(self):
self.working = False
self.wait()
def run(self):
while self.working == True:
file_str = 'File index {0}'.format(self.num)
self.num += 1
# 发出信号
self.sinOut.emit(file_str)
# 线程休眠2秒
self.sleep(2)
if __name__ == "__main__":
app = QApplication(sys.argv)
demo = MainWidget()
demo.show()
sys.exit(app.exec_())
效果如下:
分离 UI 主线程与工作线程
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
global sec
sec = 0
class WorkThread(QThread):
trigger = pyqtSignal()
def __int__(self):
super(WorkThread, self).__init__()
def run(self):
for i in range(2000000000):
pass
# 循环完毕后发出信号
self.trigger.emit()
def countTime():
global sec
sec += 1
# LED显示数字+1
lcdNumber.display(sec)
def work():
# 计时器每秒计数
timer.start(1000)
# 计时开始
workThread.start()
# 当获得循环完毕的信号时,停止计数
workThread.trigger.connect(timeStop)
def timeStop():
timer.stop()
print("运行结束用时", lcdNumber.value())
global sec
sec = 0
if __name__ == "__main__":
app = QApplication(sys.argv)
top = QWidget()
top.resize(300, 120)
# 垂直布局类QVBoxLayout
layout = QVBoxLayout(top)
# 加个显示屏
lcdNumber = QLCDNumber()
layout.addWidget(lcdNumber)
button = QPushButton("测试")
layout.addWidget(button)
timer = QTimer()
workThread = WorkThread()
button.clicked.connect(work)
# 每次计时结束,触发 countTime
timer.timeout.connect(countTime)
top.show()
sys.exit(app.exec_())
效果:
19.3 事件处理
PyQt5 为事件处理提供了两种机制:高级的信号与槽机制,以及低级的事件处理程序。本节只介绍低级的事件处理程序,即 processEvents() 函数的使用方法,它的作用是处理事件,简单地说,就是刷新页面。
对于执行很耗时的程序来说,由于 PyQt 需要等待程序执行完毕才能进行下一步,这个过程表现在界面上就是卡顿;而如果在执行这个耗时程序时不断地运行 QApplication.processEvents(),那么就可以实现一边执行耗时程序,一边刷新页面的功能,给人的感觉就是程序运行很流畅。因此 QApplication.processEvents() 的使用方法就是,在主函数执行耗时操作的地方,加入 QApplication.processEvents()。
实时刷新界面例子:
from PyQt5.QtWidgets import QWidget, QPushButton, QApplication, QListWidget, QGridLayout
import sys
import time
class WinForm(QWidget):
def __init__(self, parent=None):
super(WinForm, self).__init__(parent)
self.setWindowTitle("实时刷新界面例子")
self.listFile = QListWidget()
self.btnStart = QPushButton('开始')
layout = QGridLayout(self)
layout.addWidget(self.listFile, 0, 0, 1, 2)
layout.addWidget(self.btnStart, 1, 1)
self.btnStart.clicked.connect(self.slotAdd)
self.setLayout(layout)
def slotAdd(self):
for n in range(10):
str_n = 'File index {0}'.format(n)
self.listFile.addItem(str_n)
QApplication.processEvents()
time.sleep(1)
if __name__ == "__main__":
app = QApplication(sys.argv)
form = WinForm()
form.show()
sys.exit(app.exec_())
效果: