【pyqt5学习——信号与槽】实例计时器(解决界面卡顿问题)

目录

一、方法一:另开线程

1、什么是信号与槽

1)GUI控件(信号)与槽

2)自定义信号与槽

2、实战1:计时器(不自定义信号槽和不使用多线程)

1)界面设计——利用qt-designer设计,然后pyuic编译为py文件

2)重写UI类,编写逻辑文件

 3)在内置信号槽函数中增加大循环——正常运行

4)在按钮控件绑定的槽函数中增加大循环——GUI假死卡顿

3、实战2:计时器——利用多线程解决GUI卡顿的问题

1)GUI卡顿的常见原因

2)解决卡顿方法——多线程

(1)创建线程类,重写run函数

(2)在GUI类初始化函数__init__中实例化线程类

(3)在对应的槽函数中启动线程

(4)完整代码

4、实战3:计时器——利用自定义信号槽进行参数传递

1)在线程类中自定义信号(带参信号)

2)在线程类中发射信号

3)在GUI类中绑定信号与槽函数,接收信号发射的参数

4)完整代码

5、总结

二、方法二:QtWidgets.QApplication.processEvents()



一、方法一:另开线程

1、什么是信号与槽

按一下开关,灯亮了

在上面的描述中,“按一下开关”这个动作就是信号,而槽指的是完成这个动作的时候会发生的事情。可以理解为只有触发了信号,才能让相应的槽函数(事件)发生

在GUI中常见的信号与槽就是按钮控件和按钮绑定的回调函数。当单击\释放\双击按钮时,会根据信号运行不同的回调函数,也就是槽函数。常见的信号与槽形式如下:

1)GUI控件(信号)与槽

此类信号与槽,信号主要是针对GUI上控件,槽函数可以是GUI自带的,也可以是自定义的槽函数

创建此类信号与槽的步骤:

  • 创建控件即按钮
    self.pushButton = QtWidgets.QPushButton(self.centralwidget)
  • 编写槽函数
    # 自定义槽函数
    def slotEvent(self):
         for i in range(1000000):
              print(i)
    
    # GUI自带槽函数——界面退出事件
    def slotEvent(self):
        exit()
    
  • 根据需求将控件相关动作(信号)与对应的槽函数绑定起来

控件名.动作.connect(槽函数)

    self.pushButton.clicked.connect(self.slotEvent)
    信号:单击按钮    槽函数:slotEvent()函数

2)自定义信号与槽

  • 导入相应模块
    from PyQt5.Qt import pyqtSignal
  • 自定义信号(可以带参或者不带参)

 信号名 = pyqtSignal(类型)      

         intSignal = pyqtSignal(int)

  • 定义槽函数——同1)

# 自定义槽函数
def slotEvent(self):
      for i in range(1000000):
           print(i)

# GUI自带槽函数——界面退出事件
def slotEvent(self):
      exit()

  • 发射信号
    信号名.emit(参数内容)
    self.intSignal.emit(val)

注意:信号发射时的参数类型个数,必需保证和定义时的参数类型和个数一致。

  • 接收信号(信号绑定对应的槽函数)

信号名.connect(槽函数)

        self.intSignal.connect(self.slotEvent)

2、实战1:计时器(不自定义信号槽和不使用多线程)

开发环境:pycharm + window10 + pyqt5

功能:

1、按下开始计时按钮后,计数板开始计数,同时按钮文字修改为停止检测

2、按下停止计时按钮后,计数板停止计数,并且计时归零,同时按钮文字修改为开始计时

1)界面设计——利用qt-designer设计,然后pyuic编译为py文件

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'timerWithoutThread.ui'
#
# Created by: PyQt5 UI code generator 5.15.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(307, 165)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout.setObjectName("verticalLayout")
        self.lcdNumber = QtWidgets.QLCDNumber(self.centralwidget)
        self.lcdNumber.setObjectName("lcdNumber")
        self.verticalLayout.addWidget(self.lcdNumber)
        self.pushButton = QtWidgets.QPushButton(self.centralwidget)
        self.pushButton.setObjectName("pushButton")
        self.verticalLayout.addWidget(self.pushButton)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 307, 23))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "计时器——不含进程"))
        self.pushButton.setText(_translate("MainWindow", "开始计时"))


