➣ Reading Time: 10 minutes

看完這篇文章你會得到的成果圖

與昨天一樣,不過我們今天要談一個 PyQt 中非常重要的 QThread 概念 !

前言

今天要談一個 PyQt 中非常重要的 QThread 概念!
我們要修改昨天的 QProgressBar 功能,將它潛在問題修改並解決他。

之前內容的重點複習 (前情提要)

我們接下來的討論,會基於讀者已經先讀過我 day5 文章 的架構下去進行程式設計
如果還不清楚我程式設計的邏輯 (UI.py、controller.py、start.py 分別在幹麻)
建議先閱讀 day5 文章後再來閱讀此文。

此篇文章的範例程式碼 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 把訊號送回來。

⭐Python PyQt5 相關文章整理⭐:
⭐基礎知識與架構篇⭐:
1.【PyQt5】Day 1 – 安裝 PyQt,建立自己的第一支 PyQt5 程式
2.【PyQt5】Day 2 – 利用 Qt designer 建立第一支有自己介面的 PyQt5 程式
3.【PyQt5】Day 4 – 重要的 Qt 程式邏輯觀念,務必先有此觀念後面才會懂自己在幹嘛
4.【PyQt5】Day 5 – 開始來設計我們的 controller.py,改以「程式角度」來說明如何建立 PyQt 的系統
⭐基本元件應用篇⭐:
1.【PyQt5】Day 6 – 我們的第一個 output 手段 – Qlabel
2.【PyQt5】Day 7 – 我們的第一個 input 手段 – QPushButton
3.【PyQt5】Day 8 – 我們的第二個 input 手段 – QLineEdit
4.【PyQt5】Day 9 – 以 QLineEdit, QTextEdit, QPlainTextEdit 作為文字的輸入
6.【PyQt5】Day 14 - 使用 QSlider 製作可拖曳的滑條
8.【PyQt5】Day 19 - 使用 QProgressBar,製作進度條的功能
⭐介面系統控制篇⭐:
5.【PyQt5】Day 10 – 以 QFileDialog 讀取系統的檔案、資料夾
7.【PyQt5】Day 18 / Project 使用 QTimer,自製碼表(計時器) PyQt5 stopwatch DIY
1.【PyQt5】Day 20 - PyQt 最重要的 QThread 概念 / 為什麼 windows, mac, ubuntu (linux) 程式會「沒有回應」?
2.【PyQt5】Day 21 – 透過 PyQt 實現滑鼠監聽總整理,完全掌握滑鼠控制 (listen mouse)
3.【PyQt5】Day 22 – PyQt 視窗的個性化/屬性控制 setWindowFlags,禁止放大縮小、永遠顯示於最上層/最下層
4.【PyQt5】Day 23 – 使用系統內建的調色盤 QColorDialog,來替我們選擇顏色 QColor (Color Picker)
5.【PyQt5】Day 24 / Project 偵測滑鼠目前指示顏色的小工具 (滴管工具), 利用 QCursor 偵測滑鼠, QApplication 取得截圖
⭐影像處理篇⭐:
1.【PyQt5】Day 11 – 以 Qlabel 在 PyQt 中顯示圖片 (基於 QImage 使用 OpenCV)
2.【PyQt5】Day 12 – 建立一個可以縮放圖片大小的顯示器 (基於 QImage 使用 OpenCV)
3.【PyQt5】Day 13 – 使用 QVBoxLayout, QscrollArea 製作出捲軸,以高解析度檢視圖片 (基於 QImage 使用 OpenCV)
4.【PyQt5】Day 15 / Project 與檔案功能整合,製作出可讀取圖片並可縮放的 UI 介面 (使用 PyQt + OpenCV)
5.【PyQt5】Day 16 - 在 PyQt5 中取得圖片座標 (滑鼠位置) mousePressEvent,觀察圖片在 Qt 中產生的方式,對原圖進行座標換算處理
6.【PyQt5】Day 17 / Project 製作標註 roi 工具, 開始導入 OpenCV 作為繪圖引擎, 在圖上畫點並顯示座標
⭐project 篇⭐:
1.【PyQt5】Day 25 / Project 自己做一個影片播放器 DIY video player (結合 PyQt + OpenCV)
2.【PyQt5】Day 26 / Project 替我們影片播放器增加一個顯示進度的滑條 video player add slider (與昨日 bottleneck 處理細節)
3.【PyQt5】Day 27 / Project 製作影片 ROI 標註工具 (PyQt 結合 OpenCV 在圖上畫點畫線)
4.【PyQt5】Day 28 / final project – 1 來搞一個自己的 photoshop 吧!UI 篇 + 純程式架構篇 (結合 PyQt + OpenCV)
5.【PyQt5】Day 29 / final project – 2 來搞一個自己的 photoshop 吧!後段程式細節篇 (結合 PyQt + OpenCV)
6.【PyQt5】Day 30 / final project – 3 來搞一個自己的 photoshop 吧!把每個方法封裝起來製作出還原功能吧! (結合 PyQt + OpenCV)
⭐打包程式篇⭐:
1.【PyQt5】Day 3 – 用 pyinstaller 將 python 程式打包,把每天的成果分享給你的親朋好友
⭐【喜歡我的文章嗎? 歡迎幫我按讚~ 讓基金會請創作者喝一杯咖啡!
如果喜歡我的文章,請幫我在下方【按五下Like】 (Google, Facebook 免註冊),會由 「LikeCoin」 贊助作者鼓勵繼續創作,讀者們「只需幫忙按讚,完全不用出錢」哦!

likecoin-steps