在寫Python時大部分人都習慣使用整合開發環境(IDE)與終端機直接做程式的編修,但當要把程式分享給其他人使用的時候,搭配一個設計界面會可以讓使用起來更加直覺,撰風個人通常會把程式寫完初版後,都會加上圖形介面(GUI),若要把檔案轉給不同單位的人的時候,可以再將程式轉成EXE檔案,這樣即使其他人沒有安裝我在Python中使用的模組,也能夠無痛且友善的使用我的程式,說到底就是貼心!

說到Python的圖形化介面模組,最常見的包括PyQtTkinter。先說結論,PyQt功能強大,且可以設計出精美的介面;而Tkinter是Python自帶的標準GUI庫,適合快速開發小型應用程序,耗費的資源也比較少,難度也較低。撰風身為一個對版面要求很高的人,用過幾次Tkinter後果斷轉用PyQt開發的Pyside6模組。這工具雖然學習難度稍高,但我真的很喜歡掌控排版的成就。在使用PyQt的模組時必須要注意是否可免費商用,PySide6使用LGPL許可證,這意味著開發者可以在不開放源碼的情況下使用它來開發商業應用,所以若要學習Python的GUI的話,採用Tkinter或PySide6都是很好的選擇。

特點PyQtTkinter
優點– 現代化界面,外觀美觀
– 性能優越,適合複雜圖形和動畫
– 功能豐富,支持多種控件和插件
– 易學易用,適合初學者
– 跨平台兼容性良好
– 輕量級,資源消耗較低
缺點– 學習曲線較陡,需要時間掌握
– 需要額外安裝庫,不是標準庫的一部分
– 功能有限,不適合高級應用
– 界面設計較為簡單,不夠現代化
PyQt和Tkinter比較表

在Pyside6的學習上,網路上有很多資料,撰風第一次是從Hong-bin,Chen在Medium上發表的這一篇文章入門的,也能參考PySide5的資料來學習。爾後再自己進一步摸索學習,建立出了自己習慣的模板,本篇就是整理自己常用的寫法,以便之後自己可以快速使用。

PySide6模板與入門學習
PySide6模板與入門學習

安裝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())
如何搜尋Qt Designer的路徑
如何搜尋Qt Designer的路徑

UI介面

以下是撰風個人常用的PySide6的UI介面設計,經過幾次拖拉元件設計後形成一個常用的模板。模板的程式碼如下,直接複製貼到Notepad++記事本這類的文本軟體儲存為.ui檔即可使用,也能再用Qt Designer開啟後再做編修。其程式的寫法與html很相似,所以若有一些html、CSS概念的人可以更快上手。真的不會,就善用GoogleChatGPT,絕對是很好的免費老師。

除了主要介面外,我還會加上一個子介面,做為程式的版本紀錄與操作說明,雖然GUI已經很友善了,但如果真的還是不會的話,還有操作說明可以查看操作步驟,主打就是一個貼心。

撰風的UI介面設計模板
撰風的UI介面設計模板

主要介面: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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;1. &lt;br/&gt;2.&lt;br/&gt;3.&lt;br/&gt;4.&lt;br/&gt;5.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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.pymethod_generate_data.py是否正確運行,提高開發效率。此外,如果有其他介面需要使用相同的邏輯,也能直接調用這些方法而不必重新編寫,使代碼更具重用性。

在這method_generate_data.py中,撰風使用了一個簡單的計時器來模擬處理進度,透過QTimer,程式可以在每次觸發事件時更新進度條和顯示訊息,並讓介面持續保持互動性。這只是簡易的程式邏輯模板,之後可以再編修成符合自己需求的設計。若之後的程式運作有用到迴圈的話,建議可以再部分改用QThread將耗時操作移到背景執行,這樣可以保持主介面的流暢性與回應性,避免界面卡住。

小提醒,在介面中可以加入自己的小圖示,除了介面中的圖片可以由QLabel的pixmap屬性設定來插入圖片,而介面的視窗名稱和ico圖示可以在Python中定義,如下程式碼所示。撰風此前也有分享過如何透過Python生成ico圖檔的程式,大家也能參考。

撰風的Python運作程式效果
撰風的Python運作程式效果

主程式: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學習紀錄
Python基礎語法
主題 文章
編譯器與IDE
  • 註解:Comments
  • 模組:pip install.Module
  • 印出:print
  • 資料型態:numbers.string.list.tuples.dictionary
  • 運算:算數運算子.關係運算子.邏輯運算子
  • 判斷式:if else
  • 迴圈:while.for
  • 自定義函數:def
  • 矩陣:Array
  • 迭代器:Iterators
  • 物件導向:類(Classes).物件(Objects).封裝(Encapsulation).繼承(Inheritance).多型(Polymorphism)
  • 讀取儲存:Files I/O
  • 資料儲存:Json
  • 嘗試:Try Except
  • 互動:Input
  • 日期:Date
常用模組
  • Math:數學
  • Numpy:矩陣與線性代數運算
  • Random:隨機數
  • Matplotlib:資料作圖
  • OpenCV:影像資訊處理庫
  • pandas:配合Excel、CSV、Jason等數據處理
影像處理
  • 影像讀取與屬性修改
  • 網路攝影機
網路爬蟲
  • 網站爬蟲:Web Crawler
圖形介面
  • Tkinter:基礎GUI介面
  • PySide:Qt框架的GUI介面
  • CustomTkinter:基礎GUI介面美化版
  • Eel:結合html、JavaScript、CSS的網頁GUI介面
檔案打包
Python應用學習
主題 文章
影像應用
  • 圖像辨識
  • 肢體追蹤:Google MediaPipe
機器學習
  • 數據讀取
  • 資料預處理
  • 訓練集與測試集
  • 特徵縮放
  • 代入模型
  • 模型訓練與學習率測試
  • 損失函數:均方誤差.二元交叉熵.Scikit-Learn
深度學習
  • Keras
  • PyTorch
  • TensorFlow
語言分析
  • SnowNLP
  • Bert WWM
圖像生成
  • Stable Diffusion
寫一點小工具
小小作品
Python學習資源
常用網站
編輯 ] 筆記 » 程式語言 » Python
Python|HTML|CSS|JavaScript|Blender|Unreal Engine
0

Facebook留言

Wordpress留言 (0)

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *