2018年10月12日 星期五

Python 內建 GUI 模組 tkinter 測試 (三) : 版面管理員

版面管理員 (Layout Manabger) 用來設定 GUI 應用程式中元件的擺放方式, 選擇適當的版面是GUI 程式首要的工作. Tkinter 透過三個方法提供了三種版面管理員 :
  1. pack()
  2. grid()
  3. place()
其中 pack() 為流水式排版, 預設元件會依加入先後順序由上而下, 由左而右自行排列; grid() 為表格式排版, 元件是依據所指定的索引位置, 如同二維陣列元素一般放入表格中; 而 place() 則可指定絕對或相對座標將元件精確擺放到視窗版面中.

注意 : 一個視窗容器中不能同時使用 pack 與 grid 排版, 但 place 卻可以與 pack 或 grid 同時使用.

本系列之前的文章參考 :

Python 內建 GUI 模組 tkinter 測試 (一) : 建立視窗
Python 內建 GUI 模組 tkinter 測試 (二) : 對話框

本篇測試參考了下列幾本書 :

# Python GUI 設計活用 tkinter 之路王者歸來 (深石, 洪錦魁)
# Python 程式設計學習經典:工程分析x資料處理x專案開發 (碁峰, 黃立政等, 第 9 章)
# 科學運算 : Python 程式理論與應用 (上奇, 楊珮璐等, 第 12 章)
# Tkinter GUI Application Development Blueprints (Packt, Bhaskar Chaudhary)
# Tkinter GUI Application Development Hotshot (Packt, Bhaskar Chaudhary)
# Python GUI Programming Cookbook (Packt, Burkhard A. Meier)
# Python and Tkinter Programming (Manning, John E. Grayson)
Python GUI programming with Tkinter (Packt, Alan D Moore)
# Tkinter GUI Programming by Example (Packt, David Love)

tkinter 的說明文件參考 :

https://pythonspot.com/en/tkinter/
Graphical User Interfaces with Tk


一. 使用 pack() 管理版面 : 

pack() 方法的參數全部都是可有可無的, 沒有傳入參數時, 加入版面的元件以預設值排版, 常用參數如下 :


 pack() 參數 說明
 side 排列方向 : TOP (預設), BOTTOM, LEFT, RIGHT
 fill 填滿所分配空間之方向 : NONE (預設), X, Y, BOTH
 expand 填滿容器 : True/False (預設)
 padx/pady 元件邊框與容器之距離 (px, 預設=0)
 ipadx/ipady 元件內容 (文字/圖像) 與其邊框之距離 (px, 預設=0)
 anchor 元件在容器中的錨定位置 : E, W, S, N, CENTER (預設), NE, SE, SW, NW


參考 :

The Tkinter Pack Geometry Manager

pack() 的詳細介面可輸入下列指令查詢 :

>>> import tkinter
>>> help(tkinter.Pack)

元件用 pack() 加入版面時預設會由上而下, 由左而右排版, 例如 :


測試 1 : pack() 預設排版

import tkinter as tk
from tkinter import ttk

root=tk.Tk()
root.title("pack() 測試")
root.geometry("200x100")

ttk.Button(root, text="南志鉉").pack()
ttk.Button(root, text="都敬秀").pack()
ttk.Button(root, text="百日的郎君").pack()
root.mainloop()

此程式在視窗版面中放了三個按鈕, 在呼叫 pack() 放入按鈕時未傳入參數, 故以預設之 TOP 由上而下排版 :



在呼叫 pack() 若傳入 BOTTOM/LEFT/RIGHT 可改變排版方式, 例如 :


測試 2 : pack() 全部指定排版

import tkinter as tk
from tkinter import ttk

root=tk.Tk()
root.title("pack() 測試")
root.geometry("200x100")

ttk.Button(root, text="南志鉉").pack(side=tk.BOTTOM)
ttk.Button(root, text="都敬秀").pack(side=tk.BOTTOM)
ttk.Button(root, text="百日的郎君").pack(side=tk.BOTTOM)
root.mainloop()

