花一個小時,學會用這個實用至上主義的 GUI 庫!

作者 | 天元浪子

概述

1.1 Tkinter是什麼?

Tkinter是Python自帶的GUI庫,Python的IDEL就是Tkinter的應用實例。Tkinter可以看作是Tk和inter的合體。詞根inter之意不言自明,而Tk則是工具控制語言Tcl(Tool Command Language)的一個圖形工具箱的擴展,它提供各種標準的GUI接口。

和其他GUI庫相比,Tkinter有一個與生俱來的優勢:無需安裝就可以直接使用。當然,也有很多人——曾經我也是其中之一,認為這恰是Tkinter的唯一優點。不過,後來我改變了看法。相較於wx或Qt多如牛毛的控制元件和元件,Tk只用十幾個控制元件就可以滿足幾乎所有的應用需求,用最低的學習成本、最簡單的方式解決問題,這不正是實用至上主義的典範嗎?

從實用主義的角度看,Qt的博大精深就是尾大不掉,Wx的精緻嚴謹就是循規蹈矩。如果你正在尋找一款用於桌面程式設計的GUI庫,並且只打算花一個小時學會使用它,那麼就請選擇Tkinter吧。這款以學習曲線平緩和易於嵌入為特定目標而設計的GUI庫,也許正是你苦苦追尋的真愛。

1.2 Tkinter的組織架構

Tkinter模組提供了一個名為Tk的窗體類、十幾個基本控制元件,多個類型對象,若干常量,以及一個可選主題的控制元件包ttk和各種對話方塊元件。可以把ttk理解為增強的控制元件包,它提供了更多、更美觀的控制元件。Tkinter模組的組織架構如下圖所示。

