Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

FastTreeView

3.44/5 (10 votes)
15 Aug 2007CPOL7 min read 1   1.1K  
TreeView control in which nodes dynamically load themselves while expanding

Screenshot - FastTreeView.gif

Introduction

TreeView is a very useful control. Unfortunately it is slow, especially when adding many nodes at one time. I actually needed such functionality, so I created my own control that displays a tree. With my tendencies towards generalization, my control is universal enough to share it on The Code Project.

My tree control is characterized by high performance, lower memory consuming and more abilities:

  • Individual sorting for nodes
  • Advanced ownerdrawing support, including measuring of nodes
  • Multiselection
  • Individual events individual for nodes
In this article I will explain how loading-on-demand works, how to implement this and, finally how to use it in my control. I am also going to discuss drawing vertical lines problem.

Mechanism

Normally, we load the whole structure into the TreeView and it runs very well. However, if there is much data, this technique is useless. The idea is to load sub-nodes of given nodes when the user wants to see them. For example, Windows Explorer shows only the root C:\ and, after clicking the "plus" button, all subdirectories of drive C are scanned and displayed.

Using the code

Let's play with the file system. Our goal is to display a tree with all directories on drive C, like in Explorer.

What is needed

To use my control, you need to write a class representing one node of a tree. It must be able to do the following things:

  • Load its sub-nodes: This will be invoked when the user first expands a node
  • Check if it has any sub-nodes: This information is needed to know whether the "plus/minus" button should be displayed; usually it is possible without loading data
  • Convert its data to text representation: The text that will be displayed as a node label in a FastTreeView control

Programmatically speaking, it must implement the IFastTreeNodeData interface, which includes:

  • LoadChildNodes method
  • HasChildren method
  • Text property

Writing the DirectoryFastTreeNodeData class

Ok, let's start coding. I name my class DirectoryFastTreeNodeData and bring the constructor into existence:

C#
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;
    }

I think that the code above is clear. Set private field path, but only if it is valid. Another way is to throw an exception. Now I have to implement all IFastTreeNodeData members. LoadChildNodes is the boss:

C#
#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));
    }
}

Method System.IO.Directory.GetDirectories gets all subdirectories' names as an array of strings. I use it to generate new instances of the DirectoryFastTreeNodeData class and add them to the node that is passed as a parameter to the LoadChildNodes method. Now it is time for the HasChildren property.

C#
public bool HasChildren(FastTreeNode node)
{
    return System.IO.Directory.GetDirectories(path).Length != 0;
}

Although this implementation would work, it is very ugly and slow. This is because the GetDirectories method would be invoked on every painting of the node. So, I am solving the problem this way:

C#
// 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;
            }
    }
}

The last step is the Text property:

C#
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
}

Note that this code enables renaming a directory; FastTreeView has a LabelEdit feature. This is actually everything that is important.

Using created classes in the FastTreeView control

Put a FastTreeView control in your form and write the following code somewhere, e.g. in Form_Load.

C#
fastTreeView1.Nodes.Add(new DirectoryFastTreeNodeData("C:\\"));

That was actually everything. Do not copy the code; the class DirectoryFastTreeNodeData is included as a part of the FastTrees namespace.

This will give a similar effect in the demo attached to this article. However, in the demo a Windows Registry browser -- RegistryFastTreeNodeData class -- can also be found. In my opinion, this is a good solution because there appears to be a class containing all critical points of the program separated from the control's implementation. Also, the real data of a node is not public. The Text property is a bridge between a control and FastTreeNodeData and nothing else.

Features of FastTrees

FastTreeView class

  • LabelEdit property: If the item is already selected and clicked, the Text property of a node can be changed by the user through a little textbox that appears in the control.
  • Using self-loading nodes is not necessary. Nodes can be also added without implementing IFastTreeNodeData, just as a text:
    C#
    fastTreeView1.Nodes.Add("New Node");
  • Support of owner drawing (custom appearance of nodes):
    • OwnerDrawing property, which specifies ownerdrawing behaviour
    • DrawItem event, fired when an item is painted. DrawItemEventArgs has some helpful methods, like: DrawItem, DrawPlusMinus, DrawLines, DrawImage, DrawText and DrawBackground
  • Support of owner item measure: You can set the RowMeasureMode property, handle the DrawItem event and then paint the nodes yourself! The next chapter shows how to use it
  • HotTracking, MousedItem and ItemEnterMouse properties
  • LinesPen and DashedLines properties, which set a pen used to draw lines or cause not drawing of an upper part of each line; see the chapter "Drawing Lines" for more details

