總有一天會用到的筆記

本站為減緩筆者下列疑難雜症誕生:記性很差,學過就忘;智商低落,囫圇吞棗。

0%

【Python】Tkinter 和 MVC 架構

Tkinter 是一個可以用 Python 寫出 GUI,並輕鬆串到自己寫的 Python API 的套件。

Tkinter、ttkbootstrap

tkinter.ttk 是風格化的 tk 元件。而 ttkbootstrap 是風格參考至 CSS 框架 bootstrap 的 tkinter 主題插件。ttkbootstrap 提供了許多現代化的主題、簡便的主題設置方式及額外的元件。安裝和使用可參考至官方文件

製造主視窗

1
2
import tkinter as tk
import ttkbootstrap as ttk
1
2
3
style = ttk.Style(theme='lumen')
root: tk.Tk = style.master
root.geometry(f'600x400')

產生元件

我們需要實例化一個元件出來,並在建構子中指定父元件。利用 elm.gridelm.pack 等方法可以將 elm 顯示於父元件中。

一些基本的元件有:

  • Frame:一塊空白的容器,通常用來放入其他元件做分類、排版用。如果要有框框可以用 LabelFrame。
  • Button:按鈕。在 comand 參數放入函式,可以在按下按鈕時執行。
  • Label:文字
1
2
3
4
frm_elm = ttk.Labelframe(root)
frm_elm.grid()

ttk.Button(frm_elm, text='owo').grid()

在所有元件的最後,使用 root.mainloop() 顯示主視窗。

1
root.mainloop()

可以看到,LableFrame 緊貼在視窗左上角,然後 Button 填滿了 LableFrame。

佈局方法

grid 是常用的佈局方法。預先在父元件中分配好每個欄、列的比例,並指定該物件位於父元件的位置、所佔欄列數即可。

rowconfigure/columnconfigure可以配置每個欄列的比例。 rowconfigure/columnconfigure 的參數:

  • index:第一個參數。指定要配置哪些列(可塞 intlisttuple)。
  • weight:指定這些列的比例。weight=1 為單位比例。

grid 的常用參數:

  • row/column:指定該元件的左上角要放在父元件的哪個網格
  • rowspan/columnspan:該元件要跨幾個欄/列
  • padx/pady:填充外部,增加與其他元件的空隙
  • ipadx/ipady:填充內部,增加邊界與文字、圖片內容的空隙
  • sticky:對齊方式。'n' 會靠上,'ns' 會上下延伸,'nswe' 會填滿四邊,以此類推。
1
2
3
4
5
6
7
8
9
10
11
12
13
style = ttk.Style(theme='lumen')
root: tk.Tk = style.master
root.geometry(f'600x400')
root.rowconfigure(0,weight=1)
root.columnconfigure(0,weight=1)


frm_elm = ttk.Labelframe(root)
frm_elm.grid(sticky='nswe', padx=5, pady=5)
frm_elm.rowconfigure(0,weight=1)
frm_elm.columnconfigure(0,weight=1)

ttk.Button(frm_elm, text='owo').grid(sticky='we', padx=5, pady=5)

風格

ttkbootstrap 提供了非常方便的風格設置方式。只要在實例化元件時指定 bootstrap 參數即可,並且 ttkbootstrap 還提供了常數使用。

1
ttk.Button(frm_elm, text='owo', bootstyle=(ttk.DARK,ttk.OUTLINE)).grid(sticky='we', padx=5, pady=5)

Control variables

control variables 是一種特殊的物件,提供 get()set() 來存取值,就像變數一樣。元件們可以共享同一個 control variables,並在它改變時自動更新顯示。如果將 control variables 指派給輸入元件(如:Radio ButtonsEntry)時,元件也可以改變 control variables 的值。

control variables 有: DoubleVarIntVarStringVarBooleanVar

1
2
3
name = ttk.StringVar()
ttk.Entry(frm_elm, textvariable=name).grid()
ttk.Label(frm_elm, textvariable=name).grid()

RadioButton 則是透過共享 control variables 來達成只能選一個的效果。

1
2
3
4
5
name = ttk.StringVar()
ttk.Radiobutton(frm_elm, text='err', variable=name, value='err').grid()
ttk.Radiobutton(frm_elm, text='utaha', variable=name, value='utaha').grid()
ttk.Radiobutton(frm_elm, text='megumi', variable=name, value='megumi').grid()
ttk.Label(frm_elm, textvariable=name).grid()

更多資料可以參考至:

MVC

MVC 是一種軟體架構,代表 Model、View、Controller。

  • Model:資料庫、演算法,不依賴 View 和 Controller,透過 API 就能正常使用。
  • View:圖形界面,不含邏輯。
  • Controller:界面邏輯、轉發 View 層的請求到 Model、更新 View 層的顯示。

我的日麻點數計算器參考了 MVC 概念並用 Python 和 tkinter 實作。

主要概念是在 view 層裡將按鈕的 command 參數指定為 controller 層中的 handler。當按下按鈕,執行 handler 時 controller 會去查看 view 中的 control variables,把輸入資料打包後傳給 model 層。從 model 層獲得計算結果後,則呼叫 view 層的函數更新顯示。

由於 controller 和 view 會互相參考,我又想把它們寫在不同檔案裡,為了避免循環 import 的問題,我將 controller 和 view 分別打包成 class。當實例化一個 controller 物件時,它也會實例化 view 物件,並將該 view 物件的 contoller 設為 self。

架構如下:

  • view.py
1
2
3
4
5
6
7
8
9
10
11
12
13
import tkinter as tk
import ttkbootstrap as ttk

class View:

def __init__(self, _ctrl):
self.ctrl = _ctrl

def setup_ui(self):
pass

def display_result(self, result):
pass
  • controller.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import tkinter as tk
import ttkbootstrap as ttk
from .view import View

class Controller:

def __init__(self):
self.view: View = View(self)

def btn_handler(self):
pass

def runApp(self):
self.view.setup_ui()
self.view.root.mainloop()


if __name__ == '__main__':
ctrl = Controller()
ctrl.runApp()

這個專案一開始在寫 API 時一開始完全沒有考慮 GUI 的需求,好像恰好的符合了 MVC 中 Model 的獨立性......