Introduction
I needed a control that would let me relate some data together for a scientific modeling application I work on. The data has basically a group of rows from one table that relate to a group of rows from another, with each table having sub-tables. To demonstrate this behavior, I have chosen to create a simple application that shows how this control could be used to display some information about a family. A family can have multiple members and a family can have multiple addresses (home and vacation, perhaps). Each member can have multiple e-mail addresses, and each address can have multiple phone numbers. Seems like it's time for a picture...
The above picture shows how my control shows data in my little test application. Notice the family members on the left, each with e-mail addresses (hopefully fictitious), and the right side has the multiple addresses, with phone lines, etc. The extension listed on the kid's phone probably doesn't make any sense, but it's there just to show how the control can continue to have sub nodes.
Also, multiple groups are possible, whereas with the standard TreeView
, you are pretty much limited to one grouping. Some spacing is provided in between each group.
Background
For those of you interested in just using the control, I wanted the control to behave as much like the TreeView
control as I could make it. If you're familiar with that control, my further explanations should make sense. If not, take a look at the explanations for the TreeView
. Hopefully, it will all be clear enough.
For now, I'm just going to cover using the control, and not much of the code, except for the most interesting stuff. Most of the code is math to figure out where everything goes.
Using the Code
A TreeView
exposes a TreeNodeCollection
that allows you to add nodes to it. Each node can contain its own group of nodes, and so on. The DoubleTree
control exposes a DoubleTreeGroupCollection
to which you can add DoubleTreeGroup
s. A DoubleTreeGroup
has LeftNodes and RightNodes which are DoubleTreeNodeCollection
s of DoubleTreeNode
s for displaying things on the left and on the right. From there, the nodes behave like the nodes on a TreeView
.
A DoubleTreeNode
has Text
and Tag
objects, like the TreeNode
. Setting the Text
creates the text that is displayed, and the Tag
lets you relate a generic object to this particular node.
The DoubleTree
will raise events LeftItemChosen
and RightItemChosen
when you select a left or a right node, passing back the selected node and the selected group. One property that I have created is called AllowSelectionFromDifferentGroups
. When this is set to false
(the default), if you select nodes on either side from different groups, the control will automatically select the first node from the opposite side on the group you selected.
Another property is called the RootLineSize
. Additional spacing can appear between the left and right sides. This sets the length, and the line at the top of each group is expanded by the root line size amount. ItemHeight
lets you set the size of each item. GroupMargin
lets you specify the vertical space between each group and SomeRoomAtTop
lets you set some extra space at the top.
Points of Interest
Something I was trying to figure out and believe I have understood was the MeasureCharacterRanges
function. This function returns a region by which your text is bounded. Most of the examples I had seen were confusing, so this code might help. I use this region to paint the black background around the white text for selected nodes. MeasureString
returns a larger area than is really used. MeasureCharacterRanges
returns a more precise region.
SizeF sizString = p_objGraphics.MeasureString(p_objNode.Text, this.Font);
...
StringFormat objStrFormat = new StringFormat();
objStrFormat.Alignment = StringAlignment.Far;
RectangleF objLayoutRect = new RectangleF(intLeftSideOfString,
intTopOfString, sizString.Width,
sizString.Height);
p_objNode.Bounds = objLayoutRect;
if (p_objNode.m_blnIsSelected && p_objNode.Text.Length > 0)
{
CharacterRange[] ranges = {new CharacterRange(0, p_objNode.Text.Length)};
objStrFormat.SetMeasurableCharacterRanges(ranges);
Region[] objRegion = p_objGraphics.MeasureCharacterRanges(p_objNode.Text,
this.Font, objLayoutRect, objStrFormat);
p_objGraphics.FillRegion(drawBrush, objRegion[0]);
drawBrush.Color = Color.White;
}
p_objGraphics.DrawString(p_objNode.Text, this.Font, drawBrush,
objLayoutRect, objStrFormat);
This code is from the DrawLeftNode
function. First, I measure the string using the MeasureString
function. This gets you close to the right size. Most of the examples I have seen guess at the right size to start with, but this I think is much more elegant.
Eventually (...), I create a string format object and a layout rectangle. I specify a "Far" string alignment because I want the text for the left node to appear farthest possible from the left side of the layout rectangle while still being in the rectangle. This makes my text appearing on the left side right justified. The layout rectangle uses the size of the string that I measured. The rest is pretty straightforward. I create my character ranges and set the measurable character ranges on my string format object. Then, I re-measure my string with the MeasuesCharacterRanges
function, which returns a more accurate region that I then fill with black. Then I change the color so that my string appears white.
Notice that I don't bother with MeasureCharacterRanges
when writing the text of the nodes that are not selected, and instead use the initially calculated slightly larger area as the bounds of my rectangle containing the displayed text to check when someone clicks my control. I suppose if I were doing a control with more things displayed on it, I might need to tighten this up, but leaving the extra slack lets the user miss the text a little to the outside edge and still get a hit when selecting the text. This gets more evident with longer text. You might call it lazy. I call it a feature.
TODO
A few things could be added. First, I did not include the expand and collapse functionality of the TreeView
, where you can elect to view sub nodes or not with the + or - displayed. OK, call this one lazy.
A group might need some text to label it. This would perhaps be displayed at the top of the tree with a little line down to the first set of nodes. I didn't need this in my app, but you're free to add it.
I may need to add some functionality for the right click copy/paste menus or dragging and dropping. This would allow me to copy data from one group to another, which might come in handy.
The code is written to re-calculate the virtual space needed for every node's text every time. This could perhaps be optimized. I think .NET 2.0 allows you to figure out the size of a string
without having to use the Graphics
object. This might make it so that when you change a node's text, you can run the calculation outside of the paint call, which seems like a better way to organize things.
Another idea might be rather than having Text
and Tag
properties for nodes, a node might allow you to relate it to an object that implements a specific interface. This interface would include an abstract
function your object would implement that the node would call to get your text string
. This interface would also include an abstract
event that you could raise from within your object to tell the node that the text has changed. It seems silly to have to tell the node and your object that things have changed. Instead, you would update your object, and your object would inform the view through events.
History
- 31st October, 2005: Initial version
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.