Additionally:

  • SelectionMode property, which can be one of following values: None, One, MultiSimple or MultiExtended
  • HighLightBrush and HighLightTextBrush properties, which enable use of other selection styles than the default
  • NodeIcon and ExpandedNodeIcon properties, which set images for nodes
  • The GetItem method returns the item from its location, e.g. cursor position
  • PlusImage, MinusImage and ScalePlusMinus properties, which improve functionality of plus/minus buttons
  • ShowPlusMinus and ShowLines properties
  • GetFullPath method, which returns the path to the specified node using a given path separator
  • Changed event, which reports any changes to the tree structure
  • RowMeasureMode property, which sets the way how height of items are determined. The possible values are:
    • Text - Default measuring mode, height of a node depends on used font
    • Fixed - Uses value of FixedRowHeight property for measuring each node
    • Custom - Causes MeasureRow event being fired for each node painting

FastTreeNode class

  • Image property, which sets individual images for nodes
  • Sorting and SortingComparer properties, which enables the setting of sorting modes, both for the whole TreeView and for its nodes individually
  • Clicked, MouseEnter and MouseLeave events
  • ParentTreeView property, which gets a FastTreeView object which the node belongs to
  • Bounds, TextBounds, PlusMinusRectangle properties, which simplify ownerdrawing and possible customization of FastTreeView control

FileSystemFastTreeNodeData class

The class FileSystemFastTreeNodeData extends DirectoryFastTreeNodeData to display both directories and files with incredible high performance, despite associated icons are visible and hot tracking is on.

Screenshot - FileSystem.gif

Presentation Of Chosen Abilities

  • Sorting: See the picture above. Directories and files are sorted independently; folders are always "higher"
  • Multiple selection:

    Screenshot - FileSystem.gif

  • DashedLines + LabelEdit + LinesPen

    Screenshot - FileSystem.gif

  • OwnerDrawing. To apply a custom drawing method, set the OwnerDrawing property to TextOnly and handle DrawItem event. All these operations can be easily done using Windows Forms Designer. This is the example of an owner-drawing procedure:
    C#
    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));
        }
    }

    The code above uses class MyDirectoryFastTreeNodeData, which inherits from DirectoryFastTreeNodeData:

    C#
    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));
            }
        }
    }

    Add a new node to the FastTreeView control:

    C#
    fastTreeView1.Nodes.Add("[Owner-drawing show]").Nodes
        .Add(new MyDirectoryFastTreeNodeData("C:\\", "Cool Description"));

    The result:

    Screenshot - OwnerDrawing.gif

Supplement: Drawing lines

I would like to say something about drawing lines: Many people have tried to implement it, but they couldn't or had big problems with it. In a TreeView-like control, parts of lines are usually drawn during painting items. Let's look at the code:

C#
// (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);
}

As you can see, I use a while loop to check the proper conditions on each node, starting from the node that is painted. I have marked this loop with black arrows. The most important things in the code are these conditions, which determine whether the given part of the line should be drawn or not. See the picture below:

Screenshot - Lines.gif

I have marked four situations using red outlines. Two parts of the lines are blue and pink and the loop for the third case is represented by black arrows. Now look at the code:

C#
if (!(tree[0] == tempNode && tempNode == node) &&
    (indexOfTempNode < tempNode.Parent.Count - 1 ||
    tempNode == node) && !linesDashed)
    e.Graphics.DrawLine(linesPen, lineX, y, lineX, y + rowHeight / 2);

This draws the upper part of the line -- marked blue on the picture -- if:

  • The node is not the first one in the whole collection, i.e. case outside the picture, but you can see it virtually above it
  • AND the node is not the last one in its parent's node collection (II, III and IV) OR the currently tested node IS the node being drawn

The second condition must be true to draw the bottom part of the line, marked pink on the picture:

C#
if (indexOfTempNode < tempNode.Parent.Count - 1)
    e.Graphics.DrawLine(linesPen, lineX,
    y + rowHeight / 2, lineX, y + rowHeight);

This excludes situation IV, where the node is the last in the collection. If you know why these conditions look like they do or you don't believe they are really necessary, just try to modify this code and see the effect. Please post questions if something is unclear or requires more explanation. I hope this will help somebody.

Points of Interest

There are still things that could be done.

  • Support navigation with the mouse wheel and keyboard. Also horizontal scrollbar would be useful. I do not know how to do this. Help!
  • Add visual styles and other user-friendly stuff. Maybe I will do it sometime
  • As usual, hunt down bugs

History

  • 2 August, 2007 -- Original version posted
  • 3 August, 2007 -- Added new properties and the class FileSystemFastTreeNodeData, improved performance
  • 8 August, 2007 -- Improved performance, fixed bugs (thanks to crypto1024 and Four13Designs), added more code documentation
  • 11 August, 2007 -- More article text, added multiple selection support. Decreased memory consumption

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)