Python PyQt5——QThread使用方法与代码实践

2024-06-04 6517阅读

QThread

在 GUI 程序中,如果想要让程序执行一项耗时的工作,例如下载文件、I/O 存取等,深度学习模型推理,直接在 UI 线程中进行这些操作会导致整个界面卡住,出现无响应的状态。为了避免这种情况,可以将这些耗时任务放在另一个线程中执行。在 PyQt 中,可以使用 QThread 来实现这一点。

基本用法

在 PyQt5 中,要使用 QThread 创建一个线程,需要创建 QThread 的子类,并重写 QThread.run() 函数。以下是一个示例,WorkerThread 类继承自 QThread 并重写了 run() 方法:

class WorkerThread(QThread):
    def __init__(self):
        super().__init__()
    def run(self):
        # 执行耗时任务

接下来,可以使用 QThread.start() 来启动线程。在构造 WorkerThread 后,不会立即执行 run() 方法,直到调用 QThread.start() 才开始执行。如果主线程需要等待线程执行完毕,可以使用 QThread.wait(),它会等待线程完成才返回。但请注意,如果线程中包含无限循环,QThread.wait() 将会无限期等待。

下面是一个简单的示例:

import sys
import time
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QThread
class WorkerThread(QThread):
    def __init__(self):
        super().__init__()
    def run(self):
        for i in range(3):
            time.sleep(1)
            print('WorkerThread::run ' + str(i))
if __name__ == '__main__':
    app = QApplication(sys.argv)
    print('main')
    work1 = WorkerThread()
    work2 = WorkerThread()
    work1.start()
    work2.start()
    work1.wait()
    work2.wait()
    print('end of main')
    sys.exit(app.exec_())

在这个 run 函数中,有一个循环执行 3 次,并在每次循环后休眠 1 秒,然后输出一条消息。从输出结果可以看出,主线程会等待两个 WorkerThread 线程都完成后才输出 “end of main” 消息,这证明了使用 QThread.wait() 是有效的。

错误堵塞机制

在 PyQt5 开发过程中,新手容易错误地使用线程,导致界面卡住或变黑。例如,一个下载文件的程序,按下按钮后会执行大约 10 秒的工作。如果在 UI 线程中直接执行这些操作,会发现整个 GUI 程序无法进行其他操作,界面会卡住或变黑。

正确的做法是在 WorkerThread 中定义两个信号,分别为 trigger 和 finished。finished 信号在工作完成后发送,而 trigger 信号在执行过程中发送。使用 pyqtSignal 来定义自定义信号,并指定发送到目标函数的参数类型。例如,trigger = pyqtSignal(str)。

整个程序在按下按钮后,会开启另一个线程,每秒更新一次秒数到标签上,通过 self.trigger.emit(str(i+1)) 发射信号并传递第几秒的参数。第 5 秒时结束线程,并通过 self.finished.emit() 发射结束信号。

下面是一个示例代码:

import sys
import time
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QPushButton
from PyQt5.QtCore import QThread, pyqtSignal
class WorkerThread(QThread):
    trigger = pyqtSignal(str)
    finished = pyqtSignal()
    def __init__(self):
        super().__init__()
    def run(self):
        for i in range(5):
            time.sleep(1)
            self.trigger.emit(str(i+1))
            print('WorkerThread::run ' + str(i))
        self.finished.emit()
