目錄
一、方法一:另開線程
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, QtWidgetsclass 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.translateMainWindow.setWindowTitle(_translate("MainWindow", "計時器——不含進程"))self.pushButton.setText(_translate("MainWindow", "開始計時"))if __name__ == "__main__":import sysapp = 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 secclass mainUi(QtWidgets.QMainWindow,Ui_MainWindow):def __init__(self):super(mainUi, self).__init__()self.setupUi(self)self.timer = QTimer() # 實例化每秒計數器global secsec = 0self.run()# 更新計數器的數字def setTime(self):global secsec += 1self.lcdNumber.display(sec)# 開始計數def startCount(self):# 設置計時間隔并啟動,每隔1000毫秒(1秒)發送一次超時信號,循環進行,如果需要停止,可以調用timer.stop()進行停止self.timer.start(1000)# 當單擊按鈕開始計時后,按鈕文字修改為停止計時,并且綁定的槽函數發生改變self.pushButton.setText("停止計時")self.pushButton.clicked.connect(self.stopTime)# 停止計時,計時置為0def stopTime(self):global secsec = 0self.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 sysapp = QtWidgets.QApplication(sys.argv)main = mainUi()main.show()sys.exit(app.exec_())
上述其實有兩個信號,一個是單擊按鈕,一個是啟動timer計時,而timer是內置的信號,
?3)在內置信號槽函數中增加大循環——正常運行
# 更新計數器的數字def setTime(self):global secsec += 1# 在內置信號綁定的事件中增加大循環,不會造成GUI卡死for i in range(1000000000):passself.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)passclass mainUi(QtWidgets.QMainWindow,Ui_MainWindow):def __init__(self):super(mainUi, self).__init__()self.setupUi(self)self.timer = QTimer() # 實例化每秒計數器self.loopThread = newThread()global secsec = 0self.run()# 更新計數器的數字def setTime(self):global secsec += 1self.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()# 停止計時,計時置為0def stopTime(self):global secsec = 0self.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 sysapp = 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 = 0while True:print(i)i += 1self.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 secsec = 0self.run()# 更新計數器的數字def setTime(self):global secsec += 1self.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()# 停止計時,計時置為0def stopTime(self):global secsec = 0self.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 sysapp = 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,QGridLayoutclass 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_())