使用 wxPython 创建“目录树”(5)


wxPython 自带的 wx.GenericDirCtrl 可以实现目录树的效果,但是同 vscode 的目录树对比,是丑了一些,而且虽然指定了目录,但是仍把磁盘翻了个遍。

# 使用 wx.GenericDirCtrl 控件
import wx
import os

class TestFrame(wx.Frame):
    def __init__(self, *args, **kw):
        super().__init__(*args, **kw)
        self.dir_tree = wx.GenericDirCtrl(self, -1, dir=os.getcwd())

if __name__ == '__main__':
    app = wx.App()
    frm = TestFrame(None)
    frm.Show()
    app.MainLoop()
GenericDirCtrl vscode
)

这里使用 wx.TreeCtrl 里来设计出类似 vscode 的目录树,先看下最终的效果图:

1.1. 实现过程

1.1.1. 设置树的样式

默认的 TreeCtrl 控件的样式和上图的 GenericDirCtrl 是一样的,这里我们需要这种样式:

self.tree = wx.TreeCtrl(self, -1,
        style = wx.TR_DEFAULT_STYLE # 默认样式 
               |wx.TR_TWIST_BUTTONS # 结点使用 >/v 而不是 +/-
               |wx.TR_NO_LINES      # 不绘制结点之间的连线
    )

1.1.2. 设置图标

结点的图标都是得自己设置的(调用 tree.SetItemImage()),否则默认是没有图标的。
另外,文件夹与文件之间的图标不同,不同扩展名的文件图标也不同。

这里用 Images.py 保存图标的数据,如获取名为 “file_type_python” 的图标的位图为

bmp = Images.file_type_python.GetBitmap()
# 下面是等价的
# bmp = getattr(Iamges, "file_type_python").GetBitmap()

Ext2IconDict.py 中包含一个字典 IconMap.Ext2IconDict,字典的 key 代表的是文件的扩展名,如 “png,py” 等,字典的 value 代表着“相应图标的名字”,如 "file_type_python",

Ext2IconDict = {
    # ...
    "py" : "file_type_python", 
    # ...
}

这样就可以由扩展名来获取相应图标的位图了。关于如何生成 Images.py 可以参考 使用 wx.tools.img2py (4)

bmp = getattr(Iamges, IconMap.Ext2IconDict['py']).GetBitmap()

1.1.3. 获取当前工作区的所有文件,并创建结点

参考下面代码的 self.InitTree(),一般都是使用递归的思路。这一部分主要参考了 wxPython in action Chapter 15 的相关内容。

1.1.4. 目录最小、最大化

使用 AuiManager 可以实现子窗口的最大化、最小化等操作。

import wx
import wx.lib.agw.aui as aui
class TestFrame(wx.Frame):
  
    def __init__(self, *args, **kw):
        super().__init__(*args, **kw)
        self.SetTitle("目录树")
        self.SetSize(800, 600)

        self.dir_tree = DirectoryTree(self, size=(200, 600))
        self.txt = wx.TextCtrl(self, -1, value="I'm 002",style=wx.TE_MULTILINE)

        # aui manager 可实现窗口的最大/小化,拖动
        self._mgr = aui.AuiManager(agwFlags=aui.AUI_MGR_LIVE_RESIZE)
        self._mgr.SetManagedWindow(self)
        self._mgr.AddPane(self.dir_tree, aui.AuiPaneInfo().Caption("workspace").
                          Left().Layer(1).Position(1).CloseButton(True).MaximizeButton(True).MinimizeButton(True))
        self._mgr.AddPane(self.txt, aui.AuiPaneInfo().CenterPane())
        
        # 记得要 Update
        self._mgr.Update()

目录树的最小化、最大化如下图所示:

min_max_tree

1.1.5. 实现右键菜单操作

使用 Window.PopupMenu() 可以实现弹出菜单,对应的事件为 wx.EVT_CONTEXT_MENU

class TestFrame(wx.Frame):
    def __init__(self, *args, **kw):
        # ...
        self.Bind(wx.EVT_CONTEXT_MENU, self.OnTreeRightUp) # 右键弹出式菜单
        # ...
    def OnTreeRightUp(self, event):
        # ...

        menu = wx.Menu()
        menuitem = menu.Append(-1, "Send")
        self.Bind(wx.EVT_MENU, self.OnSend, menuitem)
        menu.Append(-1, "(new folder)")
        menu.Append(-1, "(new file)")
        menu.Append(-1, "(copy)")
        menu.Append(-1, "(paste)")
        menu.Append(-1, "(cut)")

        self.PopupMenu(menu)
        menu.Destroy()

        # ...

选中文件,右键点击 Send,可将文件名发送到右侧文字框:

left_pop_menu

1.1.6. 及时地更新目录树内容

每当离开应用窗口时,都可能会改变工作目录。所以更新目录树的时机为"鼠标重新点击应用程序",对应的事件为 wx.EVT_ACTIVATE,需要注意的是这个得绑定在 wx.Frame 类里,否则不起作用。

