Introduction
This tree view control is able to visualize checkbox and radio buttons, like the one you can find under Internet Explorer settings advanced tab. After Spy++ told me that Microsoft uses a normal tree view control, I fired up my Studio to write my own C# control clone named TreeViewRadioBox
.
TreeViewRadioBoxReflection
works a bit like the PropertyGrid
. The public read and writeable properties of type bool
and enum
are fetched (reflection) and displayed as checkboxes and radio buttons, grouped by their category attribute. The properties' values are automatically updated if you edit the value in the tree view.
Implementation
Basic
I tried to meet the following requirements / key features
- Derived from
System.Windows.Forms.TreeView
class
- Tree nodes are managed in collections
- XP theme aware checkbox and radio button images, even if you switch theme at run time
TreeView
familiar handling in general
No requirements without problems to solve
- You can�t derive from
TreeNodeCollection
, internal constructor
- How to draw the checkbox and radio button images, is it possible to draw on an image surface with GDI+
- How to use the XP theme API
I guess there are many ways to visualize checkboxes and radio buttons mixed in a tree view. My solution was to use images which reflect the possible states of the two control types.
The main work was to imitate the behavior of the control types and switch the images to visualize the current state. Because System.Windows.Forms.TreeView
supports checkboxes out of the box, the base functionalities like Node.Checked
property and TreeView.AfterCheck
event are provided by Microsoft.
Node and collection types
Because the behavior of a checkbox and a radio button is not equal, there is an abstract base class which defines the common behaviors and special implementations to meet the type specific behavior. The typed collections hold the associated nodes.
Class |
Description |
TreeNodeBase |
Base abstract TreeNode used for common features |
TreeNodeBaseCollection |
Collection for TreeNodeBase |
TreeNodeCheckBox |
TreeNode which represents a CheckBox |
TreeNodeGroup |
TreeNode which represents a group node |
TreeNodeRadioButton |
TreeNode which represents a RadioButton |
TreeNodeRadioButtonCollection |
Collection for TreeNodeRadioButton |
TreeNodeRadioButtonGroup |
TreeNode which represents a RadioButton parent node |
Because I can�t use the TreeNodeCollection
I had to use my own collection types. I�m not happy about the collection design because I had to painfully override every Nodes
collection with my own collection type. But I gained the easy use of collection add
method which only supports the permitted node types.
For instance, a radio button group can only hold radio buttons, no checkboxes. But a TreeNodeGroup
can hold any of the custom node types.
How it works
Every time the user selects a tree node, the AfterCheck
event is fired. The TreeViewEventArgs.Node
holds the selected node. After casting it to TreeNodeBase
type we can call the TreeNodeBase.UpdateCheckState()
method. UpdateCheckState()
is implemented for each custom node type and does the following:
TreeNodeBase |
Not implemented |
TreeNodeRadioButtonGroup |
Not implemented |
TreeNodeGroup |
Not implemented |
TreeNodeCheckbox |
If the checkbox is checked, the checked image is used, otherwise the unchecked image |
TreeNodeRadioButton |
If the radio is checked, the checked image is used, otherwise the unchecked image. The parent (RadioButtonGroup ) now cares about the optional update of a last selected radio |
Tip to suppress events
Setting the TreeNode.Checked
property from within the BeforeCheck
or AfterCheck
event causes the event to be raised multiple times and can result in unexpected behavior. The TreeViewRadioBox
control provides a simple suppress technique to avoid this.
Every time the code switches the state of the checked flag, it has to call:
internal protected void SuspendItemCheckedEvent(bool supressEvent)
{
this.suspendItemCheckedEvent_ += (supressEvent)? +1 : -1;
}
AfterCheck
event now only calls code if suspendItemCheckedEvent
equals 0. It�s important to use an integer
instead of bool
value because of nested suppress calls.
Event handling
The event ItemChecked
is fired every time a custom node's state has changed. TreeEventArgs.Node
holds the selected node. Every node type can have it's own overridden Nodes
collection and specific properties.
So we have to cast the node before we can access them.
private void treeViewItemChecked(object sender,
System.Windows.Forms.TreeViewEventArgs e)
{
if(e.Node is Raccoom.Windows.Forms.TreeNodeRadioButtonGroup)
{
((Raccoom.Windows.Forms.TreeNodeRadioButtonGroup)
e.Node).SelectedItem.Text);
}
else
{
e.Node.Text;
}
}
Custom drawing
System.Drawing.Graphics
class provides a static method which makes it possible to draw on the surface of a bitmap
.
The following code draws a checkbox
in unchecked state to a bitmap
:
Rectangle rect = new Rectangle(0,0,16,16);
Bitmap bitmap = new Bitmap(rect.Width,rect.Height);
using(System.Drawing.Graphics graphics = Graphics.FromImage(bitmap))
{
ControlPaint.DrawCheckBox(graphics, rect,
ButtonState.Normal | ButtonState.Flat);
}
bitmap.Dispose();
After adding this bitmap
to the tree view image list, you can fake an unchecked tree node.
The ControlPaint
class is not theme aware, so your checkbox will always have the classic Windows style look.
To support themes I used instead of ControlPaint
, the excellent code from Don Kackman, A Managed C++ Wrapper Around the Windows XP Theme API to draw my controls theme aware.
Basically the control supports themed and non themed OS.
Using the code
Using this control is common to using System.Windows.Forms.TreeView
. You can add nodes to the tree view or his child nodes via the Nodes
collection's add
method.
This example shows how to add group, checkbox and radio button nodes. This example assumes that you have created an instance of a TreeViewRadioBox
control on a Form
.
treeView.BeginUpdate();
We start by adding the first group node.
TreeNodeGroup cpNode = new TreeNodeGroup("Codeproject");
treeView.Nodes.Add(cpNode);
Create a radio button group.
TreeNodeRadioButtonGroup pollNode =
new TreeNodeRadioButtonGroup("Survey");
cpNode.Nodes.Add(pollNode);
TreeNodeRadioButton radioNode = new TreeNodeRadioButton
("Desktop applications cannot be replaced by network based applications");
pollNode.Nodes.Add(radioNode);
radioNode = new TreeNodeRadioButton
("There will always be a place for real desktop applications");
pollNode.Nodes.Add(radioNode);
radioNode.Checked = true;
group radioNode = new TreeNodeRadioButton
("Desktop applications will all become network based");
pollNode.Nodes.Add(radioNode);
Create a new group node and add checkbox and radio buttons.
TreeNodeGroup settingNode = new TreeNodeGroup("My settings");
cpNode.Nodes.Add(settingNode);
settingNode.Nodes.Add(new TreeNodeCheckBox("Email Surveys"));
settingNode.Nodes.Add(new TreeNodeCheckBox
("Use cookies so you don't have to log on again"));
TreeNodeGroup newsletterNode = new TreeNodeGroup("Newsletter Topics");
settingNode.Nodes.Add(newsletterNode);
newsletterNode.Nodes.Add(new TreeNodeCheckBox("Site News"));
newsletterNode.Nodes.Add(new TreeNodeCheckBox("C# articles"));
newsletterNode.Nodes.Add(new TreeNodeCheckBox("All .NET articles"));
Resume painting, finished.
treeView.EndUpdate();
After the compiler was working, it looks like:
TreeViewRadioBoxReflection
works familiar to the PropertyGrid
, it shows the reflected properties (bool
and enum
) of a type instance. This example assumes that you have created an instance of a TreeViewRadioBoxReflection
control on a Form
.
treeView.SelectedObject = new System.Windows.Forms.Form();
Conclusion
- Before you can add child nodes to a node you must add the node to the tree view, otherwise the images are wrong until the user selects the item.
Have phun with it�
Links
Screenshots with different XP themes are available here (animated GIF which opens in new window).