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]+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的选择器窗口是以像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 _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,'' ,_mouseenter)
self.tag_bind(uid,'' ,_mouseleave)
self.tag_bind(uid,'' ,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,'' ,lambda event,text=text : pick_in_mouse(event,text))
box.tag_bind(text,'' ,lambda event,text=text : pick_out_mouse(event,text))
box.tag_bind(text,'' ,lambda event,text=text : pick_sel_it(event,text))
box.tag_bind(back,'' ,lambda event,text=text : pick_in_mouse(event,text))
box.tag_bind(back,'' ,lambda event,text=text : pick_out_mouse(event,text))
box.tag_bind(back,'' ,lambda event,text=text : pick_sel_it(event,text))
bbox=box.bbox('all')
box.config(scrollregion=bbox)
box.bind('' ,__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】则简单多了。这里使用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]+5
winh=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.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('' ,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('' ,unshow)
def unshow(event):
picker.withdraw()
picker.unbind('' )
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,'' ,lambda event,text=text : pick_in_mouse(event,text))
box.tag_bind(text,'' ,lambda event,text=text : pick_out_mouse(event,text))
box.tag_bind(text,'' ,lambda event,text=text : pick_sel_it(event,text))
box.tag_bind(back,'' ,lambda event,text=text : pick_in_mouse(event,text))
box.tag_bind(back,'' ,lambda event,text=text : pick_out_mouse(event,text))
box.tag_bind(back,'' ,lambda event,text=text : pick_sel_it(event,text))
bbox=box.bbox('all')
box.config(scrollregion=bbox)
box.bind('' ,__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,'' ,_mouseenter)
self.tag_bind(uid,'' ,_mouseleave)
self.tag_bind(uid,'' ,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友情出演。
TinUI的github项目地址
pip install tinui
这样相当于一个比较粗糙的选择器吧。TinUI5将对大部分控件进行样式升级。
tkinter创新