swipecontrol直译滑动控件,参考WinUI的SwipeControl。
虽然,这个控件在平常PC端界面中不常用,因为它响应的是触摸屏或触摸板的横向滚动事件,所以不具备通用性,但是为了照顾一下希望将功能按钮隐藏在一个文本提示元素下的编写者,还是决定加入这个控件。
def add_swipecontrol(self,pos:tuple,text:str='',height=50,width=400,fg='#1a1a1a',bg='#f3f3f3',line='#fbfbfb',data:dict={'left':({'text':'✔️\nok','fg':'#202020','bg':'#bcbcbc','command':print},),'right':({'text':'❌\nclose'},)},font=('微软雅黑',12)):#绘制滑动控件
'''
pos-位置
text-文本
height-高度
width-宽度
fg-文本颜色
bg-背景颜色
line-边框颜色
data-按钮内容
font-字体
'''
这是控件的主体部分,也就是文本元素滑动后显示的内容,包括往左滑后显示的右侧内容,以及向右滑动显示的左侧内容。两部分内容互不重叠,用标记right='rights', left='left'
表示。
因为显示太多个按钮在UI设计上没有意义,因此按六分之一宽度为每个按钮宽度,也就是说按钮最好不要超过六个。
控件主体使用BasicTinUI,方便滑动时文本隐藏:
itemw=width//6#背景元素宽度。软限制为6个,多出内容没有意义
back=BasicTinUI(self,bg=line)#背景容器
backitem=self.create_window(pos,width=width+2,height=height+2,anchor='nw',window=back)
uid='swipecontrol'+str(backitem)
self.addtag_withtag(uid,backitem)
right,left='rights','lefts'#背景元素位置
center='centers'
nowmode=center#当前状态
响应按钮的内容、颜色、返回调用的函数均在data
(字典)参数中表示。
data
的设计结构如下:
{
'left':(
{'text':'content','fg':'...','bg':'...'},
... ,
),
'right':(
{'text':'content',},
... ,
)
}
其中,left
表示左侧按钮数组,right
同理。两者均可有可无。
每个按钮表示的元素是一个字典,必须包含text
键,fg, bg
可选,有默认值。
解析与创建的过程代码:
if 'left' in data:#往右滑
endx=1
for item in data['left']:
_bg=item['bg'] if 'bg' in item else '#bcbcbc'
_fg=item['fg'] if 'fg' in item else '#202020'
bitem=back.create_rectangle((endx,1,endx+itemw,1+height),fill=_bg,width=0,tags=left)
fitem=back.create_text((endx+itemw/2,1+height/2),text=item['text'],fill=_fg,font=font,tags=left)
command=item['command'] if 'command' in item else None
back.tag_bind(bitem,'' ,lambda e,func=command:_docommand(func))
back.tag_bind(fitem,'' ,lambda e,func=command:_docommand(func))
endx+=itemw
leftbbox=back.bbox(left)
leftw=leftbbox[2]-leftbbox[0]
if 'right' in data:#往左滑
endx=width+1
for item in data['right']:
_bg=item['bg'] if 'bg' in item else '#bcbcbc'
_fg=item['fg'] if 'fg' in item else '#202020'
bitem=back.create_rectangle((endx-itemw,1,endx,1+height),fill=_bg,width=0,tags=right)
fitem=back.create_text((endx-itemw/2,1+height/2),text=item['text'],fill=_fg,font=font,tags=right)
command=item['command'] if 'command' in item else None
back.tag_bind(bitem,'' ,lambda e,func=command:_docommand(func))
back.tag_bind(fitem,'' ,lambda e,func=command:_docommand(func))
endx-=itemw
rightbbox=back.bbox(right)
rightw=rightbbox[2]-rightbbox[0]
contback=back.create_rectangle((1,1,width+1,height+1),fill=bg,width=0,tags='cont')
cont=back.create_text((3,1+height/2),anchor='w',text=text,fill=fg,font=font,tags='cont')
back.itemconfig(left,state='hidden')
back.itemconfig(right,state='hidden')
back.bind('' ,move)
back.tag_bind(contback,'' ,_recenter)
back.tag_bind(cont,'' ,_recenter)
响应的事件处理:
def _docommand(func):
nonlocal nowmode
time.sleep(0.01)
_animation('center')
nowmode=center
if func!=None:
func()
swipecontrol响应三种事件:
左右滑动 - 文本滑动
按钮点击 - 响应,并重新归中
文本元素点击 - 归中
在我的一篇笔记tkinter滚动事件详解中讲到,event.state
可以判断滚动的类型,但是每次滚动事件基本都是连续触发,而swipecontrol只需要响应一次。因此可以在滚动处理事件开始解绑该事件,处理完后继续监听。
def move(event):#滚动响应
nonlocal nowmode
back.unbind('' )
if event.state!=1:
back.bind('' ,move)
return
#...
back.bind('' ,move)
然后通过对event.delta
正负值、是否存在对应一侧的按钮,以及当前状态的判断,决定是否移动,往哪边移动。
注意,移动不是单纯地向右或向左,还包括居中归位。
if event.delta<0 and 'right' in data:#左滑,显示right
if nowmode==right:
back.bind('' ,move)
return
back.itemconfig(left,state='hidden')
back.itemconfig(right,state='normal')
_animation('center')
_animation('left')
nowmode=right
elif event.delta>0 and 'left' in data:#右滑,显示left
if nowmode==left:
back.bind('' ,move)
return
back.itemconfig(right,state='hidden')
back.itemconfig(left,state='normal')
_animation('center')
_animation('right')
nowmode=left
移动处理:
def _animation(side):#移动动画
if side=='left':
for i in range(0,rightw+5,5):
back.move('cont',-5,0)
time.sleep(0.001)
back.update()
elif side=='right':
for i in range(0,leftw+5,5):
back.move('cont',5,0)
time.sleep(0.001)
back.update()
elif side=='center':
if nowmode==right:
for i in range(0,rightw+5,5):
back.move('cont',5,0)
time.sleep(0.001)
back.update()
elif nowmode==left:
for i in range(0,leftw+5,5):
back.move('cont',-5,0)
time.sleep(0.001)
back.update()
def add_swipecontrol(self,pos:tuple,text:str='',height=50,width=400,fg='#1a1a1a',bg='#f3f3f3',line='#fbfbfb',data:dict={'left':({'text':'✔️\nok','fg':'#202020','bg':'#bcbcbc','command':print},),'right':({'text':'❌\nclose'},)},font=('微软雅黑',12)):#绘制滑动控件
def _animation(side):#移动动画
if side=='left':
for i in range(0,rightw+5,5):
back.move('cont',-5,0)
time.sleep(0.001)
back.update()
elif side=='right':
for i in range(0,leftw+5,5):
back.move('cont',5,0)
time.sleep(0.001)
back.update()
elif side=='center':
if nowmode==right:
for i in range(0,rightw+5,5):
back.move('cont',5,0)
time.sleep(0.001)
back.update()
elif nowmode==left:
for i in range(0,leftw+5,5):
back.move('cont',-5,0)
time.sleep(0.001)
back.update()
def move(event):#滚动响应
nonlocal nowmode
back.unbind('' )
if event.state!=1:
back.bind('' ,move)
return
if event.delta<0 and 'right' in data:#左滑,显示right
if nowmode==right:
back.bind('' ,move)
return
back.itemconfig(left,state='hidden')
back.itemconfig(right,state='normal')
_animation('center')
_animation('left')
nowmode=right
elif event.delta>0 and 'left' in data:#右滑,显示left
if nowmode==left:
back.bind('' ,move)
return
back.itemconfig(right,state='hidden')
back.itemconfig(left,state='normal')
_animation('center')
_animation('right')
nowmode=left
back.bind('' ,move)
def _docommand(func):
nonlocal nowmode
time.sleep(0.01)
_animation('center')
nowmode=center
if func!=None:
func()
def _recenter(e):
nonlocal nowmode
_animation('center')
nowmode=center
itemw=width//6#背景元素宽度。软限制为6个,多出内容没有意义
back=BasicTinUI(self,bg=line)#背景容器
backitem=self.create_window(pos,width=width+2,height=height+2,anchor='nw',window=back)
uid='swipecontrol'+str(backitem)
self.addtag_withtag(uid,backitem)
right,left='rights','lefts'#背景元素位置
center='centers'
nowmode=center#当前状态
if 'left' in data:#往右滑
endx=1
for item in data['left']:
_bg=item['bg'] if 'bg' in item else '#bcbcbc'
_fg=item['fg'] if 'fg' in item else '#202020'
bitem=back.create_rectangle((endx,1,endx+itemw,1+height),fill=_bg,width=0,tags=left)
fitem=back.create_text((endx+itemw/2,1+height/2),text=item['text'],fill=_fg,font=font,tags=left)
command=item['command'] if 'command' in item else None
back.tag_bind(bitem,'' ,lambda e,func=command:_docommand(func))
back.tag_bind(fitem,'' ,lambda e,func=command:_docommand(func))
endx+=itemw
leftbbox=back.bbox(left)
leftw=leftbbox[2]-leftbbox[0]
if 'right' in data:#往左滑
endx=width+1
for item in data['right']:
_bg=item['bg'] if 'bg' in item else '#bcbcbc'
_fg=item['fg'] if 'fg' in item else '#202020'
bitem=back.create_rectangle((endx-itemw,1,endx,1+height),fill=_bg,width=0,tags=right)
fitem=back.create_text((endx-itemw/2,1+height/2),text=item['text'],fill=_fg,font=font,tags=right)
command=item['command'] if 'command' in item else None
back.tag_bind(bitem,'' ,lambda e,func=command:_docommand(func))
back.tag_bind(fitem,'' ,lambda e,func=command:_docommand(func))
endx-=itemw
rightbbox=back.bbox(right)
rightw=rightbbox[2]-rightbbox[0]
contback=back.create_rectangle((1,1,width+1,height+1),fill=bg,width=0,tags='cont')
cont=back.create_text((3,1+height/2),anchor='w',text=text,fill=fg,font=font,tags='cont')
back.itemconfig(left,state='hidden')
back.itemconfig(right,state='hidden')
back.bind('' ,move)
back.tag_bind(contback,'' ,_recenter)
back.tag_bind(cont,'' ,_recenter)
return back,backitem
b.add_swipecontrol((320,1300),'swipe control')
TinUI的github项目地址
pip install tinui
swipecontrol结合textbox可以实现列表显示功能,详见test/swipecontrol.py
。
tkinter创新