tkinter绘制组件(36)——树状图
- 引言
- 布局
- 函数结构
- 内容数据格式
- 整体框架
- 绘制元素与重绘宽度
- 标识元素
- 展开与闭合
- 完整函数代码
- 效果
- 测试代码
- 最终效果
- github项目
- pip下载
- 结语
引言
TinUI的第38个元素控件,也是TinUI-4.0-添加的第一个组件,代表着TinUI已经补齐tkinter原生的所有控件,可基本替代tkinter控件类型(至少是类型)。
我应该是第一个在tkinter画布上搞定树状图的冤种,真的好耗时间,代码量和TinUI的滚动条(scrollbar)差不多,但是好多细节都要处理,下面会讲。隔壁customtkinter还没有这个打算,目前正在做表格和标签栏视图,当然,后面应该也会跟上。
本来我也没打算用TinUI实现树状图,但是所有原生控件就剩它一个,而且树状图可以在分级数据显示中发挥重要作用,最终还是提上日程,在TinUI-4.0-中发布。
布局
函数结构
def add_treeview(self,pos:tuple,fg='#1a1a1a',bg='#f3f3f3',onfg='#1a1a1a',onbg='#eaeaea',oncolor='#3041d8',signcolor='#8a8a8a',width=200,height=300,font='微软雅黑 12',content=(('one',('1','2','3')),'two',('three',('a',('b',('b1','b2','b3')),'c')),'four'),command=None):#树状图
'''
pos-位置
fg-文本颜色
bg-背景色
onfg-选中文本颜色
onbg-选中背景色
oncolog-选中标识色
signcolor-滚动条提示色
width-宽度
height-高度
font-字体
content=(
a,
(b,(b1,b2,b3)),
(c,(c1,(c2,(c2-1,c2-2)),c3)),
d,
)-内容
command-回调函数,接受选中元素id层级从属列表
'''
内容数据格式
真的丧心病狂~~~~
因为涉及到层级关系,本来考虑到使用字典格式,但是突然发现,又不是每一个元素都有子级,这种json格式纯属画蛇添足。
那么就只能采取比较原始的多维元组格式了。
横着写如果没有语法高亮真的不知道在写什么,所以下面以分行写为例:
content=(
a-#1,
(b-#1,(b1-#2,b2-#2,b3-#2)),
(c-#1,(c1-#2,(c2-#2,(c2.1-#3,c2.2-#3)),c3-#2)),
d-#1
)
字母后面的-#n表示层级。
基本就是(一级,(一级,(下一级)),一级)的嵌套格式。
整体框架
整个treeview的绘制控件和滚动条绑定,是这次任务里最简单的部分。
nowid=None
fln=TinUINum()#用于寻找父级关系,目前效率比较低,之后考虑优化
frame=BasicTinUI(self,bg=bg)#主显示框,显示滚动条
box=BasicTinUI(frame,bg=bg,width=width,height=height)#显示选择内容
box.place(x=12,y=12)
cavui=self.create_window(pos,window=frame,width=width+24,height=height+24,anchor='nw')
uid='treeview'+str(cavui)
self.addtag_withtag(uid,cavui)
yscro=frame.add_scrollbar((width+12,12),widget=box,height=height,bg=bg,color=signcolor,oncolor=signcolor)#纵向
xscro=frame.add_scrollbar((12,height+12),widget=box,height=width,direction='x',bg=bg,color=signcolor,oncolor=signcolor)#横向
#id为back的uid
items=dict()#元素对象{id:(text,back,[sign]),...}
items_dict=dict()#链接关系(下一级){id:(id1,id2,id3,...),id2:(id2-1,id2-2,...),id-new:(...)...}
box.add_back((0,0,0,0),linew=0)
最后一行保证
box
里存在初始元素。
绘制元素与重绘宽度
这是本次第二简单的部分,可以从之前的listview中搬运一点过来。
首先就是元素绘制,因为有嵌套关系,所以就是循环解包,碰到一个多元组就解一个,确保所有元素在items
里的顺序。此外,考虑到存在父子级关系,所以在items_dict
中只保留一级关联,且为从上至下(从左到右)的顺序。
def endy():
return box.bbox('all')[-1]
def add_item(padx=5,texts:tuple=(),father_id=None):#添加元素
child_id=[]
for text in texts:
y=endy()+3
if type(text)==str:#单极
te=box.create_text((padx+15,y),text=text,font=font,fill=fg,anchor='nw')
back=box.add_back((),tuple([te]),fg=bg,bg=bg,linew=3)
items[back]=(te,back)
else:#存在子级
sign=box.create_text((padx,y),text='▽',font='Consolas 13',fill=signcolor,anchor='nw')#▷
te=box.create_text((padx+15,y),text=text[0],font=font,fill=fg,anchor='nw')
back=box.add_back((),tuple((sign,te)),fg=bg,bg=bg,linew=3)
items[back]=(te,back,sign)
add_item(padx+15,text[1],back)
box.tag_bind(sign,'<Button-1>',lambda event,s=sign,cid=back:close_view(s,cid))
box.tag_bind(back,'<Enter>',lambda event,_id=back:buttonin(_id))
box.tag_bind(back,'<Leave>',lambda event,_id=back:buttonout(_id))
box.tag_bind(back,'<Button-1>',lambda event,_id=back:click(_id))
box.tag_bind(te,'<Enter>',lambda event,_id=back:buttonin(_id))
box.tag_bind(te,'<Leave>',lambda event,_id=back:buttonout(_id))
box.tag_bind(te,'<Button-1>',lambda event,_id=back:click(_id))
child_id.append(back)
if father_id!=None:#存在父级
items_dict[father_id]=tuple(child_id)
最后一行代码就是对父组件的反向链接,确保所有可能存在的下一级组件均包含在内。这样做有一个好处,那就是即便是直接多层嵌套,也可以直接记录往上一级的所有节点元素,直到第一级。当然,这样的逻辑分解,其性能因电脑配置而异。接下来还会有一个比较依赖电脑性能和元素数量的操作。
对于重绘宽度,在table,listview,listbox等元素控件已经积累了经验,直接上代码:
#...
maxwidth=bbox[2]
if maxwidth<width-14:maxwidth=width-14
for i in items.keys():
old_coords=box.coords(i)
old_coords[0]=old_coords[6]=6.0
old_coords[2]=old_coords[4]=6+maxwidth
box.coords(i,old_coords)
#...
这里使用到了coords
方法,因为我是直接使用back
元素最为背景。
标识元素
treeview的标识元素和listview是一样的,但区别是每次选定后的位置重绘方法不一样。
因为listview的元素是不变的,因此使用元素序号直接计算标识元素位置。但是treeview不一样,鬼知道使用者点成什么样子。所以,只能每次控件更新后计算相应内容所在的位置,移动标识元素。
当然,这还涉及到当选定元素被折叠,或者展开元素中存在选中元素,都会涉及到额外的标识元素位置和可见性更改,但是这会在下一小节提到,本节只包括常规的标识元素更换。
def buttonin(cid):
if cid!=nowid:
box.itemconfig(cid,fill=onbg,outline=onbg)
def buttonout(cid):
if cid!=nowid:
box.itemconfig(cid,fill=bg,outline=bg)
def click(cid):
nonlocal nowid
box.itemconfig(line,state='normal')
box.itemconfig(nowid,fill=bg,outline=bg)#原来的
box.itemconfig(cid,fill=onbg,outline=onbg)#现在的
nowid=cid#互换次序
posi=box.bbox(nowid)[1]
box.moveto(line,1,posi+linew/5)
if command!=None:
fln.father_link=[cid]#父级关系
find_father_link(fln,cid)
command(fln.father_link[::-1])#[父级, 子1级, 子2级...]
这里可以看到command
接受的是一个列表。
加入我选定的元素id是12,它的从属结构为1->3->12
,那么列表就是[1,3,12]
。
其中的find_father_link
函数就是一个耗时操作,也就是根据子级寻找上一层级,直到第一层。目前我只能想到穷举法,或者是我只来得及写这种方法,如果有更好的反向寻找父级,可以向TinUI的GitHub项目中提供issue。这个函数内容如下:
def find_father_link(fln,cid):#获取元素父级关系
for i in items_dict:
if cid in items_dict[i]:
fln.father_link.append(i)
find_father_link(fln,i)
展开与闭合
这个内容是最烦人的,因为有很多级,只能够通过内部循环的方法进行界面操作。
我试过以下几种操作逻辑:
-
闭合后再展开原各级隐藏与否照旧。
-
闭合后所有子级,包括跨级,关闭,展开只展开直接下一级。
-
同【2】,但展开时展开所有子级,包括跨级。
但是,经过一次次测试,不管其它方法能不能成功,总之,我只实现了方法【2】,毕竟这是以目前最简单的方法实现较合理的功能。这是一种权衡(确信🧐。
闭合:
def close_view(sign,cid):#闭合
if box.itemcget(sign,'text')=='▷':#避免重复关闭节点
return
#重新绑定sign元素的相应方法
box.tag_bind(sign,'<Button-1>',lambda event:open_view(sign,cid))
box.itemconfig(sign,text='▷')
#获取所有子元素,包括跨级子元素
cids=get_cids(cid)
move='move'+str(cid)#单层管理命名元素
for i in cids:
#print(box.itemcget(items[i][0],'text'))
for uid in items[i]:
box.addtag_withtag(move,uid)#绑定移动tag标志
if i in items_dict:#如果这也是一个节点元素,关闭节点
close_view(items[i][-1],i)#内部关闭
bbox=box.bbox(move)
box.itemconfig(move,state='hidden')#关闭节点
index=tuple(items.keys()).index(cids[-1])+1#获取节点位置信息
if index!=len(items.keys()):
height=bbox[3]-bbox[1]#获取移动模块高度
for i in tuple(items.keys())[index:]:
for uid in items[i]:#移动所有下方元素内容
box.move(uid,0,-height)#移动下方元素
box.dtag(move)
if nowid in cids:#标识元素控制
box.itemconfig(line,state='hidden')#如果选中项被折叠
else:
click(nowid)#重新绘制位置,包括标识符
box.config(scrollregion=box.bbox('all'))
看注释,这里的注释比TinUI源文件详细很多。
展开:
def open_view(sign,cid):#展开
if box.itemcget(sign,'text')=='▽':
return
box.tag_bind(sign,'<Button-1>',lambda event:close_view(sign,cid))
box.itemconfig(sign,text='▽')
cids=items_dict[cid]
move='move'+str(cid)#单层管理命名元素
for i in cids:#只展开一层
for uid in items[i]:
box.addtag_withtag(move,uid)
box.itemconfig(move,state='normal')
bbox=box.bbox(move)
if bbox==None:return
index=tuple(items.keys()).index(cids[-1])+1
if index!=len(items.keys()):
height=bbox[3]-bbox[1]#获取移动模块高度
for i in tuple(items.keys())[index:]:
for uid in items[i]:
box.move(uid,0,height)
box.dtag(move)
if nowid in cids:#重新显示标识元素
click(nowid)
box.config(scrollregion=box.bbox('all'))
虽然这两段看起来简单,但是测试的时候还是费了一段时间。
完整函数代码
def add_treeview(self,pos:tuple,fg='#1a1a1a',bg='#f3f3f3',onfg='#1a1a1a',onbg='#eaeaea',oncolor='#3041d8',signcolor='#8a8a8a',width=200,height=300,font='微软雅黑 12',content=(('one',('1','2','3')),'two',('three',('a',('b',('b1','b2','b3')),'c')),'four'),command=None):#树状图
'''
content=(
a,
(b,(b1,b2,b3)),
(c,(c1,(c2,(c2-1,c2-2)),c3)),
d,
)
'''
def buttonin(cid):
if cid!=nowid:
box.itemconfig(cid,fill=onbg,outline=onbg)
def buttonout(cid):
if cid!=nowid:
box.itemconfig(cid,fill=bg,outline=bg)
def click(cid):
nonlocal nowid
box.itemconfig(line,state='normal')
box.itemconfig(nowid,fill=bg,outline=bg)#原来的
box.itemconfig(cid,fill=onbg,outline=onbg)#现在的
nowid=cid#互换次序
posi=box.bbox(nowid)[1]
box.moveto(line,1,posi+linew/5)
if command!=None:
fln.father_link=[cid]#父级关系
find_father_link(fln,cid)
command(fln.father_link[::-1])#[父级, 子1级, 子2级...]
def find_father_link(fln,cid):#获取元素父级关系
for i in items_dict:
if cid in items_dict[i]:
fln.father_link.append(i)
find_father_link(fln,i)
def endy():
return box.bbox('all')[-1]
def add_item(padx=5,texts:tuple=(),father_id=None):#添加元素
child_id=[]
for text in texts:
y=endy()+3
if type(text)==str:#单极
te=box.create_text((padx+15,y),text=text,font=font,fill=fg,anchor='nw')
back=box.add_back((),tuple([te]),fg=bg,bg=bg,linew=3)
items[back]=(te,back)
else:#存在子级
sign=box.create_text((padx,y),text='▽',font='Consolas 13',fill=signcolor,anchor='nw')#▷
te=box.create_text((padx+15,y),text=text[0],font=font,fill=fg,anchor='nw')
back=box.add_back((),tuple((sign,te)),fg=bg,bg=bg,linew=3)
items[back]=(te,back,sign)
add_item(padx+15,text[1],back)
box.tag_bind(sign,'<Button-1>',lambda event,s=sign,cid=back:close_view(s,cid))
box.tag_bind(back,'<Enter>',lambda event,_id=back:buttonin(_id))
box.tag_bind(back,'<Leave>',lambda event,_id=back:buttonout(_id))
box.tag_bind(back,'<Button-1>',lambda event,_id=back:click(_id))
box.tag_bind(te,'<Enter>',lambda event,_id=back:buttonin(_id))
box.tag_bind(te,'<Leave>',lambda event,_id=back:buttonout(_id))
box.tag_bind(te,'<Button-1>',lambda event,_id=back:click(_id))
child_id.append(back)
if father_id!=None:#存在父级
items_dict[father_id]=tuple(child_id)
def get_cids(cid):
cids=[]
if cid in items_dict:
for i in items_dict[cid]:
cids.append(i)
ccids=get_cids(i)
cids+=ccids
return cids
def open_view(sign,cid):#展开
if box.itemcget(sign,'text')=='▽':
return
box.tag_bind(sign,'<Button-1>',lambda event:close_view(sign,cid))
box.itemconfig(sign,text='▽')
cids=items_dict[cid]
move='move'+str(cid)#单层管理命名元素
for i in cids:#只展开一层
for uid in items[i]:
box.addtag_withtag(move,uid)
box.itemconfig(move,state='normal')
bbox=box.bbox(move)
if bbox==None:return
index=tuple(items.keys()).index(cids[-1])+1
if index!=len(items.keys()):
height=bbox[3]-bbox[1]#获取移动模块高度
for i in tuple(items.keys())[index:]:
for uid in items[i]:
box.move(uid,0,height)
box.dtag(move)
if nowid in cids:#重新显示标识元素
click(nowid)
box.config(scrollregion=box.bbox('all'))
def close_view(sign,cid):#闭合
if box.itemcget(sign,'text')=='▷':
return
box.tag_bind(sign,'<Button-1>',lambda event:open_view(sign,cid))
box.itemconfig(sign,text='▷')
cids=get_cids(cid)
move='move'+str(cid)#单层管理命名元素
for i in cids:
#print(box.itemcget(items[i][0],'text'))
for uid in items[i]:
box.addtag_withtag(move,uid)
if i in items_dict:
close_view(items[i][-1],i)
bbox=box.bbox(move)
box.itemconfig(move,state='hidden')
index=tuple(items.keys()).index(cids[-1])+1
if index!=len(items.keys()):
height=bbox[3]-bbox[1]#获取移动模块高度
for i in tuple(items.keys())[index:]:
for uid in items[i]:
box.move(uid,0,-height)
box.dtag(move)
if nowid in cids:#标识元素控制
box.itemconfig(line,state='hidden')
else:
click(nowid)#重新绘制位置
box.config(scrollregion=box.bbox('all'))
def bindview(event):
if event.state==0:
box.yview_scroll(int(-1*(event.delta/120)), "units")
elif event.state==1:
box.xview_scroll(int(-1*(event.delta/120)), "units")
nowid=None
fln=TinUINum()#用于寻找父级关系,目前效率比较低,之后考虑优化
frame=BasicTinUI(self,bg=bg)#主显示框,显示滚动条
box=BasicTinUI(frame,bg=bg,width=width,height=height)#显示选择内容
box.place(x=12,y=12)
cavui=self.create_window(pos,window=frame,width=width+24,height=height+24,anchor='nw')
uid='treeview'+str(cavui)
self.addtag_withtag(uid,cavui)
yscro=frame.add_scrollbar((width+12,12),widget=box,height=height,bg=bg,color=signcolor,oncolor=signcolor)#纵向
xscro=frame.add_scrollbar((12,height+12),widget=box,height=width,direction='x',bg=bg,color=signcolor,oncolor=signcolor)#横向
#id为back的uid
items=dict()#元素对象{id:(text,back,[sign]),...}
items_dict=dict()#链接关系(下一级){id:(id1,id2,id3,...),id2:(id2-1,id2-2,...),id-new:(...)...}
box.add_back((0,0,0,0),linew=0)
add_item(5,content)
#重绘宽度
bbox=box.bbox(tuple(items.keys())[0])#第一个元素高度-4
linew=bbox[3]-bbox[1]
line=box.create_line((1,linew/3,1,linew*2/3),fill=oncolor,width=3,capstyle='round')
#重绘宽度
maxwidth=bbox[2]
if maxwidth<width-14:maxwidth=width-14
for i in items.keys():
old_coords=box.coords(i)
old_coords[0]=old_coords[6]=6.0
old_coords[2]=old_coords[4]=6+maxwidth
box.coords(i,old_coords)
allback=self.add_back((),tuple([cavui]),fg=bg,bg=bg,linew=3)
self.addtag_withtag(uid,allback)
box.config(scrollregion=box.bbox('all'))
box.move(line,0,-linew-height)
box.itemconfig(line,state='hidden')
box.bind('<MouseWheel>',bindview)
return items,items_dict,box,uid
效果
测试代码
def test12(cid):
for i in cid:
print(trvbox.itemcget(trvl[i][0],'text')+'/',end='')
print('')
if __name__=='__main__':
#...
trvl,_,trvbox,_=b.add_treeview((1220,1300),command=test12)
#...
最终效果
github项目
TinUI的github项目地址
pip下载
pip install tinui
结语
现在TinUI已经补齐了所有的tkinter原生控件。4.0版本还会包含基础image控件与scrollbar的平滑滚动功能,或许吧。
🔆tkinter创新🔆