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