2021年10月14日 星期四

Python 內建 GUI 模組 tkinter 測試 (十六) : Menu 元件

前陣子為了還書而放下 tkinter 轉而去看機器學習的書, 停頓了一陣子感覺有點生疏了, 剩下一些元件趕快測試完, 這樣才能全心攻略機器學習. 今天就來繼續測試 Menu (功能選單) 元件唄. 
 
本系列之前的文章參考 :   


Menu 元件 (功能選單) 是 GUI 應用程式上視窗標題列下方的選單列 (Menu bar) 選單, 用來整合應用程式所有功能, 讓使用者下拉點選要執行的選項功能, 例如通常在檔案選單底下會有開新檔案, 開啟舊檔, 另存檔案等功能選項. 




建立 Menu 元件之語法如下 (注意, 此元件僅在 tk 中提供, ttk 中無此元件) : 

menu=Menu(父容器, **參數列)

注意, 此處父容器若為 Tk 視窗物件, 則此 Menu 物件為最上層選單列 (Menu bar); 父容器若為其他 Menu 物件, 則此 Menu 物件為子選單. 選單列的 Menu 物件是頂層選單, 建立後還必須呼叫 Tk 視窗物件的 config() 方法將 menu 參數設為選單列物件, 這樣才會在視窗標題底下才會出現此選單列, 例如 :

win=tk.Tk()  
menubar=tk.Menu(win)                  
win.config(menu=menubar)

注意, 只有頂層的選單列 Menu 物件才需要與視窗物件掛勾. 

參考 :


Menu 元件常用參數如下表 : 


 Menu 元件常用參數 說明
 image 功能選單圖示 (PhotoImage 圖片物件名稱)
 tearoff 功能選單上方分隔線=True (預設有, 可分離)/False (無, 不可分離)
 bd 功能選單邊框寬度 (預設 1px)
 font 字型與尺寸 (px), 粗體 (bold) 或斜體 (italic), 底線或刪除線等


Menu 元件常用方法如下表 : 


 Menu 元件常用方法 說明
 add_command(**options) 加入選項, 參數 label=選項文字, command=呼叫的函式
 add_cascade(**options) 串接父子選單, 參數 label=子選單標籤, menu=子選單物件
 add_separator() 加入分隔線


其中 add_command() 可傳入如下參數 :
  • label : 文字選項
  • command : 所呼叫之函式名稱 (後面不要加括弧)
  • image : 圖片選項
  • bitmap : 小圖示選項)  
最常用的是 label 與 command, 前者顯示選項文字, 後者指定點選此選項時要執行之函式. 而 add_cascade() 則是用來建立一個子選單, 其常用參數如下 :
  • label : 文字選項
  • menu : 父選單名稱
  • image : 圖片選項
其中最重要的是用來指向父選單的 menu 參數. 

下面範例是最簡單的選單做法, 直接把功能選項放在頂層選單列中 : 


測試 1 : 直接在選單列 (Menu bar) 上新增選項 (1) [看原始碼]

import tkinter as tk
from tkinter import messagebox as msgbox

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")
win.geometry("300x200")

def say_hello():
    msgbox.showinfo("Info", "Hello World!")

menubar=tk.Menu(win)       # 建立頂層功能列
menubar.add_command(label="Hello", command=say_hello)     # 新增選項
menubar.add_command(label="Quit", command=win.destroy)   # 新增選項
win.config(menu=menubar)
win.mainloop()

此例在呼叫 tk.Menu() 時傳入 Tk 物件當父容器以建立一個 Menu 物件, 然後呼叫視窗物件 win 的 config() 方法將其 menu 參數指定為此 Menu 物件, 這樣這個 Menu 物件就成為視窗的選單列了. 呼叫此 Menu 物件的 add_command() 方法會將選項依序以水平排列方式加入選單列中, 結果如下 : 




可見若選項不多這樣做最簡單, 但若有很多功能選項, 則功能列很快就會被填滿. 當排列到視窗右邊界時就會折回選單列最左邊並新增一列來放置新的選項, 例如 : 


測試 2 : 直接在選單列 (Menu bar) 上新增選項 (2) [看原始碼]

import tkinter as tk
from tkinter import messagebox as msgbox

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")
win.geometry("300x200")

