1. 程式人生 > >Python Tkinter 會話窗口(轉載自https://blog.csdn.net/bnanoou/article/details/38515083)

Python Tkinter 會話窗口(轉載自https://blog.csdn.net/bnanoou/article/details/38515083)

until 運行 gif extra columns please sso alt date()

Dialog Windows

While the standard dialogs described in the previous section may be sufficient for many simpler applications, most larger applications require more complicated dialogs. For example, to set configuration parameters for an application, you will probably want to let the user enter more than one value or string in each dialog.

前面章節中介紹的標準會話對於大多數簡單應用已經足夠了,對於很多大型應用就需要更復雜的會話。

Basically, creating a dialog window is no different from creating an application window. Just use the Toplevel widget, stuff the necessary entry fields, buttons, and other widgets into it, and let the user take care of the rest. (By the way, don’t use the ApplicationWindow class for this purpose; it will only confuse your users).

基本上,創建一個會話框和創建一個應用窗口是不一樣的。使用Toplevel組件,填充上輸入框、按鈕和其他組件,讓用戶負責其他的。(不要使用ApplicationWindows類,它指揮讓你的用戶更加困擾)

But if you implement dialogs in this way, you may end up getting both your users and yourself into trouble. The standard dialogs all returned only when the user had finished her task and closed the dialog; but if you just display another toplevel window, everything will run in parallel. If you’re not careful, the user may be able to display several copies of the same dialog, and both she and your application will be hopelessly confused.

如果使用這種方式部署會話,可能會讓用戶和開發者都陷入麻煩。標準的會話框會在用戶結束任務並關閉會話框之後返回,但是如果僅僅展示另外一個toplevel窗口,所有的都會並行運行。一不小心就會展示幾個一模一樣的會話框給 用戶,用戶和應用都會陷入不可救藥的混亂。

In many situations, it is more practical to handle dialogs in a synchronous fashion; create the dialog, display it, wait for the user to close the dialog, and then resume execution of your application. The wait_window method is exactly what we need; it enters a local event loop, and doesn’t return until the given window is destroyed (either via the destroy method, or explicitly via the window manager):

很多時候采用同步風格去使用會話框是有效可行的,創建會話、顯示、等待用戶關閉會話,然後重新執行應用。wait_window方法,它會進入一個內部事件循環,直到窗口銷毀才會返回。(既可以通過destroy方法,也可以使用窗口管理器銷毀)

widget.wait_window(window)

(Note that the method waits until the window given as an argument is destroyed; the only reason this is a method is to avoid namespace pollution).

(請註意這個方法在窗口將會一直等待,除非參數windows被賦值為destroyed, 這樣做的是為了避免命名空間被汙染)

In the following example, the MyDialog class creates a Toplevel widget, and adds some widgets to it. The caller then uses wait_window to wait until the dialog is closed. If the user clicks OK, the entry field’s value is printed, and the dialog is then explicitly destroyed.

在下面的例子中,MyDialog類創建了一個Toplevel組件,並添加了一些組件給它。調用者使用wait_window去等待,直到會話被關閉。如果用戶點擊了OK,那麽輸入域的值將會被打印,會話框也會被銷毀。

Creating a simple dialog

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)

If you run this program, you can type something into the entry field, and then click OK, after which the program terminates (note that we didn’t call the mainloop method here; the local event loop handled by wait_window was sufficient). But there are a few problems with this example:

運行這個程序,用戶可以在輸入框中輸入些什麽,然後點擊OK,然後程序結束。(註意這裏沒有調用mainloop方法,wai_window操作的本地事件循環已經足夠了)。這個例子有一些小問題:

  • The root window is still active. You can click on the button in the root window also when the dialog is displayed. If the dialog depends on the current application state, letting the users mess around with the application itself may be disastrous. And just being able to display multiple dialogs (or even multiple copies of one dialog) is a sure way to confuse your users.

  • 首先根窗口依舊被激活。當會話框被顯示的時候依舊可以點擊根窗口上的按鈕。如果會話依賴於當前的應用狀態,讓用戶去操作應用本身將會是災難性的。能夠顯示多個會話(或者一個會話的多個拷貝)會讓用戶困惑

  • You have to explicitly click in the entry field to move the cursor into it, and also click on the OK button. Pressning Enter in the entry field is not sufficient.

  • 只有準確的點擊輸入框才能把光標移到其中,同樣在點擊OK按鈕的時候也需要準確的點擊。

  • There should be some controlled way to cancel the dialog (and as we learned earlier, we really should handle theWM_DELETE_WINDOW protocol too).

  • 需要一些可控的方式去取消會話(就像之前學到的,使用WM_DELETE_WINDOW)

To address the first problem, Tkinter provides a method called grab_set, which makes sure that no mouse or keyboard events are sent to the wrong window.

Tkinter 提供了grab_set方法去解決第一個問題。這個方法可以確保鼠標或者鍵盤事件不會被發送到錯誤的窗口。

The second problem consists of several parts; first, we need to explicitly move the keyboard focus to the dialog. This can be done with the focus_set method. Second, we need to bind the Enter key so it calls the ok method. This is easy, just use the bind method on the Toplevel widget (and make sure to modify the ok method to take an optional argument so it doesn’t choke on the event object).

第二個問題由幾部分組成。首先需要把鍵盤焦點放置到會話框,focuse_set方法可以完成這件事。其次,我們需要把Enter鍵和OK方法映射在一起。很簡單,只要在Toplevel組件上使用bind方法(修改ok方法的參數以確保它不會阻塞事件對象)

The third problem, finally, can be handled by adding an additional Cancel button which calls the destroy method, and also use bind and protocol to do the same when the user presses Escape or explicitly closes the window.

第三個問題可以通過添加一個額外的調用destroy方法的Cancel按鈕來解決,當用戶按下Escape或者通過窗口關閉,用bind和protocol做同樣的操作。

The following Dialog class provides all this, and a few additional tricks. To implement your own dialogs, simply inherit from this class and override the body and apply methods. The former should create the dialog body, the latter is called when the user clicks OK.

下面的Dialog類提供了所有的方法和一些技巧。直接從這個繼承這個類並重載body和apply方法就可以部署自己的會話框。

A dialog support class (File: tkSimpleDialog.py)

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 dont 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

The main trickery is done in the constructor; first, transient is used to associate this window with a parent window (usually the application window from which the dialog was launched). The dialog won’t show up as an icon in the window manager (it won’t appear in the task bar under Windows, for example), and if you iconify the parent window, the dialog will be hidden as well. Next, the constructor creates the dialog body, and then calls grab_set to make the dialog modal, geometry to position the dialog relative to the parent window, focus_set to move the keyboard focus to the appropriate widget (usually the widget returned by the body method), and finally wait_window.

首先transient把當前窗口和一個父窗口關聯起來(通常是會話框發起的應用窗口)。在窗口管理器中會話框不是被顯示為一個圖標,如果圖標化父窗口,那麽會話框也會被隱藏。構造器創建了會話框主體,然後調用grab_set方法生成可視化的會話框。focus_set方法把鍵盤焦點定位到合適的組件(通常是body方法返回的那個組件)最好是wait_window方法。

Note that we use the protocol method to make sure an explicit close is treated as a cancel, and in the buttonbox method, we bind the Enter key to OK, and Escape to Cancel. The default=ACTIVE call marks the OK button as a default button in a platform specific way.

註意我們使用protocol方法去缺乏一個精準關閉動作被當成取消動作,在buttonbox方法中,Entry鍵和Ok綁定在一起,ESCape和Cancel綁定在一起。default=Active表示OK按鈕是默認按鈕。

Using this class is much easier than figuring out how it’s implemented; just create the necessary widgets in the body method, and extract the result and carry out whatever you wish to do in the apply method. Here’s a simple example (we’ll take a closer look at the grid method in a moment).

使用這個類比理解它如何部署要容易得多。在body方法中創建必須的組件,在apply方法中提取結果和執行想做的。這裏有一個簡單例子(近距離研究一下grid方法)

Creating a simple dialog, revisited

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

And here’s the resulting dialog: Running the dialog2.py script

技術分享圖片

Note that the body method may optionally return a widget that should receive focus when the dialog is displayed. If this is not relevant for your dialog, simply return None (or omit the return statement).

請註意body方法可以得到一個得到焦點的組件。如果會話框不需要這一點,只要返回None即可(或者省略掉返回狀態)。

The above example did the actual processing in the apply method (okay, a more realistic example should probably to something with the result, rather than just printing it). But instead of doing the processing in the apply method, you can store the entered data in an instance attribute:

上面的示例使用apply方法完成了流程(一個更加生動的例子應該對結果做一些處理,而不是簡單的打印出來)。不用apply方法完成流程,可以使用一個實例屬性存儲輸入數據:

  ...
 
    def apply(self):
        first = int(self.e1.get())
        second = int(self.e2.get())
        self.result = first, second
 
d = MyDialog(root)
print d.result

Note that if the dialog is cancelled, the apply method is never called, and the result attribute is never set. The Dialog constructor sets this attribute to None, so you can simply test the result before doing any processing of it. If you wish to return data in other attributes, make sure to initialize them in the body method (or simply set result to 1 in the apply method, and test it before accessing the other attributes).

如果會話框被取消,那麽apply方法將不再會被調用,result屬性也不會被設置。Dialog構造器會把這個屬性設置為None,所以你可以在對它做任何處理之前簡單的測試結果。如果你希望在其他屬性裏返回數值,請確保在body方法中對它們做了初始化(在apply方法中把result設置為1,並在訪問其他屬性前對它做測試)。

Grid Layouts

While the pack manager was convenient to use when we designed application windows, it may not be that easy to use for dialogs. A typical dialog may include a number of entry fields and check boxes, with corresponding labels that should be properly aligned. Consider the following simple example:

在設計應用窗口的時候pack管理器是很有效的,但對於設計會話框來說不一定好用。一個典型的會話框可能包含一些排成一排的一致的輸入框或者復選框。參見下面這個簡單例子:

Simple Dialog Layout

技術分享圖片

To implement this using the pack manager, we could create a frame to hold the label “first:”, and the corresponding entry field, and use side=LEFT when packing them. Add a corresponding frame for the next line, and pack the frames and the check button into an outer frame using side=TOP. Unfortunately, packing the labels in this fashion makes it impossible to get the entry fields lined up, and if we use side=RIGHT to pack the entry field instead, things break down if the entry fields have different width. By carefully using width options, padding, side and anchor packer options, etc., we can get reasonable results with some effort. But there’s a much easier way: use the grid manager instead.

想要使用pack管理器去實現這個會話框,需要先創建一個frame去實習標簽“first”和同一排的輸入框,在使用它們的時候設置side=LEFT。 為第二行添加一個一樣的frame,使用side = TOP 把標簽們和復選框pack到一個外部frame裏面。很不幸,以這樣的方式pack這些標簽可能會導致輸入框排成一排,如果使用side=RIGHT去整合輸入框,假如這些輸入框的寬度不一樣的話會導致對不齊。使用width選項,padding、side和anchor packer選項的時候都要小心,需要付出一些努力才能得到想要的結果。但是這裏有更加簡便的辦法:使用grid管理器。

This manager splits the master widget (typically a frame) into a 2-dimensional grid, or table. For each widget, you only have to specify where in this grid it should appear, and the grid managers takes care of the rest. The following body method shows how to get the above layout:

這個管理器把主組件(通常是一個frame)分隔成二維的坐標或表格。對於每一個組件,僅需要指定它應該出現在這個坐標的什麽位置,坐標管理器會處理其他的。下面的body方法展示了如何獲得上面的布局。

Using the grid geometry maanager

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)

