tkinter绘制组件(41)——菜单按钮

  • 引言
  • 布局
  • 函数结构
  • 按钮部分
  • 菜单显示
  • 完整代码函数
  • 效果
  • 测试代码
  • 最终效果
  • github项目
  • pip下载
  • 结语


引言

TinUI5的新控件,菜单按钮,menubutton

这是一个与TinUI菜单(menubar)相关联的控件,可以当作按钮(button2)放置在窗口的任意位置。只需要单击就可以展开菜单。

在TinUI中,菜单按钮有两种展开方式,由参数side=x/y决定,稍后会详细说明。当然,两种对齐方式在超出显示屏时,均会做出调整,以便全部可见。

我才知道,在WinUI3里,像菜单、选择器、浮窗这些脱离窗口的控件,叫“浮出控件(ContextFlyout)”。不过在tkinter里无法实现,原理决定了。


布局

函数结构

def add_menubutton(self,pos:tuple,text:str,side='y',fg='#1b1b1b',bg='#fbfbfb',line='#CCCCCC',linew=1,activefg='#5d5d5d',activebg='#f5f5f5',activeline='#e5e5e5',font=('微软雅黑',12),cont=(('command',print),'-'),tran='#01FF11'):#绘制按钮展开菜单
'''
此段略过,样式是从button2挪过来的,cont是menubar的
side - 展开方向。y在下方展开,x在右侧展开
'''

按钮部分

说实话,menubar就是一个“缝合怪”,TinUI样式部分就是button2。

但是情况有变,为了给menubar提供新的样式,button2也在事件绑定等方面做出了调整,不过在这里体现不大,可以直接照抄。

def in_button(event):
            self.itemconfig(outline,outline=activeline,fill=activeline)
            self.itemconfig(uid+'button',fill=activefg)
        def out_button(event):
            self.itemconfig(back,fill=bg,outline=bg)
            self.itemconfig(outline,outline=line,fill=line)
            self.itemconfig(uid+'button',fill=fg)
        def on_click(event):
            self.itemconfig(back,fill=activebg,outline=activebg)
            self.itemconfig(uid+'button',fill=activefg)
            self.after(500,lambda : out_button(None))
            show(event)#从menu那偷过来的    
        ...
        def disable(fg='#9d9d9d',bg='#f5f5f5'):
            self.itemconfig(uid+'button',state='disable',fill=fg)
            self.itemconfig(back,state='disable',disabledfill=bg)
            self.itemconfig(outline,state='disable')
        def active():
            self.itemconfig(uid+'button',state='normal')
            self.itemconfig(back,state='normal')
            self.itemconfig(outline,state='normal')
            out_button(None)
        button=self.create_text(pos,text=text,fill=fg,font=font,anchor='nw')
        uid='menubutton'+str(button)
        self.itemconfig(button,tags=(uid,uid+'button'))
        x1,y1,x2,y2=self.bbox(uid)
        if side=='y':
            self.create_text((x2+5,(y1+y2)/2),text='\uE70D',fill=fg,font='{Segoe Fluent Icons} 12',anchor='w',tags=(uid,uid+'button'))
        elif side=='x':
            self.create_text((x2+5,(y1+y2)/2),text='\uE76C',fill=fg,font='{Segoe Fluent Icons} 12',anchor='w',tags=(uid,uid+'button'))
        ...
        #创建菜单
        menu=self.add_menubar(uid,'<Button-1>',font=font,fg=fg,bg=bg,line=line,activefg=activefg,activebg=activebg,cont=cont,tran=tran)[0]
        self.tag_unbind(uid,'<Button-1>')
        #重新绑定事件
        ...

引用自己很久以前写的代码,但现在应该写不出来的……

按钮主体代码不是重点。

在创建完uid后,有对于side的判断。这里是严格的判断,只接受小写字母x和y,默认为y。

然后是菜单部分(menu=...),没错,这次不仅是抄,而且干脆直接调用。不过需要注意,因为菜单在创建的时候会自动绑定参数给的事件,因此我们需要对这个事件(<Button-1>)解绑。

菜单显示

然后才是重点。注意到on_click函数的最后一行,有show(event),就代表我们需要重写菜单的显示方式,为什么呢?因为,我们需要菜单像滚动选择框(picker)那样在按钮周边展开,要么下方,要么在右侧。

那么问题来了,TinUI的控件是函数式创建,如何重写菜单的显示方式呢?

很简单,直接在menubar的第一个返回值,menu窗口添加一个属性wind,记录窗口数据。

反正我是TinUI开发者,内部代码想怎么改就怎么改,毕竟这个属性又不用提供该使用TinUI的人。

menu修改代码见最新TinUI库。

通过在picker里的积累,在元素周围展现浮出窗口并不难。只是需要浅浅地判断一下side的方向就行了。

def unshow(event):#重写菜单
            menu.withdraw()
            menu.unbind('<FocusOut>')
        def show(event):#显示的起始位置
            #初始位置
            maxx,maxy,winw,winh=menu.wind.data
            sx,sy=event.x_root,event.y_root
            #
            maxx,maxy,winw,winh=menu.wind.data
            bbox=self.bbox(uid)
            scx,scy=event.x_root,event.y_root#屏幕坐标
            if side=='y':
                dx,dy=round(self.canvasx(event.x,)-bbox[0]),round(self.canvasy(event.y)-bbox[3])#画布坐标差值
            elif side=='x':
                dx,dy=round(self.canvasx(event.x,)-bbox[2]),round(self.canvasy(event.y)-bbox[1])#画布坐标差值
            sx,sy=scx-dx,scy-dy
            #...

