65.9K
CodeProject 正在变化。 阅读更多。
Home

FastTreeView

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.44/5 (10投票s)

2007年8月2日

CPOL

7分钟阅读

viewsIcon

74052

downloadIcon

1140

节点在展开时动态加载自身的 TreeView 控件

Screenshot - FastTreeView.gif

引言

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 包含一些有用的方法,例如:DrawItemDrawPlusMinusDrawLinesDrawImageDrawTextDrawBackground
  • 支持所有者项目测量:您可以设置 RowMeasureMode 属性,处理 DrawItem 事件,然后自己绘制节点!下一章将展示如何使用它。
  • HotTrackingMousedItemItemEnterMouse 属性
  • LinesPenDashedLines 属性,用于设置绘制线条的画笔或导致不绘制每条线的最上面部分;有关更多详细信息,请参见“绘制线条”一章。

此外

  • SelectionMode 属性,可以是以下值之一:None, One, MultiSimpleMultiExtended
  • HighLightBrushHighLightTextBrush 属性,允许使用默认以外的选定样式。
  • NodeIconExpandedNodeIcon 属性,用于设置节点的图像。
  • GetItem 方法,根据位置(例如,鼠标位置)返回项目。
  • PlusImageMinusImageScalePlusMinus 属性,用于改进加号/减号按钮的功能。
  • ShowPlusMinusShowLines 属性。
  • GetFullPath 方法,使用给定的路径分隔符返回指定节点的路径。
  • Changed 事件,报告树结构的任何更改。
  • RowMeasureMode 属性,用于设置确定项目高度的方式。可能的值为:
    • Text - 默认测量模式,节点的高度取决于使用的字体。
    • Fixed - 使用 FixedRowHeight 属性的值来测量每个节点。
    • Custom - 导致 MeasureRow 事件在每个节点绘制时被触发。

FastTreeNode 类

  • Image 属性,用于设置节点的单独图像。
  • SortingSortingComparer 属性,允许为整个 TreeView 和其节点单独设置排序模式。
  • ClickedMouseEnterMouseLeave 事件。
  • ParentTreeView 属性,获取节点所属的 FastTreeView 对象。
  • BoundsTextBoundsPlusMinusRectangle 属性,简化了 FastTreeView 控件的所有者绘制和可能的自定义。

FileSystemFastTreeNodeData 类

FileSystemFastTreeNodeData 类扩展了 DirectoryFastTreeNodeData,能够以惊人的高性能显示目录和文件,尽管相关的图标可见且启用了热跟踪。

Screenshot - FileSystem.gif

所选功能的展示

  • 排序:参见上图。目录和文件独立排序;文件夹始终“更高”。
  • 多选

    Screenshot - FileSystem.gif

  • DashedLines + LabelEdit + LinesPen

    Screenshot - FileSystem.gif

  • 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"));

    结果

    Screenshot - OwnerDrawing.gif

补充:绘制线条

我想说一下绘制线条的事情:很多人试图实现它,但失败了或者遇到了很大的麻烦。在 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 循环在每个节点上检查适当的条件,从正在绘制的节点开始。我用黑箭头标记了这个循环。代码中最重要的是这些条件,它们决定了给定线条的一部分是否应该被绘制。请看下面的图片。

Screenshot - Lines.gif

我用红色轮廓标记了四种情况。两条线是蓝色和粉红色,第三种情况的循环用黑箭头表示。现在看代码。

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