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

Tri-State Tree View

4.92/5 (28 votes)
30 May 2011CPOL7 min read 175.8K   13.4K  
A Tri-State Tree View designed for Directory Browsing and Installers
TriStateTreeView.png Property.png

Introduction

Tri-State Tree Views are Tree View controls with checkboxes which allow for three states - Checked, UnChecked and Mixed. Checked and UnChecked are self-explanatory and work the same as usual, but the Mixed state is new and is used to indicate that not all child nodes share the same state. These controls are being used more frequently now, especially in Installers or when selecting directories to perform some work on, but the standard .NET Tree View control still only allows for checkboxes with the standard two states. The control presented here allows for all three states to be used.

Background

There are already a few Tri-State Tree View controls available, but none of them seem suited for working with directory structures.

In this implementation, I have attempted to retain the functionality of existing commercial Tri-State Tree Views - specifically that each node remembers whether it has been ticked ('Checked') and changing the ticked state of a Child Node will not change the ticked state of a Parent Node. Additionally, the control can be fully manipulated by using the keyboard, it is not required to use the mouse to change a node's Checked Status. For performance, when a Node's state is changed, only the affected nodes are recalculated - this keeps performance fast even for large trees.

There is no need for specific code in your application to handle the new functionality, unless you are adding new nodes either after the tree's creation or when a node is expanded (more on this later).

The states are handled by setting each node's StateImageIndex (a standard member of the System.Windows.Forms.TreeNode class), which refers to an image in an ImageList created during class construction. The ticked state is stored in the node's Checked member variable as usual.

The control can operate in two modes - Standard and Installer.
Standard mode never changes the Checked status of a parent node, only the image displayed is changed dependent upon the Checked state of child nodes.
Installer mode automatically sets parent nodes to Checked if all child nodes are Checked, or UnChecked if at least one child node is UnChecked. This behaviour is not visible to the user, only when programmatically accessing the nodes states.

Using the Code

Add the file TriStateTreeView.cs to your project and compile it. A new control TriStateTreeView will then appear at the top of the Toolbox and may be added to your form like any other control. Setting the control's CheckBoxes property is not required, but may be done so if you wish. The control defaults to Standard behaviour, for use in an Installer set Style to Installer in the Properties.

The control can be used like a normal TreeView, no additional code is required to use the new functionality. Nodes which are added before the tree is created will automatically display an UnChecked box.

Nodes added as a parent node as expanded will display with the same check state as their parent (if you think about selecting folders on a disk, selecting a parent folder automatically includes all sub-folders).

Checking a child node will not cause the parent node to be checked (if you think about selecting folders on a disk, selecting a sub-folder should not automatically select the parent folder).

To display a state of your own choosing, or to add nodes at any other time, it is necessary to edit the node's StateImageIndex property.

C#
// Not usually required, only use to override default functionality
// Or when adding nodes at unusual times
System.Windows.Forms.TreeNode tn;
tn.StateImageIndex = (int)RikTheVeggie.TriStateTreeView.CheckedState.UnChecked;
tn.StateImageIndex = (int)RikTheVeggie.TriStateTreeView.CheckedState.Checked;
tn.StateImageIndex = (int)RikTheVeggie.TriStateTreeView.CheckedState.Mixed; 

Using the Example

The included example project populates the tree programmatically before the window is displayed. Every other node is also expandable (it includes a 'dummy' node which is replaced when the parent is expanded) and demonstrates adding new nodes programmatically via the BeforeExpand event.

About the Code

The class is derived from the standard System.Windows.Forms.TreeView class. An enum CheckedState is used so we can refer to the states by nice semantic names rather than numbers. A variable IgnoreClickAction is created to allow us to ignore the events raised when a node's state is programmatically changed.

enum TriStateStyles stores the allowed styles of tree, the selected style is stored in the variable TriStateStyle.

To allow the style to be selected from the Properties window, set System.ComponentModel.Category etc before the getter/setter.

C#
public class TriStateTreeView : System.Windows.Forms.TreeView
{
	// <remarks>
	// CheckedState is an enum of all allowable nodes states
	// </remarks>
	public enum CheckedState : int { UnInitialised = -1, UnChecked, Checked, Mixed };

	// <remarks>
	// IgnoreClickAction is used to ignore messages generated by setting the node.
	// Checked flag in code
	// Do not set <c>e.Cancel = true</c> in <c>OnBeforeCheck</c> 
	// otherwise the Checked state will be lost
	// </remarks>
	int IgnoreClickAction = 0;
	// <remarks>

