PyQt & GUI

By 小黑

目錄

- 專有名詞介紹

- GUI 介紹 & 簡史

- Qt & PyQt 介紹 & 安裝

- 第一個 PyQt 程式

- OOP

- GUI 基本概念

- Signals & Slots

- 常見 widgets

講師介紹

小黑(吳習之)

- 建中電研 44th 網管

- 寒訓一小隊輔 & 暑訓三小隊輔

- Linux & Python 廚

- 愛玩買夠梗但實際上沒追買夠

- 成發保重

- 不是同性戀或雙性戀

- 精神狀況堪憂

一些名詞定義

OS (Operating System)

- 作業系統

- 就你現在在用的 Windows or macOS

- 負責控制電腦硬體,是使用者跟電腦的橋樑

Library

- 指一堆寫好的 functions 跟 classes

- 如同字面意思,就像一個有不少書(functions, classess) 的圖書館

- 上堂課的 React 單獨來看就算 Library

Framework

- Framework = 一堆 Libaries + 一些工具

- 也如同字面意思,就像建築物的框架(鷹架),有了它只要填上建材(程式)就行

- 上上堂課教的 Flask 就是

- React + 一堆東西也算 Framework

Library vs. Framework

- 假設你今天要寫作文

- Framework 就相當於論說文模板

- 寫論說文必須遵從固定規則(Framework 的限制)

- Library 則是修辭、文章架構或詞彙

- 你可以隨意用排比、譬喻這些修辭(Library 中的 functions)

GUI

電腦界面發展簡史

1950s - 打孔卡

打孔卡

打孔機

1960s~Now - CLI

DOS

Unix

1970~80s - Personal Computer

Apple II

IBM PC

1970s - PARC & GUI

Xerox PARC

(這機構有人在 1969 時說未來會有平板電腦)

研究中心早期構想

Xerox Alto

1980s~Now - Apple

Apple Lisa

Macintosh 128K

1980s~Now - Microsoft

Windows 1.0

Windows 95

應用程式是怎樣煉成的

電腦上的 App 是怎麼寫的?

- CLI 程式 可以直接輸出結果,但 GUI 顯然沒辦法就這樣幹

- 所以為了要開發 GUI App,作業系統的設計者(比如微軟跟蘋果)會設計整套 Library,專門給開發者寫 App

- 開發者只要調用這些 Library,就能開發出 GUI App

- 而每個 OS 的 Library 都長得不一樣,所以不同 OS 間的 Library 沒辦法通用

怎麼辦!?

寫 App 的 Framework

- 每次換一個 OS,理論上 App 就要完全重寫,而這顯然不太現實

- 於是有人搞出了 GUI Framework,可以只寫一次 App 就能在不同 OS 跑

- Framework 會根據 OS 來調用對應 Library 中的 functions,達到不同 OS 間幾乎一樣的效果

常見 Framework

Flutter (Dart)

- iOS/Android/Desktop/Web

- 背後爸爸是 Google

- 性能不差