class MyWidget(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
    def initUI(self):
        self.setWindowTitle('my window')
        self.setGeometry(50, 50, 200, 150)
        layout = QVBoxLayout()
        self.setLayout(layout)
        self.mylabel = QLabel('press button to start thread', self)
        layout.addWidget(self.mylabel)
        self.mybutton = QPushButton('start', self)
        self.mybutton.clicked.connect(self.startThread)
        layout.addWidget(self.mybutton)
        self.work = WorkerThread()
    def startThread(self):
        self.mybutton.setDisabled(True)
        self.work.start()
        self.work.trigger.connect(self.updateLabel)
        self.work.finished.connect(self.threadFinished)
        self.updateLabel(str(0))
    def threadFinished(self):
        self.mybutton.setDisabled(False)
    def updateLabel(self, text):
        self.mylabel.setText(text)
if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = MyWidget()
    w.show()
    sys.exit(app.exec_())

结果如下:

main
press button to start thread
1
2
3
4
end of main

如果不想在 threadFinished 函数中只是执行一段代码,可以省略 threadFinished 函数,改用 lambda 表达式。例如,将 self.mybutton.setDisabled(False) 操作写在 self.work.finished.connect() 中的 lambda 表达式里。

在 QWidget 里使用 QThread

在 PyQt 程序中,主线程就是所说的 UI 线程,UI 线程会处理所有控件的事务。因此,如果有耗时的工作需要执行,通常不会将其放在 UI 线程中,因为这样做会阻止其他控件的更新,导致界面卡顿或程序无响应。解决这个问题的方法是创建另一个线程来处理这些耗时的工作。

在 WorkerThread 中新增了两个信号,分别命名为 trigger 和 finished。finished 信号在工作完成后发送,而 trigger 信号在执行过程中发送。当需要自定义信号时,使用 pyqtSignal 来定义要发送到目标函数的函数原型,例如在下面的示例中,trigger = pyqtSignal(str) 表示 trigger 信号会携带一个字符串参数。

整个程序的工作流程是:当按下按钮后,会启动另一个线程,这个线程每一秒更新一次秒数到标签上。通过 self.trigger.emit(str(i+1)) 发送 trigger 信号,并传递当前的秒数作为参数。当到达第 5 秒时,线程结束,并通过 self.finished.emit() 发送 finished 信号。

这种设计允许我们在不影响 UI 响应性的情况下执行长时间的任务,同时还能更新 UI 显示当前的进度或状态。通过使用信号和槽机制,可以在工作线程和 UI 线程之间安全地传递信息,确保程序的流畅运行。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import time
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout,
                             QLabel, QPushButton)
from PyQt5.QtCore import QThread, pyqtSignal
class WorkerThread(QThread):
    trigger = pyqtSignal(str)
    finished = pyqtSignal()
    def __init__(self):
        super().__init__()
    def run(self):
        for i in range(5):
            time.sleep(1)
            self.trigger.emit(str(i+1))
            print('WorkerThread::run ' + str(i))
        self.finished.emit()
class MyWidget(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
    def initUI(self):
        self.setWindowTitle('my window')
        self.setGeometry(50, 50, 200, 150)
        layout = QVBoxLayout()
        self.setLayout(layout)
        self.mylabel = QLabel('press button to start thread', self)
        layout.addWidget(self.mylabel)
        self.mybutton = QPushButton('start', self)
        self.mybutton.clicked.connect(self.startThread)
        layout.addWidget(self.mybutton)
        self.work = WorkerThread()
    def startThread(self):
        self.mybutton.setDisabled(True)
        self.work.start()
        self.work.trigger.connect(self.updateLabel)
        self.work.finished.connect(self.threadFinished)
        self.updateLabel(str(0))
    def threadFinished(self):
        self.mybutton.setDisabled(False)
    def updateLabel(self, text):
        self.mylabel.setText(text)
if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = MyWidget()
    w.show()
    sys.exit(app.exec_())

如果你不希望 threadFinished 函数仅仅是执行一行代码,而是想去掉 threadFinished 函数,可以将 self.mybutton.setDisabled(False) 操作写在 self.work.finished.connect() 里的 lambda 表达式中。以下是使用 lambda 表达式的示例:

def startThread(self):
    self.mybutton.setDisabled(True)
    self.work.start()
    self.work.trigger.connect(self.updateLabel)
    # self.work.finished.connect(self.threadFinished)
    self.work.finished.connect(lambda: self.mybutton.setDisabled(False))
    self.updateLabel(str(0))
# def threadFinished(self):
#    self.mybutton.setDisabled(False)
def updateLabel(self, text):
    self.mylabel.setText(text)

这样,当 finished 信号被触发时,lambda 表达式中的代码将会执行,即启用按钮。


    免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

    目录[+]