为了简化思路,每当重新点击窗口时,要先判断前后的工作目录是否有发生变化,有变化才刷新树(刷新指的是:删除之前的所有子结点,再根据新目录重新创建结点),不然每次点击就刷新

1.1.7. 对结点进行排序

对结点进行排序,需要重写 TextCtrl.OnCompareItems(self, item1, item2),然后在实例调用 SortChildren()


class MyTreeCtrl(wx.TreeCtrl):
    def __init__(self, *args, **kw):
        super().__init__(*args, **kw)
    
    def OnCompareItems(self, item1, item2):
        """重写 OnCompareItems
        data = [0, 文件夹名] / 
        data = [1, 文件名]
        """
        data1 = self.GetItemData(item1)
        data2 = self.GetItemData(item2)
        if data1[0] > data2[0]:
            return 1
        elif data1[0] < data2[0]:
            return -1
        else:
            if data1[1].lower() > data2[1].lower():
                return 1
            elif data1[1].lower() < data2[1].lower():
                return -1
            else:
                return 0

# ...
    def InitTree(self):
        """初始化树"""
        
        # ...

        # 根结点展开
        self.tree.Expand(self.root_id)
        # 对子结点排序
        self.tree.SortChildren(self.root_id)

排完序的运行结果:

my_tree_dir_sort

1.2. 完整代码

1.2.1. 目录树

点击查看代码

import wx
import glob
import os
import wx.lib.agw.aui as aui

import Images
from IconMap import Ext2IconDict

class MyTreeCtrl(wx.TreeCtrl):
    def __init__(self, *args, **kw):
        super().__init__(*args, **kw)
    
    def OnCompareItems(self, item1, item2):
        """重写 OnCompareItems
        data = [0, 文件夹名] / 
        data = [1, 文件名]
        """
        data1 = self.GetItemData(item1)
        data2 = self.GetItemData(item2)
        if data1[0] > data2[0]:
            return 1
        elif data1[0] < data2[0]:
            return -1
        else:
            if data1[1].lower() > data2[1].lower():
                return 1
            elif data1[1].lower() < data2[1].lower():
                return -1
            else:
                return 0
        

class DirectoryTree(wx.Window):

    def __init__(self, *args, **kw):
        super().__init__(*args, **kw)

        self.work_path = os.getcwd() # 默认为当前的工作区目录

        # 创建树
        self.tree = MyTreeCtrl(self, -1,
            style = wx.TR_DEFAULT_STYLE # 默认样式 
                   |wx.TR_TWIST_BUTTONS # 结点使用 >/v 而不是 +/-
                   |wx.TR_NO_LINES      # 不绘制结点之间的连线
        )

        # 初始化图像列表
        self.InitImageList()

        # 初始化树
        self.InitTree()

        # 布局
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.tree, 1, wx.EXPAND|wx.LEFT|wx.BOTTOM, 2)
        self.SetSizer(sizer)

    def InitImageList(self):
        """初始化图像列表"""
        # 创建一个图像列表
        il = wx.ImageList(16, 16)
        # 扩展名到图标 ID 的字典
        self.ext_map_imageId = {}
        for ext in Ext2IconDict:
            # 文件扩展名 to imageID
            bmp = getattr(Images, Ext2IconDict[ext]).GetBitmap()
            self.ext_map_imageId[ext] = il.Add(bmp)
        # 单独添加几种特殊情况
        self.ext_map_imageId['default_file'] = il.Add(getattr(Images, 'default_file').GetBitmap())
        self.ext_map_imageId['default_folder'] = il.Add(getattr(Images, 'default_folder').GetBitmap())
        self.ext_map_imageId['default_folder_opened'] = il.Add(getattr(Images, 'default_folder_opened').GetBitmap())
        # 将图像分配给树
        self.tree.AssignImageList(il)
        
    def InitTree(self):
        """初始化树"""
        # 获取工作目录所有子文件和子目录
        self.all_files = self.GetAllFileFrom(self.work_path)
        # 设置根目录
        if self.tree.GetCount() < 1:
            self.root_id = self.tree.AddRoot(self.all_files[0], data=[0, self.all_files[0]])
        else:
            self.root_id = self.tree.GetRootItem()
        # 清楚所有子结点
        self.tree.DeleteChildren(self.root_id)
        # 递归添加子节点
        self.AddTreeNodes(self.root_id, self.all_files[1])
        # 根结点展开
        self.tree.Expand(self.root_id)
        # 对子结点排序
        self.tree.SortChildren(self.root_id)

    def GetAllFileFrom(self, path):
        """递归获取包括该目录及其子文件、子目录所有文件,
           生成一个“树状列表”,如: [root, [sub-list]]
            [root, [
                    item1,
                    [item2, [
                        item21, item22, item23
                    ],
                    item3,
                ]
            ]
        """
        sub_list = []
        for file_name in glob.iglob(os.path.join(path, "*")):
            if os.path.isdir(file_name):
                sub_list.append(self.GetAllFileFrom(file_name))
            else:
                file_name_without_root = file_name.split('\\')[-1]
                sub_list.append(file_name_without_root)
        root = path.split('\\')[-1]
        return [root, sub_list]

    def CompareTreeList(self, plist, qlist):
        """比较两个树状列表"""
        if len(plist) != len(qlist):
            return False

        res = True
        for p,q in zip(plist, qlist):
            if type(p) == str and type(q) ==str:
                if p != q:
                    return False
            elif type(p) == list and type(q) == list:
                res =  self.CompareTreeList(p, q)
            else:
                return False
        return res

    def AddTreeNodes(self, parentItem, items):
        """递归添加树结点

        Args:
            parentItem ([treeItemID]): [description]
            items ([list]): [description]
        """
        for item in items:
            if type(item) == str:
                newItem = self.tree.AppendItem(parentItem, item, data=[1, item])
                # 设置数据图像
                ext = item.split('.')[-1] # 扩展名
                if ext in self.ext_map_imageId:
                    self.tree.SetItemImage(newItem, self.ext_map_imageId[ext], which=wx.TreeItemIcon_Normal)
                else:
                    self.tree.SetItemImage(newItem, self.ext_map_imageId['default_file'], which=wx.TreeItemIcon_Normal)
            else:
                newItem = self.tree.AppendItem(parentItem, item[0], data=[0, item[0]])
                # 设置结点的图像(文件夹)
                self.tree.SetItemImage(newItem, self.ext_map_imageId['default_folder'], which=wx.TreeItemIcon_Normal)
                self.tree.SetItemImage(newItem, self.ext_map_imageId['default_folder_opened'], which=wx.TreeItemIcon_Expanded)
                # 递归调用
                self.AddTreeNodes(newItem, item[1]) 

    def IsDirChange(self):
        """判断当前工作区目录是否修改"""
        tmp_list = self.GetAllFileFrom(self.work_path)
        return not self.CompareTreeList(tmp_list, self.all_files)

    def GetSelectionItem(self):
        """返回被选中的结点"""
        return self.tree.GetFocusedItem()

    def Unselect(self):
        """取消选中的结点"""
        self.tree.Unselect()

    def GetItemData(self, id):
        """返回指定结点的data"""
        return self.tree.GetItemData(id)