if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())

2)重写UI类,编写逻辑文件

构成:

class 自定义类名(QtWidgets.QMainWindow,Ui_MainWindow):
    def __init__(self):
        super(mainUi, self).__init__() # 重写类
        self.setupUi(self)
        self.run() # 用于绑定信号与槽

  def 事件1(self):
        ...

  def 事件2(self):
        ...

  def run(self):
        控件名.动作.connect(事件1)
        控件名.动作.connect(事件2)

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/10/31 19:31
# @Author  : @linlianqin
# @Site    : 
# @File    : timerWithoutThreadLogic.py
# @Software: PyCharm
# @description:不使用线程和信号槽实现计数器

from PyQt5 import QtWidgets
from pyqt_learn.signal_slot.不使用线程和信号槽实现计数器.timerWithoutThread import Ui_MainWindow
from PyQt5.QtCore import QTimer

'''
这里的信号和槽之间的关系不是自定义的,而是单纯通过pyqt自带的来实现,主要有两个信号与槽的对应关系:
1、信号:每秒计数器计数结束    槽函数:更新计数板上的信息
2、信号:点击计数板上的按钮    槽函数:计数器开始工作计数开始
3、信号:点击计数板上的按钮    槽函数:计数器停止计数并且计数归零
注:这里点击按钮后,其实就是开始了一个循环,在循环中不断地调用每秒计数器,然后更新计数板信息

步骤:
1、每秒计数器类实例化,即QTimer
2、编写每秒计数器结束时的执行事件,即更新计数板上的数字
3、编写按钮事件,开始计数

结果:
1、按下开始计时按钮后,计数板开始计数,同时按钮文字修改为停止检测
2、按下停止计时按钮后,计数板停止计数,并且计时归零,同时按钮文字修改为开始计时
'''

global sec

class mainUi(QtWidgets.QMainWindow,Ui_MainWindow):
	def __init__(self):
		super(mainUi, self).__init__()
		self.setupUi(self)
		self.timer = QTimer() # 实例化每秒计数器
		global sec
		sec = 0
		self.run()

	# 更新计数器的数字
	def setTime(self):
		global sec
		sec += 1
		self.lcdNumber.display(sec)

	# 开始计数
	def startCount(self):
		# 设置计时间隔并启动,每隔1000毫秒(1秒)发送一次超时信号,循环进行,如果需要停止,可以调用timer.stop()进行停止
		self.timer.start(1000)
		# 当单击按钮开始计时后,按钮文字修改为停止计时,并且绑定的槽函数发生改变
		self.pushButton.setText("停止计时")
		self.pushButton.clicked.connect(self.stopTime)

	# 停止计时,计时置为0
	def stopTime(self):
		global sec
		sec = 0
		self.timer.stop()
		self.lcdNumber.display(sec)
		# 当单击按钮停止计时时,按钮文字修改为开始计时,按钮绑定槽函数改变
		self.pushButton.setText("开始计时")
		self.pushButton.clicked.connect(self.startCount)

	# 绑定信号与槽
	def run(self):
		# 每秒计数器计数结束后更新计数板数字
		self.timer.timeout.connect(self.setTime)
		# 单击按钮计数开始
		self.pushButton.clicked.connect(self.startCount)

if __name__ == '__main__':
	import sys
	app = QtWidgets.QApplication(sys.argv)
	main = mainUi()
	main.show()
	sys.exit(app.exec_())

上述其实有两个信号,一个是单击按钮,一个是启动timer计时,而timer是内置的信号,

 3)在内置信号槽函数中增加大循环——正常运行

	# 更新计数器的数字
	def setTime(self):
		global sec
		sec += 1
		# 在内置信号绑定的事件中增加大循环,不会造成GUI卡死
		for i in range(1000000000):
			pass
		self.lcdNumber.display(sec)

实践证明计时器可以正常运行,内置信号的槽函数中增加大循环,不会造成GUI界面卡顿的现象,这是因为timer计数器内部使用了另一个线程来实现计数,不会影响GUI的主线程运行,因此不卡顿

