Python Tkinter 可折叠树状浏览器,及其在类和函数、文件浏览器的应用(由idlelib tree模块修改)

模块由idlelib tree模块修改,完善一些问题,重写了获取类和函数的方法,便于获取正在编辑代码的类和函数。重写了文件浏览模块,支持添加收藏,双击py(pyw)文件会打开函数浏览器,文件浏览器支持很多文件的图标。代码基本都有注释,方便新手学习,注释不一定完全正确。
完整代码和需要的图标已经上传,类和函数更新了新的获取方式,以文章为准

模块效果图

在这里插入图片描述

效果图

导入模块,设置好需要的参数

import stat, os, sys, re, tkinter as tk
from tkinter import ttk

icon_path = os.path.join(os.path.dirname(__file__), "Icons") # 图标文件夹 os.path.join(os.path.dirname(__file__), "Icons")
module_path = __file__ # 默认函数浏览文件路径
bg_color = '#ffffff' # 常规项背景颜色
st_color = '#d9d9d9' # 选中项背景颜色

创建带滚动条的画布

class ScrolledCanvas:
    "带有滚动条和快捷键绑定的画布小部件"
    def __init__(s, master, frame, **opts):
        if 'yscrollincrement' not in opts:
            opts['yscrollincrement'] = 17
        s.master = master
        s.frame = frame
        s.frame.rowconfigure(0, weight=1)     # 行自动适应窗口大小
        s.frame.columnconfigure(0, weight=1)  # 列自动适应窗口大小
        s.canvas = tk.Canvas(s.frame, **opts) # Canvas绘图窗口
        s.canvas.grid(row=0, column=0, sticky="nsew")

        # 右侧滚动条
        s.vbar = tk.Scrollbar(s.frame, name="vbar")
        s.vbar.grid(row=0, column=1, sticky="nse")
        s.canvas['yscrollcommand'] = s.vbar.set
        s.vbar['command'] = s.canvas.yview

        # 下方滚动条"horizontal" 水平显示
        s.hbar = tk.Scrollbar(s.frame, name="hbar", orient="horizontal")
        s.hbar.grid(row=1, column=0, sticky="ews")
        s.canvas['xscrollcommand'] = s.hbar.set
        s.hbar['command'] = s.canvas.xview

        s.canvas.bind("<MouseWheel>", lambda event: s.unit_up(event) if event.delta > 0 else s.unit_down(event))
        s.canvas.bind("<Key-Prior>", s.page_up)   # PageUp键
        s.canvas.bind("<Key-Next>" , s.page_down) # PageDown键
        s.canvas.bind("<Key-Up>"   , s.unit_up)     # 上键
        s.canvas.bind("<Key-Down>" , s.unit_down)   # 下键
        s.canvas.bind("<Alt-Key-2>", s.zoom_height) # Alt+2
        s.canvas.focus_set()
    def page_up(s, event):
        # s.vbar.get()[0]为'0.0'时禁止向上
        if s.vbar.get()[0]:s.canvas.yview_scroll(-1, "page")
        return "break"
    def page_down(s, event):
        s.canvas.yview_scroll(1, "page")
        return "break"
    def unit_up(s, event):
        # s.vbar.get()[0]为'0.0'时禁止向上
        if s.vbar.get()[0]: s.canvas.yview_scroll(-1, "unit")
        return "break"
    def unit_down(s, event):
        s.canvas.yview_scroll(1, "unit")
        return "break"
    def zoom_height(s, event): # 窗口上下满屏
        import re
        geom = s.master.wm_geometry()
        m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom)
        if not m:
            s.master.bell()
            return
        width, height, x, y = map(int, m.groups())
        newheight = s.master.winfo_screenheight()
        if sys.platform == 'win32':
            newy = 0
            newheight = newheight - 72
        else:
            newy = 0
            newheight = newheight - 88
        if height >= newheight:
            newgeom = ""
        else:
            newgeom = "%dx%d+%d+%d" % (width, newheight, x, newy)
        s.master.wm_geometry(newgeom)
        return "break"

创建节点结构获取的父类-TreeItem

class TreeItem:
    """
    表示树项的父类。
    方法通常应被重写,否则将使用默认操作。
    """

    expandable = None

    def __init__(s):
        "做任何你需要做的事。"

    def _IsExpandable(s):
        "不要覆盖!由TreeNode调用。"
        if s.expandable is None:
            s.expandable = s.IsExpandable()
        return s.expandable

    def IsExpandable(s):
        "返回是否有子项。"
        return 1

    def _GetSubList(s):
        "不要覆盖!由TreeNode调用。"
        if not s.IsExpandable():
            return []
        sublist = s.GetSubList()
        if not sublist:
            s.expandable = 0
        return sublist

    def GetText(s):
        "返回要显示在标签前的字符串(如果有)。"

    def GetLabelText(s):
        "返回要显示的标签文本字符串。"

    def GetSubList(s):
        "返回组成子列表的项目列表。"

    def IsEditable(s):
        "返回是否可以编辑项目的文本。"

    def SetLabelText(s, text):
        "更改项目的文本(如果可编辑)。"

    def GetIconName(s):
        "返回要正常显示的图标的名称。"

    def GetSelectedIconName(s):
        "返回选定时要显示的图标的名称。"

    def OnDoubleClick(s):
        "在双击该项时调用。"