1.2.2. 测试类

点击查看代码
class TestFrame(wx.Frame):
    
    def __init__(self, *args, **kw):
        super().__init__(*args, **kw)
        self.SetTitle("目录树")
        self.SetSize(800, 600)

        self.dir_tree = DirectoryTree(self, size=(200, 600))
        self.txt = wx.TextCtrl(self, -1, value="I'm 002",style=wx.TE_MULTILINE)

        # aui manager 可实现窗口的最大/小化,拖动
        self._mgr = aui.AuiManager(agwFlags=aui.AUI_MGR_LIVE_RESIZE)
        self._mgr.SetManagedWindow(self)
        self._mgr.AddPane(self.dir_tree, aui.AuiPaneInfo().Caption("workspace").
                          Left().Layer(1).Position(1).CloseButton(True).MaximizeButton(True).MinimizeButton(True))
        self._mgr.AddPane(self.txt, aui.AuiPaneInfo().CenterPane())
        
        # 记得要 Update
        self._mgr.Update()

        # 绑定事件
        self.Bind(wx.EVT_ACTIVATE, self.OnActive) # 每当鼠标重新点击此窗口,检查更新目录
        self.Bind(wx.EVT_CONTEXT_MENU, self.OnTreeRightUp) # 右键弹出式菜单

    def OnActive(self, event):
        if event.GetActive():
            print("On Active!" )
            if self.dir_tree.IsDirChange():
                print("Refresh dir-tree!")
                self.dir_tree.InitTree()

    def OnTreeRightUp(self, event):
        id = self.dir_tree.GetSelectionItem()
        self.msg = ""
        if id.IsOk():
            self.msg = self.dir_tree.GetItemData(id)
            print(self.msg)

        menu = wx.Menu()
        menuitem = menu.Append(-1, "Send")
        self.Bind(wx.EVT_MENU, self.OnSend, menuitem)
        menu.Append(-1, "(new folder)")
        menu.Append(-1, "(new file)")
        menu.Append(-1, "(copy)")
        menu.Append(-1, "(paste)")
        menu.Append(-1, "(cut)")

        self.PopupMenu(menu)
        menu.Destroy()

        # 记得取消当前选择
        self.dir_tree.Unselect()

    def OnSend(self, event):
        self.txt.AppendText("\nClick: "+self.msg)
        
if __name__ == '__main__':
    app = wx.App()
    frm = TestFrame(None)
    frm.Show()
    app.MainLoop()

1.2.3. 相关图标

1.3. 相关参考

posted @ 2022-01-10 19:49  Wreng  阅读(379)  评论(0编辑  收藏  举报