pyqt5中槽函数运行时间长导致界面卡死的问题:

1.问题场景:

当用PyQt5开发一个GUI界面 ,后台逻辑执行时间长,界面就容易出现卡死、未响应等问题。

(GUI(Graphics User Interface),中文名称为图形用户界面)

2.问题原因:

  • 在PyQt中,GUI界面本身就是一个处理事件循环的主线程,当进行耗时操作时,主线程GUI需要等待操作完成后才会响应在等待这段时间,整个GUI就处于卡死的状态
  • 在windows下,系统会认为这个程序运行出错了,会自动显示未响应,如果这时有其他的操作,整个程序就会卡死崩溃。

3.解决办法:

另开一个线程来执行这个耗时操作(使用QThread)

(注意:

  • 如果说你的新线程需要做一些跟QT相关的事情,那就使用QT的线程,大多数情况下直接使用python的线程就可以,
  • 在使用 PyQt5 开发图形界面应用程序时,最好使用 QThread 而不是直接使用 Python 标准库的 threading.Thread,这样可以更好地与 Qt 的事件循环机制结合使用,避免一些潜在的问题。)

(1)使用python的线程

from threading import Thread

 例子就是:
 

print('主线程执行代码') 

# 从 threading 库中导入Thread类
from threading import Thread
from time import sleep

# 定义一个函数,作为新线程执行的入口函数
def threadFunc(arg1,arg2):
    print('子线程 开始')
    print(f'线程函数参数是:{arg1}, {arg2}')
    sleep(5)
    print('子线程 结束')


# 创建 Thread 类的实例对象
thread = Thread(
    # target 参数 指定 新线程要执行的函数
    # 注意,这里指定的函数对象只能写一个名字,不能后面加括号,
    # 如果加括号就是直接在当前线程调用执行,而不是在新线程中执行了
    target=threadFunc, 

    # 如果 新线程函数需要参数,在 args里面填入参数
    # 注意参数是元组, 如果只有一个参数,后面要有逗号,像这样 args=('参数1',)
    args=('参数1', '参数2')
)

# 执行start 方法,就会创建新线程,
# 并且新线程会去执行入口函数里面的代码。
# 这时候 这个进程 有两个线程了。
thread.start()

# 主线程的代码执行 子线程对象的join方法,
# 就会等待子线程结束,才继续执行下面的代码
thread.join()
print('主线程结束')

 快速了解:

Python中QThread、Thread、Processing的比较总结_qthread和thread区别-CSDN博客

具体了解:
多线程 和 多进程 - 白月黑羽 (byhy.net)

(2)使用QThread

from PyQt5.QtCore import QThread

通过继承QThread并重写run()方法的方式实现多线程代码的编写。
结构大体如下:

class Worker(QThread):
    def __init__(self):
        super().__init__()
    def run(self):
        --snip--

把耗时操作放到一个Worker线程中的run()函数下执行,在GUI类文件中绑定操作的地方,创建Worker进程实例,启动进程即可。

t = Worker()
t.start()

 4.注意:

  • 不要尝试在Worker开启的线程中去设置GUI界面中的控件属性,因为可能会导致未知的错误;
  • Qt建议: 只在主线程中操作界面 。
    • 在另外一个线程直接操作界面,可能会导致意想不到的问题,比如:输出显示不全,甚至程序崩溃。
    • 推荐的方法是使用自定义信号。

5.例子:
 

使用了线程和自定义信号。

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QTextEdit, QPushButton, QHBoxLayout
from PyQt5.QtCore import QThread, pyqtSignal, QObject
import time