4)在按钮控件绑定的槽函数中增加大循环——GUI假死卡顿

	# 开始计数
	def startCount(self):
		# 设置计时间隔并启动,每隔1000毫秒(1秒)发送一次超时信号,循环进行,如果需要停止,可以调用timer.stop()进行停止
		self.timer.start(1000)
		# 在GUI控件对应的槽函数内增加耗时的大循环
		for i in range(1000000):
			print(i)
			pass
		# 当单击按钮开始计时后,按钮文字修改为停止计时,并且绑定的槽函数发生改变
		self.pushButton.setText("停止计时")
		self.pushButton.clicked.connect(self.stopTime)

按钮对应的槽函数内增加大循环,在大循环运行完前GUI会出现卡顿的情况,这是因为按钮是属于GUI控件,控件对应的槽函数是在GUI主线程中进行的,因此会 使得主线程短暂性假死状态,也就是会导致GUI卡顿,等循环结束后,GUI恢复正常

3、实战2:计时器——利用多线程解决GUI卡顿的问题

1)GUI卡顿的常见原因

①含有复杂的运算

②含有耗时的循环

③time.sleep()

2)解决卡顿方法——多线程

       由卡顿的原因可以知道,卡顿主要是在GUI主线程运行的过程中,遇到了耗时的操作,这样导致在循环计算时,GUI产生假死卡顿状态。

       因此我们只需要将耗时的操作择出来,然后另开一个新的线程去执行这些耗时的操作,而主线程则用于触发这个新线程的开始,这样就可以解决GUI卡顿。

(1)创建线程类,重写run函数

run函数在线程调用start()函数时会自动运行

from PyQt5.QtCore import QTimer,QThread

# 新开一个线程进行循环的操作
class newThread(QThread):
	def __init__(self):
		super(newThread, self).__init__()

	# 大循环
	def run(self):
		for i in range(1000000):
			print(i)
			pass

(2)在GUI类初始化函数__init__中实例化线程类

self.loopThread = newThread()

(3)在对应的槽函数中启动线程

		# 开启新的线程来执行大循环
		self.loopThread.start()

(4)完整代码

       另开线程后,不会出现卡顿的现象,在进行大循环的时候,计数板上的数字依旧可以实时地进行更新,这里GUI代码没有改变

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/10/31 19:31
# @Author  : @linlianqin
# @Site    : 
# @File    : timerWithoutThreadLogic.py
# @Software: PyCharm
# @description:

from PyQt5 import QtWidgets
from pyqt_learn.signal_slot.不使用线程和信号槽实现计数器.timerWithoutThread import Ui_MainWindow
from PyQt5.QtCore import QTimer,QThread
from PyQt5.Qt import pyqtSignal


'''
这里的信号和槽之间的关系不是自定义的,而是单纯通过pyqt自带的来实现,主要有两个信号与槽的对应关系:
1、信号:每秒计数器计数结束    槽函数:更新计数板上的信息
2、信号:点击计数板上的按钮    槽函数:发射信号通知计数器开始计数
3、信号:点击计数板上的按钮    槽函数:计数器停止计数并且计数归零
4、信号:自定义信号           槽函数:计数器开始计数
注:这里点击按钮后,其实就是开始了一个循环,在循环中不断地调用每秒计数器,然后更新计数板信息

步骤:
1、每秒计数器类实例化,即QTimer
2、编写每秒计数器结束时的执行事件,即更新计数板上的数字
3、编写按钮事件,开始计数

结果:按钮对应的槽函数内增加大循环,在大循环运行完前GUI会出现卡顿的情况,这是因为按钮是属于GUI控件,控件对应的槽函数是在GUI主线程中进行的,因此会
使得主线程短暂性假死状态,也就是会导致GUI卡顿,等循环结束后,GUI恢复正常

方法:将循环部分择出来,然后另外开一个线程进行运行,然后单击按钮时,将线程开启,后台运行循环,而不会影响GUI主线程的运行,这样可以实现GUI不卡顿
'''

global sec

# 新开一个线程进行循环的操作
class newThread(QThread):
	def __init__(self):
		super(newThread, self).__init__()

	# 大循环
	def run(self):
		for i in range(1000000):
			print(i)
			pass