函数浏览页面节点获取的TreeItem子类-ModuleBrowserTreeItem

class ModuleBrowserTreeItem(TreeItem):
    """
    模块中子节点的浏览器树。
    使用TreeItem作为树结构的基础。
    """

    def __init__(s, name, tree):
        # name 要显示的名称
        # tree 该函数/类的信息(位置,图标标记,子集字典)
        s.position = tree[0]
        s.isfunction = tree[1]
        s.obj = tree[2]
        s.name = name

    def GetText(s):
        "返回要显示在函数名前的文字。"
        #if s.isfunction in ['def','class']:
        #    return s.isfunction

    def GetLabelText(s):
        "返回要显示的函数/类的名称。"
        return s.name

    def GetIconName(s):
        "返回要显示的图标的名称。"
        if s.isfunction == 'def':
            return "form1"
        elif s.isfunction == 'class':
            return "form2"
        elif s.isfunction in ['.py','.pyw']:
            return "Function2"
        else:
            return "blank"

    def IsExpandable(s):
        "判断s.obj是否有子集。"
        return len(s.obj)

    def GetSubList(s):
        "返回子级的ModuleBrowserTreeItem。"
        return [ModuleBrowserTreeItem(key, s.obj[key]) for key in s.obj.keys()]

    def OnDoubleClick(s):
        "双击返回类或函数所在的位置。"
        print (s.position)
        return s.position

文件浏览页面节点获取的TreeItem子类-FileBrowserTreeItem

