TreeView 四技
Written by Allen Lee
0. 背景故事
现在的东西动不动就用G来算,一眨眼的功夫,我那100G的硬盘已拥挤不已了,但还有很多东西想放进来啊,怎么办?好吧,现在 DVD 刻录机的价格已经平民化了,我买了一个来舒缓紧张的硬盘。这下好了,硬盘上的可用空间总是足以让我下载想要的大块头了。没过多久,我刻录的 DVD 就堆积成山,成为我房间的一道景物。为了管理这座“山”,我决定写一个 DVD 管理软件,嗯,就叫它 Cupel 吧。不难想象,Cupel 将充分使用 TreeView 控件的各种功能,现在我把开发 Cupel 的过程中使用 TreeView 的心得写下来,希望能为那些寻找这方面内容的朋友提供一些参考。
1. 填充节点
1.1 说说要求
图 1-1 类别视图
如上图所示,根节点是光盘库,它可以包含0个或多个类别节点,每个类别节点又包含0个或多个光盘节点。Cupel 通过 Cupel.Data.DiscLibrary 类来读取和储存相关数据。
1.2 进行填充
类别视图的节点应该在 Cupel 的主窗体显示之前填充好,于是我选择在 Load 事件发生时进行填充:
//Code#01
privatevoidMainForm_Load(objectsender,EventArgse)
{
TreeNodelibraryNode=newTreeNode("MyDiscLibrary");
foreach(DiscCategorycategoryinm_MyDiscLibrary.GetCategories())
{
TreeNodecategoryNode=newTreeNode(category.Name);
foreach(stringlabelincategory.GetDiscLabels())
{
categoryNode.Nodes.Add(newTreeNode(label));
}
libraryNode.Nodes.Add(categoryNode);
}
m_CategoryView.Nodes.Add(libraryNode);
m_CategoryView.ExpandAll();
}
填充节点的方法是很简单的,上面的代码有两点需要说明:
- 1) 无论是 TreeNode 还是 TreeView,节点都是包含在 Nodes 属性中的,通过该属性的 Add() 方法可以添加新的节点。正如一个 TreeNode 可以包含多个子节点,一个 TreeView也可以包含多个根节点。
- 2) 节点填充完毕后,你应该使用 TreeView.ExpandAll() 方法展开所有节点。然而,当光盘节点过多时,展开全部节点可能不太合适,此时可以考虑只展开类别节点,即把 Code #01 的 m_CategoryView.ExpandAll(); 改为 libraryNode.Expand(); 就行了。
1.3 添加图标
图 1-2 文件夹视图
对于 Windows 的用户,上面这幅图应该是很熟悉了,上面的每个节点都带有一个图标,这使得目录试图更直观。Code #01 并没有为每个节点添加图标,运行结果是每个节点将只有文字。要为节点添加图标,最简单的方法就是在创建节点时通过构造函数来指定,但在此之前,你得先创建一个 System.Windows.Forms.ImageList 实例,并用它来储存图标。这里介绍在 Visual Studio 里使用 ImageList 组件为 TreeView 提供图像资源:
- 1) 在“工具箱”中拖动 ImageList 组件到主窗体;
- 2) 在“属性”窗口中点击 Images 属性右边的“...”按钮打开“图像集合编辑器”;
- 3) 按“添加”按钮添加所需的图标。
- 4) 选中 TreeView 控件,在“属性”窗口中找到 ImageList 属性,并把它的值设为刚才的 ImageList。
至此,相关的准备工作已经完毕,接下来要做的就是修改 Code #01 为节点指定图标,这可以通过使用 TreeNode 如下的构造函数做到:
//Code#02
publicTreeNode(stringtext,intimageIndex,intseletedImageIndex)
由于在 Cupel 中无论节点是否被选中,其图标都是一样的,所以上面构造函数的后两个参数值是一样的。假设 category.ico 在 ImageList 中的索引是1,那么你可以这样指定类别节点的图标:
//Code#03
TreeNodecategoryNode=newTreeNode(category.Name,1,1);
1.4 继续思考
前面说到,每个节点可以包含0个或多个字节点,于是在用户第一次运行 Cupel 时,类别视图将只有一个根节点。这显然是不太友好的,因为面对着“一无所有”的类别视图,用户很可能会不知所措,尤其在他有很多光盘并且还没决定如何对这些光盘分类时。此时我们不妨考虑为用户提供一个默认分类,这样他就可以在此基础上构想一个更合适自己的分类,这要比凭空想出一个分类容易的多。当然,有些用户早已想出了一套很好的分类,此时我们就没必要为他提供默认分类了,而是直接让他应用自己的分类。可以看出,如果 Cupel 在第一次运行时显示一个设置向导,询问用户使用默认分类还是应用自己的分类,则会使用户感到更加友好。
无论多么好吃的东西,每天都吃也会使人感到厌倦。现今是一个个性化的时代,图 1-1 无疑显得有点单调,如果用户可以为每个类别指定一个不同的图标,甚至隶属不同类别的光盘也具有不同的图标,这将会使得 Cupel 令人眼前一亮。进一步考虑,我们可以考虑把类别视图的图标设置储存在一个配置文件,让用户可以选择应用不同的图标套装。当然,有些用户根本不在乎这点儿花样,就像那些一直支持着“Windows 经典”主题的用户一样。可以看出,如果 Cupel 在第一次运行时显示一个设置向导,询问用户使用哪个图标套装,则会使用户感到更加友好。
2. 延迟填充
2.1 说说要求
图 2-1 光盘结构视图
图 2-1 分上下两部分,上面是一个 TreeView,显示了类别视图选中的光盘节点所包含的目录结构,下面是一个 ListView,显示了光盘结构视图选中的节点的细节信息,此图实质上是一个主-从视图。
当光盘所包含的目录或文件节点比较多时,一次过填充光盘结构视图的所有节点很可能导致界面没有响应,这显然是不允许的。其实,我们没有必要一开始就把所有节点都填充上去,而应该在用户访问到某节点时才填充它的子节点。
2.2 做好准备
TreeView 中的节点信息都包含在 TreeNode 中,为了使得光盘结构视图具备延迟填充特性,以及在节点信息视图上显示选中节点的细节信息,我们有必要自定义一个用于 TreeView 的节点类,该类将派生自 TreeNode,并且包含实现相关功能的信息。
节点可分为目录节点和文件节点两类,它们既有相同之处,也有不同之处,于是我们很容易联想到建立一个继承体系:
图 2-2 节点继承图
FileSystemTreeNodeBase 类的 Properties 属性是一个 List<FileSystemTreeNodeProperty> 集合,它包含了与节点的相关信息,这些信息将会显示在节点信息视图上,实现主-从视图。另外,FileSystemTreeNodeBase 类还包含了一个 FillSubNodes 抽象方法,用于协助光盘结构视图实现延迟填充特性。由于文件节点不会有子节点,所以 FileTreeNode.FillSubNodes() 的方法体是空的。现在我们来看一下 DirectoryTreeNode.FillSubNodes():
//Code#04
publicoverridevoidFillSubNodes()
{
if(Nodes.Count==0)
{
this.TreeView.BeginUpdate();
foreach(DirectoryNodesubDirectoryNodeinm_DirectoryNode.SubDirectoryNodes)
{
DirectoryTreeNodesubDirectoryTreeNode=newDirectoryTreeNode(subDirectoryNode.Name,subDirectoryNode);
subDirectoryTreeNode.Properties.Add(newFileSystemTreeNodeProperty("Path",subDirectoryNode.FullName));
Nodes.Add(subDirectoryTreeNode);
}
foreach(FileNodefileNodeinm_DirectoryNode.FileNodes)
{
FileTreeNodefileTreeNode=newFileTreeNode(fileNode.Name);
fileTreeNode.Properties.Add(newFileSystemTreeNodeProperty("Directory",fileNode.Directory));
fileTreeNode.Properties.Add(newFileSystemTreeNodeProperty("FileName",fileNode.Name));
Nodes.Add(fileTreeNode);
}
this.TreeView.EndUpdate();
}
}
用户有可能在展开某个节点后把它折叠起来,此时该节点的 Nodes 属性就会包含它的子节点(一个例外情况就是原光盘的某个目录是空目录,即里面没有包括任何子目录和/或文件),所以我们应该首先检查 Nodes.Count 是否为0。当条件满足时,我们就对该节点进行填充,留意填充代码包含在 TreeView.BeginUpdate() 和 TreeView.EndUpdate() 之间,这样做是为了避免 TreeView 每填充一个节点就绘制一次,从而提高了效率。
2.3 按需填充
仅当某个节点包含了子节点时,我们才能展开该节点,所以在展开该节点时,就要对其子节点所包含的子节点进行填充。例如,在图 2-1 中,当我们展开根节点(即“G:\”)时,“浪客剑心”所包含的子节点就得填充好了,否则它就无法被展开,它里面的目录结构也就无法显示了。
回到 Cupel,当用户选中类别视图中的某个光盘节点,光盘结构视图就会显示该光盘的根节点及其所包含的子节点:
//Code#05
DirectoryTreeNoderootDirectoryTreeNode=
rootDirectoryTreeNode.FillSubNodes();
m_DiscInfoView.Nodes.Clear();
m_DiscInfoView.Nodes.Add(rootDirectoryTreeNode);
接着,当用户点击可展开节点左边的“+”时,将引发 TreeView 的 BeforeExpand 事件,此时是填充该节点的子节点的子节点的最佳时机:
//Code#06
privatevoidm_DiscInfoView_BeforeExpand(objectsender,TreeViewCancelEventArgse)
{
foreach(FileSystemTreeNodeBasesubNodeine.Node.Nodes)
{
subNode.FillSubNodes();
}
}
2.4 显示细节
当用户选中光盘结构视图中的某个节点时,节点信息视图将显示与该节点相关的信息,这两个视图共同组成一个主-从视图:
//Code#07
privatevoidm_DiscInfoView_NodeMouseClick(objectsender,TreeNodeMouseClickEventArgse)
{
if(e.Button==MouseButtons.Left&&e.Clicks==1)
{
m_NodeInfoView.Items.Clear();
FileSystemTreeNodeBasefileSystemTreeNode=(FileSystemTreeNodeBase)e.Node;
foreach(FileSystemTreeNodePropertypropertyinfileSystemTreeNode.Properties)
{
m_NodeInfoView.Items.Add(newListViewItem(
newstring[]
{
property.Name,
property.Value
}
)
);
}
}
}
值得注意的是,Code #07 首先检测是否为鼠标左键点击以及点击次数是否为1,这些信息都包含在类型为 TreeNodeMouseClickEventArgs 的 e 参数中。另外,e.Node 是当前选中的节点,你必须把它强制转换成 FileSystem.TreeNodeBase 类型才能访问其所包含的 Properties 属性。
2.5 继续思考
虽然我们使用了“延迟填充”,但在展开某些节点时依然会感觉到“迟钝”,出现这种情况的主要原因是该节点的子节点包含着大量子节点。此时我们可以在展开之前把鼠标指针改为等待样式,待节点展开完毕后再改为默认样式:
//Code#08
privatevoidm_DiscInfoView_BeforeExpand(objectsender,TreeViewCancelEventArgse)
{
m_DiscInfoView.Cursor=Cursors.WaitCursor;
//
}
privatevoidm_DiscInfoView_AfterExpand(objectsender,TreeViewEventArgse)
{
m_DiscInfoView.Cursor=Cursors.Default;
}
另外,这里所提出的延迟填充方案并不是最佳方案。试想一下,如果我只展开图 2-1 中的“Bleach OVA”节点,而“浪客剑心”节点里面包含着数量可观的子节点却无需展开,那么 Cupel 的运行效率将受到影响。再者,预先填充这么多不需要的节点也会造成内存空间的浪费。为了避免这些弊端,我们可以修改一下这个方案,用“伪子节点”代替真实子节点来进行填充。还是拿图 2-1 来举例,当用户展开根节点时,填充“Bleach”、“Bleach OVA”和“浪客剑心”等子节点,接着分别为这些子节点填充一个“伪子节点”。当用户继续展开“浪客剑心”节点时,它所包含的“伪子节点”将被删除,取而代之的是它原本包含的真实子节点。
3. 节点编辑
3.1 说说要求
这里所说的“节点编辑”是狭义的重命名现有节点的名字,广义上它还包括添加新节点以及移除现有节点。下图示范了 Cupel 把“Anime”节点重命名为“Cartoon”:
图 3-1 编辑类别名
对节点进行重命名时需要注意:
- 1) 新名字不能为空字符串;
- 2) 新名字不能和已存在的名字相冲突;
- 3) 新名字不允许包含某些特殊字符(可选)。
3.2 开始编辑
TreeView.LabelEdit 属性指示了节点是否允许编辑,默认情况下,它的值为 false。我们可以为类别节点提供一个上下文菜单,里面包含一个重命名菜单项,当用户点击该菜单项时,该类别节点进入编辑状态:
//Code#09
m_CategoryView.LabelEdit=true;
if(!m_CategoryView.SelectedNode.IsEditing)
{
m_CategoryView.SelectedNode.BeginEdit();
}
注意,仅当 TreeView.LabelEdit 为 true 时,TreeNode.BeginEdit() 方法才可用,否则会抛出 InvalidOperationException 异常。
3.3 完成编辑
节点完成编辑后将引发 TreeView.AfterLabelEdit 事件,该事件通过 NodeLabelEditEventHandler 委托来作用,该委托所包含的类型为 NodeLabelEditEventArgs 的参数 e 包含了完成编辑所需的信息:
- 1) e.Node 是当前编辑的节点;
- 2) e.Label 是用户为节点输入的新名字。
根据 3.1 中提到的三点要求的前两点,我们可以写出如下代码:
//Code#10
privatevoidm_CategoryView_AfterLabelEdit(objectsender,NodeLabelEditEventArgse)
{
if(e.Label!=null)
{
if(e.Label.Length>0)
{
if(!m_MyDiscLibrary.IsCategoryNameExisting(e.Label))
{
e.Node.EndEdit(false);
//
}
else
{
e.CancelEdit=true;
MessageBox.Show("类别名已存在。");
e.Node.BeginEdit();
}
}
else
{
e.CancelEdit=true;
MessageBox.Show("类别名不能为空。");
e.Node.BeginEdit();
}
}
m_CategoryView.LabelEdit=false;
}
在某些情况下,第三点要求是必须的,例如 Cupel 把类别节点影射到磁盘的目录,而 Windows 规定某些字符不能用于命名目录或文件的,此时就有必要添加相关的代码来排错了。
另外,如果编辑期间抛出异常,就有可能导致数据处于未定义状态,此时你可以用一个 try 块包围代码:
//Code#11
try
{
//
}
catch()
{
e.Node.EndEdit(true);
}
finally
{
m_CategoryView.LabelEdit=false;
}
3.4 继续思考
提供快捷键可以提高应用程序的易用性,我们在 Windows 中重命名目录或文件时通常按 F2 来进入编辑状态而不是使用右键菜单的重命名菜单项,于是我们也可以考虑在 Cupel 中提供类似的便捷:
//Code#12
privatevoidm_CategoryView_KeyUp(objectsender,KeyEventArgse)
{
if(e.KeyCode==Keys.F2&&m_CategoryView.SelectedNode!=null&&m_CategoryView.SelectedNode.Level==1)
{
RenameDiscCategory(null,EventArgs.Empty);
}
}
当你添加新的类别节点时,它会有一个默认的名字——New Category,并且处于编辑状态:
//Code#13
TreeNodenewTreeNode=newTreeNode("NewCategory",1,1);
m_CategoryView.Nodes[0].Nodes.Add(newTreeNode);
m_CategoryView.LabelEdit=true;
if(!newTreeNode.IsEditing)
{
newTreeNode.BeginEdit();
}
可以看出,它和重命名类别节点名字的代码非常相似,实质上,它等效于先添加一个新的节点,然后对该节点进行重命名。至于移除现有类别节点则更简单:
//Code#14
if(m_CategoryView.SelectedNode!=null)
{
m_CategoryView.SelectedNode.Remove();
}
当然,在实际的应用中,这是远远不够的,因为用户可能只想移除该类别,而不希望丢失其所包含的光盘节点。对于用户来说,正确的做法应该是把待移除的类别所包含的光盘节点移到别的类别节点下,然后再移除类别节点。但没有人能够保证用户一定会这样做,于是你就要有一些措施来避免不必要麻烦了,这里我介绍两个措施:
- 1) 显示一个对话框提示用户把待移除的类别节点所包含的光盘节点移动到别的类别节点,再执行删除操作,这个对话框通常是一个向导;
- 2) 类别节点移除后,原本隶属该类别的光盘节点将被移到一个“Uncategorized”类别节点下,等待用户做进一步的处理。
4. 节点拖放
4.1 说说要求
节点拖放可以用来实现更改某一光盘节点的所属类别,例如,我把图 1-1 中“Music”下的“MC0001”移到“Mix”下,就改变“MC0001”的类别了。由于每个光盘节点都必须隶属某一个分类,于是你不能把它拖放到“My Disc Library”下和类别节点并列。你更不能把一个光盘节点拖放到另一个光盘节点下。换言之,只有光盘节点是可拖动的,并且只能置于类别节点下。
4.2 基础知识
要使得控件接受用户拖放到它上面的数据,你必须把 AllowDrop 属性设为 true,这是第一步。
接下来,你要了解 TreeView 拖放操作所涉及的三个事件:ItemDrag、DragEnter 和 DragDrop。举个例子,我要把图 1-1 中“Music”下的“MC0001”移到“Mix”下,那么当我们在“MC0001”上按下鼠标左键并开始拖动时,ItemDrag 事件就触发了,然后,当“MC0001”被拖到“Music”的“地盘”上时,DragEnter 事件就触发了,最后,当我们在“Music”上松开鼠标左键时,DragDrop 事件就触发了。
从名字上很容易联想到这三个事件的用途:
- 1) ItemDrag 用于判断对象是否允许拖动,如果允许则用 DoDragDrop() 方法初始化拖放操作。例如,如果被拖动的是“Music”而不是“MC0001”,我们应该中止拖放操作;
- 2) DragEnter 则用于判断拖过来的数据是否可以接受,用户极有可能把非预期的数据拖过来,于是你有责任确保控件只接受那些可解释的数据。例如,如果用户拖过来的是一段文本,那么拖放操作就不能继续了;
- 3) 在 DragDrop 中,我们要做的就是解释用户拖放过来的数据,并对这些数据做适当的处理,当然,数据无法正确解释也是有可能发生的,所以你有责任确保这些数据不会影响到现有的正常数据。
4.3 实现拖
分享到:
相关推荐
在本文中,我们将深入探讨如何在WPF(Windows Presentation Foundation)中创建一个美观且功能丰富的TreeView控件。TreeView是WPF中的一个重要组件,用于展示层次化的数据结构,它允许用户以树形视图来浏览和操作...
### 四、总结 通过以上分析,我们可以清楚地了解到如何使用C#中的List集合动态地绑定到TreeView控件上。这种方法不仅提高了数据展示的灵活性,同时也增强了用户体验。在实际项目中,这种技术非常实用,尤其对于那些...
四、树形结构的实现 在TREEVIEW控件中,我们可以通过设置每个节点的ParentId来实现树形结构。ParentId表示当前节点的父节点的NodeId。通过这种方式,我们可以构建一个复杂的树形结构。 五、结论 使用TREEVIEW控件...
### TreeView 的序列化 在本篇文章中,我们将探讨如何对 `TreeView` 控件进行序列化,以便在程序运行期间保存其状态,并能在后续需要时恢复。本文将详细介绍使用 `.NET` 中 `System.Runtime.Serialization....
在QML(Qt Meta Language)中,`TreeView`是一个强大的组件,用于展示层次结构的数据。它在用户界面上提供了一种可滚动、可选择的列表形式,非常适合展示目录结构、组织架构或者文件系统等信息。在本教程中,我们将...
知识点四:TreeView控件的节点绑定 TreeView控件的节点绑定可以通过使用TreeNode对象和TreeView控件的Nodes属性来实现。在上面的代码中,我们使用了foreach循环来遍历DataSet中的数据,并创建相应的TreeNode对象,...
Bootstrap Treeview 实现动态加载数据并添加快捷搜索功能 Bootstrap Treeview 是一个基于 Bootstrap 的 jQuery 插件,用于显示树形结构数据。该插件支持多级列表树结构,能够以简单和优雅的方式显示继承树结构,如...
在VB.NET编程中,TreeView控件是一个非常常用且功能强大的组件,它用于显示层次结构的数据,比如文件系统、组织结构或者数据库记录等。本篇文章将深入探讨如何在VB.NET中进行TreeView节点的添加和删除操作。 一、...
在Windows桌面应用开发中,`Winform.TreeView`是一个常用且功能强大的控件,它用于显示数据的层次结构,如文件系统、组织结构等。这个压缩包提供的“好看的Winform.TreeView”是一个自定义美化版的`TreeView`控件,...
TreeView控件是一种在Windows应用程序中广泛使用的用户界面组件,它以层次化列表的形式展示项目,每个项目可以有若干子项目。这种控件特别适用于需要以树状结构展示数据的场景,如文件系统、组织结构等。 创建...
在C#编程中,`TreeView`控件常用于展示层级结构的数据,如文件系统、组织结构等。将`TreeView`中的数据导出到Excel是一种常见的需求,方便用户进行数据分析和处理。下面我们将深入探讨如何实现这个功能。 首先,...
在Windows Presentation Foundation (WPF) 中,TreeView是一个用于展示层级数据的强大控件。它允许用户以树状结构浏览和操作数据,广泛应用于文件系统、组织结构或者任何具有层次关系的数据展示。标题“最漂亮的...
在给定的“JQ TreeView树视图”主题中,我们将探讨如何使用jQuery来创建一个动态加载数据的TreeView组件。 首先,`TreeView`是一种将层次结构数据以树状结构展示的UI元素,它常用于网站和应用中以展示目录结构、...
在Windows Presentation Foundation (WPF) 中,`TreeView` 是一个常用控件,用于展示层次结构的数据。本实例探讨了如何实现`TreeView`节点的拖放(Drag & Drop)功能,这是用户界面交互中的一个重要特性,使得用户...
在C#编程中,将`TreeView`控件的数据序列化到XML文件是一个常见的需求,这样可以方便地保存用户的配置或状态,以便于下次应用启动时恢复。以下是如何实现这个功能的详细步骤和知识点: 首先,我们需要创建两个...
四、事件处理 TreeView控件有多个事件可供处理,如: - NodeMouseClick:当用户单击节点时触发,可以获取到被点击的TreeNode。 - AfterSelect:当用户选择一个新节点后触发,可以获取到新选中的TreeNode。 - ...
在本文中,我们将深入探讨如何在Windows Presentation Foundation (WPF) 中使用自定义的`Combobox+TreeView`和`DataGrid+TreeView`控件来处理大规模数据。这些控件是WPF开发中的重要组成部分,特别是在构建用户界面...
Bootstrap Treeview 是一个基于Bootstrap框架的插件,用于在网页中展示层次结构的数据,例如组织结构、文件系统或导航菜单。这个插件通过JavaScript和CSS实现了交互式的树形视图,使得用户可以轻松地展开、折叠节点...
在Windows Presentation Foundation (WPF) 中,`TreeView`控件是一种强大的工具,用于展示层次结构的数据。本主题将深入探讨如何在Visual Studio 2008中创建和自定义多种`TreeView`样式,以实现丰富的用户界面体验。...
在.NET Framework中,C#语言提供了丰富的控件库,其中包括TreeView控件,它常用于构建层级结构的数据展示,如文件系统、组织结构等。在处理`TreeView`时,`Checkboxes`功能允许用户通过交互式勾选来选择或取消选择...