- 但 Dart 偏難寫(?

React Native (JavaScript)

- iOS/Android/Desktop/Web

- 背後爸爸是 Meta

- 相對好寫(如果你平常用 React)

- 性能也算過得去

- 如果你上完 React 覺得沒問題的話…

Electron (JavaScript)

- Desktop

- 背後爸爸是 GitHub

- 基本上就是一般網頁(code 幾乎不用改)

- VSCode、Slack 就是拿 Electron 包的

- 原理是開一個瀏覽器,直接跑網頁

- 所以超肥

還有這門課的主角 Qt!

Qt & PyQt 介紹 & 安裝

Qt 介紹

- 這一三北資學術兼文書 Qt 學姐

- 今年暑假幹訓應該會看到她(?

- 所以要來幹訓!!!

不是這位

Qt 介紹

- 由同名公司開發的一套 GUI Framework

- 最初設計給 C++,後來被移植到其他語言

- 不少桌面 App 都用 Qt 開發,比如 PhotoShop、Telegram

PyQt 介紹

- 由 Riverbank Computing 搞出來的 Qt For Python

- Anki 桌面版就是用 PyQt 寫的

(PyQt 沒 logo,所以…嗯)

為什麼使用 PyQt

- PyQt 是 Python 唯一成熟的 Framework

- 其他語言也能用 Qt,所以學會 PyQt 就能轉移到其他語言

- 生態豐富、有大公司支援

(PyQt 沒 logo,所以…嗯)

PyQt vs. PySide

- PyQt 比 PySide 更早出來,生態更完整、資料更多

- PySide 是由 Qt 官方開發的,理論上相容性會更好

- PyQt 商用要付費(或必須把程式碼公開)

- PySide 可以直接商用

- 還有一部分 class & function 的名稱不同

- 除此之外沒差,一個程式要改用另一者只要改 module name 就行

PyQt 安裝

先去裝 Python ㄅ

- Python 官網:https://www.python.org/downloads/

- 建中電腦應該裝好了

- 然後理論上你們電腦也應該要有(不然你怎麼上完 Flask 的

接著打開終端機,輸入以下指令

- Windows 使用者開 cmd.exe

- Mac/Linux 使用者開 Terminal

pip install pyqt6 --break-system-packages

(如果報錯誤再加)

PyQt 架構

PyQt 可分成好幾個 modules

- PyQt 底下有十幾個 modules,而常用的有四個

- QtCore: 一些非 GUI 類的 class,包含日期處理、檔案操作、多線程

- QtGui: 各種圖形相關的 class,包含圖片、字體

- QtWidgets: 各個 widgets

- QtMultimedia: 音訊、影片相關

第一個 PyQt 程式

打開編輯器,輸入以下程式

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

def window():
    app = QApplication(sys.argv)
    win = QMainWindow()
    win.setGeometry(200, 200, 300, 300);
    win.setWindowTitle("CKEFGISC Winter!") 
    win.show()
    sys.exit(app.exec())

window()

存檔並執行

- 把檔案存到剛剛創的 ckefgisc-pyqt 資料夾下

python3 app.py

- 檔名取 app.py (其他也行,你爽就好

- 執行

- 你應該會看到這個畫面

逐行解釋

- 前三行引入需要的 libraries
- 第一行從 PyQt6 引入 QtWidgets 這個 class
- 第二行從 PyQt6.QtWidgets 引入 QApplication、QMainWindow 這兩個 class
- 第三行引入 Python 內建的 sys (處理系統相關的指令的 module)

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

逐行解釋

- 定義 function window

- 備註:不用寫成 function 也行,但這樣接下來會好改很多

def window():
    app = QApplication(sys.argv)
    win = QMainWindow()
    win.setGeometry(200, 200, 300, 300);
    win.setWindowTitle("CKEFGISC Winter!")
    win.show()
    sys.exit(app.exec())

逐行解釋

- 建立一個 QApplication 的 instance app,並傳入 sys.argv

- QApplication 是 Qt 程式的起始點,一定得寫(不然沒法用)

- sys.argv 就是執行程式時傳入的命令行參數(不過等下課程用不到)

    app = QApplication(sys.argv)

逐行解釋

- 建立一個 QMainWindow 的 instance win

- 這是程式的(唯一)Window(寫 GUI App 一定需要)

    win = QMainWindow()
    win.setGeometry(200, 200, 300, 300)

- 將 win 移動到桌面上 x=200、y=200 ,並把大小設定為 300x300

逐行解釋

- 把 win 的 title 設為 "CKEFGISC Winter!" (沒有雙引號)

    win.setWindowTitle("CKEFGISC Winter!")
    win.show()

- 讓 win 顯示出來(一定要加,不然不會顯示

    sys.exit(app.exec())

- 在 app 執行完後結束程式(一定要加,不然也不會顯示

來加點文字ㄅ!

加點文字

- 這行會建立一個新的 QLabel 、把這個 Label 的文字設定成「赤坂明我恨你」,並把它建立在 win 上

- 在 win.show() 前一行加上這行並存檔執行

- 此時螢幕上應該會出現以下畫面

    label = QtWidgets.QLabel("赤坂明我恨你", win)

要怎麼改 label 位置

- 可以用 label 的 move method 移動 label

- 把下面這行加到 label 跟 win.show() 之間重新執行

- 應該會看到類似這樣的畫面:

    label.move(50, 50)

OOP

假設你在寫整理動漫的程式

mygo_name = "BanG Dream! It's MyGO!!!!!"
mygo_year = "2023"
mygo_type = ["Horror", "Love"]
kaguya_name = "輝夜姬想讓人告白"
kaguya_year = "2015"
kaguya_type = ["Love", "Comedy"]
heroines_name = "敗北女角太多了"
heroines_year = "2021"
heroines_type = ["Love", "Comedy"]

一個個列短期還好…

但你是個選化被當(絕對不是講師)的動漫廚,你一年會看好幾部動漫(這也不是講師)。

這部很棒

這部很棒

這部很棒

這部很棒

這部很棒

這部是他媽垃圾

但長期下來很麻煩…

那既然這幾部都是「動漫」,有沒有辦法設計類似模板的東西呢?

有!

class Anime:
    def __init__(self, name, year, type):
        self.name = name
        self.year = year
        self.type = type
mygo = Anime("BanG Dream! It's MyGO!!!!!", 2023, ["Horror", "Love"])
kaguya = Anime("輝夜姬想讓人告白", 2015, ["Love", "Comedy"])
heroines = Anime("敗北女角太多了", 2021, ["Love", "Comedy"])

print(mygo.name) # BanG Dream! It's MyGO!!!!!
print(kaguya.year) # 2015
print(heroines.type) # ["Love", "Comedy"]

水啦!

Class 就像是食譜

mygo = Anime("BanG Dream! It's MyGO!!!!!", 
             2023, 
             ["Horror", "Love"])

動漫

- 名稱

- 年份

- 類型

kaguya = Anime("輝夜姬想讓人告白", 
               2015, 
      		   ["Love", "Comedy"])
heroines = Anime("敗北女角太多了", 
                 2021, 
                 ["Love", "Comedy"])

食譜

做出來的料理

而這些做出來的料理就是 Object

Class & Object

- 如同前面提到,Class 類似食譜或藍圖

- 利用 Class 建立出來的實體即為 Object

- Instance 指特定 Object,比如上面的 mygo 或 kaguya

- __init__ 這個酷 function 每次建立新 Object 都會被呼叫一次

又假設你想寫農場模擬遊戲

你定義好了「狗」這個類別

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

你想讓「狗」能叫怎麼辦?

簡單!

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
    def bark(self):
        print("在嗎?我寄去的禮物收到了嗎?希望你會喜歡。\n",
              "天氣又變冷了,記得多穿點衣服。\n",
              "你在忙吧,不用回我沒關係。\n",
              "忙完早點休息吧,晚安。")
              
dog = Dog("Aaron", 17)
dog.bark()

method

- 這些 class 內的 function 被稱為 method

- method 至少要有一個參數 self

- 不同 class 可以有相同的 method 名稱,但同一個 class 內不行(在 Python)

双叒叕你想寫一個 Minecraft

你設計出了 Block 這個 class,並拿它去設計出其他方塊。

但你忘了 Minecraft 很不科學

一個方塊可能不受重力影響、不被岩漿燒、會被安德拿走、發光、有彈性!

class Block:
    def __init__(self, name, shade, id):
        self.name = name
        self.shade = shade
        self.id = id

那要怎解?

將全部方塊定義一遍?

可是很多方塊都有重複的 attribute ㄟ

而且你是個不用 auto complete 的 Vim 廚,每次都會 key attribute 到瘋掉

那能不能將方塊共同的 attribute 獨立成一個 class Block

任何方塊繼承 Block 屬性後再調整

還真的可以ㄟ

class Block:
    def __init__(self, name, id, hardness):
        self.name = name
        self.id = id
        self.hardness = hardness
class Grass(Block):
    def __init__(self, name, id, hardness):
        super().__init__(name, id, hardness)
class Log(Block):
    def __init__(self, name, id, hardness):
        super().__init__(name, id, hardness)
        self.is_craved = False

Inheritance

- Inheritance 指一個 class 繼承另一個 class 的 attributes & methods

- 比如上面 Grass 跟 Log 繼承自 Block

- Block 即為 parent class,Grass 和 Log 即為 child class

- super().__init__(name, id hardness) 調用 parent class (Block) 的 __init__

小統整

- Class 就像食譜,可以用此做出 Object (料理)

- Object 有不同 attribute

- Object 有類似 function 的 methods

- Object 可以繼承自其他 Object

小練習 #1

- 定義一個 class People

- People 有 name, friends 兩個 attributes

- People 裡的 print_friends method 可印出 friends

小練習 #2

- 把剛剛第二個 PyQt 程式的 code 用 OOP 的方式重寫

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

def window():
    app = QApplication(sys.argv)
    win = QMainWindow()
    win.setGeometry(200, 200, 300, 300);
    win.setWindowTitle("CKEFGISC Winter!") 
    label = QtWidgets.QLabel("赤坂明我恨你", win)
    win.show()
    sys.exit(app.exec())

window()

寫 GUI 要有的基本概念

Window

- 萬物始於 Window

- 每個 GUI App 只會有一個主 Window

- Window 可以有 menu bar、tool bar

Widget

- 一個 Widget 就是一個小組件

- Button、Label 這些都是 Widget

- Widget 必須放到一個 Window 上,不然跑不了

座標

- 每個 window 的左上角座標是(0, 0)

- 左上角向右 +X,向下 +Y

Event Loop

- 可以把一個 GUI App想成 Minecraft 中的偵測器

- 有玩 Minecraft 就知道,偵測器偵測到方塊變更就會發送紅石信號

- 「方塊變更」就是一個 event,對應到 GUI App 裡就是滑鼠移動、點擊、文字輸入

- 「發送紅石信號」就是觸發的 function(handler),GUI App 可以視不同 event 做不同事

- 偵測器會一直偵測,直到消失為止;同理,GUI App 也會一直偵測 event 到退出為止

這整套機制就叫 Event Loop

Signals & Slots

QPushButton

QPushButton

- 作用:建立一個可以按的按鈕

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

def window():
    app = QApplication(sys.argv)
    win = QMainWindow()
    button = QtWidgets.QPushButton(win)
    button.setText("Hit me")
    win.setGeometry(200, 200, 600, 600);
    win.show()
    sys.exit(app.exec())

window()

Signals & Slots 介紹

Signals & Slots

- 當一個 widget 被操作(比如點擊、被輸入),widget 會發送 Signal

- Signal 可以連接到 function

- 每當 Signal 產生,Signal 所連接的 function 就會執行

- 這些被呼叫的 functions 即為 Slots

- 對應到 Event Loop,Signals 為 event,Slots 為 handler

建立 Signals & Slots 的語法

- 假設我們有個按鈕

- 還有一個 fucntion world_hello

- 如果我們希望讓按鈕被點擊後觸發 world_hello,可以這樣寫

button = QtWidgets.QPushButton("Hit me")
button.resize(200, 100)
def world_hello():
    print("Hello, it's me")
button.clicked.connect(world_hello)

Slot

Signal

來個範例

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

def print_hello():
    print("Hello, world")

def window():
    app = QApplication(sys.argv)
    win = QMainWindow()
    button = QPushButton("Hit Me", win)
    button.clicked.connect(print_hello)
    win.show()
    app.exec()

window()

執行結果

小練習

- 試根據以下 GIF 撰寫出相似程式

練習解答

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

class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setGeometry(0, 0, 500, 500)
        self.count = 0
        self.label = QtWidgets.QLabel(f"Count: {self.count}", self)
        self.label.adjustSize()
        self.btn = QtWidgets.QPushButton("Hit me", self)
        self.btn.adjustSize()
        self.btn.move(200, 200)
        self.btn.clicked.connect(self.change_counter)

    def change_counter(self):
        self.count += 1
        self.label.setText(f"Count: {self.count}")
        self.label.adjustSize()

app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec())

常用 Widgets

QLabel

- 作用:建立可以顯示圖片或文字的標籤

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

def window():
    app = QApplication(sys.argv)
    win = QMainWindow()
    win.setGeometry(200, 200, 300, 300);
    label = QtWidgets.QLabel(win)
    label.setText("大括號要換行") # 設定 label 要顯示的文字
    label.resize(200, 200)
    win.show()
    sys.exit(app.exec())

window()

範例:設定字體樣式

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

def window():
    app = QApplication(sys.argv)
    win = QMainWindow()
    win.setGeometry(200, 200, 300, 300);
    label = QtWidgets.QLabel(win)
    label.setText("To be, or not to be.")
    font = QtGui.QFont() # 利用 QtGui 中的 QFont 方法建立一個新字體
    font.setFamily("JetBrains Mono Nerd Font") # 更改字體樣式
    font.setBold(True) # 字體粗細
    font.setItalic(True) # 斜體
    font.setUnderline(True) # 底線
    font.setStrikeOut(True) # 刪除線
    font.setPointSize(20) # 字體大小
    label.setFont(font) # 將 label 的字體設定為 font
    label.adjustSize() # 讓 label 的尺寸根據文字長度調整
    win.show()
    sys.exit(app.exec())

window()

範例:顯示圖片

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

def window():
    app = QApplication(sys.argv)
    win = QMainWindow()
    win.setGeometry(200, 200, 600, 600);
    label = QtWidgets.QLabel(win)
    img = QtGui.QImage("./3/老八.JPG")
    label.resize(342, 512) 
    label.setScaledContents(True) 
    label.setPixmap(QtGui.QPixmap.fromImage(img))
    win.show()
    sys.exit(app.exec())

window()

QTextEdit

- 作用:建立一個文字輸入框

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

def window():
    app = QApplication(sys.argv)
    win = QMainWindow()
    box = QtWidgets.QTextEdit(win)
    win.setGeometry(200, 200, 600, 600);
    win.show()
    sys.exit(app.exec())
window()

範例:獲取使用者輸入的文字

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

def printText():
    print(box.toPlainText())

app = QApplication(sys.argv)
win = QMainWindow()
box = QtWidgets.QTextEdit(win)
win.setGeometry(200, 200, 600, 600);
btn = QtWidgets.QPushButton(win)
btn.move(100, 100)
btn.clicked.connect(printText)
win.show()
print(box.toPlainText())
sys.exit(app.exec())

範例:設定預設文字

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

def printText():
    print(box.toPlainText())

app = QApplication(sys.argv)
win = QMainWindow()
box = QtWidgets.QTextEdit(win)
win.setGeometry(200, 200, 600, 600);
btn = QtWidgets.QPushButton(win)
btn.move(100, 100)
btn.clicked.connect(printText)
win.show()
print(box.toPlainText())
sys.exit(app.exec())

QCheckBox

- 作用:建立一個勾選框

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

def window():
    app = QApplication(sys.argv)
    win = QMainWindow()
    win.setGeometry(200, 200, 600, 600);
    checkbox = QtWidgets.QCheckBox(win)
    checkbox.setText("這是個勾選框")
    checkbox.adjustSize()
    win.show()
    sys.exit(app.exec())

window()

QComboBox

- 作用:建立一個下拉選單

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

def window():
    app = QApplication(sys.argv)
    win = QMainWindow()
    win.setGeometry(200, 200, 600, 600);
    combobox = QtWidgets.QComboBox(win)
    combobox.addItems(["有馬加奈", "黑川赤音", "星野露比", "星野愛"])
    win.show()
    sys.exit(app.exec())

window()

實作

BMI App

- 寫一個 BMI 計算機

2025 建北電資聯合寒訓「資己資彼,百 Code 不殆」課程——PyQt

By Aaron Wu

2025 建北電資聯合寒訓「資己資彼,百 Code 不殆」課程——PyQt

  • 193