Python GUI 编程 01 : Tkinter基础控件
有时候,做个小项目,没有太多复杂的需求,直接使用Python自带的GUI包Tkinter是个不错的选择,本文参考了使用过程的一些理解,给大伙做些分享。
一个小case:长度转换import tkinter as tkfrom tkinter import ttkfrom tkinter.messagebox import showerror,showinfo,showwarningdef calculate(*args): try: value=float(feet.get) meters.set(float(0.3048*value*10000.0+0.5)/10000.0) except ValueError: passroot=tk.Tkroot.title('Feet to Meters')mainframe=ttk.Frame(root,padding='3 3 12 12',) # 设置留白距离mainframe.grid(row=0,column=0,sticky=(tk.N,tk.W,tk.E,tk.S))root.columnconfigure(0,weight=1) # 列自动延展root.rowconfigure(0,weight=1) # 行自动延展feet=tk.StringVarfeet_entry=ttk.Entry(master=mainframe,width=7,textvariable=feet)feet_entry.grid(row=1,column=2,sticky='we')meters=tk.StringVarttk.Label(mainframe,textvariable=meters).grid(row=2,column=2,sticky='we')ttk.Button(mainframe,text='Calculate',command=calculate).grid(row=3,column=3,sticky='w')ttk.Label(mainframe,text='feet').grid(column=3,row=1,sticky='w')ttk.Label(mainframe,text='is equivalent to').grid(column=1,row=2,sticky='e')ttk.Label(mainframe,text='meters').grid(column=3,row=2,sticky='w')for child in mainframe.winfo_children: # 迭代各子控件 child.grid_configure(padx=5,pady=5) # 配置widgets边距feet_entry.focus # 设置焦点控件root.bind('Return',calculate) # 根窗体绑定键盘回车事件root.mainloop面向对象的方法import tkinter as tkfrom tkinter import ttkclass FeetToMeters: def __init__(self, master): master.title('Feet to Meters') mainframe = ttk.Frame(master, padding='3 3 12 12') mainframe.grid(column=0, row=0, sticky='news') master.columnconfigure(0, weight=1) master.rowconfigure(0, weight=1) self.feet = tk.StringVar feet_entry = ttk.Entry(mainframe, width=7, textvariable=self.feet) feet_entry.grid(column=2, row=1, sticky='we') self.meters = tk.StringVar ttk.Label(mainframe, textvariable=self.meters).grid(column=2, row=2, sticky='we') ttk.Button(mainframe, text='Calculate', command=self.calculate).grid(column=3, \ row=3, sticky='w') ttk.Label(mainframe, text='feet').grid(column=3, row=1, sticky='w') ttk.Label(mainframe, text='is equivalent to').grid(column=1, row=2, sticky='e') ttk.Label(mainframe, text='meters').grid(column=3, row=2, sticky='w') for child in mainframe.winfo_children: child.grid_configure(padx=5, pady=5) feet_entry.focus master.bind('Return', self.calculate) def calculate(self, *args): try: self.value = float(self.feet.get) self.meters.set(int(0.3048*self.value*10000.0+0.5)/10000.0) except ValueError: passroot=tk.TkFeetToMeters(root)root.mainloop 基本窗体部件import tkinter as tkfrom tkinter import ttkroot = tk.Tkroot.resizable(width=False,height=True) # 主窗体尺寸是否可调#! Framef=ttk.Frame(root)f.configure( padding=(2,2), # 元组定义的留白方向:lrtb/(lr,tb)/(l,t,r,b) width=500, height=180, borderwidth=5, ) f['relief'] = 'sunken' # 浮雕样式f.pack(fill = 'both')#! Labellbl = ttk.Label(f, text='starting...\nthe second line', padding=20, justify='right', anchor=tk.CENTER)lbl.grid(row=0)lbl.bind('Enter', lambda event: lbl.configure(text='Move mouse insede'))lbl.bind('Leave', lambda e: lbl.configure(text='Move mouse outside'))lbl.bind('ButtonPress-1', lambda e: lbl.configure(text=f'Clicked left mouse button at {e.x}, {e.y}'))lbl.bind('3', lambda e: lbl.configure(text='Clicked right mouse button'))lbl.bind('Double-1', lambda e: lbl.configure(text='Double clicked'))lbl.bind('B3-Motion',lambda e: lbl.configure(text=f'rigth button dragto {e.x},{e.y}'))# #! Buttonbtn = ttk.Button(f, text='OK', default='active', command=lambda: lbl.configure(text='Redo again'))btn.grid(row=1, sticky=tk.SE)#// btn.configure(state='disabled') # 设置状态为禁用#// print(btn.instate(['!disabled'])) # 判断当前状态#! CheckButtonmeasureSystem1 = tk.StringVarchbtn1=ttk.Checkbutton(f, text='Use Metric', variable=measureSystem1, onvalue='metric', offvalue='imperial', command=lambda: print(measureSystem1.get) )chbtn1.grid(row=2, sticky=tk.SW)measureSystem2 = tk.StringVarchbtn2=ttk.Checkbutton(f, text='Use Metric', variable=measureSystem2, onvalue='metric', offvalue='imperial', command=lambda: print(measureSystem2.get) )chbtn2.grid(row=2, column=1, sticky=tk.SW)chbtn2.configure(state='disabled')print(chbtn2.instate(['alternate'])) # 状态是否可选root.bind('Return', lambda event: btn.invoke) # 调用btn#! RidioButtongender = tk.StringVar(value='Male')rad_btn1 = ttk.Radiobutton(f, text='Male', variable=gender, value='Male',\ command=lambda:print(gender.get))rad_btn2 = ttk.Radiobutton(f, text='Female', variable=gender, value='Female',\ command=lambda:print(gender.get))rad_btn3 = ttk.Radiobutton(f, text='Unsure', variable=gender, value='Unsure',\ command=lambda:print(gender.get))rad_btn1.grid(row=3, column=0, sticky=tk.W)rad_btn2.grid(row=3, column=1, sticky=tk.W)rad_btn3.grid(row=3, column=2, sticky=tk.E)#! Entryimport redef check_num(newval): valid = re.match('^[0-9a-z]*$', newval) is not None and len(newval) = 5 if not valid: showerror(title='提示', message='错误的输入') num.set(contents.get) return valid# 传递调用对象输入框内的最新值(%P)check_num_wrapper=(root.register(check_num), '%P')contents = tk.StringVarentry = ttk.Entry(f, textvariable=contents, validate='all', # 指定触发校验的事件 # 调用根窗体注册的事件方法 validatecommand=check_num_wrapper )entry.grid(row=4, sticky='se')#// entry.bind('Enter', lambda e: contents.set('')) # 重置输入框#// root.bind('ButtonPress-3', lambda e: entry.delete(0, 'end')) # 删除指定内容num = tk.StringVare = ttk.Entry(f, textvariable=num, state='disabled')e.grid(column=1, row=4, sticky='we')root.bind('ButtonPress-3', lambda e: entry.insert(0,'new content')) # 初始化输入框#// def it_has_been_written(*args):#// print('entry has been writting...')#// contents.trace_add('write', it_has_been_written)#//entry.configure(state='readonly')root.mainloop词条的输入有效性验证import tkinter as tkfrom tkinter import ttkimport reroot = tk.Tkv = tk.StringVardef test(P, i , s , V, w, v, S, d): valid = re.match('^china$', P) is not None if valid: print('correct') print(P, i , s , V, w, v, S, d) return True else: print('wrong ' \ 'entering') print(P, i , s , V, w, v, S, d) return Falsetest_wrapper = root.register(test) # 封装函数到根窗体e1=ttk.Entry( root, textvariable=v, validate='focus', # 调用根窗体注册的事件函数,并将对象的调用参数传递给事件函数 validatecommand=(test_wrapper, '%P', '%i', '%s', '%V', '%W', '%v', '%S', '%d') )e1.insert(0, "china")e2 = tk.Entry(root, textvariable=v)e1.pack(padx=5, pady=5, fill=tk.X)e2.pack(padx=5, pady=5, ill=tk.X)root.mainloop百分号占位符的意义:
'%P'--当输入框可编辑时,值为输入框的最新文本内容;
'%i'--用户插入或删除操作的位置索引,获失焦点或textvariable变量值被修改该值为-1;
'%s'--调用验证函数前输入框的文本内容;
'%V'--调用验证函数的原因;
'%W'--当前组件名;
'%v'--当前validate选项的值;
'%S'--当插入和删除操作触发验证时,表示文本被插入和删除的内容;
'%d'--操作代码0删除/1插入/2获失焦点或textvariable变量值被修改;
Case: 加法计算器import tkinter as tkfrom tkinter import ttkroot = tk.Tkframe = ttk.Frame(root) # 把整个布局放到框架中,更好调节frame.pack(padx=10, pady=10)v1 = tk.StringVarv2 = tk.StringVarv3 = tk.StringVardef validate(content): if content.isdigit: # isdigit方法,这是str的一个函数,只允许输入数字 return True else: print("invalid inputing") return False# 注册根窗体事件方法testCmd = root.register(validate) # 通过register方法转换为validatecommand选项能接收的函数ttk.Entry(frame, textvariable=v1, width=10, validate='key', \ validatecommand=(testCmd, '%P')).grid(row=0, column=0) # 用%P获取最新输入的字符串ttk.Label(frame, text='+').grid(row=0, column=1)ttk.Entry(frame, textvariable=v2, width=10, validate='key', \ validatecommand=(testCmd, '%P')).grid(row=0, column=2)ttk.Label(frame, text='=').grid(row=0, column=3)ttk.Entry(frame, textvariable=v3, width=10, state='readonly', validate='key', \ validatecommand=(testCmd, '%P')).grid(row=0, column=4)def calc: result = int(v1.get) + int(v2.get) v3.set(result)ttk.Button(frame, text='计算结果', command=calc).grid(row=1, column=2, pady=5)root.mainloopCase:邮编格式验证import tkinter as tkfrom tkinter import ttkimport re root = tk.Tkerrmsg = tk.StringVarformatmsg = "Zip should be ##### or #####-####"def check_zip(newval, op): errmsg.set('') valid = re.match('^[0-9]{5}(\-[0-9]{4})?$', newval) is not None btn.state(['!disabled'] if valid else ['disabled']) if op == 'key': # 当validate='key'时 ok_so_far = re.match('^[0-9\-]*$', newval) is not None and len(newval) = 10 if not ok_so_far: errmsg.set(formatmsg) return ok_so_far elif op == 'focusout': if not valid: errmsg.set(formatmsg) return validcheck_zip_wrapper = (root.register(check_zip), '%P', '%V') # validatecommand对象必须是元组或列表zip = tk.StringVarf = ttk.Frame(root)f.grid(column=0, row=0)ttk.Label(f, text='Name:').grid(column=0, row=0, padx=5, pady=5)ttk.Entry(f).grid(column=1, row=0, padx=5, pady=5)ttk.Label(f, text='Zip:').grid(column=0, row=1, padx=5, pady=5)e=ttk.Entry(f, textvariable=zip, validate='all', validatecommand=check_zip_wrapper, \ invalidcommand=lambda: e.focus)e.grid(column=1, row=1, padx=5, pady=5)btn=ttk.Button(f, text='Process')btn.grid(column=2, row=1, padx=5, pady=5)btn.state(['disabled'])msg=ttk.Label(f, foreground='red', textvariable=errmsg, anchor='center')msg.grid(column=0, row=2, padx=5, columnspan=3, pady=5)root.mainloop下拉列表框:Comboboximport tkinter as tkfrom tkinter import ttkfrom faker import Fakerroot = tk.Tkf = ttk.Frame(root)f.grid(column=0, row=0, sticky='news')countryVar = tk.StringVarcountry = ttk.Combobox(f, textvariable=countryVar, height=10,) # height 可见高度10行def getCountrys(n): f = Faker for _ in range(n): yield str(_+1) + " " + f.countrycountry.configure(values = list(getCountrys(15)))country.state(['readonly']) # 列表框状态为只读country.bind('ComboboxSelected',lambda e: print(f'{countryVar.get} select ...')) # 绑定虚拟事件country.current(5) # combobox.current(index) 通过设置索引值选定取值country.pack(fill=tk.X)#! 样式管理器ttk.styles = ttk.Styles.configure('Danger.TFrame', background='red', borderwidth=5, relief='raised')ttk.Frame(root, width=250, height=200, style='Danger.TFrame').gridroot.mainloop 事件处理:Event HandlingActivate: 窗体被激活Deactivate: 窗体被切换或未被激活MouseWheel: 鼠标滚轮动作KeyPress: 键盘按键被按下KeyRelease: 键盘按键释放ButtonPress: 鼠标按键按下ButtonRelease: 鼠标按键释放Motion: 鼠标运动Configure: 窗体部件大小或位置发生变化Destroy: 部件被销毁FocusIn: 部件获得键盘输入焦点FocusOut: 部件失去键盘输入焦点Enter: 鼠标进入部件范围Leave: 鼠标离开部件范围