注意, 由於 tkinter 是以別名 tk 匯入, 因此排版常數 BOTTOM/LEFT/RIGHT 須加上別名參考 tk, 否則會出現常數未定義錯誤.




由於三個按鈕全部以 BOTTOM 方式排版, 因此依加入順序由下而上排版. 下面是只有部分元件為指定排版, 其他為預設 TOP 排版範例 :


測試 3 : pack() 部分指定排版  

import tkinter as tk
from tkinter import ttk

root=tk.Tk()
root.title("pack() 測試")
root.geometry("200x100")

ttk.Button(root, text="南志鉉").pack(side=tk.BOTTOM)
ttk.Button(root, text="都敬秀").pack()
ttk.Button(root, text="百日的郎君").pack()
root.mainloop()




此例只有第一個按鈕指定 BOTTON 排版, 因此它被放在最底下; 另兩個是預設 TOP 排版, 故還是由上而下擺放.

下面範例是全部指定由左至右排版 :


測試 4 : pack() 全部指定排版

import tkinter as tk
from tkinter import ttk

root=tk.Tk()
root.title("pack() 測試")
root.geometry("220x100")

ttk.Button(root, text="南志鉉").pack(side=tk.LEFT)
ttk.Button(root, text="都敬秀").pack(side=tk.LEFT)
ttk.Button(root, text="百日的郎君").pack(side=tk.LEFT)
root.mainloop()

注意, 此例因由全部左向右排版, 因此版面大小改為 220x100.




可見全部都用 LEFT 就會由左向右排版, 如果版面不夠寬會被截掉按鈕內容.


測試 5 : pack() 混合排版

import tkinter as tk
from tkinter import ttk

root=tk.Tk()
root.title("pack() 測試")
root.geometry("300x100")

ttk.Button(root, text="南志鉉").pack()
ttk.Button(root, text="都敬秀").pack(side=tk.LEFT)
ttk.Button(root, text="百日的郎君").pack(side=tk.BOTTOM)
root.mainloop()




應該沒有會這樣惡搞排版吧! 不過奇怪的是, 第三個按鈕竟然沒有跟第一個 TOP 的對齊.

接著測試 anchor 參數, 此參數用來指定元件在父容器中的 9 個錨定方位 : E/W/N/W/NW/SW/NE/SE/CENTER (預設值)  :




注意, 若 tkinter 以別名 tk 匯入, 則須加別名參考, 例如 tk.E 等.


測試 6 : pack() 錨定位置

import tkinter as tk
from tkinter import ttk

root=tk.Tk()
root.title("pack() 測試")
root.geometry("300x250")

ttk.Button(root, text="東").pack(anchor=tk.E)
ttk.Button(root, text="西").pack(anchor=tk.W)
ttk.Button(root, text="南").pack(anchor=tk.S)
ttk.Button(root, text="北").pack(anchor=tk.N)
ttk.Button(root, text="中").pack(anchor=tk.CENTER)
ttk.Button(root, text="東南").pack(anchor=tk.SE)
ttk.Button(root, text="西北").pack(anchor=tk.NW)
ttk.Button(root, text="西南").pack(anchor=tk.SW)
ttk.Button(root, text="東北").pack(anchor=tk.NE)
root.mainloop()

此例在視窗版面的 9 個錨定方位放置按鈕, 由於 pack() 是流水式排版, 元件是按照先後順序擺放在錨定位置 :




參數 padx/pady 與 ipadx/ipady 都是用來指定間隙用的, padx/pady 指定元件之外部間隙; ipadx/ipady 則指定內部間隙 (元件邊框與內部文字或圖像之距離), 單位為 px.

從上面測試 1 與測試4 可知, 元件之間的間隙 padx 與 pady 預設大約是 1px, 但若取消視窗大小設定, 元件與視窗之間隙 pady 大約是 1px; 而 padx 大約是 20px 左右, 例如 :


測試 6 : 間隙距離 padx/pady

import tkinter as tk
from tkinter import ttk

