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日 -- 增加了更多文章内容,添加了多选支持。降低了内存消耗。




