你觉得这篇文章怎么样? 帮助我们为您提供更好的内容。
Thank you! Your feedback has been received.
There was a problem submitting your feedback, please try again later.
你觉得这篇文章怎么样?
作者 | 張嘉鈞 |
難度 | 普通 |
材料表 | Windows 或Linux 環境電腦 |
Tkinter 簡介
因為近年來Python的使用率大幅提升,根據TIOBE的資料顯示2020年全球的程式語言使用率Python穩居第三,而PyPl的套件安裝Python的數量則是第一名。為什麼提到這件事呢? Tkinter是TK GUI整合到Python中的GUI開發套件,更白話一點就是Python內建的GUI設計套件,當你使用Python開發專案的時候可以考慮使用Tkinter來製作操作介面。在Python開發GUI的榜上前兩位是Tkinter與PyQt,兩者的差別:pyqt整合度高、有圖形化介面可以使用;Tkinter因為是Python自帶的GUI套件,功能簡單但效能可能更好。如果要追求高顏質的介面,我比較推薦pyqt,但如果是只簡單的介面設計,追求效率就選擇Tkinter吧!
1、第一個 Tkinter程式 : Hello World
我們來建構一個簡單的視窗並且顯示Hello World吧!
步驟一、導入 Tkinter 函式庫 ( Python 3 ),如果使用Python 2 則T要大寫
import tkinter as tk
步驟二、定義一個視窗 名叫 window
window = tk.Tk()
步驟三、設定標題
window.title('window')
步驟四、設定像素大小
window.geometry('600x800')
步驟五、宣告一個標籤
lbl_1 = tk.Label(window, text='Hello World', bg='yellow', fg='#263238', font=('Arial', 12))
步驟六、設定放置的位置 ( 使用 grid 佈局 )
lbl_1.grid(column=0, row=0)
步驟七、主視窗迴圈顯示
window.mainloop()
完整程式碼如下:
import tkinter as tk
window = tk.Tk()
window.title('window')
window.geometry('500x100')
lbl_1 = tk.Label(window, text='Hello World', bg='yellow', fg='#263238', font=('Arial', 12))
lbl_1.grid(column=0, row=0)
window.mainloop()
這樣就完成一個簡單的GUI介面囉!結果如下:
2、視窗元件總攬
這邊順便提供一個很實用的教學文件:https://tkdocs.com/tutorial/index.html。
類別 | 介紹 |
Frame | 視窗。 |
Label | 文字標籤。 |
Button | 按鈕。 |
Canvas | 可以用來繪圖、文字等都可以,像我就會來拿放圖片。 |
Checkbutton | 核取按鈕。 |
Entry | 文字輸入欄。 |
Listbox | 列表選單。 |
Menu | 選單列的下拉式選單。 |
LabelFrame | 文字標籤視窗。 |
MenuButton | 選單的選項。 |
Message | 類似 Label ,可多行。 |
OptionMenu | 下拉式的選項選單。 |
PaneWindow | 類似 Frame ,可包含其他視窗元件。 |
Radiobutton | 單選按鈕。 |
Scale | 拉桿。 |
Scrollbar | 捲軸。 |
Spinbox | 微調器 |
Text | 文字方塊。 |
Toplevel | 新增視窗。 |
3、簡單的元件實作
由於元件的參數或詳細用法網路上已經有很多介紹了,所以我這邊快速帶過。
3-1. 標籤 ( Label )
我們在宣告標籤的時候,需要先宣告一個標籤,再給予位置,如果沒有給予位置資訊的話將不會被放在視窗上面,這邊我們使用grid來告訴視窗標籤要放在該容器中的 (0, 0) 這個位置,此外沒有給定視窗寬、高大小的話,視窗會依照Widget大小而自動去調整:
import tkinter as tk
window = tk.Tk()
window.title('window')
def create_label(txt):
lbl_1 = tk.Label(window, text=txt, bg='yellow', fg='#263238', font=('Arial', 12), width=100, height=2)
lbl_1.grid(column=0, row=0)
create_label('Hello World !!!')
window.mainloop()
稍微介紹一下元件的參數,大部分的元件在宣告的時候,可以引入的參數都雷同,第一個是你要放在哪個容器當中,我們給予window,代表這個標籤會放在window當中,而text代表文字,bg是background背景,fg是foreground前景,可以當作是文字的顏色,font是文字的格式width、height是寬與高,這邊要注意的是他們的單位是字元寬度、字元高度,而不是像素值,此外還有很多屬性可以用像是邊框、影像、對齊樣式等等。
3-2. 按鈕 ( Button )
上方宣告 label的時候是將 width 跟 height 帶入到宣告的時候,也有另外一個做法,就是使用字典的方式宣告屬性,bt_1 的 ‘width’ 屬性要定義成如何,這邊可以注意到 width 是定義字元的寬、height是定義字元的高,我個人覺得tkinter的元件大小很難控制,與解析度大小沒有一定的關係,待會在教怎麼樣可以平均大小跟自動縮放;此外這邊還有提供 activebackground跟activeforeground為按下按鈕的背景、前景顏色變化。
import tkinter as tk
window = tk.Tk()
window.title('window')
def create_button(txt):
bt_1 = tk.Button(window, text=txt, bg='red', fg='white', font=('Arial', 12))
bt_1['width'] = 50
bt_1['height'] = 4
bt_1['activebackground'] = 'red' # 按鈕被按下的背景顏色
bt_1['activeforeground'] = 'yellow' # 按鈕被按下的文字顏色 ( 前景 )
bt_1.grid(column=0, row=0)
create_button('Button')
window.mainloop()
3-3. 使用Label顯示圖片
這邊要注意的是需要將圖片轉成Tkinter 可以讀的格式:
import tkinter as tk
from PIL import Image, ImageTk
window = tk.Tk()
window.title('window')
# 放照片在UI上
def create_label_image():
img = Image.open('./images/cat_1.jpg') # 讀取圖片
img = img.resize( (img.width // 10, img.height // 10) ) # 縮小圖片
imgTk = ImageTk.PhotoImage(img) # 轉換成Tkinter可以用的圖片
lbl_2 = tk.Label(window, image=imgTk) # 宣告標籤並且設定圖片
lbl_2.image = imgTk
lbl_2.grid(column=0, row=0) # 排版位置
create_label_image()
window.mainloop()
4、我的設計方法與Grid 佈局
在開始實作複雜的GUI前,我先介紹一下我設計GUI時的做法以及使用Grid編排的方式。首先,在設計一個GUI之前我會先進行空間的區隔。
我會透過frame按照上方的圖繪分成四個顏色的區塊,灰色是主要視窗,藍色為顯示圖片的區塊,橘色為顯示文字的區塊,綠色為按鈕的區塊。
實作程式碼如下:
import tkinter as tk
from PIL import Image, ImageTk
window = tk.Tk()
window.title('Window')
div_size = 200
img_size = div_size * 2
div1 = tk.Frame(window, width=img_size , height=img_size , bg='blue')
div2 = tk.Frame(window, width=div_size , height=div_size , bg='orange')
div3 = tk.Frame(window, width=div_size , height=div_size , bg='green')
div1.grid(column=0, row=0, rowspan=2)
div2.grid(column=1, row=0)
div3.grid(column=1, row=1)
顯示結果如下:
但目前會有縮放的問題,當你放大之後他的大小仍然會保持原樣,不會跟著縮放:
倘若要能夠伸縮需要使用到 columnconfigure、rowconfigure,其中可以用的參數:
minsize |
最小的視窗大小( pixel ) |
pad |
上下左右各添加多少 ( pixel ) |
weight |
如果weight=0的話就不會進行縮放的動作,可以想像是權重值 ( 1 除以 weight ),參考下列範例當有兩個元件需要定義網格的時候:
這時候Tkinter會將六分之一的空間分配給第0欄的元件,其餘六分之五給第二欄的元件。 如果大小都是填1則是各一半
|
接下來針對先前寫好的frame添加權重,這邊我先寫了一個副函式 ( define_layout ) 用來定義grid,引入obj為UI widget、cols該widget中有幾欄、row該widget中有幾列:
def define_layout(obj, cols=1, rows=1):
def method(trg, col, row):
for c in range(cols):
trg.columnconfigure(c, weight=1)
for r in range(rows):
trg.rowconfigure(r, weight=1)
if type(obj)==list:
[ method(trg, cols, rows) for trg in obj ]
else:
trg = obj
method(trg, cols, rows)
接著套用到剛剛的框架,稍微修改了一些地方,主要在grid的部分加上了 pad以及 sticky,pad是向外拓展 ( pixel );sticky 是對齊方式,這邊用 align_mode 來統一所有的對其方式,而給予字串 ‘nswe’ 是置中的意思 ( n 上 s 下 w左 e 右):
window = tk.Tk()
window.title('Window')
align_mode = 'nswe'
pad = 5
div_size = 200
img_size = div_size * 2
div1 = tk.Frame(window, width=img_size , height=img_size , bg='blue')
div2 = tk.Frame(window, width=div_size , height=div_size , bg='orange')
div3 = tk.Frame(window, width=div_size , height=div_size , bg='green')
div1.grid(column=0, row=0, padx=pad, pady=pad, rowspan=2, sticky=align_mode)
div2.grid(column=1, row=0, padx=pad, pady=pad, sticky=align_mode)
div3.grid(column=1, row=1, padx=pad, pady=pad, sticky=align_mode)
定義好UI之後在來處理佈局問題,先來看第一行要注意的地方是如果下層UI要套用權重上層的也一定要套用,所以如果三個frame要套用的話,最主要的視窗window也需要使用weight分配:
define_layout(window, cols=2, rows=2)
define_layout([div1, div2, div3])
執行下去可以看到進行縮放的時候,我們的三個frame也會跟著拉伸:
接著再把需要的UI項目放進去,完整程式碼如下:
window = tk.Tk()
window.title('Window')
align_mode = 'nswe'
pad = 5
div_size = 200
img_size = div_size * 2
div1 = tk.Frame(window, width=img_size , height=img_size , bg='blue')
div2 = tk.Frame(window, width=div_size , height=div_size , bg='orange')
div3 = tk.Frame(window, width=div_size , height=div_size , bg='green')
window.update()
win_size = min( window.winfo_width(), window.winfo_height())
print(win_size)
div1.grid(column=0, row=0, padx=pad, pady=pad, rowspan=2, sticky=align_mode)
div2.grid(column=1, row=0, padx=pad, pady=pad, sticky=align_mode)
div3.grid(column=1, row=1, padx=pad, pady=pad, sticky=align_mode)
define_layout(window, cols=2, rows=2)
define_layout([div1, div2, div3])
im = Image.open('./images/cat_1.jpg')
imTK = ImageTk.PhotoImage( im.resize( (img_size, img_size) ) )
image_main = tk.Label(div1, image=imTK)
image_main['height'] = img_size
image_main['width'] = img_size
image_main.grid(column=0, row=0, sticky=align_mode)
lbl_title1 = tk.Label(div2, text='Hello', bg='orange', fg='white')
lbl_title2 = tk.Label(div2, text="World", bg='orange', fg='white')
lbl_title1.grid(column=0, row=0, sticky=align_mode)
lbl_title2.grid(column=0, row=1, sticky=align_mode)
bt1 = tk.Button(div3, text='Button 1', bg='green', fg='white')
bt2 = tk.Button(div3, text='Button 2', bg='green', fg='white')
bt3 = tk.Button(div3, text='Button 3', bg='green', fg='white')
bt4 = tk.Button(div3, text='Button 4', bg='green', fg='white')
bt1.grid(column=0, row=0, sticky=align_mode)
bt2.grid(column=0, row=1, sticky=align_mode)
bt3.grid(column=0, row=2, sticky=align_mode)
bt4.grid(column=0, row=3, sticky=align_mode)
bt1['command'] = lambda : get_size(window, image_main, im)
define_layout(window, cols=2, rows=2)
define_layout(div1)
define_layout(div2, rows=2)
define_layout(div3, rows=4)
window.mainloop()
最終結果:
>
</p
那布局的使用方法,大家在這裡應該也練習得差不多了,接下來會提供幾個常用的功能,像是全螢幕、按按鈕互動、即時影響讀取等等。
6、進階GUI實作
在這裡為了參數調用的方便,我會使用Class來完成接下來的實作。
6-1. 全螢幕視窗 ( bind )
我們預設按下F12的時候切換成全螢幕視窗,在按下一次則返回,這邊會使用到 bind 的函式,這個是可以將函式綁定到動作上面,像是按下左鍵、放開左鍵、按下F12等。首先我們要先能夠將視窗全螢幕,在Windows環境下我們將去調整他的attributes成為 ‘-fullscreen,而Linux環境下則調整成 ‘-zoomed’,範例程式如下:
window.attributes('-fullscreen', True) # For Windows
window.attributes('-zoomed', True) # For Linux
接著我們要想辦法辨識系統環境,在Python中自帶一個函式庫叫 Platform,可以使用Platform.system()得知現在的環境,在我的程式當中如果系統是Windows的話就回傳1不是的話就回傳 0 ,進而再給予特定的全螢幕變數。
def toggle_fullScreen(self, event):
is_windows = lambda : 1 if platform.system() == 'Windows' else 0
self.isFullScreen = not self.isFullScreen
self.window.attributes("-fullscreen" if is_windows() else "-zoomed", self.isFullScreen)
接著使用bind將動作連結到toggle_fullScreen函式:
# 切換全螢幕
self.isFullScreen = False
self.window.bind('<F12>', self.toggle_fullScreen)
接著就可以執行看看了,在給完整程式碼之前可以先看一下結果:
由於全螢幕的關係工具列也會被取消掉,不過我常做的GUI都會帶一個關閉程式的按鈕,或者我們也可以透過 bind 將 ESC 按鍵綁定關閉視窗:
def del_window(self, event):
self.window.destroy()
self.window.bind('<Escape>', self.del_window)
完整程式碼如下:
self.window = tk.Tk()
self.window.title('Window')
im = Image.open('./images/cat.jpg').resize( (300, 300) )
imTK = ImageTk.PhotoImage( im )
self.lbl_img = tk.Label(self.window, image=imTK)
self.lbl_img.image = imTK
self.lbl_img.grid(column=0, row=0, sticky='nwes')
# 切換全螢幕
self.isFullScreen = False
self.window.bind('<F12>', self.toggle_fullScreen)
self.window.mainloop()
6-2. 按按鈕開啟圖像 ( command )
整體設計概念很簡單,在一開始的時候由frame將按鈕及圖片顯示區塊隔開,宣告個別元件。
def define_layout(self, obj, cols=1, rows=1):
def method(trg, col, row):
[ trg.columnconfigure(c, weight=1) for c in range(cols) ]
[ trg.rowconfigure(r, weight=1) for r in range(rows) ]
if type(obj)==list:
[ method(trg, cols, rows) for trg in obj ]
else:
method(obj, cols, rows)
self.window = tk.Tk()
self.window.title('Window')
self.align_mode = 'nsew'
self.pad = 10
self.div_size, self.img_size = 200, 400
self.div1 = tk.Frame(self.window, width=self.div_size , height=self.div_size)
self.div2 = tk.Frame(self.window, width=self.img_size , height=self.img_size)
self.div1.grid(column=0, row=0, padx=self.pad, pady=self.pad, sticky=self.align_mode)
self.div2.grid(column=0, row=1, padx=self.pad, pady=self.pad, sticky=self.align_mode)
self.bt1 = tk.Button(self.div1, text='Cat')
self.bt1.grid(column=0, row=0, sticky=self.align_mode)
self.bt2 = tk.Button(self.div1, text='Dog')
self.bt2.grid(column=1, row=0, sticky=self.align_mode)
self.bt3 = tk.Button(self.div1, text='Clear')
self.bt3.grid(column=2, row=0, sticky=self.align_mode)
self.bt4 = tk.Button(self.div1, text='Quit')
self.bt4.grid(column=3, row=0, sticky=self.align_mode)
self.define_layout(self.window, cols=1, rows=2)
接著先來顯示圖片,由於一開始開啟程式希望是沒有圖片的所以給予空白值,但因為是空白值寬高也是0,圖片區域的frame會因此變得很小,所以我將該frame的參數grid_propagate() 設為False,這個動作目的是要不要讓該frame被子元件的圖片大小給影響,也就是維持一開始預設的大小:
self.imTK = '' # 預設給空白
self.lbl_img = tk.Label(self.div2, image=self.imTK)
self.lbl_img.image = self.imTK
self.lbl_img.grid(column=0, row=0, sticky=self.align_mode)
self.div2.grid_propagate(0) # 不會被子元件改變大小
接著就是按鈕事件的宣告,按下按鈕的時候希望執行什麼動作:
def bt1_event(self):
im = Image.open('./images/cat_1.jpg')
self.imTK = ImageTk.PhotoImage( im.resize( (self.img_size, self.img_size) ) )
self.lbl_img.configure(image=self.imTK) # image有時會被清除
self.lbl_img.image = self.imTK # 防止圖片被垃圾清掃給除掉
def bt2_event(self):
im = Image.open('./images/dog_1.jpg')
self.imTK = ImageTk.PhotoImage( im.resize( (self.img_size, self.img_size) ) )
self.lbl_img.configure(image=self.imTK) # image有時會被清除
self.lbl_img.image = self.imTK # 防止圖片被垃圾清掃給除掉
def bt3_event(self):
self.lbl_img.configure(image='')
# 綁定按鈕事件
self.bt1['command'] = self.bt1_event
self.bt2['command'] = self.bt2_event
self.bt3['command'] = self.bt3_event
# self.bt4['command'] = lambda : self.window.destroy()
self.bt4.bind('<Button-1>', self.del_window)
可以注意到綁定方法與剛剛全螢幕視窗的做法不太相同,可以在宣告按鈕的時候直接用command綁定事件,也可以透過我這種方式額外宣告,在bt4_event可以看到用bind的方法也是可以的,只是要去找一下對應的動作參數是什麼,像在這裡如果bt4被左鍵點擊定義為 <’Button-1’>,除此之外我也提供了lambda的寫法,可以再參考看看。
6-3. 隨視窗大小改變圖片大小(靜態) – bind 延伸
從上一個案例開始應用到bind之後,我們可以在視窗大小改變的時候 ( Configure ) 或許該視窗的大小或某一個元件的大小,實驗程式如下,大家可以玩玩看,注意這個是靜態的,只有在點擊按鈕生成圖片的時候才會符合縮放後的大小:
def bt1_event(self):
im = Image.open('./images/cat_1.jpg')
self.imTK = ImageTk.PhotoImage( im.resize( (self.w, self.h) ) )
self.lbl_img.configure(image=self.imTK)
self.lbl_img.image = self.imTK
def get_size(self, event, obj=''):
trg_obj = self.window if obj == '' else obj
self.w, self.h = trg_obj.winfo_width(), trg_obj.winfo_height()
print(f'\r{(self.w, self.h)}', end='')
# 每次改變狀態,都會獲取 某元件 大小
self.window.bind('<Configure>', lambda event, obj=self.div2 :self.get_size(event, obj))
# 按鈕的部分
self.bt1['command'] = self.bt1_event
6-4. 開啟攝影機進行即時影像擷取 (after)
即時影像的部分會帶到一個新的使用方法以及一個新的UI元件,第一個元件,由於是即時影像,如果有寫過OpenCV即時影像擷取的人應該知道,需要寫一個While迴圈不斷擷取幀 ( Frame ),並且透過不斷顯示獲取到的幀來構成一個即時影像畫面,而在Tkinter中也是要如此,但這邊我們會使用 「after」來模擬While迴圈;另個要介紹的Widget是對話框,我個人平常不會寫但是有時候蠻好玩的,所以也記錄下來使用方法。
先來完成簡單版本的即時影像擷取,先宣告一個架構出來:
window = tk.Tk()
window.title('Video Stream')
main = tk.Frame(window, bg="white")
main.grid()
video = tk.Label(main)
video.grid()
window.bind('<Escape>', lambda event: window.destroy())
接下來透OpenCV取得攝影機以及進行即時影像擷取,使用after來模擬While不停執行的狀況,這邊10是指10「毫秒」:
# 宣告攝影機
status, frame = 0, []
cap = cv2.VideoCapture(0)
def stream():
# 讀取當前的影像
global status, frame
status, frame = cap.read()
# 如果有影像的話
if status:
# 將 OpenCV 色改格式 ( BGR ) 轉換成 RGB
im_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
# 將 OpenCV 圖檔轉換成 PIL
im_pil = Image.fromarray(im_rgb)
# 轉換成 ImageTK
imgTK = ImageTk.PhotoImage(image=im_pil)
# 放入圖片
video.configure(image=imgTK)
# 防止圖片丟失,做二次確認
video.image = imgTK
# 10 豪秒 後執行 stream 函式,這裡是模擬 While 迴圈的部分
window.after(10, stream)
# 先執行一次
stream()
window.mainloop()
# 釋出攝影機記憶體、關閉所有視窗
cap.release()
cv2.destroyAllWindows()
對話框的部分,加在主程式裡就可以了:
def quit(self, event):
quit_check = tk.messagebox.askokcancel('提示', '是否要退出?')
if quit_check:
print('離開程式')
cv2.destroyAllWindows()
self.cap.release()
self.window.destroy()
self.window.bind('<Escape>', self.quit)
6-5. 圖片隨著視窗大小而改變 (動態)
有了模擬迴圈的功能,我們可以來玩玩看圖片的動態縮放了!由於如果一直即時改變會相當的消耗資源,所以我這邊有設定參數resize_rate,可以決定幾秒更新一次,使用的架構是之前寫的範例,先來看看結果吧!
首先是每次視窗更動的時候擷取大小:
def get_size(self, event):
self.w = self.div2.winfo_width()
self.h = self.div2.winfo_height()
self.window.bind('<ButtonRelease>', lambda event: self.get_size)
接著是Update的部分,這裡的邏輯是「當我沒有縮放視窗的時候不斷獲取當前時間,一旦更動視窗大小N秒後進行縮放,進行縮放的時候會抓取最後的視窗大小」
def update(self):
if self.w == self.div2.winfo_width() and self.h ==self.div2.winfo_height():
self.reszie_time = time.time()
if self.w != self.div2.winfo_width() or self.h !=self.div2.winfo_height():
if time.time()-self.reszie_time>= self.resize_rate and self.im is not '':
self.w, self.h = self.div2.winfo_width(), self.div2.winfo_height()
self.imTK = ImageTk.PhotoImage( self.im.resize( (self.w, self.h) ) )
self.lbl_img.configure(image=self.imTK)
self.lbl_img.image = self.imTK
self.window.after(10, self.update)
self.reszie_time, self.resize_rate = 0, 0.5
self.w, self.h = self.div2.winfo_width(), self.div2.winfo_height()
self.update()
self.window.mainloop()
結語
經過一連串小範例的實作,是否了解Tkinter的用法了?當然如果要熟悉的話還是多找幾個小專題是做看看,一定會越來越厲害的。一些邊緣裝置專題分享,或多或少都會結合小的螢幕去做觸碰操作或顯示,這時候製作GUI就很重要了!接下來的文章會模擬工廠產線問題進行小專題製作,屆時也會製作一個小介面供使用者使用,更多小技巧會收錄哦!
相關文章
Python is TIOBE's Programming Language of 2020!
评论