![]() |
Desktop Development »
Tree Controls »
TreeView Controls
Intermediate
License: The Code Project Open License (CPOL)
FastTreeViewBy Jacek GajekTreeView control in which nodes dynamically load themselves while expanding |
C# 2.0, Windows, .NET 2.0, Visual Studio, GDI+, WinForms, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||

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:
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.
Let's play with the file system. Our goal is to display a tree with all directories on drive C, like in Explorer.
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:
FastTreeView control Programmatically speaking, it must implement the IFastTreeNodeData interface, which includes:
LoadChildNodes method HasChildren method Text property 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.
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.
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. IFastTreeNodeData, just as a text:
fastTreeView1.Nodes.Add("New Node");
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 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 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 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 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.


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)
{
// 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:
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:

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 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.
There are still things that could be done.
FileSystemFastTreeNodeData, improved performance
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 15 Aug 2007 Editor: Deeksha Shenoy |
Copyright 2007 by Jacek Gajek Everything else Copyright © CodeProject, 1999-2009 Web18 | Advertise on the Code Project |