在GUI程序中,單線程常常滿足不了需求。這是因為,當程序需要執行一個非常耗時的操作(例如渲染、大量計算、數據傳輸等操作),那么整個界面則可能出現無法交互的情況(這是非常糟糕的現象,想象一下,你想點擊一下按鈕,可是鼠標卻一直是轉圈圈的狀態)
一般來說,多線程技術涉及三種方法:
計時器模塊QTimer;
多線程模塊QThread;
事件處理
QTimer計時器
當我們要周期性進行某項操作時(如定時發送數據,定時獲取傳感器數據),則可以使用定時器QTimer,該類提供了重復定時器和單次定時器。
可以在具有事件循環的線程中使用 QTimer;要在非GUI線程啟動事件循環,可以使用 exec() 。Qt使用計時器的 ”線程相關性“ 來確定哪一個線程發出 timeout() 信號,因而必須在該線程中開啟和關閉計時器,而不能從另一個線程啟動或關閉。
==作為一種特殊情況,超時為0的QTimer將盡快超時,即盡快反復執行(一旦處理完窗口系統的事件隊列中的所有事件,超時間隔為0的QTimer就會超時)。==這種情況下槽函數代碼應該很簡單,能夠快速返回,并在完成所有工作后立即停止計時器。這是在GUI程序中實現繁重工作的傳統方式,但隨著多線程的廣泛使用,零毫秒計時器將被 QThread 代替。
計時器的精確度取決于底層操作系統和硬件。大多數平臺支持1毫秒的分辨率,盡管在許多現實世界的情況下,計時器的精度不會等于這個分辨率。如果系統繁忙或無法提供所要求的精度,則所有類型的計時器都可能比預期的晚超時,在這種超時溢出的情況下,即使多個超時已經過期,Qt也只會發出一次timeout() ,然后將恢復原始間隔。
定時器的實現依賴的是CPU時鐘中斷,時鐘中斷的精度就決定定時器精度的極限。一個時鐘中斷源如何實現多個定時器呢?對于內核,簡單來說就是用特定的數據結構管理眾多的定時器,在時鐘中斷處理中判斷哪些定時器超時,然后執行超時處理動作。而用戶空間程序不直接感知CPU時鐘中斷,通過感知內核的信號、IO事件、調度,間接依賴時鐘中斷。用軟件來實現動態定時器常用數據結構有:時間輪、最小堆和紅黑樹。
另一種實現計時器的方法是繼承QObject類,并重寫 timerEvent() 事件處理器,然后調用 startTimer() 函數。具體 細節可以查看:QTimer - Qt for Python
構造函數
class PySide6.QtCore.QTimer([parent=None])
- 1
parent – PySide6.QtCore.QObject
屬性
屬性名 | 描述 |
---|---|
active | 布爾值,計時器是否正在運行 |
singleShot | 布爾值,計時器是否為單次計時器,默認為false |
interval | int類型,超時間隔,單位為毫秒。默認值為0 |
函數
函數名 | 描述 |
---|---|
start() start(msec) | 開啟計時器,如果singleShot為true,則計時器將僅激活一次。 |
stop() | 關閉計時器 |
timeout() | 計時器超時時會發出此信號 |
重復計時器
import sys
from PySide6.QtCore import QDateTime, QTimer
from PySide6.QtGui import Qt
from PySide6.QtWidgets import QWidget, QApplication, QLabel, QPushButton, QGridLayout
class LearnQThread(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle('Learn QThread')
self.resize(640, 480)
self.label = QLabel(self)
self.label.setAlignment(Qt.AlignCenter)
self.btnStart = QPushButton('start')
self.btnEnd = QPushButton('end')
layout = QGridLayout()
self.timer = QTimer()
self.timer.timeout.connect(self.showtime)
layout.addWidget(self.label, 0, 0, 1, 2)
layout.addWidget(self.btnStart, 1, 0)
layout.addWidget(self.btnEnd, 1, 1)
self.btnStart.clicked.connect(self.StartTimer)
self.btnEnd.clicked.connect(self.endTimer)
self.setLayout(layout)
"""
QTimer定時處理的任務
"""
def showtime(self):
# 獲取當前時間
time = QDateTime.currentDateTime()
# 將當前時間轉換成字符串類型
timeDisplay = time.toString('yyyy-MM-dd hh:mm:ss dddd')
self.label.setText(timeDisplay)
def StartTimer(self):
# 開啟定時器,執行時間/任務的頻率是100ms
self.timer.start(100)
self.btnStart.setEnabled(False)
self.btnEnd.setEnabled(True)
def endTimer(self):
self.timer.stop()
self.label.clear()
self.btnStart.setEnabled(True)
self.btnEnd.setEnabled(False)
if __name__ == '__main__':
app = QApplication(sys.argv)
wid = LearnQThread()
wid.show()
sys.exit(app.exec())
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
單次計時器
import sys
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QFont
from PySide6.QtWidgets import QApplication, QWidget, QLabel,QGridLayout
class LearnQTimer(QWidget):
def __init__(self):
super().__init__()
self.resize(320,240)
self.label = QLabel(self)
layout = QGridLayout()
layout.addWidget(self.label)
self.setLayout(layout)
self.label.setText('close ui')
self.label.setAlignment(Qt.AlignCenter)
QTimer.singleShot(5000, self.close) # 5秒后自動關閉窗口
if __name__ == '__main__':
app = QApplication(sys.argv)
wid = LearnQTimer()
wid.show()
sys.exit(app.exec())
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
QThread多線程
QThread類提供了一種獨立于平臺的方式來管理線程。
QThread 在 run() 中開始執行。默認情況下,run() 通過調用exec() 啟動事件循環,并在線程內運行Qt事件循環。
構造函數
class PySide6.QtCore.QThread([parent=None])
- 1
parent – PySide6.QtCore.QObject
在調用start() 之前,線程不會開始執行。
函數
函數名 | 描述 |
---|---|
exec() | run() 會調用此函數,有必要調用此函數來啟動事件處理。這只能在線程本身內調用,即當它是當前線程時。 |
exit([retcode=0]) | 線程的事件循環退出并返回一個int型整數。 |
finished() | 當發出此信號時,事件循環已經停止運行,線程中將不再處理任何事件。這個信號可以連接到deleteLater(),以釋放該線程中的對象。 |
run() | 在調用 start() 之后,新創建的線程將自動調用此函數。默認實現只調用exec() |
start([priority=QThread.Priority.InheritPriority]) | 內部通過調用 run() 開始執行線程 |
started() | 執行 run() 之前發出此信號 |
import sys
from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWidgets import *
global sec
sec = 0
class WorkThread(QThread):
trigger = Signal()
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_())
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
事件處理
PyQt為事件處理提供了兩種機制:高級的信號與槽機制以及低級的事件處理程序。本篇文博只介紹低級的事件處理程序即:processEvents()函數的使用方法,它的作用是處理事件,簡單地說,就是刷新頁面。
對于執行很耗時的程序來說,由于PyQt需要等待程序執行完畢才能進行下一步,這個過程表現在界面上就是卡頓。而如果在執行這個耗時程序時不斷地運行 QApplication.processEvents(),那么就可以實現一邊執行耗時程序,一邊刷新頁面的功能,給人的感覺就是程序運行很流暢。
因此QApplication.processEvents()的使用方法就是,在主函數執行耗時操作的地方,加入QApplication.processEvents()。
from PySide6.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_())
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31