root=tk.Tk()
root.title("pack() 測試")

ttk.Button(root, text="南志鉉").pack()
ttk.Button(root, text="都敬秀").pack()
ttk.Button(root, text="百日的郎君").pack()
root.mainloop()




測試 7 : 間隙距離 padx/pady

import tkinter as tk
from tkinter import ttk

root=tk.Tk()
root.title("pack() 測試")
#root.geometry("300x200")

ttk.Button(root, text="南志鉉").pack(padx=20, pady=10)
ttk.Button(root, text="都敬秀").pack(padx=20, pady=10)
ttk.Button(root, text="百日的郎君").pack(padx=20, pady=10)
root.mainloop()




可見與視窗之間隙, pady 如實反映設定, 而 padx 則多了約 20px 之空間. 但如果空間不夠時, 這多出來的 20px 就會消失了, 例如 :


測試 8 : 間隙距離 padx/pady

import tkinter as tk
from tkinter import ttk

root=tk.Tk()
root.title("pack() 測試")

ttk.Button(root, text="南志鉉").pack(side=tk.LEFT, pady=10)
ttk.Button(root, text="都敬秀").pack(side=tk.LEFT, pady=10)
ttk.Button(root, text="百日的郎君").pack(side=tk.LEFT, pady=10)
root.mainloop()




參數 ipadx/ipady 用來設定元件內部間隙, 即邊框與內部文字或圖像之距離, 例如 :

測試 9 : 間隙距離 ipadx/ipady

import tkinter as tk
from tkinter import ttk

root=tk.Tk()
root.title("pack() 測試")
root.geometry("200x100")

ttk.Button(root, text="南志鉉").pack(ipadx=50, ipady=10)
ttk.Button(root, text="都敬秀").pack(ipadx=50)
ttk.Button(root, text="百日的郎君").pack(ipadx=50)
root.mainloop()




與上面測試 1 相比, 可見三個按鈕的 x 軸內部間隙都放寬為 50px; 第一個按鈕的 y 軸間隙則放寬為 10px.

參數 fill 用來設定元件在 X/Y 軸方向填滿所分配到的空間, 其值為常數 X/Y/BOTH, 若以別名 tk 匯入, 則需用 tk.X/tk.Y/tk.BOTH, 例如 :


測試 10 : 填滿分配的空間 fill

import tkinter as tk
from tkinter import ttk

root=tk.Tk()
root.title("pack() 測試")
root.geometry("300x150")

ttk.Button(root, text="南志鉉").pack(side=tk.LEFT, fill=tk.BOTH)
ttk.Button(root, text="都敬秀").pack(fill=tk.X)
ttk.Button(root, text="百日的郎君").pack(fill=tk.Y)
root.mainloop()




此例中第一個按鈕指定為 LEFT 對齊與填滿 XY 軸, 結果在 Y 軸方向它填滿整個視窗高度, 可見在 LEFT 對齊時元件所分配到的高度就是視窗高度. 第二與第三個按鈕為預設 TOP 對齊, 第二個按鈕為 X 軸填滿, 第三個按鈕為 Y 軸填滿, 但實際上第三個按鈕並未填滿視窗高度, 可見在 TOP 對齊時, 元件所獲得的高度並非視窗高度, 而是足夠放元件之高度. 從下面的範例可了解此特性 :


測試 11 : 填滿分配的空間 fill

import tkinter as tk
from tkinter import ttk

root=tk.Tk()
root.title("pack() 測試")
root.geometry("300x150")

ttk.Button(root, text="南志鉉").pack(side=tk.LEFT, fill=tk.BOTH)
ttk.Button(root, text="都敬秀").pack(side=tk.LEFT, fill=tk.X)
ttk.Button(root, text="百日的郎君").pack(side=tk.LEFT, fill=tk.BOTH)
root.mainloop()




此例中三個按鈕都是 LEFT 對齊, 因此都被分配了整個視窗高度, 第一與第三個按鈕設定 X/Y 軸填滿故佔據全部視窗高度. 第二個按鈕則僅填滿被分配到的 X 軸寬度.

