在寫Python時大部分人都習慣使用整合開發環境(IDE)與終端機直接做程式的編修,但當要把程式分享給其他人使用的時候,搭配一個設計界面會可以讓使用起來更加直覺,撰風個人通常會把程式寫完初版後,都會加上圖形介面(GUI),若要把檔案轉給不同單位的人的時候,可以再將程式轉成EXE檔案,這樣即使其他人沒有安裝我在Python中使用的模組,也能夠無痛且友善的使用我的程式,說到底就是貼心!
說到Python的圖形化介面模組,最常見的包括PyQt和Tkinter。先說結論,PyQt功能強大,且可以設計出精美的介面;而Tkinter是Python自帶的標準GUI庫,適合快速開發小型應用程序,耗費的資源也比較少,難度也較低。撰風身為一個對版面要求很高的人,用過幾次Tkinter後果斷轉用PyQt開發的Pyside6模組。這工具雖然學習難度稍高,但我真的很喜歡掌控排版的成就。在使用PyQt的模組時必須要注意是否可免費商用,PySide6使用LGPL許可證,這意味著開發者可以在不開放源碼的情況下使用它來開發商業應用,所以若要學習Python的GUI的話,採用Tkinter或PySide6都是很好的選擇。
特點 | PyQt | Tkinter |
---|---|---|
優點 | – 現代化界面,外觀美觀 – 性能優越,適合複雜圖形和動畫 – 功能豐富,支持多種控件和插件 | – 易學易用,適合初學者 – 跨平台兼容性良好 – 輕量級,資源消耗較低 |
缺點 | – 學習曲線較陡,需要時間掌握 – 需要額外安裝庫,不是標準庫的一部分 | – 功能有限,不適合高級應用 – 界面設計較為簡單,不夠現代化 |
在Pyside6的學習上,網路上有很多資料,撰風第一次是從Hong-bin,Chen在Medium上發表的這一篇文章入門的,也能參考PySide5的資料來學習。爾後再自己進一步摸索學習,建立出了自己習慣的模板,本篇就是整理自己常用的寫法,以便之後自己可以快速使用。
內容索引
Toggle安裝Pyside6和Qt Designer
在IDE(例如我是使用Visual Studio Code)中輸入pip install pyside6,安裝完成後就能透過程式碼使用Pyside6的各種GUI模組,但直接用程式碼來寫各種元件很不直覺,可以搭配Qt Designer來做拖曳元件的介面設計,裡面有很多實用的元件,直接把元件拖曳擺放成喜歡的樣子,之後再用Python呼叫與連結就可以了。
Qt Designer在安裝完PySide6後也會一併安裝完成,查找這個工具很簡單,用以下程式會列出安裝庫的主要路徑,把//
改為/
後輸入到檔案總管,就可以找到PySide6的資料夾,點入後可以找到designer.exe,此即Qt Designer。
import site
print(site.getsitepackages())
UI介面
以下是撰風個人常用的PySide6的UI介面設計,經過幾次拖拉元件設計後形成一個常用的模板。模板的程式碼如下,直接複製貼到Notepad++或記事本這類的文本軟體儲存為.ui
檔即可使用,也能再用Qt Designer開啟後再做編修。其程式的寫法與html很相似,所以若有一些html、CSS概念的人可以更快上手。真的不會,就善用Google或ChatGPT,絕對是很好的免費老師。
除了主要介面外,我還會加上一個子介面,做為程式的版本紀錄與操作說明,雖然GUI已經很友善了,但如果真的還是不會的話,還有操作說明可以查看操作步驟,主打就是一個貼心。
主要介面:Main_UI.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>410</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>20</x>
<y>20</y>
<width>361</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<family>Noto Sans TC</family>
<pointsize>12</pointsize>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">background:#333; color:white;</string>
</property>
<property name="text">
<string>Main</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
<widget class="QLabel" name="label_operation">
<property name="geometry">
<rect>
<x>340</x>
<y>19</y>
<width>31</width>
<height>31</height>
</rect>
</property>
<property name="text">
<string/>
</property>
</widget>
<widget class="QWidget" name="gridLayoutWidget">
<property name="geometry">
<rect>
<x>19</x>
<y>70</y>
<width>361</width>
<height>321</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="4" column="0" colspan="2">
<widget class="QTextEdit" name="textEdit_InfoBox">
<property name="styleSheet">
<string notr="true">background: #eee; border: 1px solid gray;</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="lineEdit_Input"/>
</item>
<item row="0" column="0">
<widget class="QPushButton" name="pushButton_Load">
<property name="text">
<string>Method_Load</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_2">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="comboBox_Format"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QPushButton" name="pushButton_Generate">
<property name="text">
<string>Method_Generate</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>
說明頁面:Main_UI_Operation.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>310</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>20</x>
<y>20</y>
<width>361</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<family>Noto Sans TC</family>
<pointsize>12</pointsize>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">background:#333; color:white;</string>
</property>
<property name="text">
<string>Main_UI_Operation</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
<widget class="QLabel" name="label_operation">
<property name="geometry">
<rect>
<x>340</x>
<y>19</y>
<width>31</width>
<height>31</height>
</rect>
</property>
<property name="text">
<string/>
</property>
</widget>
<widget class="QGroupBox" name="groupBox">
<property name="geometry">
<rect>
<x>20</x>
<y>60</y>
<width>361</width>
<height>101</height>
</rect>
</property>
<property name="font">
<font>
<kerning>true</kerning>
</font>
</property>
<property name="title">
<string>版本說明</string>
</property>
<property name="flat">
<bool>false</bool>
</property>
<property name="checkable">
<bool>false</bool>
</property>
<widget class="QWidget" name="gridLayoutWidget_2">
<property name="geometry">
<rect>
<x>10</x>
<y>20</y>
<width>341</width>
<height>71</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>程式維護</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="label_7">
<property name="text">
<string>JFSBLOG.COM</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>更新日期</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_3">
<property name="text">
<string>2024/11/7</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>50</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>創建日期</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="label_5">
<property name="text">
<string>2024/11/7</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QGroupBox" name="groupBox_2">
<property name="geometry">
<rect>
<x>20</x>
<y>170</y>
<width>361</width>
<height>121</height>
</rect>
</property>
<property name="title">
<string>操作說明</string>
</property>
<widget class="QLabel" name="label_9">
<property name="geometry">
<rect>
<x>18</x>
<y>28</y>
<width>331</width>
<height>101</height>
</rect>
</property>
<property name="text">
<string><html><head/><body><p>1. <br/>2.<br/>3.<br/>4.<br/>5.</p></body></html></string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</widget>
</widget>
<resources/>
<connections/>
</ui>
Python程式
設計完介面後,在此使用Python載入.ui檔案,我個人習慣將介面和邏輯方法分開寫,這樣能使程式碼更具模組化,便於維護。介面負責顯示和用戶交互,而方法則專注於具體的業務邏輯或數據處理。
方法被分離出來後,可以獨立於介面進行重用與測試。比如,在不啟動完整的 GUI 程序的情況下,可以單獨測試method_load_data.py
和method_generate_data.py
是否正確運行,提高開發效率。此外,如果有其他介面需要使用相同的邏輯,也能直接調用這些方法而不必重新編寫,使代碼更具重用性。
在這method_generate_data.py
中,撰風使用了一個簡單的計時器來模擬處理進度,透過QTimer
,程式可以在每次觸發事件時更新進度條和顯示訊息,並讓介面持續保持互動性。這只是簡易的程式邏輯模板,之後可以再編修成符合自己需求的設計。若之後的程式運作有用到迴圈的話,建議可以再部分改用QThread
將耗時操作移到背景執行,這樣可以保持主介面的流暢性與回應性,避免界面卡住。
小提醒,在介面中可以加入自己的小圖示,除了介面中的圖片可以由QLabel的pixmap屬性設定來插入圖片,而介面的視窗名稱和ico圖示可以在Python中定義,如下程式碼所示。撰風此前也有分享過如何透過Python生成ico圖檔的程式,大家也能參考。
主程式:Main.py
# 匯入 PySide6 的 GUI 組件
from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QLabel, QLineEdit, QComboBox, QTextEdit, QProgressBar
from PySide6.QtGui import QIcon
from PySide6.QtCore import QFile, Qt, QCoreApplication
from PySide6.QtUiTools import QUiLoader # 用於載入 .ui 檔案
from Method_Load import method_load_data
from Method_Generate import method_generate_data
# --------------------------------------------------------------------------- // ## 定義說明窗口類別
class HelpWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("操作說明") # 設定視窗標題
self.setWindowIcon(QIcon("圖示.ico")) # 設定視窗圖示
# 載入 Main_UI_Operation.ui,該 UI 定義了說明視窗的界面
loader = QUiLoader()
ui_file = QFile("Main_UI_Operation.ui")
self.ui = loader.load(ui_file, self) # 將 .ui 文件內容載入到 HelpWindow 中
ui_file.close() # 關閉 .ui 文件
# 設置窗口大小
self.resize(400, 300)
# --------------------------------------------------------------------------- // ## 定義主窗口類別
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts) # 設定共享 OpenGL 上下文屬性
# 載入主界面 UI 檔案
self.ui = load_ui("Main_UI.ui")
self.setCentralWidget(self.ui) # 將 UI 設為主視窗的中心部件
self.setWindowTitle("窗口標題") # 設定主窗口標題
self.setWindowIcon(QIcon("圖示.ico")) # 設定主窗口圖示
self.resize(400, 410) # 設定窗口大小
# 取得 UI 中的各個元件,並賦值到對應屬性
self.label_operation = self.ui.findChild(QLabel, "label_operation")
self.pushButton_Load = self.ui.findChild(QPushButton, "pushButton_Load")
self.lineEdit_Input = self.ui.findChild(QLineEdit, "lineEdit_Input")
self.comboBox_Format = self.ui.findChild(QComboBox, "comboBox_Format")
self.pushButton_Generate = self.ui.findChild(QPushButton, "pushButton_Generate")
self.textEdit_InfoBox = self.ui.findChild(QTextEdit, "textEdit_InfoBox")
self.progressBar = self.ui.findChild(QProgressBar, "progressBar")
# 隱藏進度條
self.progressBar.setVisible(False)
# 設定超連結到“操作說明”標籤
self.label_operation.setText('<a href="#" style="color:white;">說明</a>')
self.label_operation.setOpenExternalLinks(False) # 防止打開外部瀏覽器
self.label_operation.linkActivated.connect(self.open_help_window) # 連結被點擊時開啟幫助窗口
# 設定按鈕點擊事件
self.pushButton_Load.clicked.connect(self.load_data) # 點擊“載入”按鈕時執行 load_data 函數
self.pushButton_Generate.clicked.connect(self.generate_data) # 點擊“生成”按鈕時執行 generate_data 函數
# 幫助窗口初始化為 None,僅在需要時創建
self.help_window = None
# --------------------------------------------------------------------------- // ### 開啟或激活說明窗口
def open_help_window(self):
if self.help_window is None or not self.help_window.isVisible(): # 若窗口未開啟或已關閉
self.help_window = HelpWindow() # 創建 HelpWindow 實例
self.help_window.setWindowModality(Qt.NonModal) # 設定窗口為非模態窗口 (NonModal、WindowModal、ApplicationModal)
self.help_window.show() # 顯示窗口
else:
self.help_window.activateWindow() # 如果窗口已開啟則激活它
# --------------------------------------------------------------------------- // ### 載入資料的操作邏輯(此處為示範打印)
def load_data(self):
message = method_load_data(self) # 呼叫子程式中的 load_data 函數並接收回傳訊息
self.textEdit_InfoBox.append(message) # 將訊息顯示在 textEdit_InfoBox
# --------------------------------------------------------------------------- // ### 生成資料的操作邏輯(此處為示範打印)
def generate_data(self):
self.progressBar.setVisible(True)
self.progressBar.setValue(0)
method_generate_data(self) # 將自身(MainWindow 實例)傳入子模組的 generate_data 函數
# --------------------------------------------------------------------------- // ## 定義一個函數用於載入 UI 檔案
def load_ui(ui_file_path):
loader = QUiLoader()
ui_file = QFile(ui_file_path)
ui = loader.load(ui_file) # 載入 .ui 檔案
ui_file.close() # 關閉檔案
return ui # 返回載入的 UI 元件
# --------------------------------------------------------------------------- // ## 主程式執行區
if __name__ == "__main__":
app = QApplication([]) # 建立應用程式實例
window = MainWindow() # 建立主窗口實例
window.show()
app.exec() # 開始應用程式事件循環
方法程式:Method_Load.py
def method_load_data(main_window):
message = "成功連接到子程式Method_Load.py的方法"
print(message) # 仍然保留在終端上顯示
return message # 回傳訊息給主程式
方法程式:Method_Generate.py
from PySide6.QtWidgets import QMessageBox
from PySide6.QtCore import QTimer
def method_generate_data(main_window):
# 清空文字區域
main_window.textEdit_InfoBox.clear()
# 確認連接到子程式
main_window.textEdit_InfoBox.append("成功連接到子程式Method_Generate.py的方法\n")
main_window.textEdit_InfoBox.append("模擬迴圈處理測試...")
# 初始化進度相關參數
main_window.progress_value = 0
main_window.iteration = 1
# 定義一個 QTimer 來控制進度更新
main_window.timer = QTimer()
main_window.timer.timeout.connect(lambda: update_progress(main_window)) # 每次 timeout 執行 update_progress
main_window.timer.start(1000) # 每 2 秒觸發一次
def update_progress(main_window):
loop_times = 6
if main_window.iteration <= loop_times: # 假設總共 10 個步驟
# 更新文字區域
message = f"模擬處理第{main_window.iteration}圈並回傳訊息..."
main_window.textEdit_InfoBox.append(message)
# 更新進度條
main_window.progress_value = main_window.iteration * ( 100 / loop_times) # 進度值從 0 到 100
main_window.progressBar.setValue(main_window.progress_value)
# 更新步數
main_window.iteration += 1
else:
# 停止計時器並隱藏進度條
main_window.timer.stop()
main_window.progress_value = 100
main_window.progressBar.setValue(main_window.progress_value)
main_window.textEdit_InfoBox.append("模擬迴圈處理完成")
QMessageBox.information(main_window, "訊息", "模擬迴圈處理完成")
QMessageBox.warning(main_window, "警告", "警告視窗測試")
將程式打包成執行檔
當介面、邏輯等程式都寫完後,撰風個人會建議打包成執行檔。好處是可以簡化部署,以及提高安全性,撰風個人的程式沒有那麼複雜,所以我的重點會在簡化部署,直白來說可以讓沒有在使用Python或安裝對應模組的其他人也能直接執行我的程式。
如何把程式打包成EXE檔案,撰風此前有介紹過PyInstaller
這個模組。不過後來撰風更常用的是auto-py-to-exe
這個模組,這個部分有機會我再來分享。不過請記得,打包完程式後,原本的程式還是要保留著喔,因為執行檔無法再進行程式的編修。
相關列表
Python學習紀錄
主題 | 文章 |
編譯器與IDE |
|
|
|
常用模組 |
|
影像處理 |
|
網路爬蟲 |
|
圖形介面 |
|
檔案打包 |
|
主題 | 文章 |
影像應用 |
|
機器學習 |
|
深度學習 |
|
語言分析 |
|
圖像生成 |
|
小小作品 |
常用網站 |
[ 編輯 ] 筆記 » 程式語言 » Python |
Python|HTML|CSS|JavaScript|Blender|Unreal Engine |
Facebook留言
Wordpress留言 (0)