下面是Python Pyqt5多线程更新UI代码实例(防止界面卡死)的完整攻略。
1. 背景
在使用PyQt5进行GUI开发时,假如某个计算机密集型的操作耗时较长,那么就可能会导致界面卡死,影响用户体验。为了避免这种情况,可以利用多线程更新UI的方式来解决。
2. 实现过程
2.1 创建线程
在PyQt5中利用QThread创建线程,在其run方法中执行需要进行的计算密集型操作,例如:
class Worker(QThread):
finishSignal = pyqtSignal(int)
def run(self):
# 计算密集型操作
# ...
def __del__(self):
self.wait()
2.2 进行信号槽连接
在主程序窗口中连接信号槽,将相关信号链接到主程序窗口的槽函数中。在槽函数中使用QMetaObject.invokeMethod() 方法来实现跨线程UI更新,例如:
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
self.worker = Worker(self)
self.worker.finishSignal.connect(self.update_ui)
def on_start_button_clicked(self):
self.worker.start()
@pyqtSlot(int)
def update_ui(self, progress):
QMetaObject.invokeMethod(self.progress_bar, "setValue", Qt.QueuedConnection, Q_ARG(int, progress))
以上代码中,在worker的finishSignal信号被触发时,将会执行主程序窗口的update_ui槽函数,该函数中通过QMetaObject.invokeMethod()来更新UI。
2.3 代码完整示例
import sys
import time
from PyQt5.QtCore import pyqtSignal, QThread, QMetaObject, Qt
from PyQt5.QtWidgets import QApplication, QMainWindow
from ui_mainwindow import Ui_MainWindow
class Worker(QThread):
finishSignal = pyqtSignal(int)
def run(self):
for i in range(101):
time.sleep(0.1)
self.finishSignal.emit(i)
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
self.worker = Worker(self)
self.worker.finishSignal.connect(self.update_ui)
self.start_button.clicked.connect(self.on_start_button_clicked)
def on_start_button_clicked(self):
self.worker.start()
@pyqtSlot(int)
def update_ui(self, progress):
QMetaObject.invokeMethod(self.progress_bar, "setValue", Qt.QueuedConnection, Q_ARG(int, progress))
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
以上代码中,我们通过Worker线程来模拟计算密集型操作,每隔0.1秒发射一次finishSignal信号,在主界面窗口的progress_bar进度条中进行更新。
3. 示例说明
3.1 示例1 - 多线程更新UI
我们在代码中添加一个QLabel控件,用于展示worker线程中的计数器变量。完整代码如下:
import sys
import time
from PyQt5.QtCore import pyqtSignal, QThread, QMetaObject, Qt
from PyQt5.QtWidgets import QApplication, QMainWindow
from ui_mainwindow import Ui_MainWindow
class Worker(QThread):
finishSignal = pyqtSignal(int)
def __init__(self, parent=None):
super().__init__(parent)
self.count = 0
def run(self):
for i in range(101):
time.sleep(0.1)
self.count = i
self.finishSignal.emit(i)
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
self.worker = Worker(self)
self.worker.finishSignal.connect(self.update_ui)
self.start_button.clicked.connect(self.on_start_button_clicked)
def on_start_button_clicked(self):
self.worker.start()
@pyqtSlot(int)
def update_ui(self, progress):
QMetaObject.invokeMethod(self.progress_bar, "setValue", Qt.QueuedConnection, Q_ARG(int, progress))
QMetaObject.invokeMethod(self.label, "setText", Qt.QueuedConnection, Q_ARG(str, f"Count: {self.worker.count}"))
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
在点击start_button按钮事件后,我们会启动Worker线程执行计算密集型操作,此时在主窗口的之前创建好的progress_bar进度条上将会由10%到100%逐渐切换。而在主程序窗口的label标签中将会展示一个不断递增的,从0开始的计数器变量,该变量与worker线程的运行是在同步的。
3.2 示例2 - 多线程同时操作UI组件
在本示例中我们主要演示多个线程如何同时更新同一个UI组件。我们假设我们有两个人在分别进行下棋操作,他们会在不同的线程内进行操作和计算,而在UI显示这些下棋操作的时候,他们都需要更新同一个UI棋盘组件。我们将棋盘组件设计成一个QLabel,使用字母“A”来代表落子,而使用字母“B”来代表可以下子的空位。
import sys
import time
from PyQt5.QtCore import pyqtSignal, QThread, QMetaObject, Qt
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel
from ui_mainwindow import Ui_MainWindow
class PlayWorker(QThread):
finishSignal = pyqtSignal(list) # 信号带参数
def __init__(self, parent=None):
super().__init__(parent)
self.player = None # 已放置棋子
self.step = None # 待放置位置
def run(self):
if self.step and self.player:
self.finishSignal.emit([self.player, self.step])
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
self.last_player = "" # 上一个下棋的人
self.chessboard = [["B" for i in range(3)] for j in range(3)] # 用列表记录棋盘状态
self.chessboard_labels = [[QLabel() for i in range(3)] for j in range(3)] # 用于显示棋盘的标签
for row in range(3):
for col in range(3):
self.grid.addWidget(self.chessboard_labels[row][col], row, col)
self.play_worker_player_1 = PlayWorker(self)
self.play_worker_player_1.finishSignal.connect(self.update_chessboard)
self.play_worker_player_2 = PlayWorker(self)
self.play_worker_player_2.finishSignal.connect(self.update_chessboard)
self.player_1_button.clicked.connect(self.on_player_1_button_clicked)
self.player_2_button.clicked.connect(self.on_player_2_button_clicked)
def on_player_1_button_clicked(self):
self.last_player = "A"
for row in range(3):
for col in range(3):
if self.chessboard[row][col] == "B":
self.chessboard[row][col] = self.last_player
self.play_worker_player_1.player = self.last_player
self.play_worker_player_1.step = (row, col)
self.play_worker_player_1.start()
def on_player_2_button_clicked(self):
self.last_player = "B"
for row in range(3):
for col in range(3):
if self.chessboard[row][col] == "B":
self.chessboard[row][col] = self.last_player
self.play_worker_player_2.player = self.last_player
self.play_worker_player_2.step = (row, col)
self.play_worker_player_2.start()
@pyqtSlot(list)
def update_chessboard(self, move):
player, step = move[0], move[1]
row, col = step[0], step[1]
self.chessboard_labels[row][col].setText(player)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
在上述代码中,我们创建了两个PlayWorker线程,它们分别代表不同的人。当任意一个人点击了自己的按钮,我们将遍历整个棋盘,找到下棋的空白位置。根据选手不同选举合适的worker线程来执行信号槽通信操作,在update_chessboard槽函数中进行棋盘UI组件的更新。
4. 总结
在本篇文章中我们讲解了如何利用PyQt5创建QThread线程并与主线程进行通信,实现多线程更新UI的效果。并展示了两个完整的程序案例来进一步演示该技术。虽然该技术可以解决界面卡死的问题,但使用多线程也会带来一定的额外开销,并且如果线程之间没有合理的同步机制,那么会存在操作顺序异常的风险,请开发人员谨慎使用。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Python Pyqt5多线程更新UI代码实例(防止界面卡死) - Python技术站