tkinter绘制组件(36)——树状图

引言

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)

展开与闭合

这个内容是最烦人的,因为有很多级,只能够通过内部循环的方法进行界面操作。

我试过以下几种操作逻辑:

  1. 闭合后再展开原各级隐藏与否照旧。

  2. 闭合后所有子级,包括跨级,关闭,展开只展开直接下一级。

  3. 同【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)
    #...

最终效果

在这里插入图片描述

2023-1-9更新

修复展开或闭合时触发click的逻辑错误。

2023-11-12修复

修复多级展开错误的问题

2024-2-11修复

修复部分情况下多级展开时提示元素错位的问题


github项目

TinUI的github项目地址

pip下载

pip install tinui

结语

现在TinUI已经补齐了所有的tkinter原生控件。4.0版本还会包含基础image控件,或许吧。

关于TinUI-4发布的简短预告,见划水公告

🔆tkinter创新🔆

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要使用Tkinter绘制折线图,你可以按照以下步骤进行操作: 1. 导入Tkinter模块和绘图相关模块: ```python import tkinter as tk from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg ``` 2. 创建一个Tkinter窗口和画布: ```python root = tk.Tk() canvas = tk.Canvas(root) canvas.pack() ``` 3. 创建一个Figure对象,并添加一个子图(Axes): ```python fig = Figure(figsize=(6, 4), dpi=100) ax = fig.add_subplot(111) ``` 4. 定义折线图的数据: ```python x = [1, 2, 3, 4, 5] y = [2, 4, 6, 8, 10] ``` 5. 在子图上绘制折线图: ```python ax.plot(x, y) ``` 6. 创建一个FigureCanvasTkAgg对象,并将其添加到Tkinter画布上: ```python canvas = FigureCanvasTkAgg(fig, master=canvas) canvas.draw() canvas.get_tk_widget().pack() ``` 7. 运行Tkinter的事件循环: ```python tk.mainloop() ``` 完整的代码示例如下: ```python import tkinter as tk from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg # 创建Tkinter窗口和画布 root = tk.Tk() canvas = tk.Canvas(root) canvas.pack() # 创建Figure和Axes对象 fig = Figure(figsize=(6, 4), dpi=100) ax = fig.add_subplot(111) # 定义折线图的数据 x = [1, 2, 3, 4, 5] y = [2, 4, 6, 8, 10] # 在子图上绘制折线图 ax.plot(x, y) # 创建FigureCanvasTkAgg对象并添加到Tkinter画布上 canvas = FigureCanvasTkAgg(fig, master=canvas) canvas.draw() canvas.get_tk_widget().pack() # 运行Tkinter的事件循环 tk.mainloop() ``` 这样就可以通过Tkinter绘制一个简单的折线图了。你可以根据需要修改数据和绘图样式。希望对你有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值