一般情况下,应用程序都是单线程运行的,但是对于GUI程序来说,单线程有时候满足不了需求。比如,如果需要执行一个特别耗时的操作,在执行过程中整个程序就会卡顿,这时候用户可能以为程序出错,就把程序关闭了;或者Windows系统也认为程序运行出错,自动关闭了程序。要解决这种问题就涉及多线程的知识。
一般来说,多线程技术涉及三种方法:
如果要在应用程序中周期性地进行某项操作,比如周期性地检测主机的 CPU值,则需要用到QTimer(定时器),QTimer类提供了重复的和单次的定时器。要使用定时器,需要先创建一个QTimer实例,将其timeout信号连接到相应的槽,并调用start()。然后,定时器会以恒定的间隔发出 timeout信号。
当窗口控件收到timeout信号后,它就会停止这个定时器。这是在图形用户界面中实现复杂工作的一个典型方法,随着技术的进步,多线程在越来越多的平台上被使用,最终QTimer对象会被线程所替代。
QTimer类中的常用方法
方法 | 描述 |
---|---|
start(milliseconds) | 启动或重新启动定时器,时间间隔为毫秒。如果定时器已经运行,它将被停止并重新启动。如果singleShot信号为真,定时器将仅被激活一次 |
Stop() | 停止定时器 |
QTimer类中的常用信号:
信号 | 描述 |
---|---|
singleShot | 在给定的时间间隔后调用一个槽函数时发射此信号 |
timeout | 当定时器超时时发射此信号 |
案例
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(WinForm, self).__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__':
from pyqt5_plugins.examples.exampleqmlitem import QtCore
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
app = QApplication(sys.argv)
win = WinForm()
win.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("Hello,10秒!")
label.setWindowFlags(Qt.SplashScreen | Qt.FramelessWindowHint)
label.show()
QTimer.singleShot(10000,app.quit)
sys.exit(app.exec_())
QThread是Qt线程类中最核心的底层类。由于PyQt的跨平台特性,QThread要隐藏所有与平台相关的代码。
要使用QThread开始一个线程,可以创建它的一个子类然后覆盖其QThread.run()函数。
from PyQt5.QtCore import QThread
class Thread(QThread):
def __init__(self):
super(Thread, self).__init__()
def run(self):
pass
thread = Thread()
thread.start()
在使用线程时可以直接得到Thread实例,调用其start()函数即可启动线程。线程启动之后,会自动调用其实现的run方法,该方法就是线程的执行函数。
业务的线程任务就写在 run()函数中,当run()退出之后线程基本就结束了。QThread有started和 finished信号,可以为这两个信号指定槽函数,在线程启动和结束时执行一段代码进行资源的初始化和释放操作。更灵活的使用方法是,在自定义的QThread实例中自定义信号,并将信号连接到指定的槽函数,当满足一定的业务条件后发射此信号。
QThread类中的常用方法和信号
方法:
方法 | 描述 |
---|---|
start() | 启动线程 |
wait() | 阻止线程,直到满足如下条件之一:与此QThread对象关联的线程已完成执行(即从 run()返回时)。如果线程完成执行,此函数将返回True:如果线程尚未启动,此函数也返回True;等待时间的单位是亳秒。如果时间是ULONG_MAX(默认值),则等待,永远不会超时(线程必须从 run()返回);如果等待超时,此函数将返回False |
sleep() | 强制当前线程睡眠 |
信号
信号 | 描述 |
---|---|
started | 在开始执行run()函数之前,从相关线程发射此信号 |
finished | 当程序完成业务逻辑时,从相关线程发射此信号 |
QThread实例
当在窗口中显示的数据比较简单时,可以把读取数据的业务逻辑放在窗口的初始化代码中,但如果读取数据的时间比较长,比如网络请求的时间比较长,则可以把这部分逻辑放在QThread线程中,实现界面的数据显示和数据读取的分离,满足MVC设计模式的要求。
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__':
from pyqt5_plugins.examples.exampleqmlitem import QtCore
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
app = QApplication(sys.argv)
win = MainWidget()
win.show()
sys.exit(app.exec_())
模拟长时间读取数据导致界面卡死
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
global sec
sec=0
def setTime():
global sec
sec+=1
# LED显示数字+1
lcdNumber.display(sec)
def work():
# 计时器每秒计数
timer.start(1000)
for i in range(2000000000):
pass
timer.stop()
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()
# 每次计时结束,触发setTime
timer.timeout.connect(setTime)
button.clicked.connect(work)
top.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_())
PyQt为事件处理提供了两种机制:高级的信号与槽机制,以及低级的事件处理程序。这里只介绍低级的事件处理程序,即proccssEvents()函数的使用方法,它的作用是处理事件,简单地说,就是刷新页面。
对于执行很耗时的程序来说,由于 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_())
个人感觉这更像卡出来的