Three State Treeview - Part 2






4.85/5 (14 votes)
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 Treenode
s - 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-Checkbox
es, 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.
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 ofStateImageIndex
, and how to avoid multiple Before-/After-Check-Events while updating theTreenode
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 commonTreeView
raises redundant Before-/After-Checked-Events, andThreeStateTreeview
toggled the nodes appearance, although it shouldn't.
NowThreeStateTreeview
suppresses those redundant Events. That may be a bug-workaround for the commonTreeView
as well.
Thank you, Christo! - BugFix of Standard-Treeview, when doubleclicking the Checkbox of a node.