def say_hello():
    msgbox.showinfo("Info", "Hello World!")

menubar=tk.Menu(win)
menubar.add_command(label="Hello-1", command=say_hello)
menubar.add_command(label="Hello-2", command=say_hello)
menubar.add_command(label="Hello-3", command=say_hello)
menubar.add_command(label="Hello-4", command=say_hello)
menubar.add_command(label="Hello-5", command=say_hello)
menubar.add_command(label="Quit", command=win.destroy)
win.config(menu=menubar)
win.mainloop()

此例在功能列上添加了 5 個 Hello 功能選項, 超出邊界時會折返左邊新增一列來放置, 但將視窗拉寬到能放得下全部選項時又會自動變成一列, 結果如下 :





一般應用程式做法會將選項根據功能放入子選單中收納, 這樣的階層式選單結構可讓使用者快速找到選項. 呼叫父選單物件的 add_cascade() 方法時將子選單物件傳給 menu 參數就可以將子選單與父選單串接在一起了 :

父選單.add_cascade(label="子選單標籤", menu=子選單物件)  

例如可以用子選單概念改寫上面範例, 將其中原本直接放在選單列上的 5 個 Hello 選項收納到 hello_menu 子選單中, 例如 :


測試 3 : 使用子選單收納功能項 [看原始碼]

import tkinter as tk
from tkinter import messagebox as msgbox

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")
win.geometry("300x200")

def say_hello():
    msgbox.showinfo("Info", "Hello World!")

menubar=tk.Menu(win)                  # 建立頂層父選單 (選單列)
hello_menu=tk.Menu(menubar)     # 在選單列下建立一個子選單
hello_menu.add_command(label="Hello-1", command=say_hello)    # 子選單新增選項
hello_menu.add_command(label="Hello-2", command=say_hello)
hello_menu.add_command(label="Hello-3", command=say_hello)
hello_menu.add_command(label="Hello-4", command=say_hello)
hello_menu.add_command(label="Hello-5", command=say_hello)
menubar.add_cascade(label="Hello", menu=hello_menu)          # 將子選單串接到父選單
menubar.add_command(label="Quit", command=win.destroy)    # 選單列新增選項
win.config(menu=menubar)    # 設定視窗的選單列
win.mainloop()

此例建立一個含有 5 個選項的子選單物件 file_menu, 然後呼叫父選單 (menubar) 的 add_cascade() 方法時將子選單物件傳給 menu 參數即可串接父子選單, 結果如下 : 




接層式選單結構可以有很多層, 只要由上而下依序呼叫父選單的 add_cascade() 就可一層一層往下串接, 例如上面的 5 個 Hello 可以分成 a, b 兩群分別放入第二層子選單 :

menubar=tk.Menu(win)
hello_menu=tk.Menu(menubar)
hello_menu_a=tk.Menu(hello_menu)
hello_menu_b=tk.Menu(hello_menu)
hello_menu.add_cascade(label="Hello-a", menu=hello_menu_a)
hello_menu.add_cascade(label="Hello-b", menu=hello_menu_b)
menubar.add_cascade(label="Hello", menu=hello_menu)

完整程式碼如下面範例所示 : 


測試 4 : 串接多層子選單 [看原始碼]

import tkinter as tk
from tkinter import messagebox as msgbox

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")
win.geometry("300x200")

def say_hello():
    msgbox.showinfo("Info", "Hello World!")

menubar=tk.Menu(win)                             # 建立頂層父選單 (選單列)
hello_menu=tk.Menu(menubar)                # 在選單列下建立一個子選單
hello_menu_a=tk.Menu(hello_menu)       # 在子選單 hello_menu 下建立一個孫選單 a
hello_menu_a.add_command(label="Hello-1", command=say_hello)   # 孫選單 a 新增選項
hello_menu_a.add_command(label="Hello-2", command=say_hello)
hello_menu_a.add_command(label="Hello-3", command=say_hello)
hello_menu.add_cascade(label="Hello-a", menu=hello_menu_a)    # 串接子選單與孫選單 a
hello_menu_b=tk.Menu(hello_menu)       # 在子選單 hello_menu 下建立一個孫選單 b
hello_menu_b.add_command(label="Hello-4", command=say_hello)    # 孫選單 b 新增選項
hello_menu_b.add_command(label="Hello-5", command=say_hello)
hello_menu.add_cascade(label="Hello-b", menu=hello_menu_b)    # 串接子選單與孫選單 b
menubar.add_cascade(label="Hello", menu=hello_menu)    # 串接子選單與選單列
menubar.add_command(label="Quit", command=win.destroy)
win.config(menu=menubar)   
win.mainloop()

