使用 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()
目录树的最小化、最大化如下图所示:
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,可将文件名发送到右侧文字框:
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)
排完序的运行结果:
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. 相关图标
-
IconMap.py
:https://gitee.com/iam002/sketch/blob/dev/IconMap.py -
Images.py
:https://gitee.com/iam002/sketch/blob/dev/Images.py
1.3. 相关参考
-
wxPython in Action by Noel Rappin, Robin Dunn, Chapter 10 and Chapter 15
- 蓝奏云下载: https://wwe.lanzoup.com/b01oz824f ,密码:g7np
- 配套代码:https://github.com/freephys/wxPython-In-Action