class mainUi(QtWidgets.QMainWindow,Ui_MainWindow):

	def __init__(self):
		super(mainUi, self).__init__()
		self.setupUi(self)
		self.timer = QTimer() # 实例化每秒计数器
		self.loopThread = newThread()
		global sec
		sec = 0
		self.run()

	# 更新计数器的数字
	def setTime(self):
		global sec
		sec += 1
		self.lcdNumber.display(sec)

	# 开始计数
	def startCount(self):
		# 设置计时间隔并启动,每隔1000毫秒(1秒)发送一次超时信号,循环进行,如果需要停止,可以调用timer.stop()进行停止
		self.timer.start(1000)
		# 当单击按钮开始计时后,按钮文字修改为停止计时,并且绑定的槽函数发生改变
		self.pushButton.setText("停止计时")
		self.pushButton.clicked.connect(self.stopTime)
		# 开启新的线程来执行大循环
		self.loopThread.start()

	# 停止计时,计时置为0
	def stopTime(self):
		global sec
		sec = 0
		self.timer.stop()
		self.lcdNumber.display(sec)
		# 当单击按钮停止计时时,按钮文字修改为开始计时,按钮绑定槽函数改变
		self.pushButton.setText("开始计时")
		self.pushButton.clicked.connect(self.startCount)

	# 绑定信号与槽
	def run(self):
		# 每秒计数器计数结束后更新计数板数字
		self.timer.timeout.connect(self.setTime)
		# 单击按钮发射自定义信号
		# self.pushButton.clicked.connect(self.startCount)
		# self.pushButton.clicked.connect(lambda:self.signal.emit()) # 这里通过lambda将语句函数化
		self.pushButton.clicked.connect(self.startCount)

if __name__ == '__main__':
	import sys
	app = QtWidgets.QApplication(sys.argv)
	main = mainUi()
	main.show()
	sys.exit(app.exec_())

4、实战3:计时器——利用自定义信号槽进行参数传递

GUI增加了一个label控件,用于实时更新显示大循环的循环次数

1)在线程类中自定义信号(带参信号)

 带参的类型根据需要传递的参数类型来确定

	# 自定义一个带整数参数的信号,用于点击按钮时使用
	intSignal = pyqtSignal(int)

2)在线程类中发射信号

这行代码一般出现在获取得到要传递的参数内容的位置,每获得一个新的参数内容,发射一次信号,因此这里在每循环一次就发射一次信号,信号带参,由信号绑定的槽函数接受参数

self.intSignal.emit(val)

3)在GUI类中绑定信号与槽函数,接收信号发射的参数

注意:这里槽函数接收的参数个数和类型和信号发射的参数个数和类型是一致的。


		# 将自定义信号连接到修改标签事件槽函数,这里信号发射会返回参数,然后槽函数会自动接收返回的参数
		self.loopThread.intSignal.connect(self.setLabel)
	def setLabel(self,val):
		self.label.setText(str(val))

4)完整代码

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/10/31 19:31
# @Author  : @linlianqin
# @Site    : 
# @File    : timerWithoutThreadLogic.py
# @Software: PyCharm
# @description:

from PyQt5 import QtWidgets
from pyqt_learn.signal_slot.利用信号槽传递参数.timerWithoutThread import Ui_MainWindow
from PyQt5.QtCore import QTimer,QThread
from PyQt5.Qt import pyqtSignal

'''
这里的信号和槽之间的关系不是自定义的,而是单纯通过pyqt自带的来实现,主要有两个信号与槽的对应关系:
1、信号:计数器结束信号  槽函数:更新计数板上数字
2、信号:单击按钮       槽函数:开始计数,同时触发新线程开始运行
3、信号:新线程运行
注:这里点击按钮后,其实就是开始了一个循环,在循环中不断地调用每秒计数器,然后更新计数板信息

期望:单击按钮开始计数,同时GUI标签修改为循环的值

步骤:
1、创建继承QThread的类
2、重写类def __init__()
3、在类名和__init__之间自定义信号
4、在类的run方法中发射信号emit()

5、在GUI类的__init__中实例化线程
6、在GUI中将线程中的自定义信号绑定槽函数

注意:
1、发射信号时的参数需要和自定义信号时的参数类型匹配;
2、在GUI类中自定义信号绑定的槽函数的参数个数以及类型要和自定义信号定义的参数个数以及类型匹配
'''