完整代码函数

def add_menubutton(self,pos:tuple,text:str,side='y',fg='#1b1b1b',bg='#fbfbfb',line='#CCCCCC',linew=1,activefg='#5d5d5d',activebg='#f5f5f5',activeline='#e5e5e5',font=('微软雅黑',12),cont=(('command',print),'-'),tran='#01FF11'):#绘制按钮展开菜单
        #Segoe Fluent Icons x右侧展开\uE76B \uE76C,y下方展开\uE70D \uE70E,默认y
        def in_button(event):
            self.itemconfig(outline,outline=activeline,fill=activeline)
            self.itemconfig(uid+'button',fill=activefg)
        def out_button(event):
            self.itemconfig(back,fill=bg,outline=bg)
            self.itemconfig(outline,outline=line,fill=line)
            self.itemconfig(uid+'button',fill=fg)
        def on_click(event):
            self.itemconfig(back,fill=activebg,outline=activebg)
            self.itemconfig(uid+'button',fill=activefg)
            self.after(500,lambda : out_button(None))
            show(event)#从menu那偷过来的
        def unshow(event):#重写菜单
            menu.withdraw()
            menu.unbind('<FocusOut>')
        def show(event):#显示的起始位置
            #初始位置
            maxx,maxy,winw,winh=menu.wind.data
            sx,sy=event.x_root,event.y_root
            #
            maxx,maxy,winw,winh=menu.wind.data
            bbox=self.bbox(uid)
            scx,scy=event.x_root,event.y_root#屏幕坐标
            if side=='y':
                dx,dy=round(self.canvasx(event.x,)-bbox[0]),round(self.canvasy(event.y)-bbox[3])#画布坐标差值
            elif side=='x':
                dx,dy=round(self.canvasx(event.x,)-bbox[2]),round(self.canvasy(event.y)-bbox[1])#画布坐标差值
            sx,sy=scx-dx,scy-dy
            #
            if sx+winw>maxx:
                x=sx-winw
            else:
                x=sx
            if sy+winh>maxy:
                y=sy-winh
            else:
                y=sy
            menu.geometry(f'{winw+15}x{winh+15}+{x}+{y}')
            menu.attributes('-alpha',0)
            menu.deiconify()
            menu.focus_set()
            for i in [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1]:
                menu.attributes('-alpha',i)
                menu.update()
                time.sleep(0.05)
            menu.bind('<FocusOut>',unshow)
        def disable(fg='#9d9d9d',bg='#f5f5f5'):
            self.itemconfig(uid+'button',state='disable',fill=fg)
            self.itemconfig(back,state='disable',disabledfill=bg)
            self.itemconfig(outline,state='disable')
        def active():
            self.itemconfig(uid+'button',state='normal')
            self.itemconfig(back,state='normal')
            self.itemconfig(outline,state='normal')
            out_button(None)
        button=self.create_text(pos,text=text,fill=fg,font=font,anchor='nw')
        uid='menubutton'+str(button)
        self.itemconfig(button,tags=(uid,uid+'button'))
        x1,y1,x2,y2=self.bbox(uid)
        if side=='y':
            self.create_text((x2+5,(y1+y2)/2),text='\uE70D',fill=fg,font='{Segoe Fluent Icons} 12',anchor='w',tags=(uid,uid+'button'))
        elif side=='x':
            self.create_text((x2+5,(y1+y2)/2),text='\uE76C',fill=fg,font='{Segoe Fluent Icons} 12',anchor='w',tags=(uid,uid+'button'))
        x1,y1,x2,y2=self.bbox(uid+'button')
        linew-=1
        outline_t=(x1-linew,y1-linew,x2+linew,y1-linew,x2+linew,y2+linew,x1-linew,y2+linew)
        outline=self.create_polygon(outline_t,width=9,tags=uid,fill=line,outline=line)
        back_t=(x1,y1,x2,y1,x2,y2,x1,y2)
        back=self.create_polygon(back_t,width=7,tags=uid,fill=bg,outline=bg)
        #创建菜单
        menu=self.add_menubar(uid,'<Button-1>',font=font,fg=fg,bg=bg,line=line,activefg=activefg,activebg=activebg,cont=cont,tran=tran)[0]
        self.tag_unbind(uid,'<Button-1>')
        #重新绑定事件
        self.tag_bind(uid+'button','<Button-1>',on_click)
        self.tag_bind(uid+'button','<Enter>',in_button)
        self.tag_bind(uid+'button','<Leave>',out_button)
        self.tag_bind(back,'<Button-1>',on_click)
        self.tag_bind(back,'<Enter>',in_button)
        self.tag_bind(back,'<Leave>',out_button)
        self.tag_bind(outline,'<Button-1>',on_click)
        self.tag_bind(outline,'<Enter>',in_button)
        self.tag_bind(outline,'<Leave>',out_button)
        self.tkraise(uid+'button')
        funcs=FuncList(2)
        funcs.disable=disable
        funcs.active=active
        return uid+'button',back,outline,funcs,uid

效果

测试代码

b.add_menubutton((1500,50),'menubutton',cont=(('command',print),('menu',test1),'-',('TinUI文本移动',test)))

最终效果

python tkinter button网格居中_f5


github项目

TinUI的github项目地址

pip下载

pip install tinui

结语

后续,menubutton或许会和menubar混合联用。

🔆tkinter创新🔆