TK GUI 学习
Tk 使用单线程、事件驱动的编程模型.
所有 GUI 代码、事件循环和 应用程序在同一线程中运行。因此,强烈建议不要进行任何阻止事件处理程序的调用或计算。
耗时操作的处理:
- 如果需要从另一个线程与运行 Tkinter 的线程进行通信,请尽可能保持简单。
用root.event_generate("<<MyOwnEvent>>")
将虚拟事件发布到 Tkinter 事件队列,然后在代码中处理该事件。 - 也可以在长时间的callback中,自己维护一个事件循环。
https://tkdocs.com/
https://oooutlk.github.io/tk/
个人感受:
TK来自于TCL,用来做小工具UI和脚本语言的UI是很不错的。
如果想要好的UI,大规模程序,现代化的IDE支持,建议选择其他GUI框架。
Python
Calculate
from tkinter import *
from tkinter import ttk
root = Tk()
root.title("温度换算工具")
# main frame
mainframe = ttk.Frame(root, padding="3 3 12 12")
mainframe.grid(column=0, row=0, sticky=(N, W, E, S))
# 设置权重,拉伸
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
# row 1
feet = StringVar(value='30')
feet_entry = ttk.Entry(mainframe, width=7, textvariable=feet)
feet_entry.grid(column=2, row=1, sticky=(W,E))
ttk.Label(mainframe, text='°C').grid(column=3, row=1, sticky=(W,E))
# row 2
ttk.Label(mainframe, text='is equivalent to').grid(column=1, row=2, sticky=E)
meters = StringVar()
ttk.Label(mainframe, textvariable=meters).grid(column=2, row=2, sticky=(W,E))
ttk.Label(mainframe, text='°F').grid(column=3, row=2, sticky=W)
# row 3
def btn_calculate(*args):
val = float(feet.get())
meters.set(val*1.8+32)
ttk.Button(mainframe, text='Calculate', command=btn_calculate).grid(column=3, row=3, sticky=W)
# layout
for child in mainframe.winfo_children():
child.grid_configure(padx=5, pady=5)
# focus
feet_entry.focus()
# key binding
root.bind("<Return>", btn_calculate)
root.mainloop()
########### class version #############
from tkinter import *
from tkinter import ttk
class CalculateGUI:
def __init__(self, root: Tk) -> None:
root.title("温度换算工具")
# main frame
mainframe = ttk.Frame(root, padding="3 3 12 12")
mainframe.grid(column=0, row=0, sticky=(N, W, E, S))
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
# row 1
self.feet = StringVar(value='26.3')
feet_entry = ttk.Entry(mainframe, width=7, textvariable=self.feet)
feet_entry.grid(column=2, row=1, sticky=(W,E))
ttk.Label(mainframe, text='°C').grid(column=3, row=1, sticky=(W,E))
# row 2
ttk.Label(mainframe, text='is equivalent to').grid(column=1, row=2, sticky=E)
self.meters = StringVar()
ttk.Label(mainframe, textvariable=self.meters).grid(column=2, row=2, sticky=(W,E))
ttk.Label(mainframe, text='°F').grid(column=3, row=2, sticky=W)
# row 3
ttk.Button(mainframe, text='Calculate', command=self.btn_calculate).grid(column=3, row=3, sticky=W)
# layout
for child in mainframe.winfo_children():
child.grid_configure(padx=5, pady=5)
# focus
feet_entry.focus()
# key binding
root.bind("<Return>", self.btn_calculate)
def btn_calculate(self, *args):
val = float(self.feet.get())
self.meters.set(val*1.8+32)
if __name__ == '__main__':
root = Tk()
CalculateGUI(root)
root.mainloop()
Widget
- https://tcl.tk/man/tcl8.6/TkCmd/contents.htm
- https://tcl.tk/man/tcl8.6/TkCmd/ttk_widget.htm
# 三种方式修改属性
button = ttk.Button(root, text="Hello", command="buttonpressed")
button['text'] = 'goodbye'
button.configure('text')
# list all
button.configure()
widget的几个方法:
- winfo_class: 标识小部件类型的类,例如,主题按钮TButton
- winfo_children: 作为层次结构中小组件的直接子级的小组件列表
- winfo_parent: 层次结构中小组件的父级
- winfo_toplevel: 包含此小组件的顶级窗口
- winfo_width, winfo_height: 小部件的当前宽度和高度;在屏幕上出现之前不准确
- winfo_reqwidth, winfo_reqheight: “几何管理器”(Geometry Manager) 的微件请求的宽度和高度
- winfo_x, winfo_y: 小组件左上角相对于其父项的位置
- winfo_rootx, winfo_rooty: 小组件左上角相对于整个屏幕的位置
- winfo_vieweable: 小组件是显示还是隐藏(其在层次结构中的所有祖先必须可查看才能查看)
事件循环:
from tkinter import *
from tkinter import ttk
root = Tk()
l =ttk.Label(root, text="Starting...")
l.grid()
l.bind('<Enter>', lambda e: l.configure(text='Moved mouse inside'))
l.bind('<Leave>', lambda e: l.configure(text='Moved mouse outside'))
l.bind('<ButtonPress-1>', lambda e: l.configure(text='Clicked left mouse button'))
l.bind('<3>', lambda e: l.configure(text='Clicked right mouse button'))
l.bind('<Double-1>', lambda e: l.configure(text='Double clicked'))
l.bind('<B3-Motion>', lambda e: l.configure(text='right button drag to %d,%d' % (e.x, e.y)))
root.mainloop()
事件List:
- : 窗口已变为活动状态。
- : 窗口已停用。
- : 鼠标上的滚轮已移动。
- : 键盘上的键已被按下。
- : 密钥已发布。
- : 已按下鼠标按钮。
- : 鼠标按钮已松开。
- : 鼠标已移动。
- : 小组件已更改大小或位置。
- : 小部件正在被销毁。
- : 小部件已获得键盘焦点。
- : Widget 已失去键盘焦点。
- : 鼠标指针进入小部件。
- : 鼠标指针离开小部件。
- keyboard bind: https://tcl.tk/man/tcl8.6/TkCmd/keysyms.htm
也可以bind和产生自己的虚拟event: root.event_generate("<<MyOwnEvent>>")
mutable value
s = StringVar(value="abc") # default value is ''
b = BooleanVar(value=True) # default is False
i = IntVar(value=10) # default is 0
d = DoubleVar(value=10.5) # default is 0.0
width/height
f['width'] = 350 # pixels
f['width'] = 350c # 厘米
f['width'] = 350m # 毫米
f['width'] = 350i # 英寸
f['width'] = 350p # point
Padding
f['padding'] = 5 # 5 pixels on all sides
f['padding'] = (5,10) # 5 on left and right, 10 on top and bottom
f['padding'] = (5,7,10,12) # left: 5, top: 7, right: 10, bottom: 12
border
frame['borderwidth'] = 2
frame['relief'] = 'sunken'
style
s = ttk.Style()
s.configure('Danger.TFrame', background='red', borderwidth=5, relief='raised')
ttk.Frame(root, width=200, height=200, style='Danger.TFrame').grid()
widget - label
# 静态文本
label = ttk.Label(parent, text='Full name:')
# 动态
resultsContents = StringVar()
label['textvariable'] = resultsContents
resultsContents.set('New value to display')
# 图像
image = PhotoImage(file='myimage.gif')
label['image'] = image
# font
# 一般是通过style改变一类,也可以单独设置
label['font'] = "TkDefaultFont"
label['foregroundbackgroundred'] = "#ff340a"
# layout
grid, sticky
# multi-line
label['text'] = 'abc\n123' # 强制换行
label['wraplength'] = 5 # 自动换行
widget - button
action = ttk.Button(root, text="Action", default="active", command=myaction)
root.bind('<Return>', lambda e: action.invoke())
# state
b.state(['disabled']) # set the disabled flag
b.state(['!disabled']) # clear the disabled flag
b.instate(['disabled']) # true if disabled, else false
b.instate(['!disabled']) # true if not disabled, else false
b.instate(['!disabled'], cmd) # execute 'cmd' if not disabled
widget - checkbutton
measureSystem = StringVar()
check = ttk.Checkbutton(parent, text='Use Metric',
command=metricChanged, variable=measureSystem,
onvalue='metric', offvalue='imperial')
# state
check.instate(['alternate']) # 三态state
widget - radiobutton
# 一组 radio button
phone = StringVar()
home = ttk.Radiobutton(parent, text='Home', variable=phone, value='home')
office = ttk.Radiobutton(parent, text='Office', variable=phone, value='office')
cell = ttk.Radiobutton(parent, text='Mobile', variable=phone, value='cell')
widget - entry/input
username = StringVar()
name = ttk.Entry(parent, textvariable=username)
# change it
print('current value is %s' % name.get())
name.delete(0,'end') # delete between two indices, 0-based
name.insert(0, 'your name') # insert new text at a given index
# watch value change
def it_has_been_written(*args):
pass
username.trace_add("write", it_has_been_written) # trace_remove, trace_info
# password
passwd = ttk.Entry(parent, textvariable=password, show="*")
# validate TODO
widget - Text
多行编辑框
t = Text(root, width=40, height=10)
txt['state'] = 'disabled'
# pos format: linenum.charnum
t.index('2.1') # cur move
t.see('2.1') # view 移动到第二行,第一列
t.insert/delete # 插入,删除
thetext = text.get('1.0', 'end')
# scroll
t = Text(root, width = 40, height = 5, wrap = "none")
ys = ttk.Scrollbar(root, orient = 'vertical', command = t.yview)
xs = ttk.Scrollbar(root, orient = 'horizontal', command = t.xview)
t['yscrollcommand'] = ys.set
t['xscrollcommand'] = xs.set
# image
flowers = PhotoImage(file='flowers.gif')
text.image_create('sel.first', image=flowers)
# widget
b = ttk.Button(text, text='Push Me')
text.window_create('1.0', window=b)
widget - combox
countryvar = StringVar()
country = ttk.Combobox(parent, textvariable=countryvar)
# 可选内容
country['values'] = ('USA', 'Canada', 'Australia')
country.state(["readonly"]) # 不能随意填写,只能选择
# selected
country.bind('<<ComboboxSelected>>', function)
# index current
country.current # 0 based index
widget - listBox
choices = ["apple", "orange", "banana"]
choicesvar = StringVar(value=choices)
l = Listbox(parent, height=10, listvariable=choicesvar)
# 动态改变可选内容
choices.append("peach")
choicesvar.set(choices)
# 单选,多选
l.configure('selectmode', 'browse') # extended 多选
if lbox.selection_includes(2): ... # 2 是否选中
lbox.selection_set(idx) # 选中 index
lbox.see(idx) # index move 到可见范围
# 事件绑定
lbox.bind("<<ListboxSelect>>", lambda e: updateDetails(lbox.curselection())) # When a user changes the selection
lbox.bind("<Double-1>", lambda e: invokeAction(lbox.curselection())) # mouse double-click
widget - Scrollbar
tk的scrollbar 都是单独的,不是widget的一部分。需要手动代码绑定。
s = ttk.Scrollbar( parent, orient=VERTICAL, command=listbox.yview) # Every widget that can be scrolled vertically includes a method named yview
# 关联listbox item数量与scrollbar的滚动范围
listbox.configure(yscrollcommand=s.set)
l['yscrollcommand'] = s.set
# 主动scroll
s.set(0.25, 0.26) # 0~1 之间
widget - Scale
缩放,或者进度表示
# label tied to the same variable as the scale, so auto-updates
num = StringVar()
ttk.Label(root, textvariable=num).grid(column=0, row=0, sticky='we')
# label that we'll manually update via the scale's command callback
manual = ttk.Label(root)
manual.grid(column=0, row=1, sticky='we')
def update_lbl(val):
manual['text'] = "Scale at " + val
scale = ttk.Scale(root, orient='horizontal', length=200, from_=1.0, to=100.0, variable=num, command=update_lbl)
scale.grid(column=0, row=2, sticky='we')
scale.set(20)
widget - Progressbar
进度条
p = ttk.Progressbar(parent, orient=HORIZONTAL, length=200, mode='determinate') # indeterminate
# determinate 确定进度
p.maximum
p.value
# indeterminate 不确定的进度
p.start
p.stop
widget - Spinbox
带有编辑功能的选择框
spinval = StringVar()
s = ttk.Spinbox(parent, from_=1.0, to=100.0, textvariable=spinval, command=xxx)
# 也可以参考combox的用法,在一个list中选择
values, from
# event
Increment/Decrement
Layout
- pack 最早期的方式,调整改动麻烦
- grid 可以完全替代pack, 动态布局
- place
一般的application都推荐使用grid布局。
- columnspan/rowspan 跨多个单元格的占用。
- sticky 控制cell内部widget的填充方式,默认是居中。
- weight 控制多余空间grow权重。默认0 不改变。
- columnconfigure/rowconfigure 可以配置weight.
- padding/padx/pady 控制间距
from tkinter import *
from tkinter import ttk
root = Tk()
# 主frame
content = ttk.Frame(root, padding=(3,3,12,12))
content.grid(column=0, row=0, sticky=(N, S, E, W))
# 占用 0,0 的2行3列
frame = ttk.Frame(content, borderwidth=5, relief="ridge", width=200, height=100)
frame.grid(column=0, row=0, columnspan=3, rowspan=2, sticky=(N, S, E, W))
# 从第三列开始,占用2列
namelbl = ttk.Label(content, text="Name")
namelbl.grid(column=3, row=0, columnspan=2, sticky=(N, W), padx=5)
name = ttk.Entry(content)
name.grid(column=3, row=1, columnspan=2, sticky=(N, W), padx=5, pady=5)
# 第4行,每一列一个
onevar = BooleanVar(value=True)
twovar = BooleanVar(value=False)
threevar = BooleanVar(value=True)
one = ttk.Checkbutton(content, text="One", variable=onevar, onvalue=True)
two = ttk.Checkbutton(content, text="Two", variable=twovar, onvalue=True)
three = ttk.Checkbutton(content, text="Three", variable=threevar, onvalue=True)
one.grid(column=0, row=3)
two.grid(column=1, row=3)
three.grid(column=2, row=3)
# 第4行,c3
ok = ttk.Button(content, text="Okay")
ok.grid(column=3, row=3)
# 第4行,c4
cancel = ttk.Button(content, text="Cancel")
cancel.grid(column=4, row=3)
# 注意,没有row=2, 所以row=2占用高度为0
# resize时,自动grow的设定
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
content.columnconfigure(0, weight=3)
content.columnconfigure(1, weight=3)
content.columnconfigure(2, weight=3)
content.columnconfigure(3, weight=1)
content.columnconfigure(4, weight=1)
content.rowconfigure(1, weight=1)
root.mainloop()
如果想要运行过程中,query/modify grid的属性:
# list the widgets
content.grid_slaves()
for w in content.grid_slaves(row=3): print(w)
# get info for grid
namelbl.grid_info()
# modify it
namelbl.grid_configure(sticky=(E,W))
# delete
forget # lost grid options
remove # keep options when add it again
Widget - Menus
from tkinter import *
from tkinter import ttk, messagebox
root = Tk()
# 关闭默认的虚线,它会弹出菜单
root.option_add('*tearOff', FALSE)
# Menu
m = Menu(root) # root Menubar
m_edit = Menu(m) # 最外面一层的UI的Menu
m.add_cascade(menu=m_edit, label="Edit") # 添加到menubar menu中
m_edit.add_command(label="Paste", command=lambda: root.focus_get().event_generate("<<Paste>>")) # Edit的弹出选择子command
m_edit.add_command(label="Find Some thing", command=lambda: root.event_generate("<<OpenFindDialog>>"), underline=6)
root['menu'] = m # 设置GUI上的 menu
def launchFindDialog(*args):
messagebox.showinfo(message="I hope you find what you're looking for!")
root.bind("<<OpenFindDialog>>", launchFindDialog)
# 多级menu
m_multi = Menu(m);
m.add_cascade(label='Top2', menu=m_multi); # membar 上新增 Top2
m_multi.add_command(label='Top2-1') # Top2 的子菜单 Top2-1
m_multi.add_command(label='Top2-2')
m_multi.add_separator()
# check menu
check = StringVar(value='1')
m_multi.add_checkbutton(label='Check', variable=check, onvalue=1, offvalue=0)
m_multi.add_separator()
# radio menu
radio = StringVar(value='2')
m_multi.add_radiobutton(label='One', variable=radio, value=1)
m_multi.add_radiobutton(label='Two', variable=radio, value=2)
m_multi.add_separator()
# submenu
m_multi2 = Menu(m_multi);
for i in range(10):
m_multi2.add_command(label='Top2-3_'+str(i))
m_multi.add_cascade(label='Top2-3', menu=m_multi2); # 添加 menu 到 Top2-3子菜单下
# 用来测试的 Entry
ent = ttk.Entry(root)
ent.grid()
ent.focus()
# state
print( m_multi.entrycget(0, 'label')) # get label of top entry in menu
m_multi.entryconfigure('Top2-2', state=DISABLED)
m_multi.entryconfigure(3, label="change label")
m_edit.entryconfigure('Paste', accelerator='Command+V')
# conetextual menus 右键菜单
rclickm = Menu(root)
rclickm.add_command(label="Copy")
rclickm.add_command(label="Past")
if root.tk.call('tk', 'windowingsystem') == 'aqua':
pass
else:
# win32
# 只绑定 Entry 控件
ent.bind('<3>', lambda e: rclickm.post(e.x_root, e.y_root));
root.mainloop()
Image
imgobj = PhotoImage(file='myimage.gif')
label['image'] = imgobj
from PIL import ImageTk, Image
myimg = ImageTk.PhotoImage(Image.open('myimage.png'))
Canvas
canvas = Canvas(parent, width=500, height=400, background='gray75')
# 线
canvas.create_line(10, 10, 200, 50, fill='red', width=3)
canvas.create_line(10, 10, 200, 50, 90, 150, 50, 80)
canvas.itemconfigure(id, fill='blue', width=2)
# 矩形
canvas.create_rectangle(10, 10, 200, 50, fill='red', outline='blue')
# 椭圆
canvas.create_oval(10, 10, 200, 150, fill='red', outline='blue')
# 多边形
canvas.create_polygon(10, 10, 200, 50, 90, 150, 50, 80, 120, 55, fill='red', outline='blue')
# 饼、弧形
canvas.create_arc(10, 10, 200, 150, fill='yellow', outline='black', start=45, extent=135, width=5)
# Image
myimg = PhotoImage(file='pretty.png')
canvas.create_image(10, 10, image=myimg, anchor='nw')
# Text
canvas.create_text(100, 100, text='A wonderful story', anchor='nw', font='TkMenuFont', fill='red')
# Widget
b = ttk.Button(canvas, text='Implode!')
canvas.create_window(10, 10, anchor='nw', window=b)
# event bindings
canvas.tag_bind(id, '<1>', ...)
# tags
c.addtag('rectangle', 'withtag', 2)
c.addtag('polygon', 'withtag', 'rectangle')
c.dtag(2, 'polygon')
c.gettags(2)
c.find_withtag('drawing')
# scroll
h = ttk.Scrollbar(root, orient=HORIZONTAL)
v = ttk.Scrollbar(root, orient=VERTICAL)
canvas = Canvas(root, scrollregion=(0, 0, 1000, 1000), yscrollcommand=v.set, xscrollcommand=h.set)
h['command'] = canvas.xview
v['command'] = canvas.yview
widget - TreeView
tree = ttk.Treeview(parent)
tree = ttk.Treeview(root, columns=('size', 'modified'))
tree['columns'] = ('size', 'modified', 'owner')
tree.column('size', width=100, anchor='center')
tree.heading('size', text='Size')
# Inserted at the root, program chooses id:
tree.insert('', 'end', 'widgets', text='Widget Tour')
# Same thing, but inserted as first child:
tree.insert('', 0, 'gallery', text='Applications')
# Treeview chooses the id:
id = tree.insert('', 'end', text='Tutorial')
# Inserted underneath an existing node:
tree.insert('widgets', 'end', text='Canvas')
tree.insert(id, 'end', text='Tree')
# move widgets under gallery
tree.move('widgets', 'gallery', 'end')
tree.detach('widgets')
tree.delete('widgets')
tree.item('widgets', open=TRUE)
isopen = tree.item('widgets', 'open')
其他
s = ttk.Separator(parent, orient=HORIZONTAL)
lf = ttk.Labelframe(parent, text='Label') # 也叫 groupbox
# stack window layout
p = ttk.Panedwindow(parent, orient=VERTICAL)
f1 = ttk.Labelframe(p, text='Pane1', width=100, height=100)
p.add(f1)
# notebook, tabs
n = ttk.Notebook(parent)
f1 = ttk.Frame(n) # first page, which would get widgets gridded into it
f2 = ttk.Frame(n) # second page
n.add(f1, text='One')
n.add(f2, text='Two')
Window
window = Toplevel(parent) # create new top window
window.destroy() # 销毁
oldtitle = window.title()
window.title('New title')
window.geometry('300x200-5+40')
window.minsize(200,100)
window.maxsize(500,500)
window.resizable(FALSE,FALSE)
window.update_idletasks() # 强制刷新,获取最新的size
print(window.geometry())
window.attributes("-alpha", 0.5) # 窗口透明度0-1
window.attributes("-fullscreen", 1) # 全屏幕
window.attributes("-topmost", 1) # top window
thestate = window.state()
window.state('normal')
window.iconify()
window.deiconify()
window.withdraw()
window.protocol("WM_DELETE_WINDOW", callback) # 拦截关闭消息
print("width=", str(root.winfo_screenwidth()) + " height=", str(root.winfo_screenheight()))
对话框窗口:
from tkinter import filedialog
filename = filedialog.askopenfilename()
filename = filedialog.asksaveasfilename()
dirname = filedialog.askdirectory()
from tkinter import colorchooser
colorchooser.askcolor(initialcolor='#ff0000')
from tkinter import messagebox
messagebox.showinfo(message='Have a good day')
messagebox.askyesno(
message='Are you sure you want to install SuperVirus?'
icon='question' title='Install')
自定义对话框:
ttk.Entry(root).grid() # something to interact with
def dismiss ():
dlg.grab_release()
dlg.destroy()
dlg = Toplevel(root)
ttk.Button(dlg, text="Done", command=dismiss).grid()
dlg.protocol("WM_DELETE_WINDOW", dismiss) # intercept close button
dlg.transient(root) # dialog window is related to main
dlg.wait_visibility() # can't grab until window appears, so we wait
dlg.grab_set() # ensure all input goes to our window
dlg.wait_window() # block until window is destroyed
fonts
from tkinter import font
font.names()
# ('fixed', 'oemfixed', 'TkDefaultFont', 'TkMenuFont', 'ansifixed', 'systemfixed', 'TkHeadingFont',
# 'device', 'TkTooltipFont', 'defaultgui', 'TkTextFont', 'ansi', 'TkCaptionFont', 'system', 'TkSmallCaptionFont', 'TkFixedFont', 'TkIconFont')
f = font.nametofont('TkTextFont')
f.actual() # {'family': '.AppleSystemUIFont', 'size': 13, 'weight': 'normal', 'slant': 'roman', 'underline': 0, 'overstrike': 0}
f.metrics() # {'ascent': 13, 'descent': 3, 'linespace': 16, 'fixed': 0}
f.measure('The quick brown fox')
# set font for label
highlightFont = font.Font(family='Helvetica', name='appHighlightFont', size=12, weight='bold')
ttk.Label(root, text='Attention!', font=highlightFont).grid()
Theme
和CSS不同,效果类似。相对比较复杂。
>>> s = ttk.Style()
>>> s.theme_names()
('aqua', 'step', 'clam', 'alt', 'default', 'classic')
>>> s.theme_use()
'aqua'
s.theme_use('themename')
# 第三方主题 awthemes-*.zip.tcli
# https://wiki.tcl-lang.org/page/List+of+ttk+Themes
root.tk.call('lappend', 'auto_path', '/full/path/to/awthemes-9.3.1')
root.tk.call('package', 'require', 'awdark')
root.tk.call('source', '/full/path/to/themefile.tcl')
# 定制
s.configure('Emergency.TButton', font='helvetica 24', foreground='red', padding=10)
s.map('TButton',
background=[('disabled','#d9d9d9'), ('active','#ececec')],
foreground=[('disabled','#a3a3a3')],
relief=[('pressed', '!disabled', 'sunken')])
s.lookup('TButton', 'font')
s.element_options('Button.label')