For each widget that should be handled by the grid manager, you call the grid method with the row and column options, telling the manager where to put the widget. The topmost row, and the leftmost column, is numbered 0 (this is also the default). Here, the check button is placed beneath the label and entry widgets, and the columnspan option is used to make it occupy more than one cell. Here’s the result:

對於每一個坐標管理器要操作的組件,使用grid方法的row和column選項,告訴管理器把組件放置到什麽位置。最上面的那行的最左邊一列編號為0,復選框被放到標簽和輸入框的下方,使用columnspan選項可以讓它占據一個以上的單元格。這裏是結果:

Using the grid manager

技術分享圖片

If you look carefully, you’ll notice a small difference between this dialog, and the dialog shown by the dialog2.py script. Here, the labels are aligned to the left margin. If you compare the code, you’ll find that the only difference is an option called sticky.

仔細看,會發現這個會話框和dialog2.py展示的會話框有一點小小的不同。這裏標簽是靠左對齊。對比一下代碼,會發現唯一的不同就是sticky選項。

When its time to display the frame widget, the grid geometry manager loops over all widgets, calculating a suitable width for each row, and a suitable height for each column. For any widget where the resulting cell turns out to be larger than the widget, the widget is centered by default. The sticky option is used to modify this behavior. By setting it to one of E, W, S, N,NW, NE, SE, or SW, you can align the widget to any side or corner of the cell. But you can also use this option to stretch the widget if necessary; if you set the option to E+W, the widget will be stretched to occupy the full width of the cell. And if you set it to E+W+N+S (or NW+SE, etc), the widget will be stretched in both directions. In practice, the sticky option replaces the fill, expand, and anchor options used by the pack manager.

當展示frame組件的時候,坐標管理器會在所有組件上循環,為每一行計算出合適的寬度,為每一列計算出合適的高度。對於任意組件,如果單元格比組件大,那麽默認會被居住放置。sticky選項就是用來修改這個行為的。通過把它設置為E,W,S,N,NW,NE,SE,SW中的任意一個,就可以靠邊或者居中放置組件。如果需要的話也可以使用這個選項去平鋪這個組件。如果把這個選項設置為E+W,組件會被拉伸並填充到單元格的寬度。如果設置成E+W+N+S(或者NW+SE等)組件會在所有的方向被拉伸。實際使用時pack管理器會使用fill,expand和anchor選項來代替。

The grid manager provides many other options allowing you to tune the look and behavior of the resulting layout. These include padx and pady which are used to add extra padding to widget cells, and many others. See the Grid Geometry Manager chapter for details.

坐標管理器提供了一些其他的選項去調整布局的外觀和行為。包括用來調整組件單元格填充的padx和pady,其他還有很多。詳細可以查詢Grid Geometry Manager 章節。

Validating Data

What if the user types bogus data into the dialog? In our current example, the apply method will raise an exception if the contents of an entry field is not an integer. We could of course handle this with a try/except and a standard message box:

如果用戶在會話框中輸入非法數據會怎樣?在最近的例子中,如果輸入框中得到的不是整數,apply方法都會拋出異常。當然可以使用try/except和一個標準的消息框來操作。

...
 
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"
        )

There’s a problem with this solution: the ok method has already removed the dialog from the screen when the apply method is called, and it will destroy it as soon as we return. This design is intentional; if we carry out some potentially lengthy processing in the apply method, it would be very confusing if the dialog wasn’t removed before we finished. The Dialog class already contain hooks for another solution: a separate validate method which is called before the dialog is removed.

這種方案會有一個問題:當調用apply方法的時候,ok方法已經從屏幕上移除和銷毀了會話框。這是有意設計的;Dialog類已經提供了另外一種解決方案:一個獨立的validae方法,這個方法會在會話框被移除前調用。

In the following example, we simply moved the code from apply to validate, and changed it to store the result in an instance attribute. This is then used in the apply method to carry out the work.

在下面的例子中,在代碼中用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)

Note that if we left the processing to the calling program (as shown above), we don’t even have to implement the apply method.

Python Tkinter 會話窗口(轉載自https://blog.csdn.net/bnanoou/article/details/38515083)