
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:
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:
#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.
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:
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: 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:
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.
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
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.

Presentation Of Chosen Abilities
- Sorting: See the picture above. Directories and files are sorted independently; folders are always "higher"
- Multiple selection:

DashedLines + LabelEdit + LinesPen

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:
private void fastTreeView1_DrawItem
(object sender, FastTreeView.DrawItemEventArgs e)
{
e.DrawText();
if (e.Node.Data is MyDirectoryFastTreeNodeData) {
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:
class MyDirectoryFastTreeNodeData : DirectoryFastTreeNodeData
{
private string description;
static Random random = new Random();
public MyDirectoryFastTreeNodeData(string path, string descr) :
base(path)
{
description = descr;
}
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:
fastTreeView1.Nodes.Add("[Owner-drawing show]").Nodes
.Add(new MyDirectoryFastTreeNodeData("C:\\", "Cool Description"));
The result:

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:
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;
}
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:

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:
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:
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