Python Tkinter 树状浏览图,类和函数及文件浏览的应用(由idlelib tree模块修改)

news2024/11/14 14:48:29

模块由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()

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/888292.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【AGC】崩溃数据消失问题

【问题背景】 最近有开发者集成了AGC的崩溃服务&#xff0c;出现了一个问题&#xff0c;在集成完成后&#xff0c;触发崩溃事件测试&#xff0c;在AGC后台可以看到当天崩溃的数据&#xff0c;但是启动次数显示为0。等到第二天再看数据时&#xff0c;连昨天的崩溃数据都没有了。…

一文带你搞懂MySQL的隔离级别

一. 前言 最近遇到这样一个题目&#xff1a;【假设目前你们使用的数据库是MySQL&#xff0c;现在有一个事务A&#xff0c;在事务A开始时读取数据的结果是1&#xff1b;事务A中间有一段耗时操作&#xff0c;在事务A中做耗时操作的同时&#xff0c;有另外一个事务B把数据值改成了…

MFC为控件添加背景图片

1、 添加选择Bitmap导入图片&#xff0c;图片文件最好放在项目res目录中&#xff0c;同时是BMP格式。上传后的图片在资源视图&#xff0c;命名为IDB_BITMAP_M_BACK。 2、在cpp的C***Dlg::OnPaint()函数下添加如下代码 void C***Dlg::OnPaint() {CPaintDC dc(this); // device…

录制游戏视频的软件有哪些?分享3款软件!

“有录制游戏视频的软件推荐吗&#xff1f;最近迷上了网游&#xff0c;想录制点自己高端操作的游戏画面&#xff0c;但是不知道用什么软件录屏比较好&#xff0c;就想问问大家&#xff0c;有没有好用的录制游戏视频软件。” 在游戏领域&#xff0c;玩家们喜欢通过录制游戏视频…

DevExpress WinForms数据编辑器组件,提供丰富的数据输入样式!(一)

DevExpress WinForms超过80个高影响力的WinForms编辑器和多用途控件&#xff0c;从屏蔽数据输入和内置数据验证到HTML格式化&#xff0c;DevExpress数据编辑库提供了无与伦比的数据编辑选项&#xff0c;包括用于独立数据编辑或用于容器控件(如Grid, TreeList和Ribbon)的单元格。…

二、编写第一个 Spring MVC 程序(总结项目报 404 问题以及 Spring MVC 的执行流程)

文章目录 一、编写第一个 Spring MVC 程序二、项目运行时报 404错误原因总结三、Spring MVC 的执行流程 一、编写第一个 Spring MVC 程序 创建 maven 项目&#xff0c;以此项目为父项目&#xff0c;在父项目的 pom.xml 中导入相关依赖 <dependencies><dependency…

Planning Poker

计划扑克 一人一副牌&#xff0c;投票表决&#xff0c;这个功能、故事点的工作量是多少 0&#xff1a;没有工作量 &#xff1f;&#xff1a;需求不清楚 -------------------------------------- 数字越大&#xff0c;工作量越大&#xff0c;越要细化 100 和 ∞ ----------…

SQL-每日一题【1321. 餐馆营业额变化增长】

题目 表: Customer 你是餐馆的老板&#xff0c;现在你想分析一下可能的营业额变化增长&#xff08;每天至少有一位顾客&#xff09;。 计算以 7 天&#xff08;某日期 该日期前的 6 天&#xff09;为一个时间段的顾客消费平均值。average_amount 要 保留两位小数。 结果按 …

【第三阶段】kotlin语言使用replace完成加解密操作

