汪群超 Chun-Chao Wang

Dept. of Statistics, National Taipei University, Taiwan

Python GUI 的基本概念

Objective:

本單元必須安裝 PyQt 套件:
在 Python 環境:> pip install pyqt6
在 Anaconda 環境:> conda install -c anaconda pyqt
註:目前在 Anaconda 的環境,只支援到 pyqt5。因此使用本單元的程式時,請將 pyqt6 改為 pyqt5,絕大部分都可以執行。
 
  1. 學習基本的 Python GUI 應用程式,包括 Qt 與 PyQt (Qt for Python) 的程式結構、內部控制元件與程式流程。
  2. 了解 GUI 處理事件的訊號(signals)與插槽(slots 訊號的處理程序)。
Qt 為開發 GUI 程式的工具箱,以 C++ 製作。 
PyQt = Python bindings (modules) for Qt applications,由 Riverbank Computing 開發,目前最新版本為 PyQt6。與 PyQt6 對等的 PySide6,係開發 Qt 的公司 Digia 的產品,PyQt6 與 PySide6 幾乎相同,大部分情況可以交換使用。

Prerequisite:

  1. Lesson 1 的觀念與技術。
  2. 查詢與閱讀手冊的能力。

範例 1:應用視窗與內部元件的設定,製作一個簡單的 GUI 視窗如下右圖。

使用元件:label, pushbutton

注意事項:

  1. 本範例提供讀者對於 GUI 程式初淺的認識,並非將來製作 GUI 應用程式的方式。甚至故意用來凸顯這個方式的缺點,後續再逐一鋪陳比較好的製作模式。

  2. 每個出現在 GUI 視窗內的元件,都對應到一個建立的指令及數個設定其外觀特性的指令。

  3. 從主程式 main() 追溯整個 GUI 視窗的產生,第一個是 QApplication(sys.argv),率先開啟應用程式,他的目的如手冊所說:The QApplication class manages the GUI application’s control flow and main settings.

  4. 第二是建立主視窗 QMainWindow(), 主視窗裡包括了使用者介面所需的各項元件。讀者可以參照手冊,看到主視窗涵蓋的範圍。事實上,在應用程式之內,也可以使用較小型的視窗 QWidget()。

  5. 在主視窗之下,開始建立各種元件,譬如標籤(label)與按鈕(pushbutton)及其特性(property)。最後以 show() 呈現視窗及 sys.exit(app.exec()) 啟動應用程式。

  6. 下列程式多了一個自訂的函數 pushme(),這是一般 GUI 程式特有的互動式反應,也就是當使用者按下(click)按鈕時,將觸發 pushme(), 由 pushme() 完成某件事情,譬如改寫 label 上的文字。由於這個程式的寫法雖有利解釋 GUI 程式的動線,卻無法在觸發程式裡面控制其他元件。因此暫時以 print() 指令通知開發者,按鈕的處動是成功的。下一個範例,將有適當的寫法解決這個問題。

  7. 按鈕的觸動,寫在 pbut.clicked.connect(pushme),其中 pbut 代表按鈕物件,clicked 代表觸發的動作,connect 代表將連接到哪一個觸動程式(pusheme())。

  8. 這個程式也展示 GUI 元件,如 label, pushbutton 及後續將介紹的其他元件,都繼承 QtWidgets 這個上層的 class。

from PyQt6 import QtWidgets
from PyQt6.QtWidgets import QApplication, QMainWindow
import sys

def pushme():
    print('clicked...') 

def main():
    app = QApplication(sys.argv) 
    win = QMainWindow() 
    
    # Configure the main window
    win.setGeometry(200, 200, 400, 320)
    win.setWindowTitle('Hello Title')

    # create a label and its associated properties
    label = QtWidgets.QLabel(win)
    label.setText('This is my first Label')
    label.setGeometry(0, 0, 200,20) # (x, y, width, length)
    label.move(100, 100)
    label.setStyleSheet("border: 1px solid black;")
    label.adjustSize() # to fit the text length

    # create a pushbutton
    pbut = QtWidgets.QPushButton(win)
    pbut.setText('Push Me')
    pbut.clicked.connect(pushme)

    win.show()
    sys.exit(app.exec())

if __name__ == '__main__': # run by itself, not by other application
    main()

範例 2:應用視窗與內部元件的設定同範例 1,但採較正式、常見的方式處理視窗的建立,並能處理按鈕的觸發反應, 視窗如下右圖。

注意事項:

  1. 改寫了 main() 函數,只負責單純的開啟與結束的動作,另增加一個負責視窗元件建立與觸動程序的類別 MyWindow(QMainWindow) ,這個類別因繼承 QMainWindow,因此可以藉 win = MyWindow() 取代原先直接以 win = QMainWindow() 建立主視窗,兼處理一些元件的額外動作。

  2. 繼承 QMainWondow 的 MyWindow 類別裡,最重要的函數定義便是 __init__(self),依據 MyWindow 所建立的物件都能使用到這裡定義的常數、變數,譬如所有的元件,及其特性(包括觸動的反應 signal)

  3. MyWindow 除了定義主要的函數 __init__(self) 外,還定義了按鈕的反應程式 pushme()。這個函數透過傳遞過來的 self 變數,可以直接影響任何在 __init__() 內定義的元件(以 self. 為首)

  4. 在 MyWindow() 類別內處理元件被觸動的指令稱為「signal」,譬如,pbut.clicked.connect(self.pushme);而處理的函數稱為「slot」。每個 signal 必須指定特定的 slot 來處理。在此,slot 函數 pushme() 只改變了標籤元件 label 的文字內容。改為 My second text,如下後圖。