class FileBrowserTreeItem(TreeItem):
    """
    模块中子节点的浏览器树。
    使用TreeItem作为树结构的基础。
    """
    def __init__(s, tree):
        # tree 文件夹信息,格式:(名称, 属性, 完整路径, [子集...., (名称,属性,完整路径,[子集...]) ])
        # s.root = treebrowser.root # 可以做弹窗提示,比如改名的时候
        s.name = tree[0]
        s.attr = tree[1]
        s.dir  = tree[2]
        s.obj  = tree[3]

    def GetLabelText(s):
        "返回要显示的名称。"
        return s.name

    def SetLabelText(s, text):
        "更改项目的文本(如果可编辑)。"
        dir = s.dir[:s.dir.rfind(s.name)] # 上级文件夹
        if os.path.exists(dir):
            try:
                os.renames(dir + s.name, dir + text)
                s.name = text
                s.dir  = dir + text
            except:
                pass

    def GetIconName(s):
        "返回要显示的图标的名称。"
        if s.attr == 'dir': return 'dir'
        elif s.attr == 'root' : return 'pc' # 根"计算机"
        elif s.attr == 'piece' : return 'piece' # 盘符
        elif s.attr == 'collect': return 'collect' # 收藏文件夹图标
        elif s.attr in ['.txt']: return 'txt'
        elif s.attr in ['.dll', '.bin', '.cab']: return "dll"
        elif s.attr in ['.apk']: return "apk"
        elif s.attr in ['.reg']: return "tree"
        elif s.attr in ['.lnk']: return "lnk"
        elif s.attr in ['.chm']: return "help"
        elif s.attr in ['.gba','.nes','.chd','.swf']: return "game"
        elif s.attr in ['.py', '.pyw']: return "py"
        elif s.attr in ['.ini', '.db', '.bat', '.dat', '.sav', '.tag']: return "db"
        elif s.attr in ['.zip', '.rar', '.7z', '.gz']: return "zip"
        elif s.attr in ['.exe', '.iso', '.msi']: return "exe"
        elif s.attr in ['.mp3', '.flac', '.ape', '.wav']: return "mp3"
        elif s.attr in ['.ttf', '.ttc', '.fon']: return "font"
        elif s.attr in ['.pdf']: return "pdf"
        elif s.attr in ['.xml','.html','.htm']: return "html"
        elif s.attr in ['.doc', '.docx', '.rtf']: return "docx"
        elif s.attr in ['.xls', '.xlsx', '.cav']: return "xlsx"
        elif s.attr in ['.jpg', '.png', '.gif', '.bmp', '.ico', '.raw']: return "jpg"
        elif s.attr in ['.scr', '.avi', '.rmvb', '.mp4', '.flv']: return "video"
        else: return "blank"

    def IsEditable(s):
        "判断是否可以编辑节点"
        Protect = ['C:\\Windows\\','C:\\ProgramData\\','C:\\Program Files\\','C:\\Documents and Settings\\']
        for dir in Protect:
            if s.dir.count(dir): return False # 上面列出的文件夹及其子文件禁止编辑
        if s.attr == 'dir':
            if len(s.dir) == 3: return False # 禁止编辑盘符
            else: return True
        elif s.attr not in ['root','piece','collect']: # 判断文件是否可编辑
            return True
        else:
            return False

    def IsExpandable(s, skip_dir=True): 
        "判断是否有子集。"
        if len(s.obj): return True
        elif s.attr == 'root': return True
        elif s.attr in ['dir','piece','collect']:
            if skip_dir: # skip_dir是否跳过文件夹的判断
                return True # 直接返回True可以大幅提升子文件夹多的文件夹打开速度,空文件夹点击后会再识别为空
            else:
                try: len(os.listdir(s.dir))   #  可能无权限查看
                except: return False
        else:
            return False

    def GetSubList(s):
        "返回子级的FileBrowserTreeItem对象列表,文件和文件夹区分开,直接排序是按拼音排序会比较混乱"
        dirlist  = [] # 文件夹列表
        filelist = [] # 文件列表
        collect  = [] # 收藏文件夹,直接显示在一级目录

        if not s.obj: 
            # 尝试获取文件夹子集,无权限则跳过,放在这里获取而不是后面添加到文件夹子集里是为了减少不必要的运算,增加上级文件夹的打开效率
            try : s.obj = os.listdir(s.dir) 
            except: pass

        for child in s.obj:
            if isinstance(child, tuple): # 判断child是不是元组,是的话为磁盘或收藏文件夹
                collect.append(FileBrowserTreeItem(child))
                continue
            path = s.dir + child
            if not os.path.exists(path): continue # 文件不存在则跳过,主要是规避收藏夹子集传入不存在文件(夹)
            if (len(path) > 3) and s.FileStat(path): # 判断文件属性,不判断盘符,跳过隐藏文件和受保护文件
                continue
            if os.path.isdir(path): # 先处理文件夹
                path += '\\' # 文件夹尾加上'\\'
                dirlist.append(FileBrowserTreeItem((child, 'dir', path, []))) 
            else: # 处理文件
                filelist.append(FileBrowserTreeItem((child, os.path.splitext(child)[1].lower(), path, []))) # 先把文件信息存储到列表中,放文件夹列表后
        return dirlist + filelist + collect # 文件夹排在文件前面,最后是收藏夹

    def OnDoubleClick(s):
        "双击返回完整路径。"
        if s.attr in ['.py','.pyw']: # 如果文件是py文件就打开模块浏览器
            treebrowser.new_Module_node(s.dir)
        print (s.dir)
        return s.dir

    def FileStat(s, file_path):
        "判断文件是否是隐藏文件及受保护文件"
        file_stat = os.stat(file_path) # 函数获取文件的状态信息
        # st_file_attributes获取属性值
        if file_stat.st_file_attributes & 2: # 隐藏文件的属性值为2,因此我们可以通过与运算('&')来判断文件的属性值中是否包含2来判断文件是否是隐藏文件
            return True
        if stat.S_ISREG(file_stat.st_mode):  # S_ISREG判断文件是否是普通文件,判断文件是否是受保护文件
            mode = stat.S_IMODE(file_stat.st_mode) # 获取文件的权限模式
            if (mode & stat.S_IRUSR) and (mode & stat.S_IWUSR) and (mode & stat.S_IXUSR):   # 判断文件的用户权限
                return True
            elif (mode & stat.S_IRGRP) and (mode & stat.S_IWGRP) and (mode & stat.S_IXGRP): # 判断文件的组权限
                return True
            elif (mode & stat.S_IROTH) and (mode & stat.S_IWOTH) and (mode & stat.S_IXOTH): # 判断文件的其他用户权限
                return True
        return False

获取代码类和函数结构的函数cscope