fun main() {val password"ASDAFWEFWVWGEGSDFWEFEWGFS"println("原始密码&#xff1a;$password")//加密操作,就是把字符替换成数字&#xff0c;打乱加密var newPsdpassword.replace(Regex("[ADWF]")){when(it.value){//it.value 这里的每一个字…

国资委79号文解读:国央企OA办公系统信创替代落地实践与标杆案例

国资委79号文解读&#xff1a;国央企OA办公系统信创替代落地实践与标杆案例 2022年9月底&#xff0c;国资委下发了重要的国资发79号文件&#xff0c;全面指导并要求国央企落实信息化系统的信创国产化改造。其中&#xff0c;明确要求所有中央企业在2022年11月底前将安可替代总体…

有没有运动蓝牙耳机推荐?过来人告诉你选这几款不会出错

随着人们健身意识的加强&#xff0c;对于运动装备的需求也增加了不少&#xff0c;其中运动蓝牙耳机也成为了人们健身运动过程中必不可缺的一部分&#xff0c;那究竟什么款式最适合运动佩戴呢&#xff1f;作为一个深耕于运动领域以及探索耳机行业多年的爱好者&#xff0c;我为大…

电脑数据恢复,5招拯救你的数据!

“我都要对我自己无语了&#xff0c;在清理垃圾文件的时候我总是会一不小心就把重要的数据也删掉&#xff0c;然后也不知道该怎么才能恢复这些数据&#xff01;大家有什么好的方法吗&#xff1f;” 在数字化时代&#xff0c;电脑的使用越来越重要&#xff0c;它好像渐渐成了我们…

长胜证券:三大股指持续走弱 券商、地产板块发力“撑场”

周三&#xff0c;A股商场连续弱势调整&#xff0c;三大股指早盘一度在权重股的带领下轰动翻红&#xff0c;但午后再度堕入低迷。到16日收盘&#xff0c;上证指数报3150.13点&#xff0c;跌0.82%&#xff1b;深证成指报10579.56点&#xff0c;跌0.94%&#xff1b;创业板指报2132…

炒股加杠杆亏损了需要还吗?会面临什么情况?

炒股加杠杆亏损了需要还吗&#xff1f;需要的&#xff01;在正常情况下&#xff0c;投资者借入的资金需要按照合约约定的利率和期限进行还款。 当炒股加杠杆亏损时&#xff0c;通常会面临如下两种情况&#xff1a; 1. 定期还款方式&#xff1a;在一些加杠杆交易合约中&#xf…

Spring Security安全框架

1.简介 ​ Spring Security 是 Spring家族中的一个安全管理框架。相比与另外一个安全框架Shiro&#xff0c;它提供了更丰富的功能&#xff0c;社区资源也比Shiro丰富。 一般来说中大型的项目都是使用Spring Security来做安全框架来进行权限控制。小项目有Shiro的比较多&#x…

【Unity实战篇 】| 如何在小游戏中快速接入一个新手引导教程

前言 【Unity实战篇 】 | 如何在小游戏中快速接入一个新手引导教程一、简单教程描述二、接入Tutorial Master 实现游戏引导2.1 导入Tutorial Master2插件2.2 添加TutorialMasterManager脚本对象2.3 配置Tutorial&#xff0c;用于管理第一段引导内容2.4 配置Stage&#xff0c;用…

研发协同工具哪个好用?比较常用的研发协同工具及其特点

Zoho Projects是一款在线的SaaS研发协同工具&#xff0c;支持敏捷开发/DevOps/Scrum等项目协作&#xff0c;最大的特点就是“会说话”&#xff0c;意思是&#xff1a;它可以把在项目协作过程中重要和相关的消息和信息通过恰到好处的方式告诉你&#xff0c;解决&#xff1a;开发…

最小生成树,prim算法

Prim算法和Kruskal算法都是用于解决最小生成树问题的经典算法&#xff0c;它们在不同情况下有不同的适用性和特点。 Prim算法&#xff1a; Prim算法是一种贪心算法&#xff0c;用于构建一个无向图的最小生成树。算法从一个初始节点开始&#xff0c;逐步添加与当前树连接且具有…

08 - 网络通信优化之IO模型:如何解决高并发下IO瓶颈?

提到 Java I/O&#xff0c;相信你一定不陌生。你可能使用 I/O 操作读写文件&#xff0c;也可能使用它实现 Socket 的信息传输…这些都是我们在系统中最常遇到的和 I/O 有关的操作。 我们都知道&#xff0c;I/O 的速度要比内存速度慢&#xff0c;尤其是在现在这个大数据时代背景…

SqlServer 快速数据库脚本迁移

文章目录 前言数据库脚本数据库->任务->生成脚本选择数据库对象高级 如何迁移&#xff1a;脚本修改 如何使用新建数据库 前言 做工业的&#xff0c;经常遇到内网的项目&#xff0c;就是数据往本地的数据库传。由于这个问题所以我们需要新建一个数据库。最合适的就是数据…