參數 expand 用來設定元件所分配到的版面是否要擴展到最大, 且當父容器放大或縮小時是否會隨著改變位置, 其值為 True/False (預設), 也可以是 1/0 或 YES/NO (以別名 tk 匯入 tkinter 時要用 tk.YES/tk.NO), 例如上面測試 1 中的三個按鈕會緊貼在一起, 且視窗縮放時還是黏在一起不會分開, 但若將第一個按鈕設為 expand=True 則會擴充自己的版面到最大, 例如 :


測試 12 : 擴充所分配的空間 expand

import tkinter as tk
from tkinter import ttk

root=tk.Tk()
root.title("pack() 測試")
root.geometry("300x150")

ttk.Button(root, text="南志鉉").pack(expand=True)
ttk.Button(root, text="都敬秀").pack()
ttk.Button(root, text="百日的郎君").pack()
root.mainloop()




由於第一個按鈕的 expand 設為 True, 因此它所獲得的版面會擴充到最大, 因而將其餘兩個按鈕擠到最底下. 如果縮放視窗的話第一個按鈕的位置會隨之改變.


二. 使用 grid() 管理版面 : 

grid 排版是最容易理解也最好用版面管理員, 適合用來處理較複雜的版面, 它使用類似網頁表格或二維陣列的索引方式來放置元件, 每個網格只能放置一個元件, 但多個網格可用 rowspan 與 columnspan 參數合併鄰近的多個網格來放置一個元件, 其介面如下 :

元件.grid([options])

options 為可有可無之參數, 常用參數如下表 :

 grid() 參數 說明
 row 列索引
 column 行索引
 rowspan 儲存格合併列數
 columnspan 儲存格合併行數
 padx/pady 元件邊框與容器之距離 (px, 預設=1)
 ipadx/ipady 元件內容 (文字/圖像) 與其邊框之距離 (px, 預設=1)
 sticky 元件於網格中的錨定位置 : E, W, S, N, CENTER (預設)

其中 row 與 column 都是 0 起始的索引. 一般來說元件呼叫 grid() 將自己放進容器時應該傳入 row 與 column 參數指定網格索引位置, 否則預設是以 1 行網格來擺放元件. 參數 padx/pady/ipadx/ipady 用法與上面 pack() 的一樣.

grid() 的詳細介面可輸入下列指令查詢 :

>>> import tkinter
>>> help(tkinter.Grid)


測試 13 : 呼叫 grid() 時不傳參數 

import tkinter as tk
from tkinter import ttk

root=tk.Tk()
root.title("grid() 測試")
root.geometry("300x150")

ttk.Button(root, text="南志鉉").grid()
ttk.Button(root, text="都敬秀").grid()
ttk.Button(root, text="百日的郎君").grid()
root.mainloop()




可見呼叫 grid() 時若不傳參數, 預設是以 n 列 1 行的網格來依序放置元件.


測試 14 : 呼叫 grid() 時傳入索引參數

import tkinter as tk
from tkinter import ttk

root=tk.Tk()
root.title("grid() 測試")
root.geometry("300x150")

ttk.Button(root, text="南志鉉").grid(row=0, column=0)
ttk.Button(root, text="都敬秀").grid(row=0, column=1)
ttk.Button(root, text="百日的郎君").grid(row=1, column=0, columnspan=2)
root.mainloop()




此例在呼叫 grid() 時傳入了 row 與 column 參數指定擺放之索引位置, 形成 2*1 網格. 第三個按鈕傳入 columnspan 參數指定與下一行網格合併, 因此第二列只有一個網格, 預設是 CENTER 對齊, 如果要擴展按鈕所占版面, 需用 sticky 參數, 此參數與 pack() 排版之錨定參數 anchor 類似, 但只能傳入 E/W/S/N 四個常數或其相加組合. 若以 tk 別名匯入 tkinter, 則須使用 tk.E/tk.W/tk.S/tk.N, 例如 :


