對話框窗口
雖然上一節中描述的標準對話框對許多簡單的應用程序來說已經夠用了,但是大多數複雜的應用程序需要更復雜的對話框。例如腰圍應用程序設置配置參數,你科寧希望讓用戶在每個對話框中輸入多個值或者字符串。
基本上,創建對話框窗口與創建應用程序窗口沒有區別,只要使用Toplevel組件,在其中填入必要的輸入字段,按鈕和其他組件,並讓用戶處理其餘部分(順便提一下,不要使用ApplicationWindow類來實現這個目的,它會混淆你的客戶)
當然我們使用對話框窗體有一些技巧,不讓會讓自己陷入麻煩。只有當用戶完成任務並關閉對話框時,在返回標準對話框;但是如果你只是顯示另一個頂級窗口,那麼所有窗口都講並行運行,令用戶困惑。
大多數情況下,更加實用的是以同步方式處理對話框:創建對話框,顯示它,等待用戶關閉對話框,然後恢復應用程序的執行。我們可以用wait_windows方法,這個方法進入一個本地事件循環,並且不返回,直到給定的窗口被銷燬(通過destroy方法,或者明確的通過窗口管理器銷燬)
widget.wait_window(window1)
在下面的例子中,MyDialog類創建了一個Toplevel組件,並向其添加了一些組件。然後調用者使用wait_window等待,直到對話框關閉。如果用戶單擊“確定”,將打印輸入字段的值,然後顯示銷燬對話框。
from Tkinter import *
class MyDialog:
def __init__(self, parent):
top = self.top = Toplevel(parent)
Label(top, text="Value").pack()
self.e = Entry(top)
self.e.pack(padx=5)
b = Button(top, text="OK", command=self.ok)
b.pack(pady=5)
def ok(self):
print "value is", self.e.get()
self.top.destroy()
root = Tk()
Button(root, text="Hello!").pack()
root.update()
d = MyDialog(root)
root.wait_window(d.top)
當運行這個程序,你可以在輸入框中輸入一些內容,點擊OK按鈕,接着程序就終止了(注意我們沒有調用mainloop方法,本地的事件循環僅僅靠wait_window就能實現)但這個例子有以下問題:
1.根窗口依然被激活,你可以點擊根窗口的按鈕,而對話框依然顯示,如果對話框依賴當前應用程序的狀態,而用戶亂操作應用程序本身就可能變成災難,顯示多個對話框也會混淆用戶
2.你必須光標移到對話框中的輸入框,還要單擊確定按鈕,比較繁瑣
3.應該有一些受控的方式來取消對話框,比如我們前面學到的wm_delete_window協議
爲了解決問題1.Tkinter提供了一個叫grab_set的方法,它確保不讓鼠標或鍵盤事件發送到錯誤的窗口
第二個問題的解決辦法有幾個部分組成;首先,我們要明確的將鍵盤焦點移動到對話框,可以用focus_set方法來完成。其次我們要綁定Enter鍵,再調用ok方法。
第三個問題,我們可以添加一個額外的取消按鈕來調用destroy方法,當然也可以使用協議來做
以下就是實現這些思想的新的Dialog類
from Tkinter import *
import os
class Dialog(Toplevel):
def __init__(self, parent, title = None):
Toplevel.__init__(self, parent)
self.transient(parent)
if title:
self.title(title)
self.parent = parent
self.result = None
body = Frame(self)
self.initial_focus = self.body(body)
body.pack(padx=5, pady=5)
self.buttonbox()
self.grab_set()
if not self.initial_focus:
self.initial_focus = self
self.protocol("WM_DELETE_WINDOW", self.cancel)
self.geometry("+%d+%d" % (parent.winfo_rootx()+50,
parent.winfo_rooty()+50))
self.initial_focus.focus_set()
self.wait_window(self)
#
# construction hooks
def body(self, master):
# create dialog body. return widget that should have
# initial focus. this method should be overridden
pass
def buttonbox(self):
# add standard button box. override if you don't want the
# standard buttons
box = Frame(self)
w = Button(box, text="OK", width=10, command=self.ok, default=ACTIVE)
w.pack(side=LEFT, padx=5, pady=5)
w = Button(box, text="Cancel", width=10, command=self.cancel)
w.pack(side=LEFT, padx=5, pady=5)
self.bind("<Return>", self.ok)
self.bind("<Escape>", self.cancel)
box.pack()
#
# standard button semantics
def ok(self, event=None):
if not self.validate():
self.initial_focus.focus_set() # put focus back
return
self.withdraw()
self.update_idletasks()
self.apply()
self.cancel()
def cancel(self, event=None):
# put focus back to the parent window
self.parent.focus_set()
self.destroy()
#
# command hooks
def validate(self):
return 1 # override
def apply(self):
pass # override
主要的技巧就在構造函數中。首先使用了 transient使用(瞬態)函數,用於將這個窗口和他的父窗口(通常是啓動對話框的應用程序窗口)相關聯,該對話框不會再窗口管理器中顯示爲圖標(例如,它不會顯示在windows下的任務欄中),如果你對父窗口進行圖標化,該對話框也會被隱藏。接下來狗仔函數創建對話框的body,接着調用grab_set 是對話框模塊化,geometry函數相對父窗口定位對話框,focus_set把鍵盤焦點移動到合適的組件上(通常返回body的方法,如果重寫的話),最後調用wait_window
注意文明使用protocol 方法來確保無論按cancel按鈕還是其他顯式的調用退出都會執行 cancel方法。我們還綁定了回車鍵 到OK回調函數,Escape鍵調用Cancel函數。我們使用default=ACTIVE使得OK 按鈕作爲默認激活的按鈕。
使用這個類比剛纔的解釋要簡單的多,只要在body方法中創建必要的組件,並提取結果,並在apply方法中執行你想要做的操作,這裏有個簡單的例子(稍後我們仔細講講網格法)
import tkSimpleDialog
class MyDialog(tkSimpleDialog.Dialog):
def body(self, master):
Label(master, text="First:").grid(row=0)
Label(master, text="Second:").grid(row=1)
self.e1 = Entry(master)
self.e2 = Entry(master)
self.e1.grid(row=0, column=1)
self.e2.grid(row=1, column=1)
return self.e1 # initial focus
def apply(self):
first = int(self.e1.get())
second = int(self.e2.get())
print first, second # or something
以下是運行樣子。
注意,body方法的返回值是可選的,這個返回值是你希望獲得焦點的那個控件,如果你覺得無所謂,你可以直接返回None或者乾脆不返回
上面那個例子確實在apply方法中做了具體的操作(好吧,應該有比僅僅打印出來更好的操作),但除了操作之外,你應該把從文本框中得到的輸入值保存在實例的屬性中。比如這樣:
...
def apply(self):
first = int(self.e1.get())
second = int(self.e2.get())
self.result = first, second
d = MyDialog(root)
print d.result
注意,如果對話框取消的話,apply方法不會被調用,result屬性也不會被設置。Dialog類的構造函數會把它設置爲None,所以在使用result之前,你可以簡單的測試一下。如果你希望返回其他屬性,確保在body方法中初始化了,或者在apply方法中簡單的把result設置爲1,並且在使用其他屬性之前測試它)
Grid佈局
當我們設計應用程序的時候使用pack 比較方便,但在做這個對話框窗體就不太好用了。一個典型的對話框窗口包含大量的輸入框和檢查框,要對齊比較難。比如下面這個例子
Simple Dialog Layout
如果使用pack管理,我們要做很多很複雜,現在我們有更方便的辦法了grid
Grid把父窗口網格化,每一個控件放在一個格子裏
def body(self, master):
Label(master, text="First:").grid(row=0, sticky=W)
Label(master, text="Second:").grid(row=1, sticky=W)
self.e1 = Entry(master)
self.e2 = Entry(master)
self.e1.grid(row=0, column=1)
self.e2.grid(row=1, column=1)
self.cb = Checkbutton(master, text="Hardcopy")
self.cb.grid(row=2, columnspan=2, sticky=W)
這段代碼使用了grid方法中的row和column參數,告訴管理器在哪個格子裏放控件,最小行和最小列從0開始。在這裏檢查框放置在Label和Entry的下面,columnspan參數表示這個控件佔據了多列。下面就是樣子了:
如果你仔細觀察,你會發現剛纔這一段代碼和那個類中的代碼不同,label文本緊貼左邊,比較代碼你會發現差別就在代碼中的sticky。
當顯示組件時,grid管理器循環遍歷所有組件,計算每行覈實的寬度,以及每列覈實高度,對於單元格大於組件的,默認組件在格子中間,這個sticky選項就能靠某一邊,可以考哪些邊呢?可以靠這些: E,W,S,N,NW,NE, SE或SW中的一個,如果設置爲E+W,那麼就會被橫向拉伸
其他詳細信息看grid 管理器這章
驗證數據
如果用戶的輸入非法怎麼辦,在下面的例子中,如果輸入框寫入的內容不是整形,appley方法將會提交一個異常
...
def apply(self):
try:
first = int(self.e1.get())
second = int(self.e2.get())
dosomething((first, second))
except ValueError:
tkMessageBox.showwarning(
"Bad input",
"Illegal values, please try again"
)
這個解決拌飯有個問題,當apply方法被調用時,ok方法已經關閉了對話框,而且當我們返回時對話框都已經銷燬了。這是故意這麼設計的,爲什麼呢?當我們在apply方法中執行一個比較耗時的任務,那麼當我們完成之前對話框都沒有移除的話,我們就會覺得很奇怪。在這個Dialog勒種其實已經預留下了解決辦法,一個獨立的validate方法,用這個來判斷對話框是否被移除了。
在以下的例子中,我們僅僅把代碼從apply中轉椅到validate中,並且把結果保存在這個實例的屬性中。然後再在apply方法中處理這些工作
...
def validate(self):
try:
first= int(self.e1.get())
second = int(self.e2.get())
self.result = first, second
return 1
except ValueError:
tkMessageBox.showwarning(
"Bad input",
"Illegal values, please try again"
)
return 0
def apply(self):
dosomething(self.result)