tkinter绘制组件(15)——调节框
引言
在GUI界面中,调节框有很多种类,而TinUI中的调节框(scalebar)则是对应到tkinter中的滑动调节组件,但是外观借鉴了uwp(WinUI)的滑动调节组件。
滑动调节组件为使用者提供了直观的选值操作,是为使用者提供范围、数字选择的一种常见组件。现在,我们将吧滑动调节框移植到TinUI中。
布局
函数结构
def add_scalebar(self,pos:tuple,width=200,fg='#4258cc',activefg='#aeb5d7',bg='#99a3d5',data=(1,2,3,4,5),start=1,command=None):#绘制调节框
'''
pos::位置
width::宽度
fg::选值覆盖区域的颜色
activefg::选值滑动按钮被激活时的颜色
bg::选值未覆盖区域的颜色
data::选值范围
start::初始选值,第一个选值为0,第二个选值为1……
command::选值后调用该函数,必须接受一个参数,被选定的值
'''
组件绘制&功能规划
因为scalebar
的取值操作涉及到一定的转化和运算,因此我们这次将组件绘制与选值功能分开实现。
scalebar
需要实现的功能有:
- 按钮能够被直接拖动
- 覆盖区域随按钮位置改变而改变
- 按钮和覆盖范围不能够超过调节范围
- 单词选值结束后调整覆盖区域和按钮的位置,并且调用返回函数
覆盖与未覆盖区域
一般地,选值未覆盖区域位于最底层,而且在所有操作中不会发生改变,因此,我们可以直接绘制未覆盖区域。
back=self.create_line((pos[0],pos[1],pos[0]+width,pos[1]),fill=bg,width=3)
接下来就是覆盖区域。
覆盖区域将会随着调节按钮的位置改变而改变。因为我们允许了设置初始选值的参数,那么首先,我就应当先得到初始选值所对应到调节框中覆盖区域的位置。
先获取选值范围中每个选项对应的覆盖区域位置:
dash_t=width//(len(data)-1)#选项区域间隔。种树问题很熟悉有没有
s=pos[0]#调节线段起点
dash=[s]#调节线段的终点位置
for i in data[1:]:
s+=dash_t
dash.append(s)
del s
覆盖范围的获得与种树问题一样。
然后通过参数start
绘制覆盖区域:
active=self.create_line((pos[0],pos[1],dash[start],pos[1]),fill=fg,width=3)
同时,根据之前绘制进度条、表格、单选框等组件,我们拥有了一个重要经验——我们如果要改变一个画布对象的尺寸,那么唯一的办法就是重绘。所以,我们先来定义一个tag标志,方便之后的操作:
name='scaleactive'+str(active)
self.addtag_withtag(name,active)#为重绘绑定tag名称
绘制滑动调节按钮
滑动调节按钮是调节框最重要的部件,它需要响应很多事件,不过,我们先从最简单的绘制开始。调节按钮同样受到start
参数的影响:
button=self.create_rectangle((dash[start],pos[1]-15,dash[start]+10,pos[1]+17),width=0,fill=fg)
接着,按钮起码需要响应鼠标的进出事件:
self.tag_bind(button,'<Enter>',lambda event:self.itemconfig(button,fill=activefg))
self.tag_bind(button,'<Leave>',lambda event:self.itemconfig(button,fill=fg))
调节按钮的滑动
其实在我的另一个专栏tkinter实验中,就涉及到了移动卡片、组件可视化移动&调节等复合组件的使用,其中,就包含了组件的拖动代码。这一些拖动代码不会在这里进行详细解释,如果不明白,请转战上面提到的专栏中查找相关文章。
直接给代码:
def mousedown(event):
scale.startx=self.canvasx(event.x)#定义起始横坐标
def drag(event):
move=self.canvasx(event.x)-scale.startx#将窗口坐标转化为画布坐标
#防止按钮被拖出范围
if self.canvasx(event.x)<pos[0] or self.canvasx(event.x)>pos[0]+width:
return
self.move(button,move,0)
self.delete(name)
#重绘覆盖区域
active=self.create_line((pos[0],pos[1],move+scale.startx,pos[1]),fill=fg,width=3,tags=name)
#重新定义画布中的起始拖动位置
scale.startx=self.canvasx(event.x)
scale=TinUINum()#定义数据记录结构体
#...
self.tag_bind(button,'<Button-1>',mousedown)
self.tag_bind(button,'<B1-Motion>',drag)
因为目前调节框仅支持横向布局,因此只考虑x坐标。同时,因为event
返回的属性是相对于窗口的坐标,因此需要使用canvasx函数将窗口坐标转化为画布坐标。当调节按钮被拖动时,就需要实时计算和重绘部件位置,一次来达到流畅的视觉效果。而不同于窗口组件拖动,画布是可以调节可视范围的,所以每一次拖动就要重新更新起始位置。
在此基础上,实现纵向的调节框也很容易,只不过目前不想做~~(懒)~~而已。
返回选值并调整部件
当拖动完毕,也就是鼠标松开后,我们就需要确定当前的选值,并且调用回调函数了。
这里有一个细节要注意,那就是用户不可能每次都拖动到与选值范围中某一个值完全对应的位置,因为选值是有限的,而拖动位置比较随意。因此,每次拖动完毕后,我们不仅需要回调指定函数,还要根据最终的选值重绘覆盖区域和按钮的位置。
那么问题来了,既然我们无法确定按钮的位置是否能够准确匹配选值的相应位置,那么我们怎么确定选值和进行重绘?
放心,还记得dash
列表吗?在data
中的每一个选值,实际上都可以对应到dash
中的每一个x位置。我们首先计算出当前横坐标与dash
列表中最近的数值,然后再映射到data
集中相应的选值就可以了。
def check(event):
end=int(self.canvasx(event.x))#获取完成拖动的坐标
#最值判断
if end<pos[0]:end=pos[0]
if end>pos[0]+width:end=pos[0]+width
#获取最接近的取值
rend=min(dash,key=lambda x:abs(x-end))
#获取取值位置
num=dash.index(rend)
if command!=None:
#根据取值位置映射到data选值
command(data[num])
==min(dash,…)==即在
dash
中取出任何一个值,算出最接近当前坐标的数值。
接着绑定完成拖动事件:
self.tag_bind(button,'<ButtonRelease-1>',check)#矫正位置
完整代码函数
def add_scalebar(self,pos:tuple,width=200,fg='#4258cc',activefg='#aeb5d7',bg='#99a3d5',data=(1,2,3,4,5),start=1,command=None):#绘制调节框
def mousedown(event):
scale.startx=self.canvasx(event.x)
def drag(event):
move=self.canvasx(event.x)-scale.startx
if self.canvasx(event.x)<pos[0] or self.canvasx(event.x)>pos[0]+width:
return
self.move(button,move,0)
self.delete(name)
active=self.create_line((pos[0],pos[1],move+scale.startx,pos[1]),fill=fg,width=3,tags=name)
scale.startx=self.canvasx(event.x)
def check(event):
end=int(self.canvasx(event.x))
if end<pos[0]:end=pos[0]
if end>pos[0]+width:end=pos[0]+width
rend=min(dash,key=lambda x:abs(x-end))
num=dash.index(rend)
if command!=None:
command(data[num])
scale=TinUINum()#记录数据结构体
back=self.create_line((pos[0],pos[1],pos[0]+width,pos[1]),fill=bg,width=3)
dash_t=width//(len(data)-1)
s=pos[0]#调节线段起点
dash=[s]#调节线段的终点位置
for i in data[1:]:
s+=dash_t
dash.append(s)
del s
active=self.create_line((pos[0],pos[1],dash[start],pos[1]),fill=fg,width=3)
name='scaleactive'+str(active)
self.addtag_withtag(name,active)#为重绘绑定tag名称
button=self.create_rectangle((dash[start],pos[1]-15,dash[start]+10,pos[1]+17),width=0,fill=fg)
self.tag_bind(button,'<Enter>',lambda event:self.itemconfig(button,fill=activefg))
self.tag_bind(button,'<Leave>',lambda event:self.itemconfig(button,fill=fg))
self.tag_bind(button,'<Button-1>',mousedown)
self.tag_bind(button,'<B1-Motion>',drag)
self.tag_bind(button,'<ButtonRelease-1>',check)#矫正位置
return name,back,button
效果
测试代码
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,30,'这里用来输入')
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')
a.mainloop()
最终效果
目前还没打算重绘(没错,我改主意了)。
2021-8新功能
可以允许用户点击选值区域直接选值。
2022-1-26新样式
2022-5-28新样式
- 线段不用重新绘制,而是用调节尺寸进行更新,动作更丝滑
- 仿winui3的圆形标识元素
2022-6-5新样式
- 标识线段圆角
2024-6-25新样式
优化交互圆形标识符
2024-8-23修复
修复禁用状态下,按钮仍然响应鼠标进入的问题。
github项目
pip下载
pip install tinui
结语
现在你可以将TinUI运用到你自己的tkinter项目了,甚至可以将TinUI最为窗口的唯一部件。
我开通了我自己的个人博客,欢迎访问。
🔆tkinter创新🔆