Click here to Skip to main content
Click here to Skip to main content

Simple Tri-State TreeView

By , 30 Mar 2011
 
TriStateTreeView

Introduction

Microsoft provides a TreeView control which isn't able to display tree state checkboxes. There are many ways to implement this feature such as window proc hooking and the other stuff. This article will show you another, more simpler way using techniques provided by the .NET Framework only.

This control only inherits the original tree view control replacing CheckBoxes property usage with usage of the StateImageList and primarily overriding the node click event procedure to handle setting the tri-state.
To conform to all Windows UI styles, the state image list will be built dynamically using the CheckBoxRenderer class provided by the .NET Framework while the control is created.

Background

There may be needs to have a tree view control showing on parent nodes that check states its children aren't all the same. Further there may be needs, refreshing check states up / down the tree while checking a sub-/super-node.

Using techniques such as window proc hooking isn't the right way for everyone because it isn't always easy to debug. Further using static images would avoid displaying right checkboxes on different Windows UIs.

Using the Control / Code

Add the TriStateCheckBoxTreeView.cs to your project and simply drop the control to a form. You'll find the differences to the normal tree view control provided by the .NET Framework in the property grid. There is now a property to enable tri-state usage directly below the CheckBoxes-property.

PropertyGrid.png

Check state checking is done as normal using the Checked property from the tree node. To determine all three possible states, you may check the StateImageIndex giving the following three possible indexes:

  • 0 - Unchecked
  • 1 - Checked
  • 2 - Mixed

Rendering checkboxes using CheckBoxRenderer

Bitmap GetCheckBoxBitmap(CheckBoxState myState)
{
    Bitmap bmpCheckBox = new Bitmap(16, 16);
    Graphics gfxCheckBox = Graphics.FromImage(bmpCheckBox);
    CheckBoxRenderer.DrawCheckBox(gfxCheckBox, new Point(2, 2), myState);
    gfxCheckBox.Save();
 
    return bmpCheckBox;
}

Setting the tri-state

protected override void OnNodeMouseClick(TreeNodeMouseClickEventArgs e)
{
 Stack<TreeNode> stNodes;
 TreeNode tnBuffer;
 bool bMixedState;
 int iSpacing;
 int iIndex;
 
    base.OnNodeMouseClick(e);
    iSpacing = ImageList == null ? 0 : 18;			// if user clicked area
    if (e.X > e.Node.Bounds.Left - iSpacing ||		// *not* used by the state
        e.X < e.Node.Bounds.Left - (iSpacing + 16))		// image we can leave here.
    { return; }
 
    tnBuffer = e.Node;					// buffer clicked node and
    tnBuffer.Checked = !tnBuffer.Checked;			// flip its check state.
 
    stNodes = new Stack<TreeNode>(tnBuffer.Nodes.Count);	// create a new stack and
    stNodes.Push(tnBuffer);					// push buffered node first.
    do {							// let's pop node from stack,
        tnBuffer = stNodes.Pop();				// inherit buffered node's
	tnBuffer.Checked = e.Node.Checked;			// check state and push
        for (int i = 0; i < tnBuffer.Nodes.Count; i++)		// each child on the stack
            stNodes.Push(tnBuffer.Nodes[i]);			// until there is no node
    } while (stNodes.Count > 0);				// left.
			
    bMixedState = false;
    tnBuffer = e.Node;					// re-buffer clicked node.
    while (tnBuffer.Parent != null) {			// while we get a parent we
        foreach (TreeNode tnChild in tnBuffer.Parent.Nodes)	// determine mixed check states
            bMixedState |= (tnChild.Checked != tnBuffer.Checked);// and convert current check
        iIndex = (int)Convert.ToUInt32(tnBuffer.Checked);	// state to state image index.
        tnBuffer.Parent.Checked = bMixedState || (iIndex > 0);	// set parent's check state and
        if (bMixedState)					// state image in dependency
            tnBuffer.Parent.StateImageIndex = CheckBoxesTriState ? 2 : 1;
        else						// of mixed state.
            tnBuffer.Parent.StateImageIndex = iIndex;
        tnBuffer = tnBuffer.Parent;				// finally buffer parent and
    }							// loop here.
}

Limitations

This code has been written as simple as possible. There is one limitation you should keep in mind: If you're adding nodes from code you have to call control's Refresh() after adding has been completed to ensure all nodes got the right state.

History

  • 17 February 2010: First version
  • 18 February 2010: Extended sections "Introduction", "Background", "Using the control / code", fixed demo project
  • 18 March 2010: Small cosmetics to comments
  • 30 March 2011: Updated demo project

License

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

About the Author

DreamCatcher2k10
Software Developer
Germany Germany
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionASP.NETmemberrohith11923 Sep '12 - 20:22 
How can i do the same in ASP.Net ?? i am trying for that. Please give any suggestions or solutions for that.
QuestionProblem with RTL check in checkBoxmemberefirozen17 Jun '12 - 19:06 
The tree I am using is defined RTL.
I have a problem with the direction of the check. The check picture turns like in a mirror to the other side. I would like the check to stay in the useual direction and not to reverse. Only the direction of the tree and text should be RTL.
 
Please can anyone help me SOS!!!
E.R.