def cscope(file = None, retarn_args=False, line_num=True, skip_comment=True, text=''):
    '''
    file         需要获取类和函数结构的文件路径
    retarn_args  函数名是否包含args
    line_num     True返回行数,False返回位置
    skip_comment 是否跳过三引号注释,要考虑多种情况,会增加运算;最好的办法是关闭,然后规范代码,将注释内缩进设为一致
    text         需要获取类和函数的字符串
    '''
    if file:
        dir, base = os.path.split(file)
        name, ext = os.path.splitext(base)
        if os.path.normcase(ext) not in [".py",".pyw"]:
            return {base:(0,ext,{})}

        text = open(file, 'r', encoding='utf-8').read()
    else:
        base = '类和函数'
        ext  = '.py'

    flines = []
    idx = lpos = 0
    lst = text.splitlines() #获取文件行列表
    if  not (lst) : lst.append(u'')
    for index, line in enumerate(lst):
        lnum = index+1    # 行数
        ln = line.strip() # 去除空格后的字符串
        if not (ln) : # 跳过空行
            lpos += len(line) + 1
            continue 
        ind = line.find(ln[0]) # 缩进
        flines.append((idx, lnum, ind, lpos, ln)) # (序号,行数,字符串缩进,行首的位置,去除空格后的字符串)
        idx += 1 # 序号
        lpos += len(line) + 1 # 下一行行首的位置

    last = root = {}
    end = {u'class' : u'', u'def' : u'()'}
    lev = [(0, root)] # 用来临时存储同一缩进的函数
    comment = [False, (), # 用来存储注释信息
               [("'''", '"', r'\"(.*?)\"'), 
                ('"""', "'", r"\'(.*?)\'")]] # 这里要换行,否则12两种三引号跳过方式无法正常判断
    for idx, lnum, ind, lpos, ln in flines: # 序号,行数,字符串缩进,行的位置,去除空格后的字符串
        if skip_comment: # 判断是否处理三引号注释
            if comment[0]: # 本行在注释范围内
                flines[idx] = (idx, lnum, flines[(idx - 1)][2], lpos, ln) # 将本行缩进改为上一行的缩进
                if ln.count(comment[1][0]): # 结束跳过
                    comment[0] = False # 本行为注释行最后一行,结束跳过注释
                continue
            else:
                for mark in comment[2]:
                    Mnum = ln.count(mark[0])
                    if Mnum: # 存在三引号
                        sign = 3 # 使用哪种跳过方式,这里用第3种相对完美
                        if sign == 1: # 正则表达式处理-不完美
                            pattern = mark[2]  # 指定引号内的内容获取-正则表达式,无法判断复杂情况,比如需要判断的引号分别在不同字符串里面
                            matches = re.findall(pattern, ln) # 获取在指定引号内的字符串
                            for string in matches:
                                Mnum -= string.count(mark[0]) # 减去在引号内的三引号数量
                            if divmod(Mnum ,2)[1]: # 如果还存在引号,且为奇数个
                                comment[:2] = [True, mark]
                        elif sign == 2: # 引号数量判断方法-不完美
                            Lnum = ln.find(mark[0])  # 最左侧三引号位置
                            Rnum = ln.rfind(mark[0]) # 最右侧三引号位置
                            Ynum = divmod(ln[:Lnum].count(mark[1]) ,2)[1] # 判断三引号前有几个不同引号,如果为奇数则三引号在字符串内
                            Jnum = ln[:Rnum].rfind('#') # 三引号前'#'的位置
                            if Jnum < 0 or (Jnum >= 0 and (ln[Jnum:Rnum].count('"') or ln[Jnum:Rnum].count("'"))): 
                                # 不存在'#' 或 三引号和'#'之间存在引号。只是三引号出现的其中一种情况,都处理会增加计算
                                if Lnum == Rnum and not Ynum: # 只存在一个三引号,且不是存在字符串内
                                    comment[:2] = [True, mark]                         
                                elif Lnum < Rnum and Ynum and divmod(ln[Lnum:Rnum].count(mark[1]) ,2)[1]: # 存在多个三引号,且最左侧的在字符串里面
                                    comment[:2] = [True, mark]
                        elif sign == 3: # 转换成列表切片判断,相对完美
                            text = ln
                            index = idx - 1
                            while flines[index][4][-1] == '\\': # 上一行结尾是'\'
                                text = flines[index][4] + '>>>' + text # 合并上一行一起判断, 加'>>>'是为了后面协助判断跳过'#'注释
                                index -= 1
                            strlist = list(text) # 转为列表
                            marks = [False, False, '', 0, 0] # [是否在引号内, 是否在三引号内, 什么引号, 引号位置, 连续相同引号的数量]

                            index = 0
                            while True: # 这里用 whlie 不是 for 是为了方便修改 strlist
                                if index == len(strlist): break
                                string = strlist[index]

                                if string in ["'",'"']: # 如果存在引号
                                    if marks[0]: # 在引号内
                                        if index-marks[3] == 1 and string == marks[2]: # 前一个是相同引号的
                                            marks[3] = index  # 更新引号位置
                                            marks[4] += 1     # 连续引号数量加1
                                            if marks[4] == 3: # 连续3个引号
                                                if marks[1]: marks = [False, False, '', 0, 0]  # 原来在3引号内,重置marks
                                                else: marks[1] = True # 不在3引号内,将后续的设为在3引号内
                                            elif marks[4] == 6: marks = [False, False, '', 0, 0] # 连续6个引号,重置marks
                                        elif index-marks[3] > 1 and string == marks[2]: # 两相同引号中间有间隔
                                            marks[3] = index # 更新引号位置
                                            if marks[1]: marks[4] = 1 # 三引号内部
                                            else: marks[0] = False
                                    else: marks = [True, marks[1], string, index, 1] # 在引号外,新记录引号信息
                                elif marks[0]: # 引号内部,且不是引号
                                    if marks[4] == 2: marks = [False, False, '', 0, 0] # 连续两个引号后不是引号的, 说明是空字符串, 重置marks
                                    else: marks[4] = 0 # 重置连续引号
                                if string == '#' and not marks[0]: # 跳过#注释
                                    text = text[index:]
                                    if text.count('\\>>>'): # '#'号注释结尾有'\',且后面右三引号注释的情况
                                        text = text[text.find('\\>>>')+4:] # 重置text从下一行开始
                                        strlist = list(text)
                                        index = 0
                                    else:
                                        break
                                index += 1
                            if marks[1]: # 开始跳过
                                comment[:2] = [True, (marks[2]*3, '', '')] # 记录三引号信息,跳过后面注释
                                break # 跳过本行下一个三引号的判断

        # 根据缩进级别更新lev列表,用于存储同一缩进级别的函数
        if ind < lev[-1][0]: # 缩进 < lev最后标记的缩进
            if idx > 0 :     # 序号 > 0
                rastrow = flines[(idx - 1)] # 上一行列表
                if rastrow[-1][-1] in (u',', u'\\') : # 上一行是以','或'\'结尾,跳过元组、列表、字典的换行缩进,以及长字符串中间的'\'换行
                    flines[idx] = (idx, lnum, rastrow[2], lpos, ln) # 将本行缩进改为上一行的缩进,方便下一行比对
                    continue
                if ln[0] == '#': # '#'注释
                    flines[idx] = (idx, lnum, rastrow[2], lpos, ln) # 将本行缩进改为上一行的缩进,方便下一行比对
                    continue
            try :
                while ind < lev[-1][0] : # 缩进 < 上一个缩进记录
                    lev.pop()   # 弹出lev最后一项,直至当前缩进和存储的最后一个缩进同级
            except IndexError : 
                return None
        elif ind > lev[-1][0] : # 缩进 > lev最后标记的缩进
            lev.append((ind, last)) # lev添加(缩进,字典)
        t = ln.split() # 以空格分割成列表
        if t[0] in end.keys() : # 字符串列表第一位是'class'或'def'
            try:
                if retarn_args: # 保留args
                    t = ln.split(' ', 1)
                    tok = t[1].split(u':')[0] # 剪切'('及':'之前的部分
                    name = tok
                else:
                    tok  = t[1].split(u'(')[0].split(u':')[0] # 剪切'('及':'之前的部分
                    name = tok + end[t[0]]  # 加上end设置好的结尾
                if line_num: num = lnum # line_num 返回行数
                else: num = lpos + ind  # 返回位置
                if name in lev[-1][1] : # name和lev最后一项的字典里的函数重名
                    name = (u'%s%s*%d' % (tok, end[t[0]], num)) # name = tok:+字符串位置+end设置好的结尾
                last = {} # 重置last
                lev[-1][1][name] = (num, t[0], last) # lev最后一个元组标记的字典加入{name:(字符串位置,{})}
            except:pass
    root = {base:(0, ext, root)} # 以文件名做树状图的根
    return root

