tkinter绘制组件(40)——滚动选值框
- 引言
- 布局
- 函数结构
- 文本展示
- 选择器布局
- 完整函数代码
- 效果
- 测试代码
- 最终效果
- github项目
- pip下载
- 结语
引言
2023年基本没有怎么更新TinUI组件部分,而滚动选值框(picker),是在2023年底、2024年初磨洋工磨出来的。
因为一些原因,TinUI更新速度在这段时间被放得“极缓”,但是好歹还是冒了个泡。picker作为TinUI5预发布组件,将在TinUI4.7(5-pre1)首次可用。这也是2024年暑假之前,TinUI唯一的大更新。
前情提要结束。开始正题。
本控件目的可以参考WinUI的TimePikcer和DataPicker,不过更加通用,没有限制数据选择类型。滚动选值框(选择器)提供了一套标准化方式,可使用户选择强相关的系列取值。
布局
函数结构
def add_picker(self,pos:tuple,height=250,fg='#1b1b1b',bg='#fbfbfb',outline='#ececec',activefg='#1b1b1b',activebg='#f6f6f6',onfg='#eaecfb',onbg='#3748d9',font=('微软雅黑',10),text=(('year',60),('season',100),),data=(('2022','2023','2024'),('spring','summer','autumn','winter')),tran='#01FF11',command=None):#绘制滚动选值框
'''
pos-位置
height-选择框高度
fg-文本颜色
bg-背景色
outline-边框色
activefg-选择时文本颜色
activebg-选择时背景颜色
onfg-选定时文本颜色
onbg-选定时背景颜色
font-字体
text-文本内容,需要与`data`对应。`((选值文本,元素宽度),...)`
data-选值内容,需要与`text`对应
tran-透明处理规避颜色
command-响应接受函数。需要接受一个参数:所有选值列表,全被选定时触发
'''
文本展示
这一部分比较简单,就是通过text
参数中给定的文本和元素宽度,在当前画布上绘制文本元素。既然比较复杂的文字排版table
控件早就加入到TinUI中了,这个小操作不在话下。如果不太熟悉或没看懂绘制逻辑,可以看看本专栏的表格绘制。
out_line=self.create_polygon((*pos,*pos),fill=outline,outline=outline,width=9)uid='picker'+str(out_line)self.addtag_withtag(uid,out_line)back=self.create_polygon((*pos,*pos),fill=bg,outline=bg,width=7,tags=uid)end_x=pos[0]+9y=pos[1]+9texts=[]#文本元素#测试文本高度txtest=self.create_text(pos,text=text[0][0],fill=fg,font=font)bbox=self.bbox(txtest)self.delete(txtest)uidheight=bbox[3]-bbox[1]for i in text:t,w=i#文本,宽度tx=self.create_text((end_x,y),anchor='w',text=t,fill=fg,font=font,tags=(uid,uid+'content'))texts.append(tx)end_x+=wif text.index(i)+1==len(text):#最后一个省略分隔符_outline=outlineoutline=''self.create_line((end_x-3,pos[1],end_x-3,pos[1]+uidheight),fill=outline,tags=(uid,uid+'content'))outline=_outlinedel _outline
不过需要注意的是,因为picker的选择器窗口是以像menu
一样地使用子窗口,因此我们需要先行确定窗口的宽度。
顺便绑定一下响应事件。
def _mouseenter(event):self.itemconfig(back,fill=activebg,outline=activebg)for i in texts:self.itemconfig(i,fill=activefg)def _mouseleave(event):self.itemconfig(back,fill=bg,outline=bg)for i in texts:self.itemconfig(i,fill=fg)def show(event):#这部分待会会用来现实选择器窗口......del _outlinewidth=end_x-pos[0]+9#窗口宽度cds=self.bbox(uid+'content')#变换背景元素尺寸coords=(cds[0],cds[1],cds[2],cds[1],cds[2],cds[3],cds[0],cds[3])self.coords(out_line,coords)self.coords(back,coords)#绑定事件self.tag_bind(uid,'<Enter>',_mouseenter)self.tag_bind(uid,'<Leave>',_mouseleave)self.tag_bind(uid,'<Button-1>',show)
选择器布局
这才是重点。
选择器应该遵循以下布局要求:
-
有几个选项,就要有几个选择器元素,且宽度与
text
中指定宽度基本一致 -
需要有确定和取消按钮
-
窗口默认位置应该与在TinUI中的文本元素对应
对于要求【1】,我参考了自己写的listbox
代码。(真的是万事开头难,现在应该写不出当时的代码了……)
然后通过循环创建选择器元素。
def _loaddata(box,items,mw):#这是listbox中的逻辑与绘制代码def __set_y_view(event):box.yview_scroll(int(-1*(event.delta/120)), "units")#mw: 元素宽度for i in items:end=box.bbox('all')end=5 if end==None else end[-1]text=box.create_text((5,end+7),text=i,fill=fg,font=font,anchor='nw',tags=('textcid'))bbox=box.bbox(text)#获取文本宽度back=box.create_rectangle((3,bbox[1]-4,3+mw,bbox[3]+4),width=0,fill=bg)box.tkraise(text)box.choices[text]=[i,text,back,False]#用文本id代表键,避免选项文本重复带来的逻辑错误#box.all_keys.append(text)box.tag_bind(text,'<Enter>',lambda event,text=text : pick_in_mouse(event,text))box.tag_bind(text,'<Leave>',lambda event,text=text : pick_out_mouse(event,text))box.tag_bind(text,'<Button-1>',lambda event,text=text : pick_sel_it(event,text))box.tag_bind(back,'<Enter>',lambda event,text=text : pick_in_mouse(event,text))box.tag_bind(back,'<Leave>',lambda event,text=text : pick_out_mouse(event,text))box.tag_bind(back,'<Button-1>',lambda event,text=text : pick_sel_it(event,text))bbox=box.bbox('all')box.config(scrollregion=bbox)box.bind('<MouseWheel>',__set_y_view)...for i in data:barw=text[__count][1]#本选择列表元素宽度pickbar=BasicTinUI(picker,bg=bg)pickbar.place(x=end_x,y=y,width=barw,height=height-50)maxwidth=0pickbar.newres=''#待选pickbar.res=''#选择结果#pickbar.all_keys=[]#[a-id,b-id,...]pickbar.choices={}#'a-id':[a,a_text,a_back,is_sel:bool]_loaddata(pickbar,i,barw)pickerbars.append(pickbar)__count+=1end_x+=barw+3del __count
要求【2】则简单多了。这里使用button2
,但是需要调整背景元素。也相当于在TinUI的自身应用中给出控件元素返回值的操作范例。
okpos=((5+(width-9)/2)/2,height-22)ok=bar.add_button2(okpos,text='✔️',font='{Segoe UI Emoji} 12',fg=fg,bg=bg,line='',activefg=activefg,activebg=activebg,activeline='',anchor='center',command=set_it)bar.coords(ok[1],(10,height-35,(width-9)/2-5,height-35,(width-9)/2-5,height-9,10,height-9))nopos=(((width-9)/2+width-4)/2,height-22)no=bar.add_button2(nopos,text='❌',font='{Segoe UI Emoji} 12',fg=fg,bg=bg,line='',activefg=activefg,activebg=activebg,activeline='',anchor='center',command=cancel)bar.coords(no[1],((width-9)/2+5,height-35,width-9,height-35,width-9,height-9,(width-9)/2+5,height-9))readyshow()
代码中的readyshow
,就是要求【3】的内容了。
这部分主要用来计算和记录选择器窗口的位置信息,稍后会用在show
函数中。
def readyshow():#计算显示位置allpos=bar.bbox('all')#菜单尺寸winw=allpos[2]-allpos[0]+5winh=allpos[3]-allpos[1]+5#屏幕尺寸maxx=self.winfo_screenwidth()maxy=self.winfo_screenheight()wind.data=(maxx,maxy,winw,winh)
不同于menu
,picker
的窗口需要直接贴近文本元素,因此需要额外计算文本元素边缘与点击的位置差,然后在与屏幕坐标相减。
此外,picker采用淡入动画。
def show(event):#显示的起始位置#初始位置maxx,maxy,winw,winh=wind.databbox=self.bbox(uid)scx,scy=event.x_root,event.y_root#屏幕坐标dx,dy=round(self.canvasx(event.x,)-bbox[0]),round(self.canvasy(event.y)-bbox[3])#画布坐标差值sx,sy=scx-dx,scy-dyif sx+winw>maxx:x=sx-winwelse:x=sxif sy+winh>maxy:y=sy-winhelse:y=sypicker.geometry(f'{winw+15}x{winh+15}+{x-4}+{y}')picker.attributes('-alpha',0)picker.deiconify()picker.focus_set()for i in [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1]:picker.attributes('-alpha',i)picker.update()time.sleep(0.05)picker.bind('<FocusOut>',unshow)
好了,到此,picker的两部分内容已经完成绘制。完整的逻辑代码会在下方给出。
完整函数代码
def add_picker(self,pos:tuple,height=250,fg='#1b1b1b',bg='#fbfbfb',outline='#ececec',activefg='#1b1b1b',activebg='#f6f6f6',onfg='#eaecfb',onbg='#3748d9',font=('微软雅黑',10),text=(('year',60),('season',100),),data=(('2022','2023','2024'),('spring','summer','autumn','winter')),tran='#01FF11',command=None):#绘制滚动选值框def _mouseenter(event):self.itemconfig(back,fill=activebg,outline=activebg)for i in texts:self.itemconfig(i,fill=activefg)def _mouseleave(event):self.itemconfig(back,fill=bg,outline=bg)for i in texts:self.itemconfig(i,fill=fg)def set_it(e):#确定选择results=[]#结果列表for ipicker in pickerbars:num=pickerbars.index(ipicker)if ipicker.newres=='':#没有选择unshow(e)returnipicker.res=ipicker.newrestx=texts[num]self.itemconfig(tx,text=ipicker.res)results.append(ipicker.res)unshow(e)if command!=None:command(results)def cancel(e):#取消选择for ipicker in pickerbars:if ipicker.res=='':passunshow(e)#以后或许回考虑元素选择复原,也不一定,或许不更改交互选项更方便def pick_in_mouse(e,t):box=e.widgetif box.choices[t][-1]==True:#已被选中returnbox.itemconfig(box.choices[t][2],fill=activebg)box.itemconfig(box.choices[t][1],fill=activefg)def pick_out_mouse(e,t):box=e.widgetif box.choices[t][-1]==True:#已被选中box.itemconfig(box.choices[t][2],fill=onbg)box.itemconfig(box.choices[t][1],fill=onfg)else:box.itemconfig(box.choices[t][2],fill=bg)box.itemconfig(box.choices[t][1],fill=fg)def pick_sel_it(e,t):box=e.widgetbox.itemconfig(box.choices[t][2],fill=onbg)box.itemconfig(box.choices[t][1],fill=onfg)box.choices[t][-1]=Truefor i in box.choices.keys():if i==t:continuebox.choices[i][-1]=Falsepick_out_mouse(e,i)box.newres=box.choices[t][0]def readyshow():#计算显示位置allpos=bar.bbox('all')#菜单尺寸winw=allpos[2]-allpos[0]+5winh=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.databbox=self.bbox(uid)scx,scy=event.x_root,event.y_root#屏幕坐标dx,dy=round(self.canvasx(event.x,)-bbox[0]),round(self.canvasy(event.y)-bbox[3])#画布坐标差值sx,sy=scx-dx,scy-dyif sx+winw>maxx:x=sx-winwelse:x=sxif sy+winh>maxy:y=sy-winhelse:y=sypicker.geometry(f'{winw+15}x{winh+15}+{x-4}+{y}')picker.attributes('-alpha',0)picker.deiconify()picker.focus_set()for i in [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1]:picker.attributes('-alpha',i)picker.update()time.sleep(0.05)picker.bind('<FocusOut>',unshow)def unshow(event):picker.withdraw()picker.unbind('<FocusOut>')def _loaddata(box,items,mw):def __set_y_view(event):box.yview_scroll(int(-1*(event.delta/120)), "units")#mw: 元素宽度for i in items:end=box.bbox('all')end=5 if end==None else end[-1]text=box.create_text((5,end+7),text=i,fill=fg,font=font,anchor='nw',tags=('textcid'))bbox=box.bbox(text)#获取文本宽度back=box.create_rectangle((3,bbox[1]-4,3+mw,bbox[3]+4),width=0,fill=bg)box.tkraise(text)box.choices[text]=[i,text,back,False]#用文本id代表键,避免选项文本重复带来的逻辑错误#box.all_keys.append(text)box.tag_bind(text,'<Enter>',lambda event,text=text : pick_in_mouse(event,text))box.tag_bind(text,'<Leave>',lambda event,text=text : pick_out_mouse(event,text))box.tag_bind(text,'<Button-1>',lambda event,text=text : pick_sel_it(event,text))box.tag_bind(back,'<Enter>',lambda event,text=text : pick_in_mouse(event,text))box.tag_bind(back,'<Leave>',lambda event,text=text : pick_out_mouse(event,text))box.tag_bind(back,'<Button-1>',lambda event,text=text : pick_sel_it(event,text))bbox=box.bbox('all')box.config(scrollregion=bbox)box.bind('<MouseWheel>',__set_y_view)out_line=self.create_polygon((*pos,*pos),fill=outline,outline=outline,width=9)uid='picker'+str(out_line)self.addtag_withtag(uid,out_line)back=self.create_polygon((*pos,*pos),fill=bg,outline=bg,width=7,tags=uid)end_x=pos[0]+9y=pos[1]+9texts=[]#文本元素#测试文本高度txtest=self.create_text(pos,text=text[0][0],fill=fg,font=font)bbox=self.bbox(txtest)self.delete(txtest)uidheight=bbox[3]-bbox[1]for i in text:t,w=i#文本,宽度tx=self.create_text((end_x,y),anchor='w',text=t,fill=fg,font=font,tags=(uid,uid+'content'))texts.append(tx)end_x+=wif text.index(i)+1==len(text):#最后一个省略分隔符_outline=outlineoutline=''self.create_line((end_x-3,pos[1],end_x-3,pos[1]+uidheight),fill=outline,tags=(uid,uid+'content'))outline=_outlinedel _outlinewidth=end_x-pos[0]+9#窗口宽度cds=self.bbox(uid+'content')coords=(cds[0],cds[1],cds[2],cds[1],cds[2],cds[3],cds[0],cds[3])self.coords(out_line,coords)self.coords(back,coords)self.tag_bind(uid,'<Enter>',_mouseenter)self.tag_bind(uid,'<Leave>',_mouseleave)self.tag_bind(uid,'<Button-1>',show)#创建窗口picker=Toplevel(self)picker.geometry(f'{width}x{height}')picker.overrideredirect(True)picker.attributes('-topmost',1)picker.withdraw()#隐藏窗口picker.attributes('-transparent',tran)wind=TinUINum()#记录数据bar=BasicTinUI(picker,bg=tran)bar.pack(fill='both',expand=True)bar.create_polygon((9,9,width-9,9,width-9,height-9,9,height-9),fill=bg,outline=bg,width=9)bar.lower(bar.create_polygon((8,8,width-8,8,width-8,height-8,8,height-8),fill=outline,outline=outline,width=9))__count=0end_x=8y=9pickerbars=[]#选择UI列表for i in data:barw=text[__count][1]#本选择列表元素宽度pickbar=BasicTinUI(picker,bg=bg)pickbar.place(x=end_x,y=y,width=barw,height=height-50)maxwidth=0pickbar.newres=''#待选pickbar.res=''#选择结果#pickbar.all_keys=[]#[a-id,b-id,...]pickbar.choices={}#'a-id':[a,a_text,a_back,is_sel:bool]_loaddata(pickbar,i,barw)pickerbars.append(pickbar)__count+=1end_x+=barw+3del __count#ok buttonokpos=((5+(width-9)/2)/2,height-22)ok=bar.add_button2(okpos,text='✔️',font='{Segoe UI Emoji} 12',fg=fg,bg=bg,line='',activefg=activefg,activebg=activebg,activeline='',anchor='center',command=set_it)bar.coords(ok[1],(10,height-35,(width-9)/2-5,height-35,(width-9)/2-5,height-9,10,height-9))#cancel buttonnopos=(((width-9)/2+width-4)/2,height-22)no=bar.add_button2(nopos,text='❌',font='{Segoe UI Emoji} 12',fg=fg,bg=bg,line='',activefg=activefg,activebg=activebg,activeline='',anchor='center',command=cancel)bar.coords(no[1],((width-9)/2+5,height-35,width-9,height-35,width-9,height-9,(width-9)/2+5,height-9))readyshow()#texts=[],pickerbars=[]return picker,bar,texts,pickerbars,uid
效果
测试代码
b.add_picker((1400,230),command=print)
最终效果
左下角是expander友情出演。
github项目
TinUI的github项目地址
pip下载
pip install tinui
结语
这样相当于一个比较粗糙的选择器吧。TinUI5将对大部分控件进行样式升级。
🔆tkinter创新🔆