為了讓影像辨識程式更容易操作,我開始用 PyQt5 製作圖形介面。這篇從安裝與 Designer 開始,整理 .ui 轉換、信號傳遞、背景工作與設定保存,作為開發桌面應用程式時的查閱筆記。
本篇重點- 用 Qt Designer 建立介面並轉換 .ui 檔案
- 透過信號連接背景工作與介面更新
- 保存設定,讓下次啟動能延續操作狀態
原始實作記錄於 2022 年;以下操作畫面與程式範例保留當時版本。
安裝 PyQt5 與開啟 Designer
開始實作前的參考資料
剛開始學習時,我參考下方的 PyQt5 教學,先認識視窗與常見元件,再回到自己的需求做小範例。教學提供的是起點,實際使用時仍需對照元件文件與執行結果。
本文整理我在介面開發時常用的 QT5 程式碼,重點放在介面檔案、信號、背景工作與設定保存的連接方式。
實作背景
我開始接觸 PyQt5,是為了替視覺缺陷辨識程式設計操作介面。把常用功能放進視窗,讓使用者更容易操作,也讓我有機會整理介面與背景工作之間的關係。
轉換 .ui 檔案為 .py 程式碼檔案
Designer 負責排列元件,Python 程式則負責元件的行為。先確認介面能開啟,再加入按鈕事件與背景工作,比一次放入所有功能更容易查找問題。
在終端機切換到 .ui 檔案所在的目錄,依下方指令把介面定義轉成 Python 檔案。請把範例中的輸入與輸出檔名改成自己的檔名。
1
| pyuic5 -o qt_test.py -x qt_test.ui
|
QtCore.pyqtSignal 信號
使用 QT5 時,介面透過事件迴圈處理操作。QT5 的介面元件應由主執行緒更新,不要直接在 QThread多執行緒 中操作。背景工作完成後,可送出信號,由主執行緒接收並處理 你要執行的元件指令。信號也能傳遞數值等資料,供接收端決定如何更新畫面。
自訂 pyqtSignal 通常宣告在 QObject 子類別中,並依需求指定要傳遞的資料型態。
信號表示某件事已發生;透過 connect,可以把信號連接到接收資料的函式。例如,背景工作完成後傳回結果,由介面函式更新標籤或按鈕狀態。
類別形式的信號範例(點擊展開)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import QObject, pyqtSignal
class Ui_MainWindow(QObject): label_show_ON = pyqtSignal(int) def label_show(self,show): if show == 1: self.label.setText("A") if show == 2: self.label.setText("B")
def A(self): self.label_show_ON.emit(1)
def B(self): self.label_show_ON.emit(2) def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") MainWindow.resize(221, 100) self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName("centralwidget") self.pushButtonA = QtWidgets.QPushButton(self.centralwidget) self.pushButtonA.setGeometry(QtCore.QRect(20, 20, 75, 23)) self.pushButtonA.setObjectName("pushButtonA") self.pushButtonB = QtWidgets.QPushButton(self.centralwidget) self.pushButtonB.setGeometry(QtCore.QRect(130, 20, 75, 23)) self.pushButtonB.setObjectName("pushButtonB") self.label = QtWidgets.QLabel(self.centralwidget) self.label.setGeometry(QtCore.QRect(20, 50, 181, 21)) self.label.setAlignment(QtCore.Qt.AlignCenter) self.label.setObjectName("label") MainWindow.setCentralWidget(self.centralwidget) self.statusbar = QtWidgets.QStatusBar(MainWindow) self.statusbar.setObjectName("statusbar") MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow): translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(translate("MainWindow", "TEST")) self.pushButtonA.setText(translate("MainWindow", "Button A")) self.pushButtonB.setText(translate("MainWindow", "Button B")) self.label.setText(translate("MainWindow", "TextLabel")) self.label_show_ON.connect(self.label_show) self.pushButtonA.clicked.connect(self.A) self.pushButtonB.clicked.connect(self.B)
if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) MainWindow = QtWidgets.QMainWindow() ui = Ui_MainWindow() ui.setupUi(MainWindow) MainWindow.show() sys.exit(app.exec_())
|
QThread 多執行緒
需要持續讀取相機或執行較長的運算時,可以把工作與介面事件處理分開。下方保留 QThread 與 Python threading 的範例,閱讀時請注意工作如何啟動、如何回傳資料,以及視窗關閉時如何結束工作。
一般的寫法(點擊展開)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| from PyQt5 import QtWidgets from PyQt5.QtCore import QThread import sys, time
app = QtWidgets.QApplication(sys.argv)
Form = QtWidgets.QWidget() Form.setWindowTitle('oxxo.studio') Form.resize(300, 200)
label_a = QtWidgets.QLabel(Form) label_a.setGeometry(10, 10, 100, 30)
label_b = QtWidgets.QLabel(Form) label_b.setGeometry(10, 50, 100, 30)
def a(): for i in range(0,5): label_a.setText(str(i)) print('A:',i) time.sleep(0.5)
def b(): for i in range(0,50,10): label_b.setText(str(i)) print('B:',i) time.sleep(0.5)
thread_a = QThread() thread_a.run = a thread_a.start()
thread_b = QThread() thread_b.run = b thread_b.start()
Form.show() sys.exit(app.exec_())
|
使用類別管理背景工作(點擊展開)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import QThread import time
class Ui_MainWindow(object): def a(self): for i in range(0,5): self.label_1.setText(str(i)) print('A:',i) time.sleep(1)
def b(self): for i in range(0,50,10): self.label_2.setText(str(i)) print('B:',i) time.sleep(1)
def run(self): self.thread_a.start() self.thread_b.start()
def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") MainWindow.resize(221, 100) self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName("centralwidget") self.pushButton = QtWidgets.QPushButton(self.centralwidget) self.pushButton.setGeometry(QtCore.QRect(20, 20, 75, 23)) self.pushButton.setObjectName("pushButton") self.label_1 = QtWidgets.QLabel(self.centralwidget) self.label_1.setGeometry(QtCore.QRect(20, 50, 71, 21)) self.label_1.setAlignment(QtCore.Qt.AlignCenter) self.label_1.setObjectName("label_1") self.label_2 = QtWidgets.QLabel(self.centralwidget) self.label_2.setGeometry(QtCore.QRect(130, 50, 71, 21)) self.label_2.setAlignment(QtCore.Qt.AlignCenter) self.label_2.setObjectName("label_2") MainWindow.setCentralWidget(self.centralwidget) self.statusbar = QtWidgets.QStatusBar(MainWindow) self.statusbar.setObjectName("statusbar") MainWindow.setStatusBar(self.statusbar) self.retranslateUi(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate("MainWindow", "TEST")) self.pushButton.setText(_translate("MainWindow", "Button A")) self.thread_a = QThread() self.thread_a.run = self.a self.thread_b = QThread() self.thread_b.run = self.b self.pushButton.clicked.connect(self.run)
if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) MainWindow = QtWidgets.QMainWindow() ui = Ui_MainWindow() ui.setupUi(MainWindow) MainWindow.show() sys.exit(app.exec_())
|
QThread 常用方法
| 方法 |
參數 |
說明 |
| start() |
無 |
啟動執行緒 |
| wait() |
無 |
等待該執行緒結束 |
| sleep() |
Sec |
等待該執行緒幾秒 |
關閉介面前保存目前設定
先列出需要延續的狀態,例如路徑、選項或數值,再檢查保存內容與重新啟動後讀回的結果是否一致。設定保存與背景工作結束是兩件事,關閉視窗時都需要確認。
在關閉介面前保存設定,並在下一次啟動時讀取,讓使用者能接續上次的操作狀態。
1 2 3 4 5 6 7
| def setupUi(self, WindowForms): self.settings = QSettings('MyCompany', 'yolov5-AINTEC') self.settings_save = QThread()
self.settings_save.run = self.settings_save_data self.settings_save.start()
|
把設定值寫入保存檔案
1 2 3 4 5 6 7
| def settings_save_data(self): try: while True: self.settings.setValue('Setting_bool1', self.SettingOther_radioButton_1.isChecked()) except: pass
|
啟動時讀取保存的設定
1 2 3
| a = self.settings.value('CCDtest1', defaultValue=0, type=int) b = self.settings.value('Setting_bool1', defaultValue=False, type=bool) c = self.settings.value('threshold_mode_1', defaultValue='無')
|
實作後的檢查順序
先確認按鈕與基本畫面能正常操作,再加入耗時工作。工作執行期間試著拖曳視窗、操作按鈕並關閉程式,觀察是否仍能回應,以及背景工作是否確實結束。
執行緒與 GUI 的分工可對照 Qt 5 官方執行緒說明。下方原始範例仍屬當時的實作紀錄,沒有因文字修訂而重新驗證執行結果。