鼠标滚动事件函数

def wheel_event(event, widget=None):
    """处理滚轮事件。
    在Windows上,滚轮向上滚动时,event.delta = 120*n。
    参数widget是必需的,以便浏览器标签绑定可以将底层画布传递过去。
    此函数依赖于widget.yview不会被子类覆盖。
    """
    # 判断滚轮是否向上滚动
    up = {tk.EventType.MouseWheel: event.delta > 0,
          tk.EventType.ButtonPress: event.num == 4}
    # 如果没有传入widget参数,则使用event中的widget属性
    widget = event.widget if widget is None else widget

    lines = 5
    if up[event.type]: # 如果滚轮向上滚动
        lines = -5 if widget.canvasy(0) else 0 # lines = -5,如果可见区域的顶部纵坐标为0则lines = 0 防止过度向下

    widget.yview(tk.SCROLL, lines, 'units') # 调用widget的yview方法进行滚动
    # 返回'break'以阻止事件继续传播
    return 'break'

绘制树状图的主要类-TreeNode

class TreeNode:
    # 初始化方法,传入画布、父节点、项对象
    def __init__(s, canvas, parent, item):
        s.canvas = canvas
        s.parent = parent # 接收父节点TreeNode对象,只在TreeNode内部传递,外部传入None
        s.item = item     # TreeItem子类对象
        s.state = 'collapsed' # 选中标记为未选中
        s.selected = False    # 判断是否选中
        s.children = []       # 存储子节点TreeNode对象
        s.x = s.y = None
        s.iconimages = {} # 图标的PhotoImage实例缓存

    def destroy(s): 
        "退出"
        for c in s.children[:]: 
            s.children.remove(c) # 删除所有子节点
            c.destroy()
        s.parent = None # 将父节点设置为None

    def geticonimage(s, name):
        "根据名称获取图标文件,生成tkImage对象返回"
        try: return s.iconimages[name] # 如果存在同名图标,返回已经生成的tkImage对象
        except KeyError: pass
        file, ext = os.path.splitext(name) # 获取文件名和后缀
        ext = ext or ".gif" # 没有后缀的以".gif"为后缀
        fullname = os.path.join(icon_path, file + ext) # 连接文件路径
        image = tk.PhotoImage(master=s.canvas, file=fullname) # 生成tkImage对象
        s.iconimages[name] = image # 将tkImage对象缓存在s.iconimages
        return image

    def select(s, event=None):
        "选中节点的方法,单击图标也可调动"
        if s.selected:
            return
        s.deselectall()   # 全不选
        s.selected = True # 指示是否有选中项
        s.canvas.delete(s.image_id) # 删除旧的图标对象
        s.drawicon() # 绘制新的图标
        s.drawtext() # 绘入文本内容

    def deselect(s, event=None):
        "取消选择节点的方法"
        if not s.selected:
            return
        s.selected = False
        s.canvas.delete(s.image_id) # 删除图标对象
        s.drawicon() 
        s.drawtext()

    def deselectall(s):
        "全不选所有节点的方法,并调用 s.parent.deselectall 方法递归操作父节点"
        if s.parent: 
            s.parent.deselectall() # 操作父节点递归操作全不选
        else:
            s.deselecttree() 

    def deselecttree(s):
        "全不选所有节点的方法, 并调用 s.children-deselecttree() 方法递归操作子节点"
        if s.selected:
            s.deselect()
        for child in s.children:
            child.deselecttree() # 操作子节点递归操作全不选

    def flip(s, event=None): 
        "双击节点"
        if s.state == 'expanded': # 选中节点为展开状态
            s.collapse() # 收起节点
        else:
            s.expand()   # 张开节点
        s.item.OnDoubleClick() # 执行s.item的双击函数
        return "break"

    def expand(s, event=None):
        "判断当前节点是否可展开"
        if not s.item._IsExpandable(): # 判断有无子项
            return
        if s.state != 'expanded': # 选中节点未展开
            s.state = 'expanded'  # 选中节点设置为展开
            s.update() 
            s.view() # 更新视野

    def collapse(s, event=None):
        "收起选中的节点"
        if s.state != 'collapsed': # 选中节点未关闭
            s.state = 'collapsed'  # 选中节点设置为关闭
            s.update()

    def view(s):
        "更新视野内容"
        top = s.y - 2
        bottom = s.lastvisiblechild().y + 17
        height = bottom - top
        visible_top = s.canvas.canvasy(0)        # 获取可见区域的顶部纵坐标
        visible_height = s.canvas.winfo_height() # 获取画布的可见高度
        visible_bottom = s.canvas.canvasy(visible_height)   # 获取画布可见区域底部的y坐标
        if visible_top <= top and bottom <= visible_bottom: # 如果画布的顶部在可见区域内,则直接返回
            return
        x0, y0, x1, y1 = s.canvas._getints(s.canvas['scrollregion']) # 获取滚动区域的坐标信息
        if top >= visible_top and height <= visible_height: # 如果当前可见区域在画布顶部和底部之间, 则计算滚动比例
            fraction = top + height - visible_height
        else: # 否则,只滚动到可见区域的顶部
            fraction = top
        fraction = float(fraction) / y1 # 将滚动比例转换为浮点数,并除以y1得到最终的滚动比例
        s.canvas.yview_moveto(fraction) # 将画布滚动到指定的比例位置

    def lastvisiblechild(s):
        "返回节点的最后一个可见子节点。"
        if s.children and s.state == 'expanded':     # 如果当前节点有子节点且当前节点状态为展开
            return s.children[-1].lastvisiblechild() # 返回最后一个可见且未展开子节点的对象
        else:
            return s # 返回当前节点对象

    def update(s): 
        "刷新画布"
        if s.parent: # 存在父节点
            s.parent.update() # 刷新父节点
        else:
            oldcursor = s.canvas['cursor'] # 保存当前光标样式
            s.canvas['cursor'] = "watch"   # 转圈光标
            s.canvas.update() #更新画布
            s.canvas.delete(tk.ALL) # 删除画布上的所有对象
            s.draw(7, 5) # 在画布上绘制新的图形,左顶点坐标(7, 5)
            x0, y0, x1, y1 = s.canvas.bbox(tk.ALL) # 获取包含内容的画布边界框的位置和大小
            s.canvas.configure(scrollregion=(0, 0, x1, y1)) # 设置画布的滚动区域
            s.canvas['cursor'] = oldcursor # 恢复光标样式

    def draw(s, x, y):
        # XXX 这个硬编码的几何常数太多了!
        dy = 20 # 设置默认的间距
        s.x, s.y = x, y # 更新对象的位置
        s.drawicon()    # 绘制图标
        s.drawtext()    # 绘制文本
        if s.state != 'expanded': # 如果状态不是展开,则返回当前y值加上间距
            return y + dy

        # 画子节点
        if not s.children: # 如果子节点对象列表为空
            sublist = s.item._GetSubList() # 获取组成子元素的项目列表
            if not sublist:
                # _IsExpandable() 方法错误地允许了这种情况
                return y+17
            for item in sublist:
                # s代表当前实例对象,通过s.XXXX()可以访问当前实例对象的方法或属性
                # s.__class__代表当前实例对象所属的类, s.__class__.XXXX()可以访问类的方法或属性。 这种方式可以用于在实例方法中调用类方法, 或者在实例方法中访问类的属性
                child = s.__class__(s.canvas, s, item) # 为每个子元素创建一个新的对象
                s.children.append(child)

        # 计算子节点的起始位置和上一个子节点的结束位置
        cx = x+20
        cy = y + dy
        cylast = 0

        # 遍历子节点对象,绘制连接线并递归调用draw方法
        for child in s.children:
            cylast = cy
            s.canvas.create_line(x+9, cy+7, cx, cy+7, fill="gray50") # 树状图的横线,"gray50" 灰色值50,"gray100"则为白色,值越小颜色越深
            cy = child.draw(cx, cy)
            if child.item._IsExpandable(): # 判断是否有子项
                if child.state == 'expanded':
                    iconname = "minusnode" # 展开后显示的减号图标
                    callback = child.collapse
                else:
                    iconname = "plusnode"  # 收起后显示的加号图标
                    callback = child.expand
                image = s.geticonimage(iconname) # 获取图标tkImage对象
                id = s.canvas.create_image(x+9, cylast+7, image=image) # 绘制图标
                # 在画布上绑定单击和双击事件,直到画布被删除:
                s.canvas.tag_bind(id, "<1>", callback)
                s.canvas.tag_bind(id, "<Double-1>", lambda x: None)

        id = s.canvas.create_line(x+9, y+10, x+9, cylast+7, fill="gray50") # 绘制竖向连接线并调整其位置
        s.canvas.tag_lower(id) # 将连接线置于其他元素之下
        return cy # 返回最后一个子节点的结束位置作为最终结果

    def drawicon(s):
        "绘制图标"
        if s.selected: # 如果选中
            imagename = (s.item.GetSelectedIconName() or # 选中时图标
                         s.item.GetIconName() or         # 设定好的图标
                         "openfolder")
        else:
            imagename = s.item.GetIconName() or "folder" # 设定好的图标
        image = s.geticonimage(imagename) # 获取图标的tk对象
        id = s.canvas.create_image(s.x, s.y, anchor="nw", image=image) # 将图标绘入画布
        s.image_id = id # 将图像ID存储在对象的属性中
        s.canvas.tag_bind(id, "<1>", s.select)      # 鼠标左键单击
        s.canvas.tag_bind(id, "<Double-1>", s.flip) # 鼠标左键双击

    def drawtext(s):
        "绘入文字"
        # 计算文本的x和y坐标
        textx = s.x+20-1
        texty = s.y-4

        text = s.item.GetText() # 获取显示在标签前的文字,可以用来注释
        if text: 
            id = s.canvas.create_text(textx, 
                                      texty, # 如果上下有偏移这里+-调整
                                      anchor="nw",
                                      text=text)
            s.canvas.tag_bind(id, "<1>", s.select)
            s.canvas.tag_bind(id, "<Double-1>", s.flip)
            x0, y0, x1, y1 = s.canvas.bbox(id)
            textx = max(x1, 10) + 10 # 标签和文字间的间隙宽度
        
        labeltext = s.item.GetLabelText()  or "<no text>" # 获取标签文字
        
        # 如果存在entry属性,调用edit_finish方法
        try:
            s.entry
        except AttributeError:
            pass
        else:
            s.edit_finish() # 保存编辑后的内容

        # 如果不存在label属性,则创建新的Label
        try:
            s.label
        except AttributeError: 
            # label显示文字内容, label主要是为了放置编辑框Entry, 不需要编辑框的可以使用create_text替代, 减少画布上的窗口部件
            s.label = tk.Label(s.canvas, text=labeltext, 
                               bg=bg_color, # 背景颜色,这里设置为和画布一样
                               bd=0, padx=2, pady=2)
        if s.selected:
            s.label['bg'] = st_color # 更改背景颜色为选中背景颜色
        else:
            s.label['bg'] = bg_color # 更改背景颜色为默认背景颜色

        # 在画布上创建一个窗口来显示标签,并绑定事件
        id = s.canvas.create_window(textx, texty,
                                    anchor="nw", window=s.label)
        s.label.bind("<1>", s.select_or_edit)
        s.label.bind("<Double-1>", s.flip)
        s.label.bind("<MouseWheel>", lambda e: wheel_event(e, s.canvas))
        s.text_id = id

    def select_or_edit(s, event=None):
        "单击label"
        if s.selected and s.item.IsEditable(): # 为已选中节点且是可编辑节点
            s.edit(event)   # 生成输入框
        else:
            s.select(event) # 选中节点

    def edit(s, event=None):
        "生成输入框"
        s.entry = tk.Entry(s.label, bd=0, highlightthickness=1, width=0) # Entry输入框
        s.entry.insert(0, s.label['text']) # 输入框内插入项目文本
        s.entry.selection_range(0, tk.END) # 选中所有文本
        s.entry.pack(ipadx=5)
        s.entry.focus_set() # 设置焦点,focus_get() 获取焦点部件名称
        s.entry.bind("<Return>", s.edit_finish) # 回车键保存
        s.entry.bind("<Escape>", s.edit_cancel) # Esc取消,关闭Entry

    def edit_finish(s, event=None):
        "保存编辑后的内容"
        try:
            entry = s.entry
            del s.entry
        except AttributeError:
            return
        text = entry.get() # 获取编辑框内文本
        entry.destroy()
        if text and text != s.item.GetLabelText():
            s.item.SetLabelText(text) # 更改节点的文本
        text = s.item.GetLabelText()  # 重新获取节点文本
        s.label['text'] = text
        s.drawtext() # 重绘选项
        s.canvas.focus_set() # 设置焦点

    def edit_cancel(s, event=None):
        "取消保存"
        try:
            entry = s.entry
            del s.entry
        except AttributeError:
            return
        entry.destroy()
        s.drawtext() # 重绘选项
        s.canvas.focus_set() # 设置焦点

