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-响应接受函数。需要接受一个参数:所有选值列表,全被选定时触发 '''
文本展示
这一部分比较简单,就是通过
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]+9 y=pos[1]+9 texts=[]#文本元素 #测试文本高度 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+=w if text.index(i)+1==len(text):#最后一个省略分隔符 _outline=outline outline='' self.create_line((end_x-3,pos[1],end_x-3,pos[1]+uidheight),fill=outline,tags=(uid,uid+'content')) outline=_outline del _outline
不过需要注意的是,因为picker的选择器窗口是以像
顺便绑定一下响应事件。
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 _outline width=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】,我参考了自己写的
然后通过循环创建选择器元素。
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=0 pickbar.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+=1 end_x+=barw+3 del __count
要求【2】则简单多了。这里使用
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()
代码中的
这部分主要用来计算和记录选择器窗口的位置信息,稍后会用在
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)
不同于
此外,picker采用淡入动画。
def show(event):#显示的起始位置 #初始位置 maxx,maxy,winw,winh=wind.data bbox=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-dy if sx+winw>maxx: x=sx-winw else: x=sx if sy+winh>maxy: y=sy-winh else: y=sy picker.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) return ipicker.res=ipicker.newres tx=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=='': pass unshow(e) #以后或许回考虑元素选择复原,也不一定,或许不更改交互选项更方便 def pick_in_mouse(e,t): box=e.widget if box.choices[t][-1]==True:#已被选中 return box.itemconfig(box.choices[t][2],fill=activebg) box.itemconfig(box.choices[t][1],fill=activefg) def pick_out_mouse(e,t): box=e.widget if 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.widget box.itemconfig(box.choices[t][2],fill=onbg) box.itemconfig(box.choices[t][1],fill=onfg) box.choices[t][-1]=True for i in box.choices.keys(): if i==t: continue box.choices[i][-1]=False pick_out_mouse(e,i) box.newres=box.choices[t][0] 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 bbox=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-dy if sx+winw>maxx: x=sx-winw else: x=sx if sy+winh>maxy: y=sy-winh else: y=sy picker.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]+9 y=pos[1]+9 texts=[]#文本元素 #测试文本高度 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+=w if text.index(i)+1==len(text):#最后一个省略分隔符 _outline=outline outline='' self.create_line((end_x-3,pos[1],end_x-3,pos[1]+uidheight),fill=outline,tags=(uid,uid+'content')) outline=_outline del _outline width=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=0 end_x=8 y=9 pickerbars=[]#选择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=0 pickbar.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+=1 end_x+=barw+3 del __count #ok button 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)) #cancel button 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() #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创新??