from PyQt6 import QtWidgets
from PyQt6.QtWidgets import QApplication, QMainWindow
import sys

class MyWindow(QMainWindow): # inherit QMainWindow that provides a main application window
    def __init__(self):
        super(MyWindow, self).__init__()
        self.setGeometry(200, 200, 400, 320)
        self.setWindowTitle('Hello Title')
        
        self.label = QtWidgets.QLabel(self) # self = mainwindow
        self.label.setText('This is my first Label')
        self.label.setGeometry(0, 0, 200,20) # (x, y, width, length)
        self.label.move(200, 100)
        self.label.setStyleSheet("border: 1px solid red;")
        self.label.adjustSize()

        pbut = QtWidgets.QPushButton(self)
        pbut.setText('Push Me')
        # signal
        pbut.clicked.connect(self.pushme)

    # slot
    def pushme(self):
        self.label.setText('My second text')
        self.label.adjustSize()

def main():
    app = QApplication(sys.argv)
    win = MyWindow()
    win.show()
    sys.exit(app.exec())

if __name__ == '__main__':
    main()  

範例 3:應用範例 2 的架構,加入下拉式選單與單行編輯元件,配合按鈕的按壓反應,讀取下拉式選單的選取項目與單行編輯元件的內容,結合後寫在標籤元件上。如此大約初俱 GUI 的應用模式,如下右上下兩張圖。

使用元件:label, pushbutton, lineEdit, comboBox

注意事項:

  1. 本範例的主視窗 MyWindow() 繼承 QWidget,而非上個範例的 QMainWindow。這兩個類別率有不同,都經常被當作主視窗物件的來源(來自類別 QtWidgets)。初學,先不管兩者的差別,慢慢熟練並閱讀更多文件後,便能理解。

  2. 本範例的 GUI 除了加入下拉式選單(comboBox)與單行編輯元件(lineEdit)外,也利用元件的垂直排列器 QVBoxLayout 即 Vertical Box Layout,將四個元件以垂直方向排列整齊。設計者不需要控制元件的大小,自然能排列好。

  3. 下列程式中,大部分的指令都能自我說明清楚,不需要一一解釋。只是有些流程要交代。譬如,元件的垂直排列器 QVBoxLayout 要先設定(QVBoxLayout()),之後再一一將要加入排列的元件加進來(addWidget())。最後 setLayout() 完成排列。

  4. 學習 GUI 應用程式設計,除了掌握視窗的開啟、結束與布局外,必須掌握式窗內每個元件的建立、特性設定與內容的讀取與寫入。反應程式(slot)go(self) 便處理了下拉式選單與單行編輯元件內容的讀取,合併後寫入標籤中。go() 函數中的指令應該夠明白。至於每個元件的類別內有哪些 method 可用,譬如,下拉式選單 comboBox 的 currentText() 可以取得目前選項的文字內容。單行編輯器的 text() 可以取得目前輸入的文字內容…等。每個元件內容的設定與取出有很多 method 可供使用,一般人學習時,看一個學一個,真找不到時,也可以查使用手冊,一定可以解決。

from PyQt6.QtWidgets import QApplication, QWidget, \
    QVBoxLayout, QComboBox, QLineEdit, QPushButton, QLabel
import sys

class MyWindow(QWidget): # inherits from QWidget, a convenient widget for an empty window.
    def __init__(self):
        super(MyWindow, self).__init__()
        self.setGeometry(200, 200, 400, 200)
        # self.setMinimumSize(400, 200)
        self.setWindowTitle('Add more components')

        # Add a vertical layout
        self.vbox = QVBoxLayout()
        # The available greetings
        self.greetings = ['hello', 'goodbye', 'heyo']
        # The greeting combo box
        self.greeting = QComboBox(self)
        # Add the greetings
        # list(map(self.greeting.addItem, self.greetings))
        self.greeting.addItems(self.greetings)
        # The recipient textbox
        self.recipient = QLineEdit('world', self)
        # The greeting + recipient
        self.greet_rep = QLabel('Hello World', self)
        self.greet_rep.setStyleSheet("border: 1px solid red;")
        # The Go button
        self.go_button = QPushButton('&Go')
        # signal
        self.go_button.clicked.connect(self.go)

        # Add the controls to the vertical layout
        self.vbox.addWidget(self.greeting)
        self.vbox.addWidget(self.recipient)
        self.vbox.addWidget(self.greet_rep)
        # A very stretchy spacer to force the button to the bottom
        self.vbox.addStretch(100)
        self.vbox.addWidget(self.go_button)
        # Use the vertical layout for the current window
        self.setLayout(self.vbox)
    
    # slot
    def go(self):
        str1 = self.greeting.currentText()
        # str1 = self.greetings[self.greeting.currentIndex()]
        str2 = self.recipient.text()
        self.greet_rep.setText(str1 + ' ' + str2)

def main():
    app = QApplication(sys.argv)
    win = MyWindow()
    win.show()
    sys.exit(app.exec())

if __name__ == '__main__':
    main()   
商學院  7F16
ccw@gm.ntpu.edu.tw
(02)8674-1111 
ext 66777

部落格統計

  • 99,324 點擊次數
%d bloggers like this: