Click here to Skip to main content
15,897,704 members
Articles / Desktop Programming / Windows Forms

Three State Treeview - Part 2

Rate me:
Please Sign up or sign in to vote.
4.85/5 (16 votes)
22 Mar 2014CPOL2 min read 113.6K   3.3K   42   29
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)


Written By
Germany Germany
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralRe: Draw standard indeterminated checkboxes Pin
Christo66719-May-10 5:46
Christo66719-May-10 5:46 
SuggestionRe: Draw standard indeterminated checkboxes Pin
jeffyoung123430-Aug-11 23:05
jeffyoung123430-Aug-11 23:05 
General(hopefully) fixed Pin
Mr.PoorEnglish31-Aug-11 0:02
Mr.PoorEnglish31-Aug-11 0:02 
GeneralOne bug Pin
a010405022-Nov-09 2:39
a010405022-Nov-09 2:39 
GeneralRe: One bug Pin
Mr.PoorEnglish22-Nov-09 5:40
Mr.PoorEnglish22-Nov-09 5:40 
GeneralRe: One bug Pin
a010405022-Nov-09 6:59
a010405022-Nov-09 6:59 
GeneralRe: One bug Pin
Mr.PoorEnglish22-Nov-09 7:38
Mr.PoorEnglish22-Nov-09 7:38 
QuestionHow to make the checkboxes look like real Windows checkboxes Pin
jdmwood21-May-09 6:18
jdmwood21-May-09 6:18 
Great article - thanks!

I'd like to share some extra code which will allow this control to use the native Windows look for the check boxes.

Basically, what I wanted was the checkboxes look they normally do in Windows. Sure, I could screen grab the text boxes and save the images, but then I'd have to do the same in Vista. Also, if a new look for text boxes appears in future versions of Windows, I'd have to repeat (I love the fact that Windows will (usually) use the new look and feel for .NET forms controls).

So, what I did was added code to "paint" a real System.Windows.Forms.CheckBox in each of the three states.

(This uses a common technique for screen dumping controls as described here)

Here is how I did it:

1) Removed the bit in InitializeComponent() where the designer populates the "imageList1" with images from the resource file (TODO: this will probably break the designer view).

2) Instead, in the constructor, I add the images myself, and screen dumps of sample CheckBox objects:

ThreeStateTreeView()
{
  ...
  imageList1 = new System.Windows.Forms.ImageList();
  imageList1.Images.Add(GetCheckboxIm(CheckState.Unchecked);
  imageList1.Images.Add(GetCheckboxIm(CheckState.Checked);
  imageList1.Images.Add(GetCheckboxIm(CheckState.Indeterminate);
  ...
}

...

public Image GetCheckBoxIm(CheckState state)
{
  CheckBox cb = new CheckBox();
  cb.FlatStyle = FlatStyle.System;
  cb.Text = "";
  cb.Width = 16;
  cb.Height = 16;
  cb.CheckState = state;
  cb.UseVisualStyleBackColor = false;

  cb.BackColor = Color.White;   // TODO: Fix this so it matches the BackColor of your tree view

  Bitmap bm1 = new Bitmap(16, 16);
  Graphics g1 = Graphics.FromImage(bm1);

  PaintControl(g1, cb);
  g1.Dispose();
           
 return bm1;
}

// Native Win32 constants
private const int WM_PRINT = 0x317, PRF_CLIENT = 4, PRF_CHILDREN = 0x10, PRF_NON_CLIENT = 2;
private const int COMBINED_PRINTFLAGS = PRF_CLIENT | PRF_CHILDREN | PRF_NON_CLIENT;

// Used to render the control into our graphics context
[DllImport("USER32.DLL")]
private static extern int SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, int lParam);

// Paint the control into the graphics (works even if control isn't visible)
public static void PaintControl(Graphics graphics, Control control)
{ 
  IntPtr hWnd = control.Handle;
  IntPtr hDC = graphics.GetHdc();
  SendMessage(hWnd, WM_PRINT, hDC, COMBINED_PRINTFLAGS);
  graphics.ReleaseHdc(hDC);
}


One thing I haven't quite fixed is that you get a slight bit of the border of the rendered CheckBox which may not match the background of your TreeView (see TODO).

I've also not tested on Vista yet, but I think it should work.

One final thing is that this isn't really tolerant to a checkbox being larger than 16x16. I guess this might happen in the future with Windows UI elements? Or possibly if the user chanegs the DPI setting it might break. It's probably possible to deal with this by making sure that the ImageList is the right size and possibly setting other properties in the TreeView.

John Wood
AnswerRe: How to make the checkboxes look like real Windows checkboxes Pin
grahamoneale29-Apr-10 14:15
grahamoneale29-Apr-10 14:15 
GeneralRe: How to make the checkboxes look like real Windows checkboxes Pin
jdmwood224-Jun-10 4:24
jdmwood224-Jun-10 4:24 

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

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.