class Worker(QObject):
    # progress信号用于报告训练进度,传递字符串日志和当前周期。
    progress = pyqtSignal(str, int)
    # finished信号用于显示训练完成,中断信息。
    finished = pyqtSignal(int, float)  # 修改finished信号,增加损失和。

    def __init__(self, epochs_per_cycle, cycles):
        super().__init__()
        # 设置每个周期的训练轮数和总周期数。
        self.epochs_per_cycle = epochs_per_cycle
        self.cycles = cycles
        # _running标志用于控制训练循环的运行状态。
        self._running = True

    def stop(self):
        # 将_running标志设置为False,用于停止训练。
        self._running = False

    def run(self):
        self._running = True
        total_train_loss = 0  # 初始化总训练损失
        # 使用for循环遍历每个周期
        for cycle in range(1, self.cycles + 1):
            # 检查_running标志是否为True,否则中断循环。
            if not self._running:
                self.finished.emit(1, total_train_loss)
                break
            # 发射progress信号报告当前周期。
            self.progress.emit(f'Cycle: {cycle}', cycle)
            for epoch in range(1, self.epochs_per_cycle + 1):
                if not self._running:
                    break
                # 模拟训练时间延迟,计算并记录假设的训练损失、测试损失和测试准确率,更新进度条描述。
                time.sleep(0.5)  # Simulate training time
                train_loss = epoch * 0.1  # Dummy train loss
                total_train_loss += train_loss  # 累积训练损失
                test_loss = epoch * 0.2  # Dummy test loss
                test_acc = epoch * 0.01  # Dummy test accuracy
                log = f'Epoch: {epoch}, train_loss: {train_loss:.4f}, test_loss: {test_loss:.4f}, test_acc: {test_acc:.4f}, {int((100 * epoch) / self.epochs_per_cycle)}%'
                self.progress.emit(log, cycle)

        # 如果所有周期正常完成,发射finished信号传递 0 表示训练正常的全部完成,并传递总训练损失。
        if self._running:
            self.finished.emit(0, total_train_loss)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.worker = None
        self.thread = None

    def initUI(self):
        self.setWindowTitle("Training Progress")

        # 设置窗口大小
        self.resize(800, 400)
        # 创建并配置文本编辑框text_edit,用于显示训练日志。
        self.text_edit = QTextEdit(self)
        # 将该文本编辑器设置为只读模式
        self.text_edit.setReadOnly(True)

        # 创建并配置开始和停止按钮,并连接相应的槽函数。
        self.start_button = QPushButton("Start Training", self)
        self.start_button.clicked.connect(self.start_training)

        self.stop_button = QPushButton("Stop Training", self)
        self.stop_button.setEnabled(False)
        self.stop_button.clicked.connect(self.stop_training)

        # 布局
        button_layout = QHBoxLayout()
        button_layout.addWidget(self.start_button)
        button_layout.addWidget(self.stop_button)

        layout = QVBoxLayout()
        layout.addWidget(self.text_edit)
        layout.addLayout(button_layout)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

    def start_training(self):
        # 如果已有线程在运行,则停止现有线程
        if self.thread is not None and self.thread.isRunning():
            self.worker.stop()
            # quit() 只是请求线程退出其事件循环,并不会立即停止线程。
            self.thread.quit()
            # wait() 是阻塞当前线程,直到目标线程完全退出。
            self.thread.wait()

        # 禁用开始按钮,启用停止按钮,清空日志窗口
        self.start_button.setEnabled(False)
        self.stop_button.setEnabled(True)
        self.text_edit.clear()

        # 在训练开始前,输出开始训练的信息
        info_text = "We are about to start training.\n"
        info_text += f"Number of cycles: {10}\n"  # 这里填入训练的循环次数
        info_text += f"Epochs per cycle: {5}"  # 这里填入每个周期的 epochs 数
        self.text_edit.append(info_text)
        self.text_edit.append("Starting training...")

        # 创建新线程和新Worker对象,将Worker对象移动到新线程。
        self.thread = QThread()
        self.worker = Worker(epochs_per_cycle=5, cycles=10)
        # moveToThread 方法是 QObject 的一个方法,它用于将对象移动到另一个线程。
        # Worker 对象被移动到 self.thread 所代表的新线程中,这意味着 Worker 对象中的槽函数(如 run 方法)将在线程 self.thread 中执行,而不是在主线程中。
        self.worker.moveToThread(self.thread)

        # 连接信号和槽:progress信号连接update_log方法,finished信号连接training_finished方法,
        self.worker.progress.connect(self.update_log)
        self.worker.finished.connect(self.training_finished)
        # 线程启动信号连接worker.run方法。
        self.thread.started.connect(self.worker.run)

        # 启动线程。
        self.thread.start()

    # 定义stop_training方法,停止训练过程。
    def stop_training(self):
        # 启用开始按钮
        self.stop_button.setEnabled(True)
        # 禁用停止按钮,使其在训练过程中不可点击。防止用户在训练已经停止后再次点击停止按钮。
        self.stop_button.setEnabled(False)

        # 检查 self.worker 是否已经被实例化。如果 self.worker 不是 None,则表示训练过程正在运行。
        if self.worker is not None:
            # 调用worker.stop方法,停止Worker对象中的训练循环。
            self.worker.stop()
            # 退出并等待线程结束
            self.thread.quit()
            # 这将阻塞主线程,直到 QThread 完全退出。
            self.thread.wait()

    def update_log(self, log, cycle):
        # 获取当前 QTextEdit 对象的光标位置
        cursor = self.text_edit.textCursor()
        # 获取当前 QTextEdit 对象中的纯文本内容。
        text = self.text_edit.toPlainText()

        # 将文本内容按换行符分割成一个字符串列表,每个元素表示一行。
        lines = text.split('\n')

        # 计算当前周期的起始行索引
        # 每个周期有两行,一行显示 Cycle: 信息,另一行显示 Epoch: 信息。
        # 例如,第1周期的索引为0,第2周期的索引为2,以此类推。
        cycle_start_line_idx = (cycle - 1) * 2 + 4

        # 检查 log 是否包含 "Cycle:" 字符串。
        if "Cycle:" in log:
            # 如果当前行数少于或等于周期起始行索引,说明是新周期。
            if len(lines) <= cycle_start_line_idx:
                lines.append(log)  # 将 log 添加到 lines 列表末尾。
            else:  # 否则,更新现有的周期日志信息。
                lines[cycle_start_line_idx ] = log
        # 如果 log 中不包含 "Cycle:" 字符串,处理 Epoch: 信息。
        else:
            # 计算 Epoch: 信息的行索引。
            epoch_log_idx = cycle_start_line_idx + 1
            # 如果当前行数少于或等于 Epoch: 信息行索引,添加新行。
            if len(lines) <= epoch_log_idx:
                lines.append(log)  # 将 log 添加到 lines 列表末尾。
            # 否则,更新现有的 Epoch: 日志信息。
            else:
                lines[epoch_log_idx] = log  # 在指定行索引处更新 log。

        # 将更新后的日志内容合并成一个字符串,并设置为 QTextEdit 的文本内容。
        self.text_edit.setPlainText('\n'.join(lines))
        # 将光标移动到文本末尾。
        cursor.movePosition(cursor.End)
        # 将光标位置设置回 QTextEdit。
        self.text_edit.setTextCursor(cursor)

    def training_finished(self, cycle, total_train_loss):
        # 中断结束
        if cycle == 1:
            self.text_edit.append("Training interruption !!!")
        # 正常结束
        elif cycle == 0:
            self.text_edit.append("Training ends normally !!!")
            self.text_edit.append(f"Total Training Loss: {total_train_loss:.4f}")
        else:
            self.text_edit.append("Abnormal end of training !!!")

        # 禁用开始按钮,启用停止按钮
        self.start_button.setEnabled(True)
        self.stop_button.setEnabled(False)


# 主程序
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())

上述内容参考自:
PyQt5执行耗时操作导致界面卡死或未响应的原因及解决办法_python_脚本之家

  • 11
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值