測試 15 : 呼叫 grid() 時傳入 sticky 參數

import tkinter as tk
from tkinter import ttk

root=tk.Tk()
root.title("grid() 測試")
root.geometry("300x150")

ttk.Button(root, text="南志鉉").grid(row=0, column=0)
ttk.Button(root, text="都敬秀").grid(row=0, column=1)
ttk.Button(root, text="百日的郎君").grid(row=1, column=0, columnspan=2,
                                    sticky=tk.E+tk.W)
root.mainloop()




此例第三個按鈕設定 sticky 參數為 E 與 W, 亦即元件擴展至容器東西兩端.


三. 使用 place() 管理版面 : 

此排版法是指定以父容器左上角為原點之座標位置來精確放置元件, 但使用的機會較少, 因為只要某個元件的位置更改, 整個版面中的元件可能都要隨之調整, 通常是用來製作客製化的版面管理員之用. 其介面如下 :

元件.place([options])

place() 函數的常用參數如下 :

 place() 參數 說明
 x 相對於視窗左上角之 x 座標
 y 相對於視窗左上角之 y 座標
 width 指定元件寬度 (px)
 height 指定元件高度 (px)
 relx 相對於父容器寬度之比率 x 座標 (0~1)
 rely 相對於父容器高度之比率 y 座標 (0~1)
 relwidth 相對於父容器寬度之比率 (0~1)
 relheight 相對於父容器高度之比率 (0~1)
 anchor 元件在容器中的錨定位置 : E, W, S, N, CENTER (預設), NE, SE, SW, NW

其中 (x, y) 或 (relx, rely) 是必須傳入之定位參數, 雖然沒有傳入任何參數也不會出現錯誤訊息, 但因為缺乏定位資訊, 因此元件也不會出現在視窗上. place() 的詳細介面可輸入下列指令查詢 :

>>> import tkinter
>>> help(tkinter.Place)

place() 提供兩種定位法 :
  1. 絕對定位 :
    以 (x, y) 參數指定絕對座標
    以 (width, height) 指定絕對大小
  2. 相對定位 :
    以 (relx, rely) 參數指定相對座標
    以 (relwidth, relheight) 參數指定相對大小
兩者的差別是, 在視窗縮放時, 以絕對定位法放置的元件不會改變其位置與大小; 而以相對定位法放置的元件則會依照相對比率隨之縮放, 例如 :


測試 16 : 絕對定位 vs 相對定位

import tkinter as tk
from tkinter import ttk

root=tk.Tk()
root.title("place() 測試")
root.geometry("250x100")

ttk.Button(root, text="絕對定位").place(x=25, y=25)
ttk.Button(root, text="相對定位").place(relx=0.5,rely=0.5)
root.mainloop()




此例相對定位按鈕傳入 (relx=0.5, rely=0.5), 表示其初始左上角座標是視窗寬度與高度的 0.5 倍, 即 250*0.5=125, 100*0.5=50, 因此程式開始執行時相對定位的按鈕左上角位於視窗中央, 而絕對定位的按鈕左上角位於 (50, 50), 縮放視窗時相對定位的按鈕會隨之重新定位, 而絕對定位之按鈕則固定不動.

上例中相對定位之元件僅用 (relx, rely) 來設定相對座標, 還可用參數 (relwidth, relheight) 來控制元件相對於視窗寬度與高度之倍率 (0.0~1.0), 如下例所示 :


測試 17 : 相對定位元件之寬度與高度設定

import tkinter as tk
from tkinter import ttk

root=tk.Tk()
root.title("place() 測試")
root.geometry("250x100")

ttk.Button(root, text="絕對定位").place(x=25, y=25)
ttk.Button(root, text="相對定位").place(relx=0.5, rely=0.5, relwidth=0.5, relheight=0.5)
root.mainloop()




參數 relx=0.5, rely=0.5 表示按鈕的寬度與高度為視窗的一半, 即使視窗縮放相對定位按鈕還是佔據視窗右下角 1/4 空間.

沒有留言 :