FastTreeView
节点在展开时动态加载自身的 TreeView 控件
引言
TreeView
是一个非常有用的控件。不幸的是,它速度很慢,尤其是在一次添加大量节点时。我实际上需要这种功能,所以我创建了自己的控件来显示树。出于我倾向于通用化的习惯,我的控件足够通用,可以在 The Code Project 上分享。
我的树控件的特点是高性能、低内存消耗和更多功能
- 节点的独立排序
- 高级所有者绘制支持,包括节点测量
- 多选
- 节点各自的独立事件
机制
通常,我们将整个结构加载到 TreeView
中,它运行得很好。但是,如果数据量很大,这种技术就没用了。其思想是在用户想要查看给定节点的子节点时加载它们。例如,Windows Explorer 只显示根目录 C:\,在单击“加号”按钮后,驱动器 C 的所有子目录都会被扫描并显示。
使用代码
让我们来操作文件系统。我们的目标是显示一个包含驱动器 C 上所有目录的树,就像在 Explorer 中一样。
需要什么
要使用我的控件,您需要编写一个代表树中一个节点的类。它必须能够执行以下操作:
- 加载其子节点:这将在用户首次展开节点时调用
- 检查它是否有任何子节点:此信息对于知道是否应显示“加号/减号”按钮至关重要;通常可以在不加载数据的情况下完成
- 将数据转换为文本表示:将显示为
FastTreeView
控件中节点标签的文本
从编程角度讲,它必须实现 IFastTreeNodeData
接口,其中包括:
LoadChildNodes
方法HasChildren
方法Text
属性
编写 DirectoryFastTreeNodeData 类
好的,让我们开始编码。我将我的类命名为 DirectoryFastTreeNodeData
并创建构造函数。
public class DirectoryFastTreeNodeData : IFastTreeNodeData
{
string path = "";
public DirectoryFastTreeNodeData(string _path)
{
if (!System.IO.Directory.Exists(_path))
throw new System.IO.DirectoryNotFoundException(
"Directory '" + _path + "' does not exist");
path = _path;
}
我认为上面的代码很清楚。设置私有字段 path
,但仅当它是有效路径时。另一种方式是抛出异常。现在我必须实现所有 IFastTreeNodeData
成员。 LoadChildNodes
是主要部分。
#region IFastTreeNodeData Members
public void LoadChildNodes(FastTreeNode node)
{
string[] dirs = System.IO.Directory.GetDirectories(path);
foreach (string dir in dirs)
{
node.Add(new DirectoryFastTreeNodeData(dir));
}
}
System.IO.Directory.GetDirectories
方法获取所有子目录的名称,并以字符串数组的形式返回。我使用它来生成 DirectoryFastTreeNodeData
类的新实例,并将它们添加到作为参数传递给 LoadChildNodes
方法的节点中。现在轮到 HasChildren
属性了。
public bool HasChildren(FastTreeNode node)
{
return System.IO.Directory.GetDirectories(path).Length != 0;
}
尽管此实现有效,但它非常丑陋且缓慢。这是因为 GetDirectories
方法将在每次绘制节点时被调用。所以,我通过这种方式解决问题:
// Enumeration of possible states of the HasSubDirs property.
// The alternative is using nullable type "bool?", which may
// be true, false or null.
enum HasSubDirsState { Has, DoesNotHas, NotChecked }
HasSubDirsState HasSubDirs = HasSubDirsState.NotChecked;
public bool HasChildren(FastTreeNode node)
{
switch (HasSubDirs)
{
case HasSubDirsState.Has:
return true;
case HasSubDirsState.DoesNotHas:
return false;
default: // == HasSubDirsState.NotChecked
// GetDirectories will be invoked just once.
if (System.IO.Directory.GetDirectories(path).Length != 0)
{
HasSubDirs = HasSubDirsState.Has;
return true;
}
else
{
HasSubDirs = HasSubDirsState.DoesNotHas;
return false;
}
}
}
最后一步是 Text
属性。
public string Text
{
get
{
string text = System.IO.Path.GetFileName(path);
return text == "" ? path : text;
}
set
{
if (value != Text)
try
{
System.IO.Directory.Move(path,
System.IO.Path.GetDirectoryName(path) + "\\" + value);
}
catch
{
MessageBox.Show("Cannot rename",
"Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
#endregion
}
请注意,此代码启用了重命名目录的功能;FastTreeView
具有 LabelEdit
功能。这实际上就是所有重要的内容。
在 FastTreeView 控件中使用创建的类
将 FastTreeView
控件放入您的窗体中,并将以下代码写在某个地方,例如在 Form_Load
中。
fastTreeView1.Nodes.Add(new DirectoryFastTreeNodeData("C:\\"));
这实际上就是全部内容。不要复制代码;DirectoryFastTreeNodeData
类包含在 FastTrees
命名空间中。
这将产生与本文随附的演示中类似的效果。然而,在演示中,还可以找到一个 Windows 注册表浏览器 -- RegistryFastTreeNodeData
类。依我看,这是一个很好的解决方案,因为一个类包含了程序中所有关键点,并且与控件的实现分开了。此外,节点的实际数据不是公共的。 Text
属性是控件与 FastTreeNodeData
之间的桥梁,仅此而已。
FastTrees 的功能
FastTreeView 类
LabelEdit
属性:如果项目已选中并单击,用户可以通过出现的小文本框更改节点的Text
属性。- 使用自加载节点不是必需的。也可以在不实现
IFastTreeNodeData
的情况下添加节点,只需作为文本即可。fastTreeView1.Nodes.Add("New Node");
- 支持所有者绘制(节点自定义外观)
OwnerDrawing
属性,指定所有者绘制行为DrawItem
事件,在绘制项目时触发。DrawItemEventArgs
包含一些有用的方法,例如:DrawItem
、DrawPlusMinus
、DrawLines
、DrawImage
、DrawText
和DrawBackground
。
- 支持所有者项目测量:您可以设置
RowMeasureMode
属性,处理DrawItem
事件,然后自己绘制节点!下一章将展示如何使用它。 HotTracking
、MousedItem
和ItemEnterMouse
属性LinesPen
和DashedLines
属性,用于设置绘制线条的画笔或导致不绘制每条线的最上面部分;有关更多详细信息,请参见“绘制线条”一章。
此外
SelectionMode
属性,可以是以下值之一:None, One, MultiSimple
或MultiExtended
。HighLightBrush
和HighLightTextBrush
属性,允许使用默认以外的选定样式。NodeIcon
和ExpandedNodeIcon
属性,用于设置节点的图像。GetItem
方法,根据位置(例如,鼠标位置)返回项目。PlusImage
、MinusImage
和ScalePlusMinus
属性,用于改进加号/减号按钮的功能。ShowPlusMinus
和ShowLines
属性。GetFullPath
方法,使用给定的路径分隔符返回指定节点的路径。Changed
事件,报告树结构的任何更改。RowMeasureMode
属性,用于设置确定项目高度的方式。可能的值为:Text
- 默认测量模式,节点的高度取决于使用的字体。Fixed
- 使用FixedRowHeight
属性的值来测量每个节点。Custom
- 导致MeasureRow
事件在每个节点绘制时被触发。
FastTreeNode 类
Image
属性,用于设置节点的单独图像。Sorting
和SortingComparer
属性,允许为整个TreeView
和其节点单独设置排序模式。Clicked
、MouseEnter
和MouseLeave
事件。ParentTreeView
属性,获取节点所属的FastTreeView
对象。Bounds
、TextBounds
、PlusMinusRectangle
属性,简化了FastTreeView
控件的所有者绘制和可能的自定义。
FileSystemFastTreeNodeData 类
FileSystemFastTreeNodeData
类扩展了 DirectoryFastTreeNodeData
,能够以惊人的高性能显示目录和文件,尽管相关的图标可见且启用了热跟踪。
所选功能的展示
- 排序:参见上图。目录和文件独立排序;文件夹始终“更高”。
- 多选
DashedLines
+LabelEdit
+LinesPen
OwnerDrawing
。要应用自定义绘制方法,请将OwnerDrawing
属性设置为TextOnly
并处理DrawItem
事件。所有这些操作都可以使用 Windows Forms 设计器轻松完成。这是一个所有者绘制过程的示例。private void fastTreeView1_DrawItem (object sender, FastTreeView.DrawItemEventArgs e) { // Use default text painting e.DrawText(); if (e.Node.Data is MyDirectoryFastTreeNodeData) { // Draw additional text, using Description property. e.PaintArgs.Graphics.DrawString (((MyDirectoryFastTreeNodeData)e.Node.Data).Description, fastTreeView1.Font, Brushes.DarkGray, new Rectangle(e.Node.TextBounds.Right, e.Node.TextBounds.Y, e.TreeArea.Width - e.Node.TextBounds.Right, e.Node.TextBounds.Height)); } }
上面的代码使用了
MyDirectoryFastTreeNodeData
类,该类继承自DirectoryFastTreeNodeData
。class MyDirectoryFastTreeNodeData : DirectoryFastTreeNodeData { private string description; static Random random = new Random(); public MyDirectoryFastTreeNodeData(string path, string descr) : base(path) { description = descr; } // Just added new property: Description public string Description { get { if (description == null) return "Description no " + random.Next().ToString(); else return description; } set { description = value; } } public override void LoadChildNodes(FastTreeNode node) { string[] dirs = System.IO.Directory.GetDirectories(Path); foreach (string dir in dirs) { node.Nodes.Add(new MyDirectoryFastTreeNodeData(dir, null)); } } }
将新节点添加到
FastTreeView
控件。fastTreeView1.Nodes.Add("[Owner-drawing show]").Nodes .Add(new MyDirectoryFastTreeNodeData("C:\\", "Cool Description"));
结果
补充:绘制线条
我想说一下绘制线条的事情:很多人试图实现它,但失败了或者遇到了很大的麻烦。在 TreeView
类的控件中,线条的某些部分通常在绘制项目时进行绘制。让我们看一下代码。
// (From DrawItem method)
// Draw vertical lines
if (showLines)
{
int indexOfTempNode;
while (tempNode.Parent != null)
{
indexOfTempNode = tempNode.Parent.IndexOf(tempNode);
if (indexOfTempNode < tempNode.Parent.Count)
{
if (!(tree[0] == tempNode && tempNode == node) &&
(indexOfTempNode < tempNode.Parent.Count - 1 ||
tempNode == node) && !linesDashed)
e.Graphics.DrawLine(linesPen,
lineX, y, lineX, y + rowHeight / 2);
if (indexOfTempNode < tempNode.Parent.Count - 1)
e.Graphics.DrawLine(linesPen,
lineX, y + rowHeight / 2, lineX, y + rowHeight);
lineX -= intend;
}
tempNode = tempNode.Parent;
}
// Small horizontal line
e.Graphics.DrawLine(linesPen,
intend * node.Level - intend / 2, y + rowHeight / 2,
intend * node.Level - 1, y + rowHeight / 2);
}
正如您所看到的,我使用一个 while
循环在每个节点上检查适当的条件,从正在绘制的节点开始。我用黑箭头标记了这个循环。代码中最重要的是这些条件,它们决定了给定线条的一部分是否应该被绘制。请看下面的图片。
我用红色轮廓标记了四种情况。两条线是蓝色和粉红色,第三种情况的循环用黑箭头表示。现在看代码。
if (!(tree[0] == tempNode && tempNode == node) &&
(indexOfTempNode < tempNode.Parent.Count - 1 ||
tempNode == node) && !linesDashed)
e.Graphics.DrawLine(linesPen, lineX, y, lineX, y + rowHeight / 2);
这会绘制线条的上半部分 -- 图片中标为蓝色 -- 如果:
- 该节点不是整个集合中的第一个节点,即图片之外的情况,但您可以在其上方看到它。
- 并且该节点不是其父节点集合中的最后一个节点(II、III 和 IV) OR 当前测试的节点 IS 正在绘制的节点。
第二种条件必须为真才能绘制线条的下半部分,图片中标为粉红色。
if (indexOfTempNode < tempNode.Parent.Count - 1)
e.Graphics.DrawLine(linesPen, lineX,
y + rowHeight / 2, lineX, y + rowHeight);
这排除了情况 IV,即节点是集合中的最后一个。如果您知道这些条件为什么看起来是这样,或者您不相信它们确实是必要的,请尝试修改此代码并查看效果。如果有任何不清楚或需要更多解释的地方,请随时提问。希望这能有所帮助。
关注点
还有一些事情可以做。
- 支持使用鼠标滚轮和键盘进行导航。水平滚动条也很有用。我不知道该怎么做。求助!
- 添加视觉样式和其他用户友好的功能。也许我以后会做。
- 像往常一样,修复 bug。
历史
- 2007年8月2日 -- 发布原始版本。
- 2007年8月3日 -- 添加了新属性和
FileSystemFastTreeNodeData
类,提高了性能。 - 2007年8月8日 -- 提高了性能,修复了 bug(感谢 crypto1024 和 Four13Designs),增加了更多代码文档。
- 2007年8月11日 -- 增加了更多文章内容,添加了多选支持。降低了内存消耗。