	// TriStateStyles is an enum of all allowable tree styles
	// All styles check children when parent is checked
	// Installer automatically checks parent if all children are checked, 
	// and unchecks parent if at least one child is unchecked
	// Standard never changes the checked status of a parent
	// </remarks>
	public enum TriStateStyles : int { Standard = 0, Installer };

	// Create a private member for the tree style, and allow it to be 
	// set on the property sheer
	private TriStateStyles TriStateStyle = TriStateStyles.Standard;

	[System.ComponentModel.Category("Tri-State Tree View")]
	[System.ComponentModel.DisplayName("Style")]
	[System.ComponentModel.Description("Style of the Tri-State Tree View")]
	public TriStateStyles TriStateStyleProperty
	{
		get { return TriStateStyle; }
		set { TriStateStyle = value; } 
	}
};

The constructor derives a new class from the standard System.Windows.Forms.TreeView class:

C#
public TriStateTreeView() : base()
{
	StateImageList = new System.Windows.Forms.ImageList();

	// populate the image list, using images from the 
	// System.Windows.Forms.CheckBoxRenderer class
	for (int i = 0; i < 3; i++)
	{
		// Create a bitmap which holds the relevant check box style
		// see http://msdn.microsoft.com/en-us/library/ms404307.aspx and 
		// http://msdn.microsoft.com/en-us/library/
		// system.windows.forms.checkboxrenderer.aspx

		System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(16, 16);
		System.Drawing.Graphics chkGraphics = 
				System.Drawing.Graphics.FromImage(bmp);
		switch ( i )
		{
			// 0,1 - offset the checkbox slightly so it 
			// positions in the correct place
			case 0:
				System.Windows.Forms.CheckBoxRenderer.DrawCheckBox
				(chkGraphics, new System.Drawing.Point(0, 1), 
				System.Windows.Forms.VisualStyles.
				CheckBoxState.UncheckedNormal);
				break;
			case 1:
				System.Windows.Forms.CheckBoxRenderer.DrawCheckBox
				(chkGraphics, new System.Drawing.Point(0, 1), 
				System.Windows.Forms.VisualStyles.CheckBoxState.
				CheckedNormal);
				break;
			case 2:
				System.Windows.Forms.CheckBoxRenderer.DrawCheckBox
				(chkGraphics, new System.Drawing.Point(0, 1), 
				System.Windows.Forms.VisualStyles.
				CheckBoxState.MixedNormal);
				break;
		}

		StateImageList.Images.Add(bmp);
	}
}

OnCreateControl is invoked by the system before the tree is first displayed. The default CheckBoxes functionality is disabled as we're providing our own implementation with an additional state. The helper function UpdateChildState() is used to set each node to display an empty check box.

C#
protected override void OnCreateControl()
{
	base.OnCreateControl();
	CheckBoxes = false;	// Disable default CheckBox functionality if 
				// it's been enabled

	// Give every node an initial 'unchecked' image
	IgnoreClickAction++;	// we're making changes to the tree, 
				// ignore any other change requests
	UpdateChildState(this.Nodes, (int)CheckedState.UnChecked, false, true);
	IgnoreClickAction--;
}

OnAfterCheck is invoked by the system whenever the state of a checkbox is changed, whether by a user or code. Before doing any work here, we check the IgnoreClickAction variable to ensure that it's safe to do so (we don't want to do anything if we're explicitly changing states in code).

After setting the new state in StateImageIndex, any Child nodes are set to the same state (e.g., if a parent is selected, it automatically selects all children). Finally, any Parent nodes are informed of the new state as they may need to change their own state to Mixed.

C#
protected override void OnAfterCheck(System.Windows.Forms.TreeViewEventArgs e)
{
	base.OnAfterCheck(e);

	if (IgnoreClickAction > 0)
	{
		return;
	}

	IgnoreClickAction++;	// we're making changes to the tree, 
				// ignore any other change requests

	// the checked state has already been changed, 
	// we just need to update the state index

	// node is either ticked or unticked. Ignore mixed state, 
	// as the node is still only ticked or unticked regardless of state of children
	System.Windows.Forms.TreeNode tn = e.Node;
	tn.StateImageIndex = tn.Checked ? (int)CheckedState.Checked : 
				(int)CheckedState.UnChecked;
		// force all children to inherit the same state as the current node
	UpdateChildState(e.Node.Nodes, e.Node.StateImageIndex, e.Node.Checked, false);

	// populate state up the tree, possibly resulting in parents with mixed state
	UpdateParentState(e.Node.Parent);

	IgnoreClickAction--;
}