此例在子選單 hello_menu 下分別建立 hello_menu_a 與 hello_menu_b 兩個孫選單, 將上例中的 5 個 Hello 選項中的 Hello-1 到 Hello-3 放在孫選單 a 內, 將 Hello-4 與 Hello-5 放在孫選單 b 內, 再呼叫 hello_menu 的 add_cascade() 方法先後將孫選單 a 與 b 串接到子選單, 最後呼叫 menubar 的 add_cascade() 將子選單 hello_menu 串接到選單列, 結果如下 :





依此方式分層呼叫父選單的 add_cascade() 方法串接子選單即可建立多層選單. 

上面的所有範例中的選單最上方都有一條虛線, 如果點擊此虛線會將該選單從選單列或其父選單中拆出來, 變成獨立的浮動選單 :





這是因為在呼叫 tk.Menu() 建立選單時 tearoff 參數的預設值為 True (1) 的關係. 如果要禁止選單被扯下來, 可以在建立 Menu 物件時將 tearoff 參數數為 0 或 False (選單列的 Menu 不用設), 例如上面範例的不可分離版如下 : 


測試 5 : 將 tearoff 參數設為 False 使選單不可分離 [看原始碼]

import tkinter as tk
from tkinter import messagebox as msgbox

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")
win.geometry("300x200")

def say_hello():
    msgbox.showinfo("Info", "Hello World!")

menubar=tk.Menu(win)      # 選單列不用設 tearoff
hello_menu=tk.Menu(menubar, tearoff=0)                    # 選單不可分離
hello_menu_a=tk.Menu(hello_menu, tearoff=False)     # 選單不可分離
hello_menu_a.add_command(label="Hello-1", command=say_hello)
hello_menu_a.add_command(label="Hello-2", command=say_hello)
hello_menu_a.add_command(label="Hello-3", command=say_hello)
hello_menu.add_cascade(label="Hello-a", menu=hello_menu_a)
hello_menu_b=tk.Menu(hello_menu, tearoff=0)           # 選單不可分離
hello_menu_b.add_command(label="Hello-4", command=say_hello)
hello_menu_b.add_command(label="Hello-5", command=say_hello)
hello_menu.add_cascade(label="Hello-b", menu=hello_menu_b)
menubar.add_cascade(label="Hello", menu=hello_menu)
menubar.add_command(label="Quit", command=win.destroy)
win.config(menu=menubar)
win.mainloop()

此例除選單列外, 其餘的 Menu 元件以參數 tearoff=0 設定為不可分離, 結果如下 : 





與上例結果相比, 可知選單最上面那條虛線不見了, 因此無法選單分離了. 

選單中選項可以加入分隔線做為區隔, 例如上面範例 3 的 hello_menu 選單有五個 Hello 選項, 可以呼叫選單物件的 add_separator() 方法來加入分隔線, 例如 :


測試 6 : 呼叫 add_separator() 在選單中加入分隔線 [看原始碼]

import tkinter as tk
from tkinter import messagebox as msgbox

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")
win.geometry("300x200")

def say_hello():
    msgbox.showinfo("Info", "Hello World!")

menubar=tk.Menu(win)
hello_menu=tk.Menu(menubar, tearoff=False)
hello_menu.add_command(label="Hello-1", command=say_hello)
hello_menu.add_command(label="Hello-2", command=say_hello)
hello_menu.add_separator()     # 加入分隔線
hello_menu.add_command(label="Hello-3", command=say_hello)
hello_menu.add_command(label="Hello-4", command=say_hello)
hello_menu.add_separator()     # 加入分隔線
hello_menu.add_command(label="Hello-5", command=say_hello)
menubar.add_cascade(label="Hello", menu=hello_menu)
menubar.add_command(label="Quit", command=win.destroy)
win.config(menu=menubar)
win.mainloop()

