学习设计图形用户界面(简称 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)