QuestionAlternative version submittedmemberFred_Informatix5 Mar '12 - 22:12 
I submitted an alternative version. I tried to fix most of the problems I encountered.
GeneralMy vote of 5memberDreamcooled26 Feb '12 - 5:15 
Works perfectly! Thanks!
QuestionHow can I invert the checkstates for this treeview? [modified]memberStijnBollen19 Jan '12 - 23:40 
How can I invert the checkstates for this Treeview?
 
I've already tried a couple of things, but so far haven't been very successful in my attempts...

-- modified 20 Jan '12 - 5:57.
QuestionBug: Unchecking a mixed state node does not update parents mixed statememberoyvdahl5 Dec '11 - 2:15 
I found a bug in OnNodeMouseClick.
 
The following line only allows a clicked node's StateImageIndex to become 1 or its earlier state. That means if it has state 2 from before, it will stay at 2:
 
tnBuffer.StateImageIndex =  tnBuffer.Checked ? 1 : tnBuffer.StateImageIndex;
 

Exchange with this to fix it:
 
tnBuffer.StateImageIndex =  tnBuffer.Checked ? 1 : 0;
 

 
-Oyvind
BugRefresh has no effect [modified]memberMember 824238016 Sep '11 - 0:24 
UPDATE!: I found the solution! See my reaction!
 
If I add nodes by code (some checked some not) and afterwards call the Refresh function the parent does not get updated Frown | :(
 
When I manually uncheck a checked node the parent node will update. However the problem stays with the rest of the children.
 
Can anyone help me? I'm using VS2010.

modified 23 Sep '11 - 10:11.

AnswerRe: Refresh has no effect [modified]memberMember 824238023 Sep '11 - 4:10 
I FOUND THE SOLUTION!
 
Replace the Refresh part with:
{
            Stack<TreeNode> stNodes;
            TreeNode tnStacked;
 
            base.Refresh();
 
            if (!CheckBoxes)												// nothing to do here if
                return;														// checkboxes are hidden.

            base.CheckBoxes = false;										// hide normal checkboxes...

            stNodes = new Stack<TreeNode>(this.Nodes.Count);				// create a new stack and
            foreach (TreeNode tnCurrent in this.Nodes)						// push each root node.
                stNodes.Push(tnCurrent);
 
           _bPreventCheckEvent = true;
            while (stNodes.Count > 0)
            {										// let's pop node from stack,
                tnStacked = stNodes.Pop();									// set correct state image
                if (tnStacked.StateImageIndex == -1)						// index if not already done
                    tnStacked.StateImageIndex = tnStacked.Checked ? 1 : 0;	// and push each child to stack
                if (!tnStacked.Checked)
                {
                    if (tnStacked.Nodes.Count > 0 && AllChildsChecked(tnStacked))
                    {
                        tnStacked.StateImageIndex = 1;
                        tnStacked.Checked = true;
                    }
                    else if (tnStacked.Nodes.Count > 0 && HasCheckedChild(tnStacked))
                    {
                        tnStacked.StateImageIndex = 2;
                    }
                }
                for (int i = 0; i < tnStacked.Nodes.Count; i++)				// too until there are no
                    stNodes.Push(tnStacked.Nodes[i]);						// nodes left on stack.
            }
            _bPreventCheckEvent = false;
        }
        protected Boolean HasCheckedChild(TreeNode tn)
        {
            Boolean hasChecked = false;
            foreach (TreeNode child in tn.Nodes)
            {
                if (!hasChecked)
                {
                    if (child.Checked)
                    {
                        hasChecked = true;
                    }
                    else
                    {
                        hasChecked = HasCheckedChild(child);
                    }
                }
            }
            return hasChecked;
        }
        protected Boolean AllChildsChecked(TreeNode tn)
        {
            Boolean allChildsChecked = true;
            foreach (TreeNode child in tn.Nodes)
            {
                if (allChildsChecked)
                {
                    if (!child.Checked)
                    {
                        allChildsChecked = false;
                    }
                    else
                    {
                        allChildsChecked = AllChildsChecked(child);
                    }
                }
            }
            return allChildsChecked;
        }


modified 23 Sep '11 - 10:23.

BugStateImageIndex issuememberdymarski5 Jul '11 - 7:26 
Hi there! Really awesome functionality but it appears that the StateImageIndex property doesn't return the correct value.
 
Add a AfterCheck event to the control and make it assign the StateImageIndex to the node's text, then play with checking/unchecking the nodes and you will see the the nodes are renamed improperly.
 
Tried it with VS2008 on Win7 and on WinXP with the same result. It seems that the StateImageIndex property returns the previous value, not the actual one.
Viaceslav Dymarski

GeneralRe: StateImageIndex issuememberIznogood11 Sep '11 - 0:18 
Just add
 
tnBuffer.StateImageIndex = tnBuffer.Checked ? 1: 0;
 
when it fills the stack with the child nodes.
 
Like that :
do { // let's pop node from stack,
tnBuffer = stNodes.Pop(); // inherit buffered node's
tnBuffer.Checked = e.Node.Checked;// check state and push
tnBuffer.StateImageIndex = tnBuffer.Checked ? 1: 0; // Update stateimageindex
for (int i = 0; i < tnBuffer.Nodes.Count; i++) // each child on the stack
stNodes.Push(tnBuffer.Nodes[i]);// until there is no node
} while (stNodes.Count > ); // left.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130523.1 | Last Updated 30 Mar 2011
Article Copyright 2010 by DreamCatcher2k10
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid