tkinter绘制组件(21)——滚动条
tkinter绘制组件(21)滚动条
tkinter绘制组件(21)——滚动条
引言
滚动条关系到所有可视范围可变的组件显示功能,所有与用户交互很少,但它就像一个机器中的螺丝钉,缺一不可。但同时,滚动条是唯一一个双向绑定组件,计算参数多,绘制难度大。
我应该是我已知的开源tkinter拓展组件库开发者中第一个用画布绘制出滚动条的了,完成时间在2022年2月6日,第一版最终样式修改时间在2022年2月中旬。
做好准备。之前我在绘制TinUI表格(table)控件时,说那是我最难绘制出的组件,但是前几天我完成scrollbar的绘制后,发现我用了150行左右的代码。
我知道,150行代码根本不算多,相比于TinUI的1000+行和TinGroup的6000+行,真的不算什么,但是相比于其它单个组件的绘制,这已经十分“给面子”了。
注意了,因为源码量有点大,这里只考虑纵向滚动条。但是放心,TinUI均支持横向和纵向滚动条。
布局
函数结构
def add_scrollbar(self,pos:tuple,widget,height:int=200,direction='y',bg='#f0f0f0',color='#999999',oncolor='#89898b'):#绘制滚动条
'''
pos::起始位置
widget::绑定的组件,如文本框、画布、列表框。注意是tkinter组件,不是TinUI组件
height::长度
direction::方向,x或y。大小写无关
bg::背景色
color::滚动条未激活颜色
oncolor::滚动条激活颜色
'''
背景
这次的滚动条是仿winUI3的,因此背景框的两端为圆角。
if mode=='y':
back=self.create_polygon((pos[0]+5,pos[1]+5,pos[0]+5,pos[1]+height-5,pos[0]+5,pos[1]+5),
width=12,outline=bg)
uid='scrollbar'+str(back)
self.itemconfig(back,tags=uid)
上标和下标
我发现字符集本来就有“▲▼”两个字符,因此我就不绘制了。
top=self.create_text(pos,text='▲',font='微软雅黑 8',anchor='nw',fill=oncolor,tags=uid)
bottom=self.create_text((pos[0],pos[1]+height),text='▼',font='微软雅黑 8',anchor='sw',fill=oncolor,tags=uid)
横向时,使用
angle
参数,设置角度为90°。
滚动块
起始位置
因为我们无法确定在绑定滚动条前,被绑定组件的可视范围在哪,不过我们可以先粗略地绘制一下滚动块,再通过后期的样式调整设置它的范围。
sc=self.create_polygon((pos[0]+5,pos[1]+20,pos[0]+5,pos[1]+height-20,pos[0]+5,pos[1]+20,),width=3,outline=color,tags=uid)
接下来,我们计算出滚动块的三个参数:
- 最小位置
- 最大位置
- 可动范围
其中,可动范围可以继续计算出滚动块顶端在可动范围中的百分比,继而显示绑定组件的可视范围。记住,这计划很重要。
样式
这个版块我们很熟悉,直接跳过,等一下看源码。
组件控制滚动块
这个很简单了,直接绑定组件的yscrollcommand
参数事件,调整滚动条的位置。
def widget_move(sp,ep):#控件控制滚动条滚动
if mode=='y':
startp=start+canmove*float(sp)
endp=start+canmove*float(ep)
self.coords(sc,(pos[0]+5,startp+5,pos[0]+5,endp-5))
#绑定组件
widget.config(yscrollcommand=widget_move)
滚动条控制组件
滚动块
先来看看滚动块如何控制组件。借鉴之前的调整框(spinbox)的滑块,我们不难得出以下代码:
def mousedown(event):
if mode=='y':
scroll.start=self.canvasy(event.y)#定义起始纵坐标
elif mode=='x':
scroll.start=self.canvasx(event.x)#横坐标
def drag(event):
bbox=self.bbox(sc)
if mode=='y':#纵向
move=self.canvasy(event.y)-scroll.start#将窗口坐标转化为画布坐标
#防止被拖出范围
if bbox[1]+move<start-1 or bbox[3]+move>end+1:
return
self.move(sc,0,move)
elif mode=='x':#横向
move=self.canvasx(event.x)-scroll.start
if bbox[0]+move<start-1 or bbox[2]+move>end+1:
return
self.move(sc,move,0)
#重新定义画布中的起始拖动位置
scroll.start+=move
sc_move()
def sc_move():#滚动条控制控件滚动
bbox=self.bbox(sc)
if mode=='y':
startp=(bbox[1]-start)/canmove
widget.yview('moveto',startp)
#...
scroll=TinUINum()
作者:你以为这就可以了?
读者:对啊,这可是直接copy调整框的拖动代码啊。
作者:你往上拖没毛病,你往下试试😏。
读者:what?居然拖不动!
没想到吧,因为在你改变组件可视范围时,组件同时也在调用widget_move
参数,这样就会形成逻辑矛盾。解决的办法是是用一个中间变量,限制组件控制滚动条。
def widget_move(sp,ep):#控件控制滚动条滚动
if mode=='y' and use_widget:
#...
def mousedown(event):
nonlocal use_widget#当该值为真,才允许响应widget_move函数
use_widget=False
#...
def mouseup(event):
nonlocal use_widget
use_widget=True
#...
use_widget=True#是否允许控件控制滚动条
标识
注意到我们之前绘制的上标和下标了吗?它们有大用处。
def topmove(event):#top
bbox=self.bbox(sc)
if mode=='y':
move=-(bbox[3]-bbox[1])/2
if bbox[1]+move<start:
move=-(bbox[1]-start)
self.move(sc,0,move)
sc_move()
def bottommove(event):#bottom
bbox=self.bbox(sc)
if mode=='y':
move=(bbox[3]-bbox[1])/2
if bbox[3]+move>end:
move=(end-bbox[3])
self.move(sc,0,move)
sc_move()
点一次划过半个滚动块,除非超出滚动范围。
背景响应
当你点击滚动条背景时,滚动条也会滚动,但是每个UI库响应这个事件的效果都不一样。TinUI选择点哪滚哪,以上(左)端为标准。
def backmove(event):#bottom
bbox=self.bbox(sc)
if mode=='y':
posy=self.canvasy(event.y)
move=posy-bbox[1]
if move>0 and move+bbox[3]>end:
move=end-bbox[3]
if move<0 and move+bbox[1]<start:
move=start-bbox[1]
self.move(sc,0,move)
sc_move()
绑定所有事件
逻辑代码完成了,只剩下绑定该绑定的事件了。
self.tag_bind(sc,'<Button-1>',mousedown)
self.tag_bind(sc,'<ButtonRelease-1>',mouseup)
self.tag_bind(sc,'<B1-Motion>',drag)
#绑定样式
self.tag_bind(sc,'<Enter>',enter)
self.tag_bind(sc,'<Leave>',leave)
#绑定点击滚动
self.tag_bind(top,'<Button-1>',topmove)
self.tag_bind(bottom,'<Button-1>',bottommove)
self.tag_bind(back,'<Button-1>',backmove)
完整代码函数
def add_scrollbar(self,pos:tuple,widget,height:int=200,direction='y',bg='#f0f0f0',color='#999999',oncolor='#89898b'):#绘制滚动条
#滚动条宽度7px,未激活宽度3px;建议与widget相隔5xp
def enter(event):#鼠标进入
self.itemconfig(sc,outline=oncolor,width=7)
def leave(event):#鼠标离开
self.itemconfig(sc,outline=color,width=3)
def widget_move(sp,ep):#控件控制滚动条滚动
if mode=='y' and use_widget:
startp=start+canmove*float(sp)
endp=start+canmove*float(ep)
self.coords(sc,(pos[0]+5,startp+5,pos[0]+5,endp-5))
elif mode=='x' and use_widget:
startp=start+canmove*float(sp)
endp=start+canmove*float(ep)
self.coords(sc,(startp+5,pos[1]+5,endp+5,pos[1]+5))
def mousedown(event):
nonlocal use_widget#当该值为真,才允许响应widget_move函数
use_widget=False
if mode=='y':
scroll.start=self.canvasy(event.y)#定义起始纵坐标
elif mode=='x':
scroll.start=self.canvasx(event.x)#横坐标
def mouseup(event):
nonlocal use_widget
use_widget=True
def drag(event):
bbox=self.bbox(sc)
if mode=='y':#纵向
move=self.canvasy(event.y)-scroll.start#将窗口坐标转化为画布坐标
#防止被拖出范围
if bbox[1]+move<start-1 or bbox[3]+move>end+1:
return
self.move(sc,0,move)
elif mode=='x':#横向
move=self.canvasx(event.x)-scroll.start
if bbox[0]+move<start-1 or bbox[2]+move>end+1:
return
self.move(sc,move,0)
#重新定义画布中的起始拖动位置
scroll.start+=move
sc_move()
def topmove(event):#top
bbox=self.bbox(sc)
if mode=='y':
move=-(bbox[3]-bbox[1])/2
if bbox[1]+move<start:
move=-(bbox[1]-start)
self.move(sc,0,move)
elif mode=='x':
move=-(bbox[2]-bbox[0])/2
if bbox[0]+move<start:
move=-(bbox[0]-start)
self.move(sc,move,0)
sc_move()
def bottommove(event):#bottom
bbox=self.bbox(sc)
if mode=='y':
move=(bbox[3]-bbox[1])/2
if bbox[3]+move>end:
move=(end-bbox[3])
self.move(sc,0,move)
elif mode=='x':
move=(bbox[2]-bbox[0])/2
if bbox[2]+move>end:
move=(end-bbox[2])
self.move(sc,0,move)
sc_move()
def backmove(event):#back
bbox=self.bbox(sc)
if mode=='y':
posy=self.canvasy(event.y)
move=posy-bbox[1]
if move>0 and move+bbox[3]>end:
move=end-bbox[3]
if move<0 and move+bbox[1]<start:
move=start-bbox[1]
self.move(sc,0,move)
elif mode=='x':
posx=self.canvasx(event.x)
move=posx-bbox[0]
if move>0 and move+bbox[2]>end:
move=end-bbox[2]
if move<0 and move+bbox[0]<start:
move=start-bbox[0]
self.move(sc,move,0)
sc_move()
def sc_move():#滚动条控制控件滚动
bbox=self.bbox(sc)
if mode=='y':
startp=(bbox[1]-start)/canmove
widget.yview('moveto',startp)
elif mode=='x':
startp=(bbox[0]-start)/canmove
widget.xview('moveto',startp*1.2)
if direction.upper()=='X':
mode='x'
elif direction.upper()=='Y':
mode='y'
else:
return None
#上标、下标 ▲▼
if mode=='y':
#back=self.create_rectangle((pos[0],pos[1],pos[0]+10,pos[1]+height),fill=bg,width=0)
back=self.create_polygon((pos[0]+5,pos[1]+5,pos[0]+5,pos[1]+height-5,pos[0]+5,pos[1]+5),
width=12,outline=bg)
uid='scrollbar'+str(back)
self.itemconfig(back,tags=uid)
top=self.create_text(pos,text='▲',font='微软雅黑 8',anchor='nw',fill=oncolor,tags=uid)
bottom=self.create_text((pos[0],pos[1]+height),text='▼',font='微软雅黑 8',anchor='sw',fill=oncolor,tags=uid)
#sc=self.create_rectangle((pos[0],pos[1]+15,pos[0]+10,pos[1]+height-15),fill=color,width=0,tags=uid)
sc=self.create_polygon((pos[0]+5,pos[1]+20,pos[0]+5,pos[1]+height-20,pos[0]+5,pos[1]+20,),
width=3,outline=color,tags=uid)
#起始和终止位置
start=pos[1]+15
end=pos[1]+height-15
canmove=end-start
#绑定组件
widget.config(yscrollcommand=widget_move)
elif mode=='x':
back=self.create_polygon((pos[0]+5,pos[1]+5,pos[0]+height-5,pos[1]+5,pos[0],pos[1]+5),
width=12,outline=bg)
uid='scrollbar'+str(back)
self.itemconfig(back,tags=uid)
top=self.create_text((pos[0]+2,pos[1]+11),text='▲',angle=90,font='微软雅黑 8',anchor='w',fill=oncolor,tags=uid)
bottom=self.create_text((pos[0]+height,pos[1]),text='▼',angle=90,font='微软雅黑 8',anchor='se',fill=oncolor,tags=uid)
sc=self.create_polygon((pos[0]+20,pos[1]+5,pos[0]+height-20,pos[1]+5,pos[0]+20,pos[1]+5),
width=3,outline=color,tags=uid)
start=pos[0]+8
end=pos[0]+height-13
canmove=(end-start)*0.95
widget.config(xscrollcommand=widget_move)
scroll=TinUINum()
use_widget=True#是否允许控件控制滚动条
self.tag_bind(sc,'<Button-1>',mousedown)
self.tag_bind(sc,'<ButtonRelease-1>',mouseup)
self.tag_bind(sc,'<B1-Motion>',drag)
#绑定样式
self.tag_bind(sc,'<Enter>',enter)
self.tag_bind(sc,'<Leave>',leave)
#绑定点击滚动
self.tag_bind(top,'<Button-1>',topmove)
self.tag_bind(bottom,'<Button-1>',bottommove)
self.tag_bind(back,'<Button-1>',backmove)
return top,bottom,back,sc,uid
效果
测试代码
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 modern way to show tkinter widget in your application')
m1=b.add_title((0,680),'test TinUI scrolled',size=2,angle=24)
b.add_paragraph((20,290),''' TinUI是基于tkinter画布开发的界面UI布局方案,作为tkinter拓展和TinEngine的拓展而存在。目前,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,330),350,'这里用来输入')
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/')
b.add_link((400,530),'执行print函数',print)
_,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\nworld','c'),('you\ncan','2','3'),('I','II','have a dream, then try your best to get it!')))
b.add_paragraph((300,850),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')
b.add_info((680,140),info_text='this is info widget in TinUI')
mtb=b.add_paragraph((0,720),'测试菜单(右键单击)')
b.add_menubar(mtb,cont=(('command',print),('menu',test1),'-',('TinUI文本移动',test)))
ttb=b.add_paragraph((0,800),'TinUI能做些什么?')
b.add_tooltip(ttb,'很多很多')
b.add_back(pos=(0,0),uids=(ttb,),bg='cyan')
_,_,ok3,_=b.add_waitbar3((600,800),width=240)
b.add_button((600,750),text='停止带状等待框',command=lambda event:ok3())
textbox=b.add_textbox((890,100),text='这是文本输入框,当然,无法在textbox的参数中绑定横向滚动'+'\n换行'*30)[0]
textbox['wrap']='none'
b.add_scrollbar((1095,100),textbox)
b.add_scrollbar((890,305),textbox,direction='x')
a.mainloop()
最终效果
没错,我就是第一个用tkinter画布绘制出滚动条的tkinter爱好者。
隔壁customtkinter的滚动条于2022年6月份首次添加,比TinUI晚四个月。
2022-8-15修复
修复横向滚动条到最大值后按右侧按钮,滚动条向下移动的问题。
2022-8-25新样式
- 背景元素圆角左右对称(宽度改为13)
- 不使用时保持静默状态
2024-2-11新样式
- 上下(左右)顶点标识符使用Segoe Fluent Icons
github项目
pip下载
pip install tinui
修改开源协议
从2022年2月开始,TinUI(包括TinUIXml技术)使用GPLv3开源协议。
开源条款见TinUI的github项目地址。
结语
TinUI推出了最新的现代化xml布局方式——TinUIXml。赶快去使用吧!!!
🔆tkinter创新🔆
更多推荐
所有评论(0)