OnAfterExpand is invoked by the system whenever a node is expanded. UpdateChildState is called here just in case new children are added as a side-effect of expanding the node (see OnBeforeExpand in the attached example).

C#
protected override void OnAfterExpand(System.Windows.Forms.TreeViewEventArgs e)
{
	// If any child node is new, give it the same check state as the current node
	// So if current node is ticked, child nodes will also be ticked
	base.OnAfterExpand(e);

	IgnoreClickAction++;	// we're making changes to the tree, 
				// ignore any other change requests
	UpdateChildState(e.Node.Nodes, e.Node.StateImageIndex, e.Node.Checked, true);
	IgnoreClickAction--;
}

The helper function UpdateChildState is used to replace the state of Child nodes with that of the Parent. It is usually called with ChangeUninitialisedNodesOnly = false so that nodes are always changed, but may be called with ChangeUninitialisedNodesOnly = true so that only uninitialised nodes are changed (e.g., so that we don't override any explicit status set by the user when creating the tree).

C#
protected void UpdateChildState(System.Windows.Forms.TreeNodeCollection Nodes, 
	int StateImageIndex, bool Checked, bool ChangeUninitialisedNodesOnly)
{
	foreach (System.Windows.Forms.TreeNode tnChild in Nodes)
	{
		if (!ChangeUninitialisedNodesOnly || tnChild.StateImageIndex == -1)
		{
			tnChild.StateImageIndex = StateImageIndex;
			tnChild.Checked = Checked;	// override 'checked' state
						// of child with that of parent

			if (tnChild.Nodes.Count > 0)
			{
				UpdateChildState(tnChild.Nodes, StateImageIndex, 
				Checked, ChangeUninitialisedNodesOnly);
			}
		}
	}
}

The helper function UpdateParentState is used to notify Parent nodes that their Child has changed and they may need to change their own state as a result. It is here that we determine whether to make a node as Mixed.

In Installer mode, the Parent node's Checked state is automatically updated based on the current Child nodes.

The code here could be compacted, but I like the explicit ifs as it makes the logic easier to follow especially for newcomers to the language.

C#
protected void UpdateParentState(System.Windows.Forms.TreeNode tn)
{
	// Node needs to check all of it's children to see if any of them 
	// are ticked or mixed
	if (tn == null)
		return;

	int OrigStateImageIndex = tn.StateImageIndex;

	int UnCheckedNodes = 0, CheckedNodes = 0, MixedNodes = 0;

	// The parent needs to know how many of it's children are Checked or Mixed
	foreach (System.Windows.Forms.TreeNode tnChild in tn.Nodes)
	{
		if (tnChild.StateImageIndex == (int)CheckedState.Checked)
			CheckedNodes++;
		else if (tnChild.StateImageIndex == (int)CheckedState.Mixed)
		{
			MixedNodes++;
			break;
		}
		else
			UnCheckedNodes++;
	}

	if (TriStateStyle == TriStateStyles.Installer)
	{
		// In Installer mode, if all child nodes are checked 
		// then parent is checked
		// If at least one child is unchecked, then parent is unchecked
		if (MixedNodes == 0)
		{
			if (UnCheckedNodes == 0)
			{
				// all children are checked, 
				// so parent must be checked
				tn.Checked = true;
			}
			else
			{
				// at least one child is unchecked, 
				// so parent must be unchecked
				tn.Checked = false;
			}
		}
	}
            
	// Determine the parent's new Image State
	if (MixedNodes > 0)
	{
		// at least one child is mixed, so parent must be mixed
		tn.StateImageIndex = (int)CheckedState.Mixed;
	}
	else if (CheckedNodes > 0 && UnCheckedNodes == 0)
	{
		// all children are checked
		if (tn.Checked)
			tn.StateImageIndex = (int)CheckedState.Checked;
		else
			tn.StateImageIndex = (int)CheckedState.Mixed;
	}
	else if (CheckedNodes > 0)
	{
		// some children are checked, the rest are unchecked
		tn.StateImageIndex = (int)CheckedState.Mixed;
	}
	else
	{
		// all children are unchecked
		if (tn.Checked)
			tn.StateImageIndex = (int)CheckedState.Mixed;
		else
			tn.StateImageIndex = (int)CheckedState.UnChecked;
	}

	if (OrigStateImageIndex != tn.StateImageIndex && tn.Parent != null)
	{
		// Parent's state has changed, notify the parent's parent
		UpdateParentState(tn.Parent);
	}
}

The control can be navigated by the keyboard cursor keys, but by default the Space Bar does nothing. By handling OnKeyDown and checking for Space, we can enable full functionality via the keyboard.

Simply toggling the node's Checked state will cause OnAfterCheck to be called by the system.

C#
protected override void OnKeyDown(System.Windows.Forms.KeyEventArgs e)
{
	base.OnKeyDown(e);

	// is the keypress a space?  If not, discard it
	if (e.KeyCode == System.Windows.Forms.Keys.Space)
	{
		// toggle the node's checked status.  
		// This will then fire OnAfterCheck
		SelectedNode.Checked = !SelectedNode.Checked;
	}
}

One problem I ran into during development was that clicking anywhere on a node (checkbox, label, etc.) would cause the node's Checked state to change. By overriding OnNodeMouseClick, I was able to test for the current location of the mouse and ignore any clicks which were not on the StateImage (the CheckBox).

As with OnKeyDown, simply toggling the node's Checked state causes OnAfterCheck to be called by the system.

C#
protected override void OnNodeMouseClick
	(System.Windows.Forms.TreeNodeMouseClickEventArgs e)
{
	base.OnNodeMouseClick(e);

	// is the click on the checkbox?  If not, discard it
	System.Windows.Forms.TreeViewHitTestInfo info = HitTest(e.X, e.Y);
	if (info == null || info.Location != 
		System.Windows.Forms.TreeViewHitTestLocations.StateImage)
	{
		return;
	}
			
	// toggle the node's checked status.  This will then fire OnAfterCheck
	System.Windows.Forms.TreeNode tn = e.Node;
	tn.Checked = !tn.Checked;
}

About the Example

The example code included here demonstrates adding nodes programmatically before the tree is fully created (i.e., before it's displayed to the user) and adding nodes programmatically when a parent node is expanded.

The example was created by starting a new C# Windows Forms Application, adding the TriStateTreeView.cs file to the project, compiling, then dropping the TriStateTreeView control onto the form (Form1). The function triStateTreeView1_BeforeExpand was associated with the BeforeExpand event of the tree.

Nothing fancy going on in the Form's constructor, just call the helper function PopulateTree with the empty tree.

C#
public Form1()
{
	UseWaitCursor = true;

	InitializeComponent();

	PopulateTree(triStateTreeView1.Nodes, "");

	UseWaitCursor = false;
}

This helper function adds 5 nodes to the specified subtree (ParentNodes). PreText is placed before each node's label just to keep things looking pretty.

To demonstrate child nodes, every other node creates a 'dummy' child which will be replaced when the node is expanded.

C#
private void PopulateTree(TreeNodeCollection ParentNodes, string PreText)
{
	// Add 5 nodes to the current node.  Every other node will have a child
	for (int i = 0; i < 5; i++)
	{
		TreeNode tn = new TreeNode(PreText + (i + 1).ToString());
		if (i % 2 == 0)
		{
			// add a 'dummy' child node which will be replaced 
			// at runtime when the parent is expanded
			tn.Nodes.Add("");
		}

		// There is no need to set special properties on the node 
		// if adding it at form creation or when expanding a parent node.
		// Otherwise, set 
		// tn.StateImageIndex = 
		// (int)RikTheVeggie.TriStateTreeView.CheckedState.UnChecked;
		ParentNodes.Add(tn);
	}
}

By handling the BeforeExpand event, we can check for 'dummy' nodes which need replacing with proper data. When such a node is detected, we simply remove it and call PopulateTree with the parent's subtree and text.

C#
private void triStateTreeView1_BeforeExpand(object sender, TreeViewCancelEventArgs e)
{
	// A node in the tree has been selected
	TreeView tv = sender as TreeView;
	tv.UseWaitCursor = true;

	if ((e.Node.Nodes.Count == 1) && (e.Node.Nodes[0].Text == ""))
	{
		// This is a 'dummy' node.  Replace it with actual data
		e.Node.Nodes.RemoveAt(0);
		PopulateTree(e.Node.Nodes, e.Node.Text);
	}

	tv.UseWaitCursor = false;
}

Points of Interest

This was developed and compiled under VS2010 for .NET 4, but will probably work for other versions too. Please add a comment if you get it working under different conditions! If there's demand, I'll add an option so that it acts like an 'installer' - that is, a Parent node will automatically get ticked when all Child nodes are ticked.

Credits

History

  • 26 May 2011 - Initial upload
  • 30 May 2011 - Added Installer mode

License

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