【没钱买ps,PyQt自己写】Day 20 - PyQt 最重要的 QThread 概念 / 为什麽 windows, mac, ubuntu (linux) 程序会「没有回应」?

看完这篇文章你会得到的成果图

与昨天一样,不过我们今天要谈一个 PyQt 中非常重要的 QThread 概念 !

前言

今天要谈一个 PyQt 中非常重要的 QThread 概念!
我们要修改昨天的 QProgressBar 功能,将它潜在问题修改并解决他。

此篇文章的范例程序码 github

https://github.com/howarder3/ironman2021_PyQt5_photoshop/tree/main/day20_qthread

重要的 QThread 概念

我们今天要来先讨论一下在 PyQt 中非常重要的东西!
这个就叫做 「QThread」 !

为什麽「QThread」很重要,其实在昨天的 QTimer 中我们就已经有说过,
我们的程序一定不会是一直线运行的,程序执行的过程中,
势必要有些「背景处理」的事情!

例如:时间就应该要背景更新,而不是我们主程序随时切换去给时间+1
这样光用想的就知道,万一上一行程序执行慢了一点,时间就会开始有越来越大的误差了吧!

为什麽我们要挑在这里先讲?

因为 ProgressBar 正好就符合这样的需求,
我们可以试着想想,如果一个 ProgressBar 正在跑,我们就不能同时做其他事情,
这样子的程序,不能说不好 (毕竟还是能动,只是要等他结束才能做其他事XD)
但不觉得很不符合使用者需求吗XD

观察 - 居然程序会「没有回应」,还不能关闭?!

我们把运行到一半的程序,强制用右上角的「X」关闭看看,
我们发现居然程序「不但关不掉」,甚至还「没有回应」了?!

这就是没有做 QThread 搞得鬼,因为在 Windows 判断一个视窗「没有回应」的判断条件中,
当我们对视窗「进行一个行为(例如:按按钮、关闭视窗)」,却没有得到回应,
没有回应的原因就是因为他被「卡在单一任务的过程中,无法给予回应」
windows 就会判定这个程序是「没有回应」的
(其实不只 windows, ubuntu, mac 在这个逻辑底下都是)

所以,我们必须把这个任务放入 QThread 执行,使得我们的主线任务可以被空出来,
能应付并接收新的其他任务。

将 ProgressBar 修改为 QThread 的版本 (修改昨天的 controller.py)

主要可以分为两个任务:

  • 定义 Thread 任务内容:class ThreadTask(QThread),名称可改
  • 从主程序去呼叫 thread 执行任务:我们使用 pyqtSignal,来传送讯号,协助我们变更值

宣告部分

注意「QThread, pyqtSignal」被宣告在「PyQt5.QtCore」里面

from PyQt5 import QtCore, QtWidgets
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtWidgets import QFileDialog
from PyQt5.QtCore import QThread, pyqtSignal

import time

from UI import Ui_MainWindow

Thread 任务宣告

宣告 pyqtSignal

我们在 ThreadTask 里面宣告一个 global 的 pyqtSignal,
并指定给 qthread_signal,(需要宣告类别)
结果就像 「qthread_signal = pyqtSignal(int)」

送出讯号 emit

我们把讯号送回去给主程序,我们透过 emit 这个 function,
可以协助我们把值送回主程序,并不影响主程序的任务。

class ThreadTask(QThread):
    qthread_signal = pyqtSignal(int)

    def start_progress(self):
        max_value = 100
        for i in range(max_value):
            time.sleep(0.1)
            self.qthread_signal.emit(i+1)

主要控制的部分

我们把按钮连结 ButtonClick() 这个 function,
我们在 ButtonClick() 这个任务当中,宣告我们的一份新的 ThreadTask 任务,
并把讯号连接至一个 function,

class MainWindow_controller(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__() # in python3, super(Class, self).xxx = super().xxx
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.setup_control()

    def setup_control(self):
        self.ui.progressBar.setMaximum(100)
        self.ui.pushButton.clicked.connect(self.ButtonClick) 
        
    def ButtonClick(self):
        self.qthread = ThreadTask()
        self.qthread.qthread_signal.connect(self.progress_changed) 
        self.qthread.start_progress()

    def progress_changed(self, value):        
        self.ui.progressBar.setValue(value)

连结 pyqtSignal 与 某一个 function()

我们需要把值的变化传至一个 function 当中,
我们利用「self.qthread.qthread_signal.connect(self.progress_changed) 」
连结「讯号 (qthread_signal)」与 「function - progress_changed()」的关系
而这个 function 会需要保留 value 作为一个栏位,
实际上在连接时,并不是以常见的形式传入这个值
(正确来说,应该是前面的 qthread_signal 被作为 value 传入)

def progress_changed(self, value):        
    self.ui.progressBar.setValue(value)

我们仔细看,value 就是代表 qthread_signal,
所以我们可以直接从 setValue 去改他。

启动 thread 用的 start_progress()

这部分就没什麽好说的,我们会需要一个 function 帮助我们启动 Thread。
并用 emit 把讯号送回来。


★ 本文也同步发於我的个人网站(会有内容目录与显示各个小节,阅读起来更流畅):【PyQt5】Day 20 - PyQt 最重要的 QThread 概念 / 为什麽 windows, mac, ubuntu (linux) 程序会「没有回应」?


<<:  Day20-JDK GUI界面概述

>>:  【第二十一天 - Javascript】

【後转前要多久】# Day29 Angular - 各种ng指示(ngClass、ngIf、ngFor...)

这里的ng并非电影电视中导演说太烂、要再拍一次的NG(No Good), 而是指Angular的ng...

追求JS小姊姊系列 Day25 -- 工具人、姐妹的存活原理:宣告变数的有效区域

前情提要: 看完记忆体储存差异,现在要来谈谈全域污染这件事。 基本scope概念 所谓的范畴Scop...

[PM日常001] 爱上Event

因为完美不可能 因为知道要办到的事有多难,所以绝对不会认为次次达标是件好事 完美是在范围(Scope...

Android Studio初学笔记-Day8-元件客制化

元件客制化(LinerLayout和Button) 前几天讲了EditText和Button,不过这...

Day.6 深入理解连结之重定址

在上篇文章我们说了「符号解析」,符号解析的任务就是:建立定义与引用之间的关联,而「重定址」的任务就是...