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

Three State Treeview - Part 2

4.85/5 (16 votes)
22 Mar 2014CPOL2 min read 116.5K   3.3K  
Treeview with Checkboxes supporting 3-state-logic

Introduction

My search for a Treeview with 3-state-checkboxes led me to this article. It explains the logic properly and promises a part 2, in which the problems about ownerdrawing the checkboxes should be solved.

But that part 2 has, for whatever reason, never been written. But the author allowed the audience to feel free to write part 2. So I felt free. ;)

Repeat the Logic

The user can only check or uncheck Treenodes - not set to indeterminate. Checking/unchecking a Node sets all childnodes to that new state. If a ParentNode contains nodes of different states, then it will display the Indeterminate - state.

The Code

The principle is to use the StateImageList-property with 3 Images: Unchecked, Checked, Indeterminate. The dual logic is done well by the Treeview as it is. Treeview uses the first two images properly to display Checked/Unchecked. Nevertheless I carefully set the proper StateImageIndices, although that's not needed (for dual logic).
But I need it to persist 3 states. When it comes to draw, I only have to draw the Indeterminated Checkbox.

A problem was that I need to use TreeViewDrawMode.OwnerDrawAll to figure out which node to draw. But I don't want to draw the nodes completely, because that's quite difficult (Checkbox, optional Icon, SelectedIcon, Text, SelectedText, Focus). I just want to add my Indeterminated-Checkbox, if necessary.
Unfortunately DrawMode.OwnerDrawAll disables the _Paint-Event, and there is no "AfterDrawNode"-Event. So I had to subclass the windowmessages, observing, when the WM_PAINT-windowmessage has passed. At that moment, I can draw my indeterminated-Checkboxes, and they will not be overdrawn by the Treeview.

So here you can look at the most important parts of the ThreeStateTreeview, and I hope, it is commented well enough to make more explanations redundant.

C#
protected override void OnAfterCheck(TreeViewEventArgs e) {
   /* Logic: All children of an (un)checked Node inherit its Checkstate
    * Parents recompute their state: if all children of a parent have same state, 
    * that one will be taken over as parents state - otherwise take Indeterminate 
    */
   if(_skipCheckEvents) return;/* changing any Treenodes .Checked-Property will raise 
                                     another Before- and After-Check. Skip'em */
   _skipCheckEvents = true;
   try {
      TreeNode nd = e.Node;
      /* uninitialized Nodes have StateImageIndex -1, 
       * so I associate StateImageIndex as follows:
       * -1: Unchecked
       *  0: Checked
       *  1: Indeterminate
       *  That corresponds to the System.Windows.Forms.Checkstate - enumeration, 
       *  but 1 less.
       *  Furthermore I ordered the images in that manner
       */
      int state = nd.StateImageIndex == 0 ? -1 : 0;      /* this state is already toggled.
          Note: -1 (Unchecked) and 1 (Indeterminate) both toggle to 0, 
                        that means: Checked */
      if((state == 0) != nd.Checked) return;       //suppress redundant AfterCheck-event
      InheritCheckstate(nd, state);         // inherit Checkstate to children
      // Parents recompute their state
      nd = nd.Parent;
      while(nd != null) {
         // At Indeterminate (==1) skip the children-query - 
         // every parent becomes Indeterminate
         if(state != 1) {
            foreach(TreeNode ndChild in nd.Nodes) {
               if(ndChild.StateImageIndex != state) {
                  state = 1;
                  break;
               }
            }
         }
         AssignState(nd, state);
         nd = nd.Parent;
      }
      base.OnAfterCheck(e);
   } finally { _skipCheckEvents = false; }
}

private void AssignState(TreeNode nd, int state) {
   bool ck = state == 0;
   bool stateInvalid = nd.StateImageIndex != state;
   if(stateInvalid) nd.StateImageIndex = state;
   if(nd.Checked != ck) {
      nd.Checked = ck;                // changing .Checked-Property raises 
                // Invalidating internally
   } else if(stateInvalid) {
      // in general: the less and small the invalidated area, the less flickering
      // so avoid calling Invalidate() if possible, and only call, if really needed.
      this.Invalidate(GetCheckRect(nd));
   }
}

private void InheritCheckstate(TreeNode nd, int state) {
   AssignState(nd, state);
   foreach(TreeNode ndChild in nd.Nodes) {
      InheritCheckstate(ndChild, state);
   }
}

public System.Windows.Forms.CheckState GetState(TreeNode nd) {
   // compute the System.Windows.Forms.CheckState from a StateImageIndex 
   // is not that complicated
   return (CheckState)nd.StateImageIndex + 1;
}

protected override void OnDrawNode(DrawTreeNodeEventArgs e) {
   // here nothing is drawn. Only collect Indeterminated Nodes, 
   // to draw them later (in WndProc())
   // because drawing Treenodes properly (Text, Icon(s) Focus, Selection...) 
   // is very complicated
   if(e.Node.StateImageIndex == 1) _indeterminateds.Add(e.Node);
   e.DrawDefault = true;
   base.OnDrawNode(e);
}

protected override void WndProc(ref Message m) {
   const int WM_Paint = 15;
   base.WndProc(ref m);
   if(m.Msg == WM_Paint) {
      // at that point built-in drawing is completed - 
      // and I paint over the Indeterminate-Checkboxes
      foreach(TreeNode nd in _indeterminateds) {
         _graphics.DrawImage(_imgIndeterminate, GetCheckRect(nd).Location);
      }
      _indeterminateds.Clear();
   }
}

Credits

  • Three State Treeview - Part 1 - Although I didn't use a line of that code, it gave me the idea of how to synchronize the Checked-Property with the 3 options of StateImageIndex, and how to avoid multiple Before-/After-Check-Events while updating the Treenode states.

History

  • 1st April, 2009: Initial post
  • 18th May, 2010: Bugfix: Christo667 reported a well hidden bug, when programmatical set a nodes Checked-property to the same value, it had before (see on Message-board). The bug-reason was, in that case the common TreeView raises redundant Before-/After-Checked-Events, and ThreeStateTreeview toggled the nodes appearance, although it shouldn't.
    Now ThreeStateTreeview suppresses those redundant Events. That may be a bug-workaround for the common TreeView as well.
    Thank you, Christo!
  • BugFix of Standard-Treeview, when doubleclicking the Checkbox of a node.

License

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