Tk窗口和操作类-TreeBrowser

class TreeBrowser:
    """创建一个树状结构的窗口。"""
    def __init__(s):
        s.node_list = [] # 存储node窗口

    def close(s, event=None):
        "关闭窗口和树节点。"
        s.root.destroy()
        for node in s.node_list: node.destroy()
        os._exit(0)

    def main(s):
        "创建浏览器tkinter部件,包括树。"
        s.root = tk.Tk()
        s.root.title("树状浏览器")
        s.root.protocol("WM_DELETE_WINDOW", s.close)
        s.root.wm_iconname("Module Browser")
        s.root.focus_set()

        #s.tab = CustomNotebook(s.root)
        s.tab = ttk.Notebook(s.root)          # 创建Notebook选项卡控件
        s.tab.pack(expand = 1, fill = "both") # 让Notebook控件显示出来
        s.tab.enable_traversal()              # 为s.tab启用键盘快捷方式,Control-Tab向后切换,Shift-Control-Tab向前切换

        # collect收藏文件夹,格式[(标题,图标标记,完整路径,子集列表),....],子集列表为空的话浏览下面所有文件(夹),也可指定显示的子集文件夹
        collect = [('收藏1','collect','C:\\',['Program Files','Users']),('收藏2','collect','C:\\',[])] 
        s.new_Module_node(path=module_path) # 函数浏览器-浏览文件
        s.new_file_node(collect) # 文件浏览器

        lah = '''
def 函数():
    def a1():
       def a11():''
       def a12():''
       def a13():''
    def a2():''
class 类:
    def b1(s):''
    def b2(s):'' 
        '''
        s.new_Module_node(text=lah) # 函数浏览器-浏览字符串

        s.root.mainloop()

    def new_node(s, text, item):
        "新建tab,并绘入node界面"
        new_tab = tk.Frame(s.tab) # 添加tab选项卡
        s.tab.add(new_tab, text = text)
        new_Frame = tk.Frame(new_tab)
        new_Frame.pack(expand=1, fill="both")
        # 创建带滚动画布,canvas画布尽量和mainloop在同一函数下否则可能快捷键会失效
        new_Canvas = ScrolledCanvas(s.root, new_Frame, bg=bg_color, highlightthickness=0, takefocus=1).canvas
        new_node = TreeNode(new_Canvas, None, item) # 将画布及内容传入TreeNode,绘制树状图
        new_node.update() # 刷新node
        new_node.expand() # 显示内容
        s.node_list.append(new_node)

    def new_Module_node(s, path=None,text=''):
        "新建模块浏览器窗口"
        # tree获取需要显示的类和函数
        tree = cscope(file=path, retarn_args=False, line_num=True, skip_comment=True, text=text) #函数名不包含args,返回行数,跳过三引号注释
        key  = next(iter(tree.keys())) # 返回第一个key文件名,作为树状图的根
        item = ModuleBrowserTreeItem(key, tree[key])
        s.new_node('函数浏览器', item)
        
    def new_file_node(s, collect=[]):
        "新建文件浏览器窗口"
        # tree 需要显示树根"计算机"以及一级结构"硬盘分区"和"收藏文件夹";chr()将整数转为字符串,再通过os.path.isdir判断A-Z是否有硬盘分区
        tree = ("计算机", "root", "",
               [( chr(i)+':', 'piece', chr(i)+':\\', []) for i in range(65,91) if os.path.isdir(chr(i)+':\\')] + collect)
        item = FileBrowserTreeItem(tree)
        s.new_node('文件浏览器', item)

最后试下效果

treebrowser = TreeBrowser()
treebrowser.main()
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值