此例在加入選項過程中利用 add_separator() 插入分隔線, 結果如下 : 




可見這兩條分隔線將選項分成了三群. 

上面為了便於了解 Menu 元件用法使用的都是玩具範例, 下面要結合之前已學過的 MeesageBox,  Text 與 Scrollbar 等元件, 以及 Python 的檔案存取函式來寫一個簡單的文字編輯器, 首先依照上面的範例來建立編輯器的基本架構, 然後再實作細部功能 :


測試 7 : 簡單的 Tkinter 文字編輯器 (1) [看原始碼]

import tkinter as tk
from tkinter import messagebox as msgbox

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")
win.geometry("300x200")

def new_file():
    msgbox.showinfo("Info", "開新檔案")
    
def old_file():
    msgbox.showinfo("Info", "開啟舊檔")    

def save_file():
    msgbox.showinfo("Info", "儲存檔案")

def about():
    msgbox.showinfo("Info", "這是 tkinter 測試")

def version():
    msgbox.showinfo("Info", "v 1.0.0")

menubar=tk.Menu(win)
file_menu=tk.Menu(menubar, tearoff=False)
file_menu.add_command(label="開新檔案", command=new_file)
file_menu.add_command(label="開啟舊檔", command=old_file)
file_menu.add_command(label="儲存檔案", command=save_file)
file_menu.add_separator()
file_menu.add_command(label="關閉", command=win.destroy)
menubar.add_cascade(label="檔案", menu=file_menu)
help_menu=tk.Menu(menubar, tearoff=False)
help_menu.add_command(label="版本", command=version)
help_menu.add_command(label="關於", command=about)
menubar.add_cascade(label="幫助", menu=help_menu)
win.config(menu=menubar)
win.mainloop()

結果如下 :





OK, 架構弄好了就可以進行細部功能的實作了, 下面範例參考之前的文章 :



測試 8 : 簡單的 Tkinter 文字編輯器 (2) [看原始碼]

import tkinter as tk
from tkinter import messagebox as msgbox
from tkinter.filedialog import askopenfilename, asksaveasfile

win=tk.Tk()                                                      
win.title("Simple Text Editor")
win.geometry("500x400")

def new_file():
    yes=msgbox.askyesno("確認", "要儲存目前的內容嗎?")  # 確認是否要儲存舊內容
    if yes:                              # 按是傳回 True
        save_file()                  # 呼叫 save_file() 先儲存編輯器目前的內容
    text.delete(1.0, "end")    # 按否直接清除 Text 元件內容
        
def old_file():
    file=askopenfilename(filetypes=(('text files', 'txt'),))    # 只能開啟 .txt 檔
    with open(file, "r", encoding="utf-8") as f:                  # 必須指定編碼為 utf-8
        text.insert("insert", f.read())                                      # 讀取檔案內容插入 Text

def save_file():
    f=asksaveasfile(mode='w', defaultextension=".txt")    # 開啟檔案儲存視窗
    if f is None:                               # 按取消傳回 None
        return
    f.write(text.get(1.0, "end"))      # 將 Text 全部內容存入檔案
    f.close() 

def quit():
    yes=msgbox.askyesno("確認", "要儲存目前的內容嗎?")
    if yes:                   # 按是傳回 True
        save_file()        # 呼叫 save_file() 先儲存編輯器目前的內容
    win.destroy()        # 關閉視窗

def about():
    msgbox.showinfo("關於此軟體", "Simple Text Editor")

def version():
    msgbox.showinfo("版本", "v 1.0.0")

# add menu 
menubar=tk.Menu(win)
win.config(menu=menubar)
file_menu=tk.Menu(menubar, tearoff=False)
file_menu.add_command(label="開新檔案", command=new_file)
file_menu.add_command(label="開啟舊檔", command=old_file)
file_menu.add_command(label="儲存檔案", command=save_file)
file_menu.add_separator()
file_menu.add_command(label="關閉", command=quit)
menubar.add_cascade(label="檔案", menu=file_menu)
help_menu=tk.Menu(menubar, tearoff=False)
help_menu.add_command(label="版本", command=version)
help_menu.add_command(label="關於", command=about)
menubar.add_cascade(label="幫助", menu=help_menu)
# add editor
scrollbar=tk.Scrollbar(win)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)    
text=tk.Text(win, height=10, bg="aqua")
text.pack(side=tk.LEFT, fill=tk.Y)
text.config(padx=5, pady=5, font="Helvetic 12")  
text.config(yscrollcommand=scrollbar.set)
scrollbar.config(command=text.yview)          
win.mainloop()

