FastTreeView
TreeView control in which nodes dynamically load themselves while expanding
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
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
methodHasChildren
methodText
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:
// 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:
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
LabelEdit
property: If the item is already selected and clicked, theText
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:fastTreeView1.Nodes.Add("New Node");
- Support of owner drawing (custom appearance of nodes):
OwnerDrawing
property, which specifies ownerdrawing behaviourDrawItem
event, fired when an item is painted.DrawItemEventArgs
has some helpful methods, like:DrawItem
,DrawPlusMinus
,DrawLines
,DrawImage
,DrawText
andDrawBackground
- Support of owner item measure: You can set the
RowMeasureMode
property, handle theDrawItem
event and then paint the nodes yourself! The next chapter shows how to use it HotTracking
,MousedItem
andItemEnterMouse
propertiesLinesPen
andDashedLines
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
orMultiExtended
HighLightBrush
andHighLightTextBrush
properties, which enable use of other selection styles than the defaultNodeIcon
andExpandedNodeIcon
properties, which set images for nodes- The
GetItem
method returns the item from its location, e.g. cursor position PlusImage
,MinusImage
andScalePlusMinus
properties, which improve functionality of plus/minus buttonsShowPlusMinus
andShowLines
propertiesGetFullPath
method, which returns the path to the specified node using a given path separatorChanged
event, which reports any changes to the tree structureRowMeasureMode
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 fontFixed
- Uses value ofFixedRowHeight
property for measuring each nodeCustom
- CausesMeasureRow
event being fired for each node painting
FastTreeNode class
Image
property, which sets individual images for nodesSorting
andSortingComparer
properties, which enables the setting of sorting modes, both for the wholeTreeView
and for its nodes individuallyClicked
,MouseEnter
andMouseLeave
eventsParentTreeView
property, which gets aFastTreeView
object which the node belongs toBounds
,TextBounds
,PlusMinusRectangle
properties, which simplify ownerdrawing and possible customization ofFastTreeView
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 theOwnerDrawing
property toTextOnly
and handleDrawItem
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) { // 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 fromDirectoryFastTreeNodeData
: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: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:
// (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:
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