對於簡單的應用需求,只需要像下面這樣匯入模組就可以了。

    from tkinter import *

    由於Tkinter模組在其__init__.py腳本中將可選主題的控制元件包ttk和各種對話方塊元件從__all__裡面排除了,上面的模組匯入方式只匯入了Tk類、基本控制元件、類型對象和常量。如果應用程序需要打開檔案、保存檔案等對話操作,或者需要更多更個性化的控制元件,就需要像下面這樣匯入模組了。

      from tkinter import *from tkinter import ttk, filedialog, messagebox

      快速體驗

      2.1 GUI設計的一般流程

      用Tkinter寫一個桌面應用程序,只需要三步:

      1. 創建一個窗體

      2. 把需要的控制元件放到窗體上,並告訴它們當有預期的事件發生時就執行預設的動作

      3. 啟動循環監聽事件

      無論這個程序有多麼簡單或多麼複雜,第1步和第3步是固定不變的,設計者只需要專注於第2步的實現。下面這段程式碼實現了一個最簡單的Hello World桌面程序。

        from tkinter import *root = Tk() # 1. 創建一個窗體Label(root, text='Hello World').pack() # 2. 添加Label控制元件root.mainloop() # 3. 啟動循環監聽事件

        不同於wx用frame表示窗體,我習慣用root作為窗體的名字。當然,你也可以用window或其他你喜歡的名字,但不要用frame,因為Tkinter為frame賦予了其他的含義。

        程式碼運行界面如上圖所示。彈出來的程序窗口既小且醜,就像一個新生的嬰兒,但這的確是一個完整的桌面應用程序。

        2.2 控制元件佈局

        所謂控制元件佈局,就是設置控制元件在窗體內的位置以及填充、間隔等屬性。在Hello world程序中,我使用了pack方法來設置控制元件Label的佈局,並把它們寫成了鏈式調用的形式。如果將控制元件的創建和佈局分寫成兩行的話,程式碼的可讀性會更好一點。

        pack方法是Tinkter最常用的佈局手段,功能強大,參數眾多,這裡只介紹pack的幾個主要參數。下表中用到了Tkinter定義的常量,比如,TOP就是tkinter.TOP,等價於字串’top’,YES就是tkinter.YES,等價於字串’yes’。

        參數說明
        side佈局方向,可選項:TOP、BOTTOM、 LEFT、RIGHT,預設默認TOP
        anchor對齊方式,可選項:E、W、N 、S、NE、NW、SE、SW、CENTER,預設默認CNETER
        expand是否佔用剩餘可用空間作為控制元件的可用空間,可選項:NO、YES,預設默認NO
        fill控制元件在指定方向上擴展至填滿自己的可用空間,可選項:X、Y、BOTH、NONE,預設默認NONE
        padx水平方向上控制元件與可用空間的留空距離,以像素表示,預設默認0
        pady垂直方向上控制元件與可用空間的留空距離,以像素表示,預設默認0

        下面的程式碼創建了標籤和按鈕兩個控制元件,使用pack方法使其上下排列,同時還演示了窗口標題、窗口圖示和窗口大小的設置方式。程式碼中用到了.ico格式的圖示檔案,想要運行這段程式碼的話,請先替換成本地檔案。

          from tkinter import *root = Tk()root.title('最簡單的桌面應用程序') # 設置窗口標題root.geometry('480x200') # 設置窗口大小root.iconbitmap('res/Tk.ico') # 設置窗口圖示label = Label(root, text='Hello World', font=("Arial Bold", 50))label.pack(side='top', expand='yes', fill='both') # 使用全部可用空間,水平和垂直兩個方向填充btn = Button(root, text='關閉窗口', bg='#C0C0C0') # 按鈕背景深灰色btn.pack(side='top', fill='x', padx=5, pady=5) # 水平方向填充,水平垂直兩個方向留白5個像素root.mainloop()

          程式碼運行界面如下圖所示,看上去比第一個Hello World程序要順眼得多。在這個界面上,雖然按鈕的名字叫做「關閉窗口」,但是目前還不能對點選操作做出任何反應。

          控制元件佈局除了pack方法外,還有place方法和grid方法,後面會有詳細的說明。

          2.3 事件驅動

          一個桌面程序不單是控制元件的羅列,更重要的是對外部的刺激——包括使用者的操作做出反應。如果把窗體和控制元件比作是桌面程序的軀體,那麼響應外部刺激就是它的靈魂。Tkinter的靈魂是事件驅動機制:當某事件發生時,程序就會自動執行預先設定的動作。

          事件驅動機制有三個要素:事件、事件函數和事件綁定。比如,當一個按鈕被點選時,就會觸發按鈕點選事件,該事件如果綁定了事件函數,事件函數就會被調用。下面的程式碼演示瞭如何將按鈕點選事件和對應的事件函數綁定在一起。

            from tkinter import *def click_button():"""點選按鈕的事件函數"""root.destroy() # 調用root的解構函式root = Tk()root.title('最簡單的桌面應用程序')root.geometry('640x320')root.iconbitmap('res/Tk.ico')label = Label(root, text='Hello World', font=("Arial Bold", 50))label.pack(side='top', expand='yes', fill='both')btn = Button(root, text='關閉窗口', bg='#C0C0C0', command=click_button) # 用command參數綁定事件函數btn.pack(side='top', fill='x', padx=5, pady=5)root.mainloop()

            現在點選按鈕就可關閉窗口了。你看,事件驅動機制是多麼的簡單和美妙!當然,綁定事件和事件函數的方法不止有本例用到的command,後面還會談到bind和bind_class兩種方式。

            2.4 物件導向使用Tkinter

            對於上一段程式碼,熟悉OOP的讀者會注意到事件函數click_button中使用了root這個全局變數。從語法和程式設計規範的角度看,這樣做沒有任何問題。不過,當桌面程序面對稍微複雜的業務邏輯時,勢必要大量使用全局變數,這給程序的安全帶來了隱患,同時也不便於程序的維護。下面的程式碼以物件導向的方式設計了一個按鈕點選計數器。

              from tkinter import *class MyApp(Tk):"""繼承Tk,創建自己的桌面應用程序類"""def __init__(self):"""建構函式"""super().__init__()self.title('按鈕點選計數器')self.geometry('320x160')self.iconbitmap('res/Tk.ico')self.counter = IntVar() # 創建一個整型變數對象self.counter.set(0) # 置其初值為0label = Label(self, textvariable=self.counter, font=("Arial Bold", 50)) # 將Label和整型變數對象關聯label.pack(side='left', expand='yes', fill='both', padx=5, pady=5)btn = Button(self, text='點我試試看', bg='#90F0F0')btn.pack(side='right', anchor='center', fill='y', padx=5, pady=5)btn.bind(sequence='', func=self.on_button) # 綁定事件和事件函數def on_button(self, evt):"""點選按鈕事件的響應函數, evt是事件對象"""self.counter.set(self.counter.get()+1)if __name__ == '__main__':app = MyApp()app.mainloop()

              這段程式碼用到了整型對象IntVar,這是Tkinter獨有的概念。當類型對象被改變時,與其關聯的控制元件文字內容會自動更新。藉助於類型對象和控制元件之間的關聯,使用者可以方便地在其他執行緒中更新UI。

              程式碼運行界面如上圖所示。每點選一次按鈕,計數器自動加1並顯示在Lable控制元件上。請注意,這個例子並沒有使用command綁定按鈕事件,而是使用了bind方法將滑鼠左鍵點選事件和事件函數on_button綁定在一起。這個用法要求事件函數on_button接受一個事件對象evt作為參數,該參數提供了和事件相關的詳細資訊。不難理解,command適用於綁定控制元件自身的事件,bind適用於綁定滑鼠和鍵盤事件。

              事件和事件對象

              3.1 滑鼠事件

              Tkinter支持的滑鼠事件如下所列。

              • – 左鍵單擊

              • – 中鍵單擊

              • – 右鍵單擊

              • – 左鍵單擊

              • – 左鍵拖動

              • – 中鍵拖動

              • – 右鍵拖動

              • – 左鍵釋放

              • – 中鍵釋放

              • – 右鍵釋放

              • – 左鍵雙擊

              • – 中鍵雙擊

              • – 右鍵雙擊

              • – 移動

              • – 滾輪

              • – 進入控制元件

              • – 離開控制元件

              下面的程式碼演示瞭如何綁定滑鼠事件,以及如何使用滑鼠事件對象。

                from tkinter import *class MyApp(Tk):"""繼承Tk,創建自己的桌面應用程序類"""def __init__(self):"""建構函式"""super().__init__()self.title('滑鼠事件演示程序')self.geometry('480x200')self.iconbitmap('res/Tk.ico')self.info = StringVar()self.info.set('')label = Label(self, textvariable=self.info, font=("Arial Bold", 18))label.pack(side='top', expand='yes', fill='both')btn = Button(self, text='確定', bg='#C0C0C0')btn.pack(side='top', fill='x', padx=5, pady=5)label.bind('', self.on_mouse)label.bind('', self.on_mouse)label.bind('', self.on_mouse)label.bind('', self.on_mouse)btn.bind('', self.on_mouse)btn.bind('', self.on_mouse)btn.bind('', self.on_mouse)btn.bind('', self.on_mouse)btn.bind('', self.on_mouse)btn.bind('', self.on_mouse)def on_mouse(self, evt):"""響應所有滑鼠事件的函數"""if isinstance(evt.num, int):self.info.set('事件類型:%s\n鍵碼:%d\n滑鼠位置:(%d, %d)\n時間:%d'%(evt.type, evt.num, evt.x, evt.y, evt.time))else:self.info.set('事件類型:%s\n滑鼠位置:(%d, %d)\n時間:%d'%(evt.type, evt.x, evt.y, evt.time))if __name__ == '__main__':app = MyApp()app.mainloop()

                這段程式碼在標籤控制元件和按鈕控制元件上綁定了多種滑鼠事件,並把這些事件綁定到了同一個事件函數上,事件函數被調用時會傳入事件對象作為參數。藉助於事件對象可以獲得事件類型、滑鼠位置、觸發時間等詳細資訊。

                當滑鼠進入或離開標籤控制元件、在標籤控制元件上移動滑鼠或滾動滾輪、在按鈕控制元件上點選滑鼠按鍵,相應的事件類型和資訊就會顯示在標籤上。程式碼運行界面如上圖所示。

                3.2 鍵盤事件

                Tkinter支持的滑鼠事件如下所列。

                • – 回車

                • – Break鍵

                • – BackSpace鍵

                • – Tab鍵

                • – Shift鍵

                • – Alt鍵

                • – Control鍵

                • – Pause鍵

                • – Caps_Lock鍵

                • – Escapel鍵

                • – PageUp鍵

                • – PageDown鍵

                • – End鍵

                • – Home鍵

                • – 左箭頭

                • – 上箭頭

                • – 右箭頭

                • – 下箭頭

                • – Print Screen鍵

                • – Insert鍵

                • – Delete鍵

                • – F1鍵

                • – Num_Lock鍵

                • – Scroll_Lock鍵

                • – 任意鍵

                下面的程式碼演示瞭如何綁定鍵盤事件,以及如何使用鍵盤事件對象。

                  from tkinter import *class MyApp(Tk):"""繼承Tk,創建自己的桌面應用程序類"""def __init__(self):"""建構函式"""super().__init__()self.title('鍵盤事件演示程序')self.geometry('480x200')self.iconbitmap('res/Tk.ico')self.info = StringVar()self.info.set('')self.info = StringVar()self.info.set('')self.lab = Label(self, textvariable=self.info, font=("Arial Bold", 18))self.lab.pack(side='top', expand='yes', fill='both')self.lab.focus_set()self.lab.bind('', self.on_key)self.btn = Button(self, text='切換焦點', bg='#C0C0C0', command=self.set_label_focus)self.btn.pack(side='top', fill='x', padx=5, pady=5)def on_key(self, evt):"""響應所有鍵盤事件的函數"""self.info.set('evt.char = %s\nevt.keycode = %s\nevt.keysym = %s'%(evt.char, evt.keycode, evt.keysym))def set_label_focus(self):"""在Label和Button之間切換焦點"""self.info.set('')if isinstance(self.lab.focus_get(), Label):self.btn.focus_set()else:self.lab.focus_set()if __name__ == '__main__':app = MyApp()app.mainloop()

                  這段程式碼在標籤控制元件上綁定了任意鍵被按下事件,在按鈕控制元件上綁定了切換焦點的事件函數。程式碼運行界面如下所示。

                  這裡需要特別說明一下,綁定鍵盤事件的控制元件必須在獲得焦點後綁定才能生效。本例點選按鈕可在按鈕和標籤之間切換焦點,請仔細體會標籤在或獲得和失去焦點後對於鍵盤事件的不同反應。

                  3.3 元件事件

                  元件是一個較為含糊的說法,大致可以認為是窗體和控制元件的統稱。Tkinter支持的元件事件較多,這裡只介紹最為常用的幾個。

                  • – 改變大小或位置

                  • – 獲得焦點時觸發

                  • – 失去焦點時觸發

                  • – 銷燬時觸發

                  下面的例子演示了窗體綁定銷燬事件的用法。通常,這樣做是為了在使用者關閉窗口前做些保護性的清理性的工作。

                    from tkinter import *def befor_quit(evt):"""關閉之前清理現場"""print('關閉之前,可以做點什麼')root = Tk()Label(root, text='Hello World').pack()root.bind('', befor_quit)root.mainloop()

                    3.4 事件對象

                    無論是滑鼠事件、鍵盤事件還是元件事件,都要求與其綁定的事件函數接受一個事件對象作為參數。一個事件對象一般包含下列資訊。

                    • widget – 觸發事件的控制元件

                    • type – 事件類型

                    • x, y – 滑鼠在窗體上的座標(以左上角為原點)

                    • x_root, y_root – 滑鼠在螢幕上的座標(以左上角為原點)

                    • num – 滑鼠事件對應的按鍵碼

                    • char – 鍵盤事件對應的字符程式碼

                    • keysym – 鍵盤事件對應的字串

                    • keycode – 鍵盤事件對應的按鍵碼

                    • width, height – 受事件影響後的控制元件寬高

                    在滑鼠事件和鍵盤事件的例子中已經演示了事件對象的用法,這裡不再贅述。

                    常用控制元件

                    4.1 窗格Frame

                    在wx等GUI庫中,Frame的含義是窗體,不過Tkinter的Frame控制元件更像一個控制元件的容器,這裡我把它稱為窗格,以免產生歧義。配合pack方法,Frame堪稱是Tkinter的佈局利器。

                      from tkinter import *class MyApp(Tk):"""繼承Tk,創建自己的桌面應用程序類"""def __init__(self):"""建構函式"""super().__init__()self.title('窗格:Frame')self.iconbitmap('res/Tk.ico')self.init_ui()def init_ui(self):"""初始化界面"""frame1 = Frame(self, bg='#90c0c0')frame1.pack(padx=5, pady=5)# Label是frame1的第1個子控制元件,從左向右佈局Label(frame1, bg='#f0f0f0', width=25).pack(side=LEFT, fill=BOTH, padx=5, pady=5)# frame2是frame1的第2個子控制元件,從左向右佈局frame2 = Frame(frame1, bg='#f0f0f0')frame2.pack(side=LEFT, padx=5, pady=5)# 3個Button是frame2的子控制元件,自上而下佈局Button(frame2, text='按鈕1', width=10).pack(padx=5, pady=5)Button(frame2, text='按鈕2', width=10).pack(padx=5, pady=5)Button(frame2, text='按鈕3', width=10).pack(padx=5, pady=5)if __name__ == '__main__':app = MyApp()app.mainloop()

                      這段程式碼最外層的frame1是為了控制窗體內上下左右的留白大小。lable和frame2同屬於frame1的子元素,分列左右。frame2裡面自上而下放置了3個按鈕。程式碼運行界面如下圖所示。

                      4.2 輸入框Entry

                      通過輸入框的textvariable參數關聯一個字串類型對象,當輸入框內容改變時會自動同步到關聯的字串類型對象——這是輸入框控制元件Entry的一個使用技巧。輸入框的另一個常用參數是justify,用來指定輸入內容的對齊方式。另外,輸入框控制元件輸入密碼時,show參數可以指定一個字符以替換實際輸入的內容。

                        from tkinter import *class MyApp(Tk):"""繼承Tk,創建自己的桌面應用程序類"""def __init__(self):"""建構函式"""super().__init__()self.title('輸入框:Entry')self.iconbitmap('res/Tk.ico')self.init_ui()def init_ui(self):"""初始化界面"""account, passwd = StringVar(), StringVar()account.set('')passwd.set('')group = LabelFrame(self, text="登入", padx=5, pady=5)group.pack(padx=20, pady=10)f1 = Frame(group)f1.pack(padx=5, pady=5)Label(f1, text='賬號:').pack(side=LEFT, pady=5)Entry(f1, textvariable=account, width=15, justify=CENTER).pack(side=LEFT, pady=5)f2 = Frame(group)f2.pack(padx=5, pady=5)Label(f2, text='密碼:').pack(side=LEFT, pady=5)Entry(f2, textvariable=passwd, width=15, show='*', justify=CENTER).pack(side=LEFT, pady=5)btn = Button(self, text='確定', bg='#90c0c0', command=lambda : print(account.get(), passwd.get()))btn.pack(fill=X, padx=20, pady=10)if __name__ == '__main__':app = MyApp()app.mainloop()

                        這段程式碼還有同時演示了帶標籤的窗格控制元件LabelFrame的用法。程式碼運行界面如下圖所示。

                        4.3 單選框Radiobutton

                        單選框通常是成組使用的,每個Radiobutton都關聯同一個整型對象,該整型對象的值就是單選框選中選項的索引號。

                          from tkinter import *class MyApp(Tk):"""繼承Tk,創建自己的桌面應用程序類"""def __init__(self):"""建構函式"""super().__init__()self.title('單選框:Radiobutton')self.iconbitmap('res/Tk.ico')self.init_ui()def init_ui(self):"""初始化界面"""f0 = Frame(self)f0.pack(padx=5, pady=5)f1 = Frame(f0)f1.pack(side=LEFT, padx=5, pady=5)g1 = LabelFrame(f1, text="你最擅長哪一個?", padx=5, pady=5)g1.pack(padx=5, pady=5)self.rb_v1 = IntVar()self.rb_v1.set(0)rb_11 = Radiobutton(g1, variable=self.rb_v1, text='Tkinter', value=0, command=self.on_radio_1)rb_12 = Radiobutton(g1, variable=self.rb_v1, text='wxPython', value=1, command=self.on_radio_1)rb_13 = Radiobutton(g1, variable=self.rb_v1, text='PyQt5', value=2, command=self.on_radio_1)rb_11.pack(ancho='w', padx=5, pady=5)rb_12.pack(ancho='w', padx=5, pady=5)rb_13.pack(ancho='w', padx=5, pady=5)f2 = Frame(f0)f2.pack(side=LEFT, padx=5, pady=5)g2 = LabelFrame(f2, text="你最擅長哪一個?", padx=5, pady=5)g2.pack(padx=5, pady=5)self.rb_v2 = IntVar()self.rb_v2.set(0)rb_21 = Radiobutton(g2, variable=self.rb_v2, text='Tkinter', value=0, indicatoron=False, command=self.on_radio_2)rb_22 = Radiobutton(g2, variable=self.rb_v2, text='wxPython', value=1, indicatoron=False, command=self.on_radio_2)rb_23 = Radiobutton(g2, variable=self.rb_v2, text='PyQt5', value=2, indicatoron=False, command=self.on_radio_2)rb_21.pack(fill=X, padx=5, pady=5)rb_22.pack(fill=X, padx=5, pady=5)rb_23.pack(fill=X, padx=5, pady=5)self.info = StringVar()self.info.set('')label = Label(self, textvariable=self.info, bg='#ffffff')label.pack(expand='yes', fill='x', padx=5, pady=10)def on_radio_1(self):"""響應第1組單選框事件的函數"""selected = ['Tkinter', 'wxPython', 'PyQt5'][self.rb_v1.get()]self.info.set('你選擇了第1組的%s'%selected)def on_radio_2(self):"""響應第2組單選框事件的函數"""selected = ['Tkinter', 'wxPython', 'PyQt5'][self.rb_v2.get()]self.info.set('你選擇了第2組的%s'%selected)if __name__ == '__main__':app = MyApp()app.mainloop()

                          這段程式碼演示了兩種不同風格的單選框控制元件。程式碼運行界面如下圖所示。

                          4.4 核取方塊Checkbutton

                          核取方塊的每一項都需要關聯一個整型對象,每當有選項被點選時,逐一檢查每一個整型對象的值,就可以獲得當前選中的選項。

                            from tkinter import *class MyApp(Tk):"""繼承Tk,創建自己的桌面應用程序類"""def __init__(self):"""建構函式"""super().__init__()self.title('核取方塊:Checkbox')self.iconbitmap('res/Tk.ico')self.init_ui()def init_ui(self):"""初始化界面"""group = LabelFrame(self, text="你擅長哪個?", padx=20, pady=5)group.pack(padx=30, pady=5)self.cb_v1 = IntVar()self.cb_v2 = IntVar()self.cb_v3 = IntVar()self.cb_v1.set(0)self.cb_v2.set(0)self.cb_v3.set(0)cb_1 = Checkbutton(group, variable=self.cb_v1, text='Tkinter', onvalue=1, offvalue=0, command=self.on_cb).pack(ancho='w', padx=5, pady=5)cb_2 = Checkbutton(group, variable=self.cb_v2, text='wxPython', onvalue=1, offvalue=0, command=self.on_cb).pack(ancho='w', padx=5, pady=5)cb_3 = Checkbutton(group, variable=self.cb_v3, text='PyQt5', onvalue=1, offvalue=0, command=self.on_cb).pack(ancho='w', padx=5, pady=5)self.info = StringVar()self.info.set('')label = Label(self, textvariable=self.info, bg='#ffffff')label.pack(expand='yes', fill='x', padx=5, pady=5)def on_cb(self):"""響應核取方塊事件的函數"""selected = list()if self.cb_v1.get():selected.append('Tkinter')if self.cb_v2.get():selected.append('wxPython')if self.cb_v3.get():selected.append('PyQt5')self.info.set(', '.join(selected))if __name__ == '__main__':app = MyApp()app.mainloop()

                            運行界面如下圖所示。

                            運行界面如下圖所示

                            4.5 計數器Spinbox

                            計數器Spinbox既可以向Entry那樣接受鍵盤輸入,也可以點選上下的箭頭實現數值的增加,適用於小幅度連續調整的場合。

                              from tkinter import *def on_spin():"""響應可調輸入框事件的函數"""info.set(str(spin_v.get()))root = Tk()root.title('可調輸入框:Spinbox')spin_v = IntVar()spin_v.set(5)entry = Spinbox(root, textvariable=spin_v, from_=0, to=9, bg='#ffffff', command=on_spin).pack(fill=X, padx=5, pady=5)info = StringVar()info.set(str(spin_v.get()))label = Label(root, textvariable=info, bg='#ffffff')label.pack(expand=YES, fill=X, padx=5, pady=5)root.mainloop()

                              在這段程式碼中,Spinbox只綁定了滑鼠事件沒有綁定鍵盤事件,因此資訊顯式區不能顯示鍵盤輸入資訊,只響應滑鼠操作。程式碼運行界面如下圖所示。

                              4.6 滑塊Scale

                              和其他控制元件相比,滑塊控制元件Scale在應用上有一點點怪異:如果用command參數綁定事件函數,則要求該函數接收一個事件對象作為參數。類似的情況還出現在控制元件命名上,比如,Radiobutton的第2個單詞首字母小寫,LabelFrame的第2個單詞首字母卻是大寫。特例破壞了一致性的美感,這也是Tkinter為人詬病的一個突出問題。

                                from tkinter import *class MyApp(Tk):"""繼承Tk,創建自己的桌面應用程序類"""def __init__(self):"""建構函式"""super().__init__()self.title('滑塊:Scale')self.geometry('240x100')self.iconbitmap('res/Tk.ico')self.init_ui()def init_ui(self):"""初始化界面"""self.scale_v = DoubleVar()self.scale_v.set(50)scale = Scale(self, variable=self.scale_v, from_=0, to=100, orient=HORIZONTAL, command=self.on_scale)scale.pack(fill=X, padx=5, pady=5)self.info = StringVar()self.info.set(str(self.scale_v.get()))label = Label(self, textvariable=self.info, bg='#ffffff')label.pack(expand=YES, fill=X, padx=5, pady=5)def on_scale(self, evt):"""響應滑塊事件的函數"""self.info.set(str(self.scale_v.get()))if __name__ == '__main__':app = MyApp()app.mainloop()

                                程式碼運行界面如下圖所示。

                                4.7 菜單按鈕Menubutton

                                下面的程式碼給出了一個完整的菜單例子。

                                  class MyApp(Tk):"""繼承Tk,創建自己的桌面應用程序類"""def __init__(self):"""建構函式"""super().__init__()self.title('菜單按鈕:Menubutton')self.geometry('300x100')self.iconbitmap('res/Tk.ico')self.init_ui()def init_ui(self):"""初始化界面"""frame_menu = Frame(self)frame_menu.pack(anchor=NW) # 菜單位於窗口左上角(North_West)mb_file = Menubutton(frame_menu, text='檔案', relief=RAISED)mb_file.pack(side='left')file_menu = Menu(mb_file, tearoff=False)file_menu.add_command(label='打開', command=lambda :print('打開檔案'))file_menu.add_command(label='保存', command=lambda :print('保存檔案'))file_menu.add_separator()file_menu.add_command(label='退出', command=self.destroy)mb_file.config(menu=file_menu)mb_help = Menubutton(frame_menu, text='幫助', relief=RAISED)mb_help.pack(side='left')help_menu = Menu(mb_help, tearoff=False)help_menu.add_command(label='關於...', command=lambda :print('幫助文件'))mb_help.config(menu=help_menu)if __name__ == '__main__':app = MyApp()app.mainloop()

                                  為了讓程式碼看起來更清晰,這裡用lambda函數代替了菜單按鈕的事件函數。其實,lambda函數就是匿名函數,它以lambda關鍵字開始,以半角冒號分隔函數參數和函數體。程式碼運行界面如下圖所示。

                                  4.8 訊息對話方塊

                                  Tkinter的訊息對話方塊子模組messagebox提供了多種對話方塊,以適應不同的應用需求,下面的程式碼演示了其中常用的七個對話方塊。前文已經提到過,子模組messagebox必須要顯式匯入才能使用。

                                    from tkinter import *from tkinter import messagebox as mbroot = Tk()root.title('訊息對話方塊')info = StringVar()info.set('')f = Frame(root)f.pack(padx=5, pady=10)Button(f, text='提示資訊', command=lambda :info.set(mb.showinfo(title='提示資訊', message='對手認負,比賽結束。'))).pack(side=LEFT, padx=5)Button(f, text='警告資訊', command=lambda :info.set(mb.showwarning(title='警告資訊', message='不能連續提和!'))).pack(side=LEFT, padx=5)Button(f, text='錯誤資訊', command=lambda :info.set(mb.showerror(title='錯誤資訊', message='著法錯誤!'))).pack(side=LEFT, padx=5)Button(f, text='Yes/No', command=lambda :info.set(mb.askyesno(title='操作提示', message='對手提和,接受嗎?'))).pack(side=LEFT, padx=5)Button(f, text='Ok/Cancel', command=lambda :info.set(mb.askokcancel(title='操作提示', message='再來一局?'))).pack(side=LEFT, padx=5)Button(f, text='Retry/Cancel', command=lambda :info.set(mb.askretrycancel(title='操作提示', message='訊息發送失敗!'))).pack(side=LEFT, padx=5)Button(f, text='Yes/No/Cancel', command=lambda :info.set(mb.askyesnocancel(title='操作提示', message='是否保存對局記錄?'))).pack(side=LEFT, padx=5)label = Label(root, textvariable=info, bg='#ffffff')label.pack(expand='yes', fill='x', padx=5, pady=20)root.mainloop()

                                    程式碼運行界面如下圖所示。

                                    點選按鈕後彈出的各個對話方塊如下圖所示。

                                    點選按鈕後彈出的各個對話方塊如下圖所示

                                    4.9 檔案對話方塊

                                    Tkinter的檔案對話方塊子模組filedialog提供了多種對話方塊,以適應不同的應用需求,下面的程式碼演示了其中常用的檔案選擇、目錄選擇和檔案保存等三個對話方塊。同樣的,子模組filedialog也必須要顯式匯入才能使用。

                                      from tkinter import *from tkinter import filedialog as fdclass MyApp(Tk):"""繼承Tk,創建自己的桌面應用程序類"""def __init__(self):"""建構函式"""super().__init__()self.title('檔案對話方塊')self.iconbitmap('res/Tk.ico')self.init_ui()def init_ui(self):"""初始化界面"""info = StringVar()info.set('')f = Frame(self)f.pack(padx=20, pady=10)Button(f, text='選擇檔案', command=lambda :info.set(fd.askopenfilename(title='選擇檔案'))).pack(side=LEFT, padx=10)Button(f, text='選擇目錄', command=lambda :info.set(fd.askdirectory(title='選擇目錄'))).pack(side=LEFT, padx=10)Button(f, text='保存檔案', command=lambda :info.set(fd.asksaveasfilename(title='保存檔案', defaultextension='.png'))).pack(side=LEFT, padx=10)label = Label(self, textvariable=info, bg='#ffffff')label.pack(expand='yes', fill='x', padx=5, pady=20)if __name__ == '__main__':app = MyApp()app.mainloop()

                                      程式碼運行界面如下圖所示。

                                      下圖是選擇打開檔案的對話窗口,選擇路徑和保存檔案與之類似。

                                      4.10 可選主題的控制元件包ttk

                                      4.10 可選主題的控制元件包ttk

                                      Tk的研發始於1989 年,第一個版本於1991年問世,彼時還是一個重實力輕顏值的年代。相比於後來的wx和Qt,Tk的控制元件更注重實用,賣相自然不會太好。好在Tkinter與時俱進,後期推出了可選主題的控制元件包ttk,算是對其控制元件顏值的補救吧。

                                      可選主題的控制元件包ttk包含了18個控制元件,其中Button、Checkbutton、Entry、Frame、Label, LabelFrame、Menubutton、PanedWindow、Radiobutton、Scale、Scrollbar和Spinbox等12個和已有的控制元件重合,只是用法上有些差異,6個新增的控制元件是Combobox、Notebook、Progressbar、Separator、Sizegrip和Treeview。

                                      之所以稱其為可選主題的控制元件包,是因為ttk提供了Style類,可統一定製所有ttk控制元件的風格。在Python的IDLE中可以方便地查看ttk包含的可用主題。

                                        >>> from tkinter import ttk>>> style = ttk.Style()>>> style.theme_names()('winnative', 'clam', 'alt', 'default', 'classic', 'vista', 'xpnative')

                                        讓我們先來看看這些主題和原來的控制元件有什麼不同。

                                          from tkinter import *from tkinter import ttkclass MyApp(Tk):"""繼承Tk,創建自己的桌面應用程序類"""def __init__(self):"""建構函式"""super().__init__()self.title('主題控制元件')self.iconbitmap('res/Tk.ico')self.init_ui()def init_ui(self):"""初始化界面"""self.style = ttk.Style()self.theme = StringVar()self.theme.set(self.style.theme_use())ttk.Button(self, text='切換主題按鈕', command=self.on_style).pack(padx=30, pady=20)ttk.Entry(self, textvariable=self.theme, justify=CENTER, width=20).pack(padx=30, pady=0)ttk.Combobox(self, value=('Tkinter', 'wxPython', 'PyQt5')).pack(padx=30, pady=20)def on_style(self):"""更換主題"""items = self.style.theme_names()new_theme = items[(items.index(self.theme.get())+1)%len(items)]self.theme.set(new_theme)self.style.theme_use(new_theme)if __name__ == '__main__':app = MyApp()app.mainloop()

                                          程式碼運行界面如下圖所示。點選按鈕可在ttk可用的主題之間循環切換,截圖為Windows原生主題。

                                          示例和技巧

                                          5.1 窗口居中

                                          本文開始的快速體驗環節,已經介紹過用窗體的geometry方法設置窗口大小,其實,它也被用來設置窗口位置。

                                            from tkinter import *class MyApp(Tk):"""繼承Tk,創建自己的桌面應用程序類"""def __init__(self):"""建構函式"""super().__init__()self.title('窗口居中')self.iconbitmap('res/Tk.ico')self.init_ui()self.center()def init_ui(self):"""初始化界面"""Label(self, text='Hello World', font=("Arial Bold", 50)).pack(expand=YES, fill='both')def center(self):"""窗口居中"""self.update() # 更新顯示以獲取最新的窗口尺寸scr_w = self.winfo_screenwidth() # 獲取螢幕寬度scr_h = self.winfo_screenheight() # 獲取螢幕寬度w = self.winfo_width() # 窗口寬度h = self.winfo_height() # 窗口高度x = (scr_w-w)//2 # 窗口左上角x座標y = (scr_h-h)//2 # 窗口左上角y座標self.geometry('%dx%d+%d+%d'%(w,h,x,y)) # 設置窗口大小和位置if __name__ == '__main__':app = MyApp()app.mainloop()

                                            運行這段程式碼,顯示的仍然是最初的Hello World,但是不管設置了多大的字號,窗口總是位於螢幕的中央。

                                            5.2 相簿

                                            Tinkter的很多控制元件都可以作為圖像顯示的容器,或者用圖片來提升顏值,只是Tinkter的圖像處理能力有點弱,比如,BitmapImage類只能處理灰度圖像,PhotoImage只能打開.gif格式和部分.png格式的圖像。幸好pillow模組提供了可用於Tinkter的PhotoImage對象,使得Tinkter也可以非常方便地使用圖像了。下面的例子使用標籤控制元件Label作為圖像容器,點選前翻後翻按鈕可在多張照片之間循環切換。

                                              from tkinter import *from tkinter import ttkfrom PIL import Image, ImageTkclass MyApp(Tk):"""繼承Tk,創建自己的桌面應用程序類"""def __init__(self):"""建構函式"""super().__init__()self.title('相簿')self.iconbitmap('res/Tk.ico')self.init_ui()self.center()def init_ui(self):"""初始化界面"""self.curr = 0self.photos = ('res/DSC03363.jpg', 'res/DSC03394.jpg', 'res/DSC03402.jpg')self.img = ImageTk.PhotoImage(Image.open(self.photos[self.curr]))self.album = Label(self, image=self.img)self.album.pack(expand=YES, fill='both', padx=5, pady=5)f = Frame(self)f.pack(padx=10, pady=20)style = ttk.Style()style.theme_use('vista')ttk.Button(f, text='<', width=10, command=self.on_prev).pack(side=LEFT, padx=10)ttk.Button(f, text='>', width=10, command=self.on_next).pack(side=LEFT, padx=10)def center(self):"""窗口居中"""self.update() # 更新顯示以獲取最新的窗口尺寸scr_w = self.winfo_screenwidth() # 獲取螢幕寬度scr_h = self.winfo_screenheight() # 獲取螢幕寬度w = self.winfo_width() # 窗口寬度h = self.winfo_height() # 窗口高度x = (scr_w-w)//2 # 窗口左上角x座標y = (scr_h-h)//2 # 窗口左上角y座標self.geometry('%dx%d+%d+%d'%(w,h,x,y)) # 設置窗口大小和位置def on_prev(self):"""前一張照片"""self.curr = (self.curr-1)%3img = ImageTk.PhotoImage(Image.open(self.photos[self.curr]))self.album.configure(image=img)self.album.image = imgdef on_next(self):"""後一張照片"""self.curr = (self.curr+1)%3img = ImageTk.PhotoImage(Image.open(self.photos[self.curr]))self.album.configure(image=img)self.album.image = imgif __name__ == '__main__':app = MyApp()app.mainloop()

                                              程式碼運行界面如下圖所示。點選前翻後翻按鈕可在多張照片之間循環切換。

                                              5.3 計算器

                                              幾乎所有的GUI課程都會用計算器作為例子,Tkinter怎能缺席呢?這個例子除了演示如何使用grid方法佈局外,還演示了在一個控制元件類的所有實例上綁定事件和事件函數,即bind_class的用法。

                                                from tkinter import *class MyApp(Tk):"""繼承Tk,創建自己的桌面應用程序類"""def __init__(self):"""建構函式"""super().__init__()self.title('計算器')self.iconbitmap('res/Tk.ico')self.init_ui()self.center()def init_ui(self):"""初始化界面"""self.screen = StringVar()self.screen.set('')Label(self, textvariable=self.screen, anchor=E, bg='#000030', fg='#30ff30', font=("Arial Bold", 16)).pack(fill=X, padx=10, pady=10)keys = [['(', ')', 'Back', 'Clear'],['7',  '8',  '9',  '/'],['4',  '5',  '6',  '*'],['1',  '2',  '3',  '-'],['0',  '.',  '=',  '+']]f = Frame(self)f.pack(padx=10, pady=10)for i in range(5):for j in range(4):if i == 0 or j == 3:Button(f, text=keys[i][j], width=8, bg='#f0e0d0', fg='red').grid(row=i, column=j, padx=3, pady=3)elif i == 4 and j == 2:Button(f, text=keys[i][j], width=8, bg='#f0e0a0').grid(row=i, column=j, padx=3, pady=3)else:Button(f, text=keys[i][j], width=8, bg='#d9e4f1').grid(row=i, column=j, padx=3, pady=3)self.bind_class("Button", "", self.on_button)def center(self):"""窗口居中"""self.update() # 更新顯示以獲取最新的窗口尺寸scr_w = self.winfo_screenwidth() # 獲取螢幕寬度scr_h = self.winfo_screenheight() # 獲取螢幕寬度w = self.winfo_width() # 窗口寬度h = self.winfo_height() # 窗口高度x = (scr_w-w)//2 # 窗口左上角x座標y = (scr_h-h)//2 # 窗口左上角y座標self.geometry('%dx%d+%d+%d'%(w,h,x,y)) # 設置窗口大小和位置def on_button(self, evt):"""響應按鍵"""if self.screen.get() == 'Error':self.screen.set('')ch = evt.widget.cget('text')if ch == 'Clear':self.screen.set('')elif ch == 'Back':self.screen.set(self.screen.get()[:-1])elif ch == '=':try:result = str(eval(self.screen.get()))except:result = 'Error'self.screen.set(result)else:self.screen.set(self.screen.get() + ch)if __name__ == '__main__':app = MyApp()app.mainloop()

                                                程式碼運行界面如下圖所示。

                                                程式碼運行界面如下圖所示

                                                5.4 秒錶

                                                以百分之一秒的頻率刷新顯示,對於任何一款GUI庫來說,都是不容小覷的負擔。不過,由於Tkinter採用了獨樹一幟的類型對象關聯控制元件機制,在計時執行緒中高速刷新標籤顯示內容卻是從容不迫、遊刃有餘。

                                                  import timeimport threadingfrom tkinter import *def on_btn():"""點選按鈕"""global t0if btn_name.get() == '開始':lcd.set('0.00')t0 = time.time()btn_name.set('停止')else:btn_name.set('開始')def watch():"""秒錶計時執行緒函數"""while True:if btn_name.get() == '停止':lcd.set('%.2f'%(time.time()-t0))else:time.sleep(0.01)root = Tk()root.title('秒錶')btn_name = StringVar() # 按鈕名btn_name.set('開始')t0 = 0 # 計時開始的時間戳lcd = StringVar() # 液晶顯示值lcd.set('0:00')f = Frame(root)f.pack(padx=20, pady=10)Label(f, textvariable=lcd, width=10, bg='#000030', fg='#30ff30', font=("Arial Bold", 24)).pack(pady=10)Button(f, textvariable=btn_name, bg='#f0e0d0', command=on_btn).pack(fill=X, pady=10)threading.Thread(target=watch).start()root.mainloop()

                                                  點選開始按鈕,秒錶自動清零並啟動計時,計時精度高達百分之一秒。程式碼運行界面如下圖所示。

                                                  5.5 畫板

                                                  Canvas元件為Tkinter的圖形繪製提供了基礎。Canvas是一個高度靈活的元件,可以用來展示圖片,也可以用來繪製圖形和圖表,創建圖形編輯器,並實現各種自定義的小部件,比如弧形、線條、橢圓形、多邊形和矩形等。

                                                    from tkinter import *import tkinter.colorchooser as tcclass MyApp(Tk):"""繼承Tk,創建自己的桌面應用程序類"""def __init__(self):"""建構函式"""super().__init__()self.title('畫板')self.iconbitmap('res/Tk.ico')self.init_ui()def init_ui(self):"""初始化界面"""self.color = '#90f010' # 當前顏色self.pen = 3 # 當前畫筆self.pos = None # 滑鼠當前位置self.rbv = IntVar() # 當前畫筆self.rbv.set(self.pen)self.cav = Canvas(self, bg='#ffffff', width=480, height=320)self.cav.pack(side=LEFT, padx=5, pady=5)self.cav.bind('', self.on_down)self.cav.bind('', self.on_up)self.cav.bind('', self.on_motion)frame = Frame(self)frame.pack(side=LEFT, anchor=N, padx=5, pady=20)Radiobutton(frame, variable=self.rbv, text='1pix', value=1, command=self.on_radio).pack(ancho='w', padx=5, pady=5)Radiobutton(frame, variable=self.rbv, text='3pix', value=3, command=self.on_radio).pack(ancho='w', padx=5, pady=5)Radiobutton(frame, variable=self.rbv, text='5pix', value=5, command=self.on_radio).pack(ancho='w', padx=5, pady=5)Radiobutton(frame, variable=self.rbv, text='7pix', value=7, command=self.on_radio).pack(ancho='w', padx=5, pady=5)Radiobutton(frame, variable=self.rbv, text='9pix', value=9, command=self.on_radio).pack(ancho='w', padx=5, pady=5)self.btn = Button(frame, text='', width=6, bg=self.color, command=self.on_btn)self.btn.pack(padx=5, pady=10)def on_radio(self):"""選擇畫筆"""self.pen = self.rbv.get()def on_btn(self):"""選擇顏色"""color = tc.askcolor()[1]if color:self.color = colorself.btn.configure(bg=self.color)def on_down(self, evt):"""左鍵按下"""self.pos = evt.x, evt.ydef on_up(self, evt):"""左鍵彈起"""self.pos = Nonedef on_motion(self, evt):"""滑鼠移動"""if not self.pos is None:line = (*self.pos, evt.x, evt.y)self.pos = evt.x, evt.yself.cav.create_line(line, fill=self.color, width=self.pen)if __name__ == '__main__':app = MyApp()app.mainloop()

                                                    這段程式碼實現了一個簡易的畫板,提供畫筆粗細和顏色選擇,拖拽滑鼠在畫板上移動即可繪製線條。程式碼運行界面如下圖所示。

                                                    集成Matplotlib

                                                    在Tkinter中使用Matplotlib繪相簿的關鍵在於,Matplotlib的後端子模組可以生成Tkinter的canvas控制元件,同時Matplotlib也可以在其上繪圖。

                                                      import numpy as npimport matplotlibmatplotlib.use('TkAgg')matplotlib.rcParams['font.sans-serif'] = ['FangSong']matplotlib.rcParams['axes.unicode_minus'] = Falsefrom matplotlib.backends.backend_tkagg import FigureCanvasTkAggfrom matplotlib.figure import Figurefrom tkinter import *class MyApp(Tk):"""繼承Tk,創建自己的桌面應用程序類"""def __init__(self):"""建構函式"""Tk.__init__(self)self.title('集成Matplotlib')self.iconbitmap('res/Tk.ico')self.init_ui()self.center()def init_ui(self):"""初始化界面"""self.fig = Figure(dpi=150)self.cv = FigureCanvasTkAgg(self.fig, self)self.cv.get_tk_widget().pack(fill=BOTH, expand=1, padx=5, pady=5)f = Frame(self)f.pack(pady=10)Button(f, text='散點圖', width=12, bg='#f0e0d0', command=self.on_scatter).pack(side=LEFT, padx=20)Button(f, text='等值線圖', width=12, bg='#f0e0d0', command=self.on_contour).pack(side=LEFT, padx=20)def center(self):"""窗口居中"""self.update() # 更新顯示以獲取最新的窗口尺寸scr_w = self.winfo_screenwidth() # 獲取螢幕寬度scr_h = self.winfo_screenheight() # 獲取螢幕寬度w = self.winfo_width() # 窗口寬度h = self.winfo_height() # 窗口高度x = (scr_w-w)//2 # 窗口左上角x座標y = (scr_h-h)//2 # 窗口左上角y座標self.geometry('%dx%d+%d+%d'%(w,h,x,y)) # 設置窗口大小和位置def on_scatter(self):"""散點圖"""x = np.random.randn(50) # 隨機生成50個符合標準正態分佈的點(x座標)y = np.random.randn(50) # 隨機生成50個符合標準正態分佈的點(y座標)color = 10 * np.random.rand(50) # 隨即數,用於對映顏色area = np.square(30*np.random.rand(50)) # 隨機數表示點的面積self.fig.clear()ax = self.fig.add_subplot(111)ax.scatter(x, y, c=color, s=area, cmap='hsv', marker='o', edgecolor='r', alpha=0.5)self.cv.draw()def on_contour(self):"""等值線圖"""y, x = np.mgrid[-3:3:60j, -4:4:80j]z = (1-y**5+x**5)*np.exp(-x**2-y**2)self.fig.clear()ax = self.fig.add_subplot(111)ax.set_title('有填充的等值線圖')c = ax.contourf(x, y, z, levels=8, cmap='jet')self.fig.colorbar(c, ax=ax)self.cv.draw()if __name__ == '__main__':app = MyApp()app.mainloop()

                                                      程式碼運行界面如下圖所示。

                                                      程式碼運行界面如下圖所示

                                                      原文連結:https://xufive.blog.csdn.net/article/details/124514094

                                                      相關文章

                                                      在 MacOS 上運行 Docker 太慢!

                                                      在 MacOS 上運行 Docker 太慢!

                                                      你是否也覺得,MacOS 中的 Docker 非常慢?本文作者想出了解決辦法,不妨來試試看。 原文連結:https://www.paolom...

                                                      滑鼠之父誕生 | 歷史上的今天

                                                      滑鼠之父誕生 | 歷史上的今天

                                                      整理 | 王啟隆 透過「歷史上的今天」,從過去看未來,從現在亦可以改變未來。 今天是 2022 年 1 月 30 日,在 203 年前的今天...