global sec

# 新开一个线程进行循环的操作
class newThread(QThread):
	# 自定义一个带整数参数的信号,用于点击按钮时使用
	intSignal = pyqtSignal(int)

	def __init__(self):
		super(newThread, self).__init__()

	# 大循环,run函数调用线程start函数时自动启动
	def run(self):
		i = 0
		while True:
			print(i)
			i += 1
			self.emit_(i)

	# 发射信号
	def emit_(self,val):
		self.intSignal.emit(val)

class mainUi(QtWidgets.QMainWindow,Ui_MainWindow):

	def __init__(self):
		super(mainUi, self).__init__()
		self.setupUi(self)
		self.timer = QTimer() # 实例化每秒计数器
		self.loopThread = newThread() # 实例化线程
		global sec
		sec = 0
		self.run()

	# 更新计数器的数字
	def setTime(self):
		global sec
		sec += 1
		self.lcdNumber.display(sec)

	# 开始计数
	def startCount(self):
		# 设置计时间隔并启动,每隔1000毫秒(1秒)发送一次超时信号,循环进行,如果需要停止,可以调用timer.stop()进行停止
		self.timer.start(1000)
		# 当单击按钮开始计时后,按钮文字修改为停止计时,并且绑定的槽函数发生改变
		self.pushButton.setText("停止计时")
		self.pushButton.clicked.connect(self.stopTime)
		# 开启新的线程来执行大循环
		self.loopThread.start()


	# 停止计时,计时置为0
	def stopTime(self):
		global sec
		sec = 0
		self.timer.stop()
		self.lcdNumber.display(sec)
		# 当单击按钮停止计时时,按钮文字修改为开始计时,按钮绑定槽函数改变
		self.pushButton.setText("开始计时")
		self.pushButton.clicked.connect(self.startCount)

	def setLabel(self,val):
		self.label.setText(str(val))


	# 绑定信号与槽
	def run(self):
		# 每秒计数器计数结束后更新计数板数字
		self.timer.timeout.connect(self.setTime)
		self.pushButton.clicked.connect(self.startCount)
		# 将自定义信号连接到修改标签事件槽函数,这里信号发射会返回参数,然后槽函数会自动接收返回的参数
		self.loopThread.intSignal.connect(self.setLabel)

if __name__ == '__main__':
	import sys
	app = QtWidgets.QApplication(sys.argv)
	main = mainUi()
	main.show()
	sys.exit(app.exec_())

结果:计数板实时计数;标签实时滚动循环的次数

5、总结

  • 涉及到耗时计算另开线程计算避免GUI卡死
  • 若需要传参,需要注意自定义信号参数类型和发射信号参数类型一致
  • 发射信号参数类型和槽函数参数类型需要保持一致

二、方法二:QtWidgets.QApplication.processEvents()

参考:pyqt5-实时刷新页面(QApplication.processEvents()) - 猿码利剑 - 博客园https://www.cnblogs.com/liugp/p/10382624.html

对于执行很耗时的程序来说,由于PyQt需要等待程序执行完毕才能进行下一步,这个过程表现在界面上就是卡顿,而如果需要执行这个耗时程序时不断的刷新界面。那么就可以使用QApplication.processEvents(),那么就可以一边执行耗时程序,一边刷新界面的功能,给人的感觉就是程序运行很流畅,因此QApplicationEvents()的使用方法就是,在主函数执行耗时操作的地方,加入QApplication.processEvents()
import sys,time
from PyQt5.QtWidgets import QWidget,QPushButton,QApplication,QListWidget,QGridLayout

class WinForm(QWidget):
    def __init__(self,parent=None):
        super(WinForm, self).__init__(parent)
        #设置标题与布局方式
        self.setWindowTitle('实时刷新界面的例子')
        layout=QGridLayout()

        #实例化列表控件与按钮控件
        self.listFile=QListWidget()
        self.btnStart=QPushButton('开始')

        #添加到布局中指定位置
        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)
    win=WinForm()
    win.show()
    sys.exit(app.exec_())

猜你喜欢

转载自blog.csdn.net/qq_45769063/article/details/121073538