由於是文字編輯器, 所以在呼叫 askopenfilename() 選取檔案時利用 filetypes 參數過濾只能挑選 .txt 檔, 呼叫 asksaveasfile() 開啟檔案儲存視窗時也是用 defaultextension 參數預設存為 .txt 檔. 另外要注意的是用 open() 開啟舊檔時必須用 encoding 參數指定為 utf-8 編碼, 否則會出現 "nicodeDecodeError: 'cp950' codec can't decode" 錯誤訊息, 參考 :


此例因為要處理目前編輯器內容要不要存檔問題, 故在點選 "關閉" 選項時不直接呼叫 win.destroy(), 而是呼叫自訂函式 quit(), 結果如下 :











嗯, 雖然很陽春, 但能 Work. 

接下來測試 Menu 物件的 post() 方法, 它會在指定的是窗位置顯示選單, 可以用來製作浮動選單, 與上面範例不同的是, 這種選單不是固定掛在選單列上, 而是在視窗中按下滑鼠右鍵才顯示在游標所在的位置, 所以不可以呼叫  win.config(menu=menubar), 而是要呼叫視窗物件的 bind() 函式去綁定按下滑鼠右鍵時的動作, 也就是呼叫 Menu 物件的 post() 函式以顯示此選單 : 

def popup(e):
    menubar.post(e.x_root, e.y_root)

win.bind("<Button-3>", popup)

此處 bind() 的第一參數  "<Button-3>" 為按下滑鼠右鍵之事件, 而 popup() 為事件處理函式, 在此函式中需呼叫頂層 Menu 物件的 post() 方法來顯示整個選單. 注意, bind() 函式的第二參數只要填入事件處理函式名稱即可, 不可加小括號, 因為加小括號是此列程式執行時立刻呼叫, 但我們要的是等使用者操作時才呼叫. 例如 :


測試 9 : 呼叫 post() 顯示浮動選單 [看原始碼]

import tkinter as tk
from tkinter import messagebox as msgbox

win=tk.Tk()                                                      
win.title("tkinter GUI 測試")
win.geometry("400x300")

def say_hello():
    msgbox.showinfo("Info", "Hello World!")

def popup(e):
    menubar.post(e.x_root, e.y_root)    # 在按鈕事件發生處顯示選單
    
menubar=tk.Menu(win, tearoff=0)
hello_menu=tk.Menu(menubar, tearoff=0)
hello_menu.add_command(label="Hello-1", command=say_hello)
hello_menu.add_command(label="Hello-2", command=say_hello)
hello_menu.add_command(label="Hello-3", command=say_hello)
hello_menu.add_command(label="Hello-4", command=say_hello)
hello_menu.add_command(label="Hello-5", command=say_hello)
menubar.add_cascade(label="Hello", menu=hello_menu)
menubar.add_command(label="Quit", command=win.destroy)
win.bind("<Button-3>", popup)     # 將按下滑鼠右鍵的事件與 popup 函式綁定
win.mainloop()

此例基本架構與上面測試 3 一樣, 差別在於不將頂層 Menu 物件指定給視窗物件的 menu 參數, 取而代之的是呼叫視窗物件的 bind() 方法, 將按下滑鼠右鍵之事件與 popup() 函式綁定, 在此 popup() 函式中呼叫頂層 Menu 物件的 post() 浮動顯示選單. 注意, 這個自訂函式會被傳入一個事件物件 e, 我們必須將代表滑鼠座標的 e.x_root 與 e.y_root 傳入 post() 以便函式能在滑鼠所在位置顯示此選單. 結果如下 : 




可見在視窗的任何地方按下滑鼠右鍵, 就會在那個座標點顯示選單, 在空白處按滑鼠左鍵選單就會自動消失. 

參考 :


沒有留言:

張貼留言