tkinter绘制组件(18)——菜单
引言
在窗口应用中,菜单可以帮助我们归类功能按钮,同时也可以让使用者方便地在需要的功能区寻找自己所需要执行的功能。TInUI基于tkinter,我们自然可以使用Menu
类。但是,tkinter自身的菜单样式仍然为默认的系统风格,而且单个菜单的样式无法单独设置。
这些原因就让我们开发一种匹配于TinUI的菜单,这次没有参考WinUI。
同以前某一些控件一样,为了减少复杂程度,我将采用之前使用的按钮和分割线来绘制匹配于TinUI的菜单组件,但也因此导致按钮的样式无法匹配新的“菜单”。我会在后期对TinUI的按钮进行更新,至少编写这篇文章的时候(2021年11月13日)仍然无法让按钮自动横向平铺。
布局
函数结构
def add_menubar(self,cid='all',bind='<Button-3>',font='微软雅黑 12',fg='#ecf3e8',bg='#2b2a33',activefg='#ecf3e8',activebg='#616161',cont=(('command',lambda event:print('')),'-')):#绘制菜单
'''
cid::响应事件的画布对象
bind::响应的时间
font::字体
fg::字体颜色
bg::背景色
activefg::选定字体颜色
activebg::选定的选项背景颜色
cont::菜单内容数据结构
---
cont格式
(('名称','绑定的函数(至少接受event参数)'),#常规格式
'-',#分割线
...
)
'''
为画布对象绑定事件
其实无论是tkinter的组件类还是tkinter.Canvas的画布对象,绑定事件的方法都是一样的,只不过画布对象要指明ID或者是tag。一般地,我们都会绑定鼠标右键单击。
def show(event):#显示的起始位置
#之后填充的代码段
self.tag_bind(cid,bind,show)
绘制菜单原型
我们将采用Toplevel+TinUI的方式绘制菜单窗口。之所以使用窗口,是因为窗口具有更多的操作功能,同时能够避免因为父组件的一些可视化问题导致菜单不可见或无效。
现在先来创建基本的窗口:
menu=Toplevel(self)
menu.overrideredirect(True)
menu.withdraw()#隐藏窗口
bar=TinUI(menu,bg=bg)
bar.pack(fill='both',expand=True)
wind=TinUINum()#记录数据
wind用来记录窗口尺寸数据。
因为我们要保证任何一个菜单选项都完全可见,但同时我们并不知道所有选项中最宽的一个按钮,所以我们需要动态记录各个按钮的宽度数据。
def endy():#获取最低位置
pos=bar.bbox('all')
return 0 if pos==None else pos[3]+10
backs=[]#按钮
funcs=[]#按钮函数接口
seps=[]#分割线
widths=[]#寻找最宽位置
for i in cont:#添加菜单内容
if i=='-':#分割线
sep=bar.add_separate((5,endy()),100,fg=activebg)
seps.append(sep)
else:
button=bar.add_button((5,endy()),i[0],fg,bg,activefg,activebg,font,command=lambda event,i=i:(i[1](event),menu.withdraw()))
backs.append(button[1])
funcs.append(button[2])
pos=bar.bbox(button[1])
width=pos[2]-pos[0]
widths.append(width)#记录各个宽度
接下来,我们将根据这些宽度重新绘制窗口:
def repaint():#重新绘制以适配
maxwidth=max(widths)
for back in backs:#调整按钮背景
pos=bar.bbox(back)
bar.coords(back,(5,pos[1],10+maxwidth,pos[3]))
for sep in seps:#调整分割线长度
pos=bar.bbox(sep)
bar.delete(sep)
bar.add_separate((5,pos[1]),maxwidth+5,fg=activebg)
#...
repaint()
计算最终菜单窗口的尺寸
这一步的目的就是为之后的动态显示提供窗口尺寸和电脑屏幕尺寸的数据。
def repaint():
#...
def readyshow():#计算显示位置
allpos=bar.bbox('all')
#菜单尺寸
winw=allpos[2]-allpos[0]+5
winh=allpos[3]-allpos[1]+5
#屏幕尺寸
maxx=self.winfo_screenwidth()
maxy=self.winfo_screenheight()
wind.data=(maxx,maxy,winw,winh)
#...
repaint()
readyshow()
菜单相应事件
完成菜单的显示任务。
为了让菜单可见,我们默认执行以下操作:
- 默认在鼠标的右下角为起始位置
- 分别计算窗口的宽度、高度是否超过屏幕显示。若有,则反向显示。
def show(event):#显示的起始位置
#初始位置
maxx,maxy,winw,winh=wind.data
sx,sy=event.x_root,event.y_root
if sx+winw>maxx:
x=sx-winw
else:
x=sx
if sy+winh>maxy:
y=sy-winh
else:
y=sy
menu.geometry(f'{winw}x{winh}+{x}+{y}')
menu.deiconify()
菜单窗口焦点
为了让菜单窗口能在我们需要的时候消失,我们需要让窗口失去焦点时隐藏,此外,每一次的显示也要让窗口获取焦点:
def show(event):
#...
menu.focus_set()
#...
menu.bind('<FocusOut>',lambda event:menu.withdraw())
完整代码函数
def add_menubar(self,cid='all',bind='<Button-3>',font='微软雅黑 12',fg='#ecf3e8',bg='#2b2a33',activefg='#ecf3e8',activebg='#616161',cont=(('command',lambda event:print('')),'-')):#绘制菜单
'''cont格式
(('名称','绑定的函数(至少接受event参数)'),#常规格式
'-',#分割线
...
)
'''
def endy():#获取最低位置
pos=bar.bbox('all')
return 0 if pos==None else pos[3]+10
def repaint():#重新绘制以适配
maxwidth=max(widths)
for back in backs:
pos=bar.bbox(back)
bar.coords(back,(5,pos[1],10+maxwidth,pos[3]))
for sep in seps:
pos=bar.bbox(sep)
bar.delete(sep)
bar.add_separate((5,pos[1]),maxwidth+5,fg=activebg)
def readyshow():#计算显示位置
allpos=bar.bbox('all')
#菜单尺寸
winw=allpos[2]-allpos[0]+5
winh=allpos[3]-allpos[1]+5
#屏幕尺寸
maxx=self.winfo_screenwidth()
maxy=self.winfo_screenheight()
wind.data=(maxx,maxy,winw,winh)
def show(event):#显示的起始位置
#初始位置
maxx,maxy,winw,winh=wind.data
sx,sy=event.x_root,event.y_root
if sx+winw>maxx:
x=sx-winw
else:
x=sx
if sy+winh>maxy:
y=sy-winh
else:
y=sy
menu.geometry(f'{winw}x{winh}+{x}+{y}')
menu.deiconify()
menu.focus_set()
self.tag_bind(cid,bind,show)
menu=Toplevel(self)
menu.overrideredirect(True)
menu.withdraw()
bar=TinUI(menu,bg=bg)
bar.pack(fill='both',expand=True)
wind=TinUINum()#记录数据
backs=[]#按钮
funcs=[]#按钮函数接口
seps=[]#分割线
widths=[]#寻找最宽位置
for i in cont:#添加菜单内容
if i=='-':
sep=bar.add_separate((5,endy()),100,fg=activebg)
seps.append(sep)
else:
button=bar.add_button((5,endy()),i[0],fg,bg,activefg,activebg,font,command=lambda event,i=i:(i[1](event),menu.withdraw()))
backs.append(button[1])
funcs.append(button[2])
pos=bar.bbox(button[1])
width=pos[2]-pos[0]
widths.append(width)
repaint()
readyshow()
menu.bind('<FocusOut>',lambda event:menu.withdraw())
return menu,bar,funcs
效果
测试代码
def test(event):
a.title('TinUI Test')
b.add_paragraph((50,150),'这是TinUI按钮触达的事件函数回显,此外,窗口标题也被改变、首行标题缩进减小')
b.coords(m,100,5)
def test1(word):
print(word)
def test2(event):
ok1()
def test3(event):
ok2()
def test4(event):
from time import sleep
for i in range(1,101):
sleep(0.02)
progressgoto(i)
def test5(result):
b.itemconfig(scale_text,text='当前选值:'+str(result))
if __name__=='__main__':
a=Tk()
a.geometry('700x700+5+5')
b=TinUI(a,bg='white')
b.pack(fill='both',expand=True)
m=b.add_title((600,0),'TinUI is a test project for futher tin using')
m1=b.add_title((0,680),'test TinUI scrolled',size=2,angle=24)
b.add_paragraph((20,290),''' TinUI是基于tkinter画布开发的界面UI布局方案,作为tkinter拓展和TinEngine的拓展而存在。目前,TinUI尚处于开发阶段。如果想要使用完整的TinUI,敬请期待。''',
angle=-18)
b.add_paragraph((20,100),'下面的段落是测试画布的非平行字体显示效果,也是TinUI的简单介绍')
b.add_button((250,450),'测试按钮',activefg='white',activebg='red',command=test,anchor='center')
b.add_checkbutton((80,430),'允许TinUI测试',command=test1)
b.add_label((10,220),'这是由画布TinUI绘制的Label组件')
b.add_entry((250,300),350,'这里用来输入')
b.add_separate((20,200),600)
b.add_radiobutton((50,480),300,'sky is blue, water is blue, too. So, what is your heart',('red','blue','black'),command=test1)
b.add_link((400,500),'TinGroup知识库','http://tinhome.baklib-free.com/')
_,ok1=b.add_waitbar1((500,220),bg='#CCCCCC')
b.add_button((500,270),'停止等待动画',activefg='cyan',activebg='black',command=test2)
bu1=b.add_button((700,200),'停止点状滚动条',activefg='white',activebg='black',command=test3)[1]
bu2=b.add_button((700,250),'nothing button 2')[1]
bu3=b.add_button((700,300),'nothing button 3')[1]
b.add_labelframe((bu1,bu2,bu3),'box buttons')
_,_,ok2=b.add_waitbar2((600,400))
b.add_combobox((600,550),text='你有多大可能去珠穆朗玛峰',content=('20%','40%','60%','80%','100%','1000%'))
b.add_button((600,480),text='测试进度条(无事件版本)',command=test4)
_,_,_,progressgoto=b.add_progressbar((600,510))
b.add_table((180,630),data=(('a','space fans over the world','c'),('you\ncan','2','3'),('I','II','have a dream, then try your best to get it!')))
b.add_paragraph((300,810),text='上面是一个表格')
b.add_onoff((600,100))
b.add_spinbox((680,100))
b.add_scalebar((680,50),command=test5)
scale_text=b.add_label((890,50),text='当前选值:2')
b.add_info((680,140),info_text='this is info widget in TinUI')
mtb=b.add_paragraph((0,720),'测试菜单(右键单击)')
b.add_menubar(mtb,cont=(('command',print),('menu',test1),'-',('TinUI文本移动',test)))
a.mainloop()
最终效果
2021-12-28新样式
- 适配菜单底色
- 任意windows系统圆角矩形菜单
2022-5-1新样式
- 出现时滚动动画
2022-5-8新样式
- 修复第二次点击即以后点击时菜单动画延迟
可对比2022-5-1的动画演示。
2022-6-3新改进
先关闭菜单,再执行指定函数。
2022-7-30新改进
修复菜单首次点击后出现明显(向左)位移的问题。
github项目
pip下载
pip install tinui
结语
现在你可以将TinUI运用到你自己的tkinter项目了,甚至可以将TinUI最为窗口的唯一部件。你也可以再次基础上开发现代化的Text部件。
我开通了我自己的个人博客,欢迎访问。
此外可以看看我在广东实验中学在干什么:个人的广东实验中学专网。
🔆tkinter创新🔆