Python 提供了大量的 GUI 庫,可用於創建功能豐富的圖形用戶界面。這些 GUI 庫大部分是第三方提供的。可選擇熟悉的 GUI 庫或者直接選擇 Python 內置的 Tkinter 庫開發圖形界面程序。
圖形用戶組件以一種“搭積木”的方式組織在一起,成為實際可用的圖形用戶界面。圖形用戶界面還需要與用戶交互操作,還應該為程序提供事件處理,事件處理負責讓程序響應用戶動態。
一、 Python 的 GUI 庫
圖形用戶界面(Graphics User Interface),簡稱 GUI。
Python 的圖形用戶界面庫有下面這些:
(1)、PyGObject:PyGObject 庫為基於 GObject 的 C 函數庫提供了內省綁定,這些庫可以支持 GTK+3 圖形界面工具集,因此 PyGObject 提供了豐富的圖形界面組件。
(2)、PyGTK:PyGTK 基於老版本的 GTK+2 的庫提供綁定,借助於底層 GTK+2 所提供的各種可視化元素和組件,同樣可以開發出在 GNOME 桌面系統上運行的軟件,因此它主要適用於 Linux/Unix 系統。PyGTK 對 GTK+2 的 C 語言進行了簡單封裝,提供了面向對象的編程接口。參考網址 https://wiki.python.org/moin/PyGtk。
(3)、PyQt:PyQt 是 Python 編程語言和 Qt 庫的成功融合。Qt 本身是一個擴展的 C++ GUI 應用開發框架,Qt 可以在 UNIX、Windows 和 Mac OS X 上完美運行,因此 PyQt 是建立在 Qt 基礎上的 Python 包裝。所以,PyQt 也能跨平台使用。
(4)、PySide:PySide 是由 Nokia 提供的對 Qt 工具集的新的包裝庫,其成熟度不如 PyQt。
(5)、wxPython:wxPython 是一個跨平台的 GUI 工具集,wxPython 以流行的 wxWidgets(原名 wxWindowss)為基礎,提供了良好的跨平台外觀。wxPython 在 Windows 上調用 Windows 的本地組件、在 MacOS 上調用 Mac OS X 的本地組件、在 Linux 上調用 Linux 的本地組件,這樣可以讓 GUI 程序在不同的平台上顯示平台對應的風格。wxPython 是一個非常流行的跨平台的 GUI 庫。官方 網址是 http://www.wxpython.org。
可根據需要選擇上面的 Python GUI 庫做圖形用戶界面開發。如果需要開發跨平台的圖形用戶界面,則推薦使用 PyQt 或 wxPython。
這里使用的 GUI 庫 Tkinter,它是 Python 自帶的 GUI 庫,直接導入 tkinter 包即可使用。
二、Tkinter GUI 編程的組件
可將一個窗口看成可被分解的一個空的容器,容器里裝了大量的基本組件,通過設置這些基本組件的大小、位置等屬性,就可以將該空的容器和基本組件組成一個整體的窗口。所以,可將圖形界面編程看作是拼圖游戲,創建圖形用戶界面的過程就是完成拼圖的過程。
學習 GUI 編程的總體步驟可分為下面三步:
(1)、了解 GUI 庫大致包含哪些組件,就相當於熟悉每個積木塊到底是些什么東西。
(2)、掌握容器及容器對組件進行布局的方法。
(3)、逐個掌握各組件的用法,則相當於深入掌握每個積木塊的功能和用法。
由於 Tkinter 庫包含的 GUI 組件較多,且各組件之間存在錯綜復雜的繼承關系。需要先通過類圖來了解各 GUI 組件,以及它們之間的關系。Tkinter 的 GUI 組件之間的繼承關系如圖一所示。
圖一 Tkinter 的 GUI 組件之間的繼承關系
從上圖可以看到,Tkinter 的 GUI 組件有兩個根父類,它們都直接繼承了 object 類。
(1)、Misc:它是所有組件的根父類。
(2)、Wm:它主要提供了一些與窗口管理器通信的功能函數。
對於 Misc 和 Wm 兩個基類而言,GUI 編程並不需要直接使用它們,但由於它們是所有 GUI 組件的父類,因此 GUI 組件都可以直接使用它們的方法。
Misc 和 Wm 派生了一個子類:Tk,它代表應用程序的主窗口。因此,所有 Tkinter GUI 編程通常都需要直接或間接使用該窗口類。
BaseWidget 是所有組件的基類,它還派生了一個子類:Widget。Widget 代表一個通用的 GUI 組件。Tkinter 所有的 GUI 組件都是Widget 的子類。
Widget 的父類有四個,除 BaseWidget 之外,還有 Pack、Place 和 Grid,這三個父類都是布局管理器,它們負責管理所包含的組件的大小和位置。
剩下的是圖一右邊的 Widget 的子類,這些都是 Tkinter GUI 編程的各種 UI 組件,也就是“積木塊”。下面是 GUI 組件功能的簡單介紹。
Tkinter類 | 名稱 | 簡介 |
---|---|---|
Toplevel | 頂層 | 容器類,可用於為其他組件提供單獨的容器;Toplevel有點類似於窗口 |
Button | 按鈕 | 代表按鈕組件 |
Canvas | 畫布 | 提供繪圖功能,包括繪制直線、矩形、橢圓、多邊形、位圖等 |
Checkbutton | 復選框 | 可供用戶勾選的復選框 |
Entry | 單行輸入框 | 用戶可輸入內容 |
Frame | 容器 | 用於裝載其他GUI組件 |
Label | 標簽 | 用於顯示不可編輯的文本或圖標 |
LabelFrame | 容器 | 也是容器組件,類似於Frame,但它支持添加標題 |
Listbox | 列表框 | 列出多個選項,供用戶選擇 |
Menu | 菜單 | 菜單組件 |
Menubutton | 菜單按鈕 | 用來包含菜單的按鈕(包括下拉式、層疊式等) |
OptionMenu | 菜單按鈕 | Menubutton的子類,也代表菜單按鈕,可通過按鈕打開一個菜單 |
Message | 消息框 | 類似於標簽,但可以顯示多行文本;后來當當Label也能顯示多行文本后,該組件基本不會用到 |
Radiobutton | 單選按鈕 | 可供用戶點選的單選按鈕 |
Scale | 滑動條 | 拖動滑塊可設定起始值和結束值,可顯示當前位置的精確值 |
Spinbox | 微調選擇器 | 用戶可通過該組件的向上、向下箭頭選擇不同的值 |
Scrollbar | 滾動條 | 用於為組件(文本域、畫布、列表框、文本框)提供滾動功能 |
Text | 多行文本框 | 顯示多行文本 |
下面來創建一個簡單的圖形窗口,代碼如下:
# 在 Python 2.x 版本中使用下面這行導入 # from Tkinter import * # 在 Python 3.x 版本中使用下這行 from tkinter import * # 創建 Tk 對象,Tk 代表窗口 root = Tk() # 設置窗口標題 root.title('窗口標題') # 創建 Label 對象,第一個參數指定將該 Label 放入 root內 w = Label(root, text='hello tkinter!') # 調用 pack 進行布局 w.pack() # 啟動主窗口 root.mainloop()
上面代碼中創建了兩個對象:Tk 和 Label。其中 Tk 代表頂級窗口,Label 代表一個簡單的文本標簽,因此需要指定將該 Label 放在哪個容器內。在這里創建 Label 時第一個參數指定了 root,表明該 Label 要放入 root 窗口內。運行結果省略。
此外,還有一種方式是不直接使用 Tk,只要創建 Frame 的子類,它的子類就會自動創建 Tk對象作為窗口。示例如下:
from tkinter import * # 定義一個繼承 Frame 類的 Application 類 class Application(Frame): def __init__(self, master=None): Frame.__init__(self, master) self.pack() # 布局 # 調用 initWidgets() 方法初始化界面 self.initWidgets() def initWidgets(self): # 創建 Label 對象,第一個參數指定將 Label 放和 root 內 w = Label(self) # 創建一個位圖 bm = PhotoImage(file = 'baidu.png') # 必須用一個不會被釋放的變量引用該圖片,否則該圖片會被回收 w.x = bm # 設置顯示的圖片是 bm w['image'] = bm w.pack() # 創建 Button 對象,第一個參數指定將該 Button 放入 root 內 okButton = Button(self, text="確定") okButton['background'] = 'yellow' # okButton.configure(background='yellow') # 這行與上面代碼作用相同 okButton.pack() # 創建 Application 對象 app = Application() # Frame 有一個默認的 master 屬性,該屬性值是 Tk 對象(窗口) print(type(app.master)) # 通過 master 屬性來設置窗口標題 app.master.title("百度一下,啥都知到") # 啟動主窗口的消息循環 app.mainloop()
上面代碼中首先創建了 Frame 的子類 Application,並在該類的構造方法中調用了自定義方法 initWidgets() 方法,這個方法名可以任意取,接下來在實例方法中 initWidgets() 創建了兩個組件,即 Label 和 Button。
在上面代碼中只是創建了 Application 的實例(Frame 容器的子類),並未創建 Tk 對象(窗口),但是運行這段代碼仍然是有窗口的。如果程序在創建任意 Widget 組件(甚至 Button)時沒有指定 master 屬性(即創建 Widget 組件時第一個參數傳入 None)那么程序會自動為該 Widget 組件創建一個 Tk 窗口,因此 Python 會自動為 Application 實例創建 Tk 對象來作為它的 master。
該段代碼與上一段代碼的區別在於:這段代碼創建 Label 和 Button 后,對 Label 進行了配置,設置了 Label 顯示的背景圖片;同時也對 Button 進行了配置,設置了 Button 的背景色。
另外,代碼中的 w.x = bm 行是增加對圖片對象的引用,防止 initWidgets() 方法結束時,阻止系統回收 PhotoImage 的圖片。運上面的代碼,在終端上的輸出是:<class 'tkinter.Tk'>,以及輸出如下圖二所示的效果。
圖二 配置 Label和Button
上面這段代碼中的 initWidgets() 方法的代碼,實際上只有3行代碼:
(1)、創建GUI組件。相當於創建 “積木塊”。
(2)、添加GUI組件,這里使用 pack() 方法添加。相當於把 “積木塊” 添加進去。
(3)、配置GUI組件。
其中創建GUI 組件與創建其他 Python 對象沒有什么區別,但通常至少要指定一個參數,用於設置該 GUI 組件屬於哪個容器(Tk組件例外,因為該組件代表頂級窗口)。
配置 GUI 組件的兩個時機:
(1)、在創建 GUI 組件時以關鍵字參數的方式配置。例如 Button(self, text="確定"),其中 text="確定" 就指定了該按鈕上的文本是 “確定”。
(2)、在創建完成之后,以字典語法進行配置。例如 okButton['background']='yellow',這種語法使得 okButton 看上去就像一個字典,它用於配置 okButton 的 background 屬性,從而改變該按鈕的背景色。
上面兩種方式是可以互換的。比如在創建按鈕之后配置該按鈕上的文本,代碼如下:
okButton['text'] = '確定'
這行代碼其實是調用 configure() 方法的簡化寫法。也就是說,這行代碼等同於如下代碼:
okButton.configure(text='確定')
也可以在創建按鈕時就配置它的文本和背景色,代碼如下:
# 創建 Button 對象時,就配置它的文本和背景色 okButton = Button(self,text="確定",background="yellow")
除可配置 background、image 等選項外,GUI 組件還可配置其它的選項,具體選項有哪些,可查看該組件的構造方法的幫助文檔。例如要查看 Button 的構造方法的幫助文檔,方法如下:
>>> import tkinter >>> help(tkinter.Button.__init__) Help on function __init__ in module tkinter: __init__(self, master=None, cnf={}, **kw) Construct a button widget with the parent MASTER. STANDARD OPTIONS activebackground, activeforeground, anchor, background, bitmap, borderwidth, cursor, disabledforeground, font, foreground highlightbackground, highlightcolor, highlightthickness, image, justify, padx, pady, relief, repeatdelay, repeatinterval, takefocus, text, textvariable, underline, wraplength WIDGET-SPECIFIC OPTIONS command, compound, default, height, overrelief, state, width
從上面幫助文檔可知,Button 支持兩組選項:標准選項(STANDARD OPTIONS)和組件特定選項(WIDGET-SPECIFIC OPTIONS)。關於這些選項的含義,可以從名字上略知一二。GUI 組件的常見選項的含義如下圖三所示,圖中的選項是大部分GUI組件都支持的。
圖三 GUI組件支持的通用選項
三、布局管理器
布局管理器是負責管理各組件的大小和位置的。此外,當用戶調整窗口大小后,布局管理器還會自動調整窗口中各組件的大小和位置。
1、 Pack 布局管理器
使用 Pack 布局管理時,當向容器中添加組件時,這些組件會依次向后排名,排列方向可以是水平的,也可以是垂直的。Pack 布局的用法示例如下,下面這段代碼向窗口中添加三個 Label 組件。
from tkinter import * # 創建窗口並設置窗口標題 root = Tk() root.title('Pack布局') for i in range(3): lab = Label(root, text="第%d個Label" % (i + 1), bg='#eee') # 調用 pack 進行布局 lab.pack() # 啟動主窗口的消息循環 root.mainloop()
上面代碼中,首先創建了一個窗口,接着循環創建三個 Label,並對這三個 Label 使用 pack() 方法進行默認的 Pack 布局。運行這段代碼,得到圖四所示的界面。
圖四 使用Pack布局
在使用 Pack 布局時,調用的 pack() 方法還可以傳入多個選項,關於 pack() 方法的選項,可通過下面的方式查看:
>>> help(tkinter.Label.pack) Help on function pack_configure in module tkinter: pack_configure(self, cnf={}, **kw) Pack a widget in the parent widget. Use as options: after=widget - pack it after you have packed widget anchor=NSEW (or subset) - position widget according to given direction before=widget - pack it before you will pack widget expand=bool - expand widget if parent size grows fill=NONE or X or Y or BOTH - fill widget if widget grows in=master - use master to contain this widget in_=master - see 'in' option description ipadx=amount - add internal padding in x direction ipady=amount - add internal padding in y direction padx=amount - add padding in x direction pady=amount - add padding in y direction side=TOP or BOTTOM or LEFT or RIGHT - where to add this widget.
從上面輸出信息可知,pack() 方法支持的選項有:
(1)、anchor:當可用空間大於組件所需求的大小時,該選項決定組件被放置在容器的何處。支持的選項有:N(北,代表上)、E(東,代表右)、S(南,代表下)、W(西,代表左)、NW(西北,代表左上)、NE(東北,右上)、SW(西南,左下)、SE(東南,右下)、CENTER(中,默認值)等這些值。
(2)、expand:該 bool 值指定當父容器增大時是否拉伸組件。
(3)、fill:設置組件是否沿水平或垂直方向填充。支持四個值:NONE、X、Y、BOTH,其中 NONE 表示不填充,BOTH 表示沿着兩個方向填充。
(3)、ipadx:指定組件在 x 方向(水平)上的內部留白(padding)。
(4)、ipady:指定組件在 y 方向(垂直)上的內部留白(padding)。
(5)、padx:指定組件在 x 方向(水平)上與其他組件的間距。
(6)、pady:指定組件在 y 方向(垂直)上與其他組件的間距。
(7)、設置組件的添加位置,可設置為 TOP、BOTTOM、LEFT 或 RIGHT 四個值的其中之一。
當做出來的界面比較復雜時,就需要使用多個容器(Frame)分開布局,然后再將 Frame 添加到窗口中。示例如下:
from tkinter import * class App: def __init__(self, master): self.master = master self.initWidgets() def initWidgets(self): # 創建第一個容器 fm1 = Frame(self.master) # 該容器放在左邊 fm1.pack(side=LEFT, fill=BOTH, expand=YES) # 向 fm1 中添加三個按鈕 # 設置按鈕從頂部開始排列,且按鈕只能在水平(X)方向上填充 Button(fm1, text='第一個').pack(side=TOP, fill=X, expand=YES) Button(fm1, text='第二個').pack(side=TOP, fill=X, expand=YES) Button(fm1, text='第三個').pack(side=TOP, fill=X, expand=YES) # 創造第二個容器 fm2 = Frame(self.master) # 該容器放在左邊排列,就會挨着 fm1 fm2.pack(side=LEFT, padx=10, expand=YES) # 向 fm2 中添加三個按鈕 # 設置按鈕從右邊開始排列 Button(fm2, text='第一個').pack(side=RIGHT, fill=Y, expand=YES) Button(fm2, text='第二個').pack(side=RIGHT, fill=Y, expand=YES) Button(fm2, text='第三個').pack(side=RIGHT, fill=Y, expand=YES) # 創建第三個容器 fm3 = Frame(self.master) # 該容器放在右邊排列,就公挨着 fm1 fm3.pack(side=RIGHT, padx=10, fill=BOTH, expand=YES) # 向 fm3 中添加三個按鈕 # 設置按鈕從底部開始排列,且按鈕只能在垂直(Y)方向上填充 Button(fm3, text='第一個').pack(side=BOTTOM, fill=Y, expand=YES) Button(fm3, text='第二個').pack(side=BOTTOM, fill=Y, expand=YES) Button(fm3, text='第三個').pack(side=BOTTOM, fill=Y, expand=YES) root = Tk() # 創建頂層窗口 root.title('Pack布局') display = App(root) root.mainloop()
在上面代碼中,創建了三個 Frame 容器,其中第一個 Frame 容器內包含三個從頂部(TOP)開始排列的按鈕,這意味着這三個按鈕會從上到下依次排列,且這三個按鈕能在水平(X)方向上填充;第二個 Frame 容器內包含三個從右邊(RIGHT)開始排列的按鈕,這意味着這三個按鈕會從右向左依次排列;第三個 Frame 容器內包含三個從底部(BOTTOM)開始排列的按鈕,這意味着這三個按鈕會從下到上依次排列,且這個三個按鈕能在重復(Y)方向上填充。運行結果,如圖五所示。
圖五 復雜的 Pack 布局
從圖五可以看到,fm1 的三個按鈕是從上到下,並且可以在水平方向上填充;fm3 的三個按鈕是從下到上,並且可以在垂直方向上填充。但是 fm2 的三個按鈕雖然設置了 “fill=Y, expand=YES”,但是卻不能在垂直方向上填充,在創建 fm2 這個容器時的代碼是:
fm2.pack(side=LEFT, padx=10, expand=YES)
這說明 fm2 本身不在任何方向上填充,因此 fm2 內的三個按鈕都不能填充。如果希望 fm2 空的三個按鈕也能在垂直方向上填充,可將 fm2 的 pack() 方法改為如下代碼:
fm2.pack(side=LEFT, padx=10, fill=BOTH, expand=YES)
由上可知,Pack 布局非常的靈活,它完全可以實現復雜的用戶界面。通常對於復雜、古怪的界面,大多是可以分解為水平排列和垂直排列,Pack 布局可以實現水平排列和垂直排列,可將多個容器進行組合,進而開發出更復雜的界面。
使用 Pack 布局進行界面開發,首先要做的事情是將程序界面進行分解,分解成水平排列的容器和垂直排列的容器,有時候還要容器嵌套容器,然后使用多個 Pack 布局的容器將它們組合在一起。
2、Grid 布局管理器
Tkinter 后來引入的 Grid 布局簡單易用,管理組件也很方便。Grid 把組件空間分解成一個網格進行維護,按照行、列的方式排列組件,組件位置由其所在的行號和列號決定。行號相同而列號不同的幾個組件會被依次上下排列,列號相同而行號不同的幾個組件會依次左右排列。
在多數場景下,Grid 是最好用的布局方式。Grid 布局的過程就是為各個組件指定行號和列號的過程,不需要為每個網格都指定大小,Grid 布局會自動為它們設置合適的大小。
容器調用組件的 grid() 方法就進行 Grid 布局,在調用 grid() 方法時可傳入多個選項,該方法支持的 ipadx、ipady、padx、pady 與pack() 方法的這些選項相同。grid() 方法還額外增加了下面這些方法:
(1)、column:指定將組件放入哪列。第一列的索引為0.
(2)、columnspan:指定組件橫跨多少列。
(3)、row:指定組件放入哪行。第一行的索引為0.
(4)、rowspan:指定組件橫跨多少行。 (5)、sticky:有點類似於 pack() 方法的 anchor 選項,同樣支持 N(北,代表上)、E(東,代表右)、S(南,代表下)、W(西,代表左)、NW(西北,代表左上)、NE(東北,右上)、SW(西南,左下)、SE(東南,右下)、CENTER(中,默認值)等這些值
下面代碼使用 Grid 布局來實現一個計算器界面。
from tkinter import * class App: def __init__(self, master): self.master = master self.initWidgets() def initWidgets(self): # 創建一個輸入組件 e = Entry(relief=SUNKEN, font=('Courier New', 24), width=25) # 對該輸入組件使用 pack 布局,放在容器(或者窗口)頂部 e.pack(side=TOP, pady=10) p = Frame(self.master) p.pack(side=TOP) # 定義字符串元組 names = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '-', '*', '/', '.', '=') # 遍歷字符串元組 for i in range(len(names)): # 創建 Button,將 Button 組件放入 p 容器中 b = Button(p, text=names[i], font=('Verdana', 20), width=6) b.grid(row=i // 4, column=i % 4) root = Tk() root.title("Grid布局") App(root) root.mainloop()
上面代碼中使用了兩個布局管理器進行嵌套,先使用 Pack 布局管理兩個組件:Entry(輸入組件)和 Frame(容器),這兩個組件會按照從上到下的方式排列。接下來使用 Grid 布局管理 Frame 容器中的 16 個按鈕,分別將 16 個按鈕放入不同的行、不同的列。運行代碼可看到如圖六所示的界面。
圖六 使用Grid布局實現計數器界面
3、Place 布局管理器
Place 布局也叫“絕對布局”,要求程序員顯式指定每個組件的絕對位置相對於其他組件的位置。要使用 Place 布局,只要調用相應組件的 place() 方法即可。該方法支持的選項如下:
(1)、x:指定組件的 X 坐標。x 為0代表位於最左邊。
(2)、y:指定組件的 Y 坐標。y 為0代表位於最右邊。
(3)、relx:指定組件的 X 坐標,以父容器總寬度為單位 1,該值應該在 0.0~1.0之間,其中0.0位於窗口最左邊,1.0位於窗口最右邊,0.5 位於窗口中間。
(4)、rely:指定組件的 Y 坐標,以父容器總高度為 1,該值應該在 0.0~1.0 之間,其中 0.0位於窗口最上邊,1.0 位於窗口最下邊,0.5位於窗口中間。
(5)、width:指定組件的寬度,以 pixel 為單位。
(6)、height:指定組件的高度,以 pixel 為單位。
(7)、relwidth:指定組件的寬度,以父容器總寬度為單位 1,該值應該在 0.0~1.0 之間,其中 1.0 代表整個窗口寬度,0.5代表窗口的一半寬度。
(8)、relheight:指定組件的高度,以父容器總高度為單位 1,該值應該在 0.1~1.0 之間,其中 1.0 代表整個窗口高度,0.5 代表窗口的一半高度。
(9)、bordermode:該屬性支持 “inside”或“outside” 屬性值,用於指定當設置組件的寬度、高度時是否計算該組件的邊框寬度。
當使用 Place 布局管理容器中的組件時,需要設置組件的 x、y 或 relx、rely 選項,Tkinter 容器內的坐標系統的原點(0, 0)在左上角,其中 X 軸向右延伸,Y 軸向下延伸。如果通過 x、y 指定坐標,單位就是 pixel(像素);如果通過 relx、rely 指定坐標,則以整個父容器的寬度、高度為1。下面代碼使用 Place 進行布局,並動態計算各 Label 的大小和位置,並通過 place() 方法設置各 Label 的大小和位置。代碼如下:
from tkinter import * import random class App: def __init__(self, master): self.master = master self.initWidgets() def initWidgets(self): # 定義字符串元組 books = ('Python 入門', 'Python 初級', 'Python 進階', 'Python 高級', 'Python 核心') for i in range(len(books)): # 生成三個隨機數 ct = [random.randrange(256) for _ in range(3)] grayness = int(round(0.299*ct[0] + 0.587*ct[1] + 0.114*ct[2])) # 將元組中的三個隨機數格式化成十六進制數,轉換成顏色格式 bg_color = "#%02x%02x%02x" % tuple(ct) # 創建 Label,設置背景色和前景色 lb = Label(root, text=books[i], fg='White' if grayness < 125 else 'Black', bg=bg_color) # 使用 place() 設置該 Label 的大小和位置 lb.place(x = 20, y = 36 + i*36, width=180, height=30) root = Tk() root.title('Place 布局') # 設置窗口的大小和位置 # width * height + x_offset + y_offset root.geometry("250x250+30+30") App(root) root.mainloop()
上面代碼中的 lb.place() 行代碼調用了 Label 的 place() 方法進行布局。調用 place() 方法時設置了x坐標、y坐標、width寬度、height高度四個選項,通過這四個選項即可控制各 Label 的位置和大小。
在代碼中使用了隨機數計算 Label 組件的背景色,並根據背景色的灰度值來計算 Label 組件的前景色。如果 grayness 小於 125,則說明背景色較深,前景色使用白色;否則說明背景色較淺,前景色使用黑色。運行這段代碼看到如七所示的界面。
圖七 使用Place布局
四、 事件處理
前面設置的各種組件中還不能響應任何操作,這是因為這些組件沒有綁定任何事件處理的緣故。
1、 簡單的事件處理
簡單的事件處理可通過 command 選項來綁定,該選項綁定一個函數或方法,當用戶單擊指定按鈕時,通過該 command 選項綁定的函數或方法就會被觸發。下面代碼為按鈕的 command 綁定事件處理方法:
from tkinter import * import random class App: def __init__(self, master): self.master = master self.initWidgets() def initWidgets(self): self.label = Label(self.master, width=20) self.label['font'] = ('Microsoft Yahei', 20) self.label['bg'] = 'deeppink' self.label.pack() bn = Button(self.master, text="單擊", command=self.change) bn.pack() # 定義事件處理方法 def change(self): self.label['text'] = "Python 編程入門" # 生成三個隨機數 ct = [random.randrange(256) for _ in range(3)] grayness = int(round(0.299*ct[0] + 0.587*ct[1] + 0.114*ct[2])) # 將元組中的三個隨機數格式化成十六進制數,轉換成顏色格式 bg_colot = "#%02x%02x%02x" % tuple(ct) self.label['bg'] = bg_colot self.label['fg'] = 'black' if grayness > 125 else 'white' root = Tk() root.title("簡單事件處理") App(root) root.mainloop()
上面代碼中,在 initWidgets() 方法中為 Button 的 command 選項指定為 self.change,這表示當該按鈕被單擊時,就會觸發當前對象的 change() 方法,該 change() 方法會改變界面上 Label 的文本和背景色。運行程序,單擊圖形界面上的“單擊”按鈕,就會看到如圖八所示的界面。
圖八 使用command綁定事件處理
2、事件綁定
前面的事件綁定方法很簡單,但是有局限性:
(1)、不能為具體事件(如鼠標移動、按鍵等)綁定事件處理方法。
(2)、程序中不能獲取事件相關信息。
為此,Python 有更靈活的事件綁定方式,所有 Widget 組件都提供了一個 bind() 方法,該方法可以為“任意”事件綁定事件處理方法。下面示例是一個為按鈕的單擊、雙擊事件綁定事件處理的方法,代碼如下:
from tkinter import * class App: def __init__(self, master): self.master = master self.initWidgets() def initWidgets(self): self.show = Label(self.master, width=30, bg='white', font=('times', 20)) self.show.pack() bn = Button(self.master, text='單擊或雙擊') bn.pack(fill=BOTH, expand=YES) # 為左鍵單擊事件綁定處理方法 bn.bind('<Button-1>', self.one) # 為左鍵雙擊事件綁定處理方法 bn.bind('<Double-1>', self.double) def one(self, event): self.show['text'] = "左鍵單擊:%s" % event.widget['text'] def double(self, event): print("左鍵雙擊,退出程序:", event.widget['text']) import sys; sys.exit() root = Tk() root.title('簡單綁定') App(root) root.mainloop()
下面這兩行代碼在上面的代碼段中為 Button 按鈕綁定了單擊、雙擊事件的處理方法:
bn.bind('<Button-1>', self.one) bn.bind('<Double-1>', self.double)
當單擊按鈕時相應的調用 self.one 方法來處理,雙擊是調用 self.double 方法來處理。在接下來定義的的事件處理方法中,都可定義一個 event 參數,該參數是傳給事件處理方法的事件對象,在上面的代碼段示例了通過事件來獲取事件源的方式,即通過 event.widget 來獲取。對於鼠標事件來說,鼠標相對當前組件的位置可通過 event 對象中的 x 和 y 屬性來獲取(獲取方法:event.x,event.y)。
運行上面的這段代碼,並且單擊界面上的“單擊或雙擊”按鈕后的結果如圖九所示,當雙擊按鈕就退出程序。
圖九 為單擊、雙擊事件綁定事件處理方法
由上例可知,Tkinter 是直接使用字符串來代表事件類型的,比如 <Button-1>
代表鼠標左鍵單擊事件、<Double-1>
代表鼠標左鍵雙擊事件。Tkinter 事件的字符串大致是按照下面的格式書寫的:
<modifier-type-detail>
其中 type 是事件字符串的關鍵部分,用來描述事件的種類,如鼠標、鍵盤事件等;modifier 代表的是事件的修飾部分,如單擊、雙擊等;detail 用於指定事件的詳情,如鼠標左鍵、右鍵、滾輪等。Tkinter 支持各種鼠標、鍵盤事件如下表所示。
事件 | 簡介 |
---|---|
<Button-detail> |
鼠標單擊,detail指定哪個鼠標按鍵單擊。左鍵是<Button-1> 、中鍵是<Button-2> 、右鍵是<Button-3> 、向上滾動是<Button-4> 、向下滾動是<Button-5> |
<modifier-Motion> |
鼠標在組件上的移動事件,modifier是鼠標按鍵。按住鼠標左鍵移動是<B1-Motion> ,按住中鍵移動是<B2-Motion> ,按住右鍵移動是<B3-Motion> |
<ButtonRelease-detail> |
鼠標釋放事件,detail是指釋放哪個鼠標鍵。釋放左鍵是<ButtonRelease-1> 、中鍵<ButtonRelease-2> 、右鍵<ButtonRelease-3> |
<Double-Button-detail> 或<Double-detail> |
雙擊向上滾輪是<Double-4> ,雙擊向下滾輪是<Double-5> |
<Enter> |
鼠標進入組件的事件。注意,這個不是按下鍵盤上的回車鍵,鍵盤上的回車鍵事件是<Return> |
<Leave> |
鼠標移動事件 |
<FocusIn> |
組件及其包含的子組件獲得焦點 |
<FocusOut> |
組件及其包含的子組件失去焦點 |
<Return> |
按下回車鍵的事件。還可以為所有按鈕綁定事件處理方法。特殊的鍵位名稱包括:Cancel、BackSpace、Tab、Return(回車)、Shift_L(左Shift,只寫Shift代表任意Shift)、Control_L(左Ctrl,只寫Control代表任意Ctrl)、Alt_L(左Alt,只寫Alt代表任意Alt)、Pause、Caps_Lock、Escape、Prior(Page Up)、Next(Page Down)、End、Home、Left、Up、Right、Down、Print、Insert、Delete、F1~F12、Num_Lock、Scroll_Lock |
<Key> |
鍵盤上任意鍵的單擊事件,程序可通過event獲取用戶單擊了哪個鍵 |
a | 鍵盤上指定鍵被單擊的事件。比如'a'代表a鍵被單擊,'b'代表b鍵被單擊,不要尖括號,...... |
<Shift-Up> |
在Shift鍵被按下時按Up鍵(上鍵),類似的有<Shift-Left> 、<Shift-Down> 、<Alt-Up> 、<Control-Up> 等 |
<Configure> |
組件大小、位置改變的事件。組件改變之后的大小、位置可通過event的width、height、x、y獲取 |
下面代碼為鼠標移動事件綁定事件處理方法,代碼如下所示:
from tkinter import * class App: def __init__(self, master): self.master = master self.initWidgets() def initWidgets(self): lb = Label(self.master, width=30, height=3) lb.config(bg='lightgreen', font=('Times', 12)) # 為綠枝移動事件綁定事件處理方法 lb.bind('<Motion>', self.motion) # 為按住左鍵時的鼠標移動事件綁定事件處理方法 lb.bind('<B1-Motion>', self.press_motion) lb.pack() # 下面創建的 self.show 是實例變量,變量類型是 Label() 類型控件 self.show = Label(self.master, width=28, height=1) self.show.config(bg='pink', font=('Courier New', 12)) self.show.pack() def motion(self, event): # 調用實例變量的 text 屬性 self.show['text'] = "鼠標移動到:(%s %s)" % (event.x, event.y) return def press_motion(self, event): self.show['text'] = "按住鼠標的位置:(%s %s)" % (event.x, event.y) return root = Tk() root.title('鼠標事件') App(root) root.mainloop()
在上面的代碼段中,為鼠標移動(<Motion>
)事件綁定的處理方法是下面這行代碼:
lb.bind('<Motion>', self.motion)
此時鼠標在 lb 組件上移動時將會不斷觸發 motion()方法。
另外,為鼠標按住左鍵時鼠標移動(<B1-Motion>
)事件綁定的事件處理方法是下面這行代碼:
lb.bind('<B1-Motion>', self.press_motion)
此時按住鼠標左鍵在 lb 組件上移動時將會不斷觸發press_motion()方法。
運行這段代碼,可看到如圖十一所示的界面,在該界面中,圖1是鼠標直接在 lb 組件上移動的結果,圖2是按住鼠標左鍵在 lb 組件上移動的結果。
圖十一 鼠標在組件上移動、按下左鍵移動的結果
下面代碼利用綁定事件處理方法實現一個簡單的、真正意義的計算器,代碼如下:
from tkinter import * class App: def __init__(self, master): self.master = master self.initWidgets() self.expr = None def initWidgets(self): # 創建一個輸入組件 self.show = Label(relief=SUNKEN, font=('Courier New', 24), width=25, bg='white', anchor=E) # 對該輸入組件使用 Pack 布局,放在容器頂部 self.show.pack(side=TOP, pady=10) p = Frame(self.master) p.pack(side=TOP) # 定義字符串元組 names = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '-', '*', '/', '.', '=') # 遍歷字符串元組 for i in range(len(names)): # 創建 Button,將 Button 放入 p 組件中 b = Button(p, text=names[i], font=('Verdana', 20), width=6) b.grid(row= i // 4, column= i % 4) # 為鼠標左鍵的單擊事件綁定事件處理方法 b.bind('<Button-1>', self.click) # 為鼠標左鍵的雙擊事件綁定事件處理方法 if b['text'] == '=': b.bind('<Double-1>', self.clean) def click(self, event): # 單擊數字或小數點 if (event.widget['text'] in ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.')): self.show['text'] = self.show['text'] + event.widget['text'] # 1, 1+1 # 如果單擊的是運算符 elif (event.widget['text'] in ('+', '-', '*', '/')): # 連接運算符 self.show['text'] = self.show['text'] + event.widget['text'] self.expr = self.show['text'] elif (event.widget['text'] == '=' and self.expr is not None): self.expr = self.show['text'] print(self.expr) # 使用 eval 函數計算表達式值 self.show['text'] = self.expr + "=" + str(eval(self.expr)) self.expr = None # 雙擊 “=” 按鈕時,清空計算結果,將表達式設為 None def clean(self, event): self.expr = None self.show['text'] = '' def cal(): """計算器主程序""" root = Tk() root.title("計算器") App(root) root.mainloop() if __name__ == '__main__': cal()
在這段代碼中,為所有按鈕的單擊事件綁定了處理方法的是下面這行代碼,用於處理計算功能:
b.bind('<Button-1>', self.click)
此外,下面這行代碼還為雙擊“=”等號按鈕綁定了事件處理方法,在處理方法中實現了清空計算結果,重新開始計算。
if b['text'] == '=': b.bind('<Double-1>', self.clean)
上面這段計算器代碼的運行結果如圖十二所示。
圖十二 簡單計算器界面