學習設計圖形用戶界面(簡稱 GUI)也許是一件令人苦惱的事兒,各種 GUI 專屬名稱,各種設計元素等讓人眼花繚亂。為了讓 GUI 設計不再是一件令人痛苦的事,PySimpleGUI 提供了一個十分 Pythonic 且學習周期短,易于擴展的接口。
1 PySimpleGUI 簡介
PySimpleGUI(https://github.com/PySimpleGUI/PySimpleGUI)倉庫對 tkinter, Qt, Remi, WxPython 進行封裝,使得 GUI 開發更加人性化。下面僅僅討論 pip install PySimpleGUI
獲得的基于 tkinter 的模塊。 本文討論的 PySimpleGUI 是 '4.14.0 Released 23-Dec-2019' 版本的模塊,它有兩個十分重要的基礎類:Element
, Window
。Element
構成了 PySimpleGUI 設計的 GUI 界面的基本元素,常用的子類有:Button
,ButtonMenu
,Canvas
,Graph
,Frame
,Tab
,TabGroup
,Column
,Pane
,Checkbox
,Radio
,Combo
,Image
,InputText
,Listbox
,Menu
,Multiline
,Output
,Text
。它們代表了 GUI 的基本組件(或者稱其為小部件)。Window
則創建了 GUI 的窗口界面。
下面直接以例子來講解這些類的使用方法。
2 從一個簡單的例子開始
現以一個簡單的例子作為 GUI 開發的引入:
import PySimpleGUI as sg
# 改變 Window 的主題
sg.theme('DarkAmber')
# 定義 Window 的布局
layout = [
[sg.Text('第一行:寫一些說明性的文字')],
[sg.Text('第二行:寫入說明性文字'), sg.InputText()],
[sg.Button('確認'), sg.Button('取消')]
]
# 創建一個 Window
window = sg.Window('Window 的標題', layout)
# 獲取 Window 的“事件”以及“取值”的循環
while 1:
event, values = window.read()
# 對 event 進行邏輯選擇
if event in (None, '取消'):
break
else:
print('您鍵入的值是', values[0])
window.close() # 關閉 Window
代碼雖然很短,但也基本交代清楚了 GUI 設計的思路:
-
sg.theme('DarkAmber')
設置 Window 的主題;(可選的) - 定義 Window 的布局
layout
; - 將
layout
傳入sg.Window
用以創建 Window; - 在一個循環里通過
window.read()
獲取Window 的“事件”以及“取值”; - 對
event
(有時也會用到values
)進行邏輯選擇; - 防止資源泄露,最后需要
window.close()
關閉 Window。
我們看看最終該代碼生成了什么樣的 Window?效果圖見圖1:
從圖1 可以看出:
- 元素(或稱小部件)
sg.Text
用于在 Window 上打印文字; - 元素
sg.Button
組成了 Window 的“按鈕”,當您點擊按鈕會觸發一些事件; - 元素
sg.InputText()
(可以簡寫為sg.Input()
)用于記錄用戶使用鍵盤輸入的信息,并以dict
的形式保存在values
之中。即values
的值為{'0': 用戶輸入的信息}
,這里的'0'
是 Window 中的類似于sg.InputText()
的元素的返回值的序號。
再來看看 layout
,它是由 [[...],...]
這樣的二維列表數據進行 Window 的布局設計的。具體而言,即 [[a], [b]]
表示了兩行的 Window 布局,第一行由元素 a
構建,第二行由元素 b
進行構建。
可以看此 layout
是 Window 的核心,它定義了 Window 的布局,所以,接下來的內容我們主要關注如何創建 layout
。
3 Window 的布局設計
前文介紹了 sg.Window
的元素 sg.Text
,sg.Button
,sg.InputText()
實現了 Window 的顯示和事件觸發機制。但是這些功能太單一了,接下來需要了解如何創建更加復雜的 Window 布局。
3.1 同步更新 Window 的信息
例2:您也許會有這樣一種需求:通過按鈕實現同步更新 Window 的信息的功能。該功能的實現需要借助 sg.Window
的 update
實例方法進行實現,在例1 中我們使用 print
函數打印 values
的值,但是其值并沒有顯示在 Window 之中,為了讓其值在 Window 中顯示,需要修改例1 為:
import PySimpleGUI as sg
# 改變 Window 的主題
sg.theme('DarkAmber')
# 定義 output 的輸出文本的樣式
output_text = {
'key' : 'output', # 文本 Key
'size' : (25, 1), # 文本占用字符框size為 25x1
'text_color' : 'white', # 文本顏色
'background_color' : 'red', # 背景顏色
'font' : ('Times', 16) # 設置字體 family 與 size
}
# 創建 Window 的布局
layout = [
[sg.Text('第一行:寫一些說明性的文字')],
[sg.Text('第二行:寫入說明性文字'), sg.InputText()],
[sg.Text('您鍵入的值是:'), sg.Text(**output_text)],
[sg.Button('確認'), sg.Button('取消')]
]
# 創建一個 Window
window = sg.Window('Window 的標題', layout)
# 獲取 Window 的“事件”以及“取值”的循環
while 1:
event, values = window.read()
if event in (None, '取消'):
break
else:
# 更新 window['output'] 的值
window['output'].update(values[0])
print(event, values)
window.close() # 關閉 Window
顯示的結果見圖2:
從圖2 可以看到 sg.Text
的主題風格是可以修改的,output_text
設置了文本框的一些屬性,其中 'size','text_color','background_color' 用于指定文本框的大小,顏色以及背景顏色;'font' 則指定了文字的字體與字號大小。指定 sg.Text
的 'key' 方便 sg.Window
查找并修改其值。
這里的 Window 只有一個 sg.InputText()
元素,所以使用 values[0]
即可獲取其值,但是,如果有多個 sg.InputText()
元素,直接使用序號獲取其值便不是很方便,為此,您需要為 sg.InputText()
傳入 'key' 參數,讓 sg.Window
可以通過 'key' 來獲取其值。
關于 event, values = window.read()
,其中 event
是 Wundow
包括:1) 點擊 Button;2) 使用 X 關閉 Window。一般地, values
收集的是 Wundow
的sg.InputText()
元素。
3.2 設置 Window 的菜單
例3:Window 的菜單是大多數 GUI 的必選元素,下面就以創建備忘錄為例來說明如何創建菜單欄:
import PySimpleGUI as sg
# 預設
sg.change_look_and_feel('DarkTeal7')
# 定義菜單選項
menu_def = [['文件', ['載入', '保存']], ['關閉', ['確認', '取消']]]
layout = [[sg.Menu(menu_def)], # 定義菜單欄
[sg.Text('To Do List', font='Helvetica 15',
relief=sg.RELIEF_GROOVE)]] # 定義文本框的風格樣式
layout += [[sg.Text(k),
sg.Checkbox('', default=True if k == 1 else False),
sg.Input()] for k in range(4)]
window = sg.Window("",layout,
grab_anywhere=True, # 非阻塞
no_titlebar=True # 移除標題欄
)
# 事件循環
while 1:
event, values = window.read()
print(event, values)
if event in (None, '確認'):
break
elif event == '保存':
window.save_to_disk('ToDoList.out')
elif event == '載入':
window.load_from_disk('ToDoList.out')
window.close()
使用 sg.Menu(menu_def)
創建了菜單欄,在 sg.Text
中參數 relief
定義了文本框的風格樣式。sg.Checkbox
(可簡寫為 sg.CBox
)是用來創建復選框的 Window 元素,如果其參數default
賦值為 True
,則該復選框是被選中的,即打勾。具體的效果見圖3:
當您填寫好表單后,點擊菜單欄的'文件'按鈕下的'保存'選項,則會利用 window.save_to_disk('ToDoList.out')
將這個 Window 的配置保存到本地磁盤,效果見圖4:
接著,您點擊菜單欄的'關閉'按鈕下的確認選項,則會關閉 Window,效果見圖5:
當您再次打開 Window 時,點擊菜單欄的'文件'按鈕下的'載入'選項,則會利用 window.load_to_disk('ToDoList.out')
將這個 Window 的配置從本地磁盤重新載入。
有時,需要為菜單欄的選項設置快捷鍵,您可以修改 menu_def
為:
menu_def = [['文件(&F)', ['載入(&L)', '保存(&S)']], ['關閉(&C)', ['確認(&Y)', '!取消(&N)']]]
即通過在字母前添加 &
來設置快捷鍵為 Alt
+ 對應的字母。而在字符串最前方添加 !
將設定該選項為不可選。具體的效果圖見圖6:
有時,需要使用鼠標右鍵的菜單,您需要修改代碼為:
import PySimpleGUI as sg
# 預設
sg.change_look_and_feel('DarkTeal7')
# 定義菜單選項
menu_def = [['文件(&F)', ['載入(&L)', '保存(&S)']], ['關閉(&C)', ['確認(&Y)', '!取消(&N)']]]
right_click_menu = menu_def[0]
layout = [[sg.Menu(menu_def)],
[sg.Text('To Do List', font='Helvetica 15',
relief=sg.RELIEF_GROOVE)]]
layout += [[sg.Text(k),
sg.Checkbox('', default=True if k == 1 else False),
sg.Input()] for k in range(4)]
window = sg.Window("",layout,
grab_anywhere=True, # 非阻塞
no_titlebar=True, # 移除標題欄
right_click_menu = right_click_menu # 添加右鍵菜單
)
# 事件循環
while 1:
event, values = window.read()
print(event, values)
if event in (None, '確認(Y)'):
break
elif event == '保存(S)':
window.save_to_disk('ToDoList.out')
elif event == '載入(L)':
window.load_from_disk('ToDoList.out')
window.close()
這樣,只要您在 sg.Text
或者 sg.Input
生成的 Window 元素所在的位置單擊鼠標右鍵便會彈出一個右鍵菜單,具體的效果見圖7 和圖8:
有時還需要按鈕菜單:
import PySimpleGUI as sg
# 預設
sg.change_look_and_feel('DarkTeal7')
# 定義菜單選項
menu_def = [['文件(&F)', ['載入(&L)', '保存(&S)']], ['關閉(&C)', ['確認(&Y)', '!取消(&N)']]]
right_click_menu = menu_def[0]
button_menu = ['提交(&M)', ['確認(&Y)', '取消(&N)']]
layout = [[sg.Menu(menu_def)],
[sg.Text('To Do List', font='Helvetica 15',
relief=sg.RELIEF_GROOVE)]]
layout += [[sg.Text(k),
sg.Checkbox('', default=True if k == 1 else False),
sg.Input()] for k in range(4)]
layout += [[sg.ButtonMenu('關閉', menu_def=button_menu, key='關閉')]]
window = sg.Window("",layout,
grab_anywhere=True, # 非阻塞
no_titlebar=True, # 移除標題欄
right_click_menu = right_click_menu # 添加右鍵菜單
)
# 事件循環
while 1:
event, values = window.read()
print(event, values)
if event in (None, '確認(Y)'):
break
elif event == '保存(S)':
window.save_to_disk('ToDoList.out')
elif event == '載入(L)':
window.load_from_disk('ToDoList.out')
elif event == '關閉':
if 'Y' in values['關閉']:
break
window.close()
顯示的效果圖見圖9:
4 設計計算機視覺的圖形用戶界面
前面 3 節的內容已經滿足對 GUI 開發的基礎,接下來便可以開發計算機視覺的圖形用戶界面。
4.1
import PySimpleGUI as sg
# 預設
sg.change_look_and_feel('LightGreen')
sg.set_options(element_padding=(0, 0))
version = '0.0.1'
# 定義菜單選項
menu_def = [
['文件(&F)', ['打開文件(&O)', '打開文件夾(&U)',
'保存(&S)', '屬性(&P)', '退出(&X)']],
['編輯(&E)', ['復制(&C)', '修改(&M)']],
['工具箱(&T)', ['---', '載入標簽(&L)']],
['幫助(&H)', '關于(&A)']
]
layout = [[sg.Menu(menu_def, tearoff=True, pad=(20,1))],
[sg.Output(size=(60,20), key='output')], # print() 的顯示結果
]
window = sg.Window("計算機視覺",layout,default_element_size=(12, 1),
grab_anywhere=True, # 非阻塞
)
# 事件循環
while True:
event, values = window.read()
print('Event = ', event)
if event in (None, '退出(X)'):
break
elif 'A' in event:
window.disappear() # 隱藏 window
sg.Popup('關于該軟件的版本號為:', version, grab_anywhere=True)
window.reappear() # 重現 window
elif 'O' in event:
file_name = sg.popup_get_file('打開文件...', no_window=True)
print(file_name)
elif 'U' in event:
folder_name = sg.popup_get_folder('打開文件夾...', no_window=True)
print(folder_name)
else:
print('新功能正在開發中...')
window.close()
import PySimpleGUI as sg
class GraphX:
def __init__(self, canvas_w, canvas_h):
self.canvas_w = canvas_w # 畫布的寬度
self.canvas_h = canvas_h # 畫布的高度
def graph(self, key="-GRAPH-", background_color='lightblue'):
param_dict = {
'canvas_size': (self.canvas_w, self.canvas_h),
'graph_bottom_left': (0, 0),
'graph_top_right': (self.canvas_w, self.canvas_h),
'key': key,
'change_submits': True, # mouse click events
'background_color': background_color,
'drag_submits': True
}
return sg.Graph(**param_dict)
def radio(self, text, group_id, key, default=False, enable_events=True):
'''自定義可選按鈕'''
param_dict = {
'text': text, # 按鈕的名稱
'group_id': group_id, # 按鈕的組號
'default': default, # 是否默認選中(bool)
'disabled': False, # 設置按鈕的狀態是否可用
'background_color': None, # 背景顏色
'text_color': None, # 文本顏色
'font': None, # 字體設置 family, size
'key': key, # sg.Window 的 key
'enable_events': enable_events # 事件驅動
}
return sg.Radio(**param_dict)
@property
def col(self):
col = [
[sg.Text('選擇單擊圖片時需要做的事情:', enable_events=True)],
[self.radio('畫矩形框', 1, '-Rect-')],
[self.radio('畫圓形', 1, '-Circle-')],
[self.radio('畫橢圓形', 1, '-Oval-')],
[self.radio('畫線段', 1, '-Line-')],
[self.radio('畫點', 1, '-Point-')],
[self.radio('擦除', 1, '-erase-')],
[self.radio('擦除全部', 1, '-clear-')],
[self.radio('Send to back', 1, '-back-')],
[self.radio('Bring to front', 1, '-front-')],
[self.radio('Move Everything', 1, '-move all-')],
[self.radio('Move Stuff', 1, '-move-', True)]]
return sg.Column(col)
@property
def layout(self):
_layout = [[self.graph("-GRAPH-", 'lightblue'), self.col]]
_layout += [[sg.Text(key='info', size=(100, 1))]]
return _layout
def window(self, finalize=True):
return sg.Window("畫圖與移動", self.layout,
finalize=finalize,
background_color='lightgreen')
def draw_image(self, graph, filename):
'''在 sg.Graph 中載入 圖片
參數
========
:filename 僅 支持 GIF 或 PNG
:location 為圖片的左上角位置坐標
'''
location = (0, self.canvas_h)
graph.draw_image(filename, location=location)
class GraphRun(GraphX):
def __init__(self, canvas_w, canvas_h):
super().__init__(canvas_w, canvas_h)
self._reset()
def _reset(self):
'''重置參數'''
# 能夠抓取新的框
self.start_point, self.end_point = [None]*2
self.dragging = False
self.prior_rect = None
def update(self, graph, values):
...
def modify(self, graph, values):
...
def run(self, filename):
window = self.window()
# 獲得 sg.Graph 元素
graph = window["-GRAPH-"]
self.draw_image(graph, filename)
#graph.bind('<Button-3>', '+RIGHT+')
while True:
event, values = window.read()
if event is None:
break # exit
if 'move' in event:
graph.set_cursor(cursor='fleur')
elif not event.startswith('-GRAPH-'):
graph.set_cursor(cursor='left_ptr')
if event == "-GRAPH-": # if there's a "Graph" event, then it's a mouse
x, y = values["-GRAPH-"]
if not self.dragging:
self.start_point = (x, y)
self.dragging = True
drag_figures = graph.get_figures_at_location((x,y))
lastxy = x, y
else:
self.end_point = (x, y)
if self.prior_rect:
graph.delete_figure(self.prior_rect)
delta_x, delta_y = x - lastxy[0], y - lastxy[1]
lastxy = x,y
if None not in (self.start_point, self.end_point):
if values['-move-']:
for fig in drag_figures:
graph.move_figure(fig, delta_x, delta_y)
graph.update()
elif values['-Rect-']:
self.prior_rect = graph.draw_rectangle(self.start_point, self.end_point, line_color='blue')
elif values['-Circle-']:
self.prior_rect = graph.draw_circle(self.start_point, self.end_point[0]-self.start_point[0], line_color='blue')
elif values['-Oval-']:
self.prior_rect = graph.draw_oval(self.start_point, self.end_point, line_color='blue')
elif values['-Line-']:
self.prior_rect = graph.draw_line(self.start_point, self.end_point, color='red', width=1)
elif values['-Point-']:
self.prior_rect = graph.draw_point(self.start_point, color='red', size=1)
elif values['-erase-']:
for figure in drag_figures:
graph.delete_figure(figure)
elif values['-clear-']:
graph.erase()
self.draw_image(graph, filename)
elif values['-move all-']:
graph.move(delta_x, delta_y)
elif values['-front-']:
for fig in drag_figures:
graph.bring_figure_to_front(fig)
elif values['-back-']:
for fig in drag_figures:
graph.send_figure_to_back(fig)
elif event.endswith('+UP'): # The drawing has ended because mouse up
info = window["info"]
info.update(value=f"grabbed rectangle from {self.start_point} to {self.end_point}")
self._reset()
window.close()
if __name__ == '__main__':
canvas_size = (800, 600)
filename = 'D:/github/test_gui/1.png'
self = GraphRun(*canvas_size)
self.run(filename)