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

DoubleTree - A Two Sided TreeView Control

3.67/5 (9 votes)
31 Oct 20056 min read 1   720  
This control allows you to visually relate many to many related groups of data together in a two sided tree display.

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...

Image 1

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 DoubleTreeGroups. A DoubleTreeGroup has LeftNodes and RightNodes which are DoubleTreeNodeCollections of DoubleTreeNodes 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.

C#
SizeF sizString = p_objGraphics.MeasureString(p_objNode.Text, this.Font);
...
StringFormat objStrFormat = new StringFormat();
// this will Right justify the string
objStrFormat.Alignment = StringAlignment.Far;

RectangleF objLayoutRect = new RectangleF(intLeftSideOfString, 
                                intTopOfString, sizString.Width, 
                                sizString.Height);

// save off the bounds of the text. We'll use it when 
// someone clicks our control to find out if they clicked me
p_objNode.Bounds = objLayoutRect;

// if we were selected, let's highlight the selection
if (p_objNode.m_blnIsSelected && p_objNode.Text.Length > 0)
{
    // this code makes the box around the text closer in. 
    // Measure string includes a little extra space
    // around the text. Using MeasureCharacterRanges provides a more 
    // accurate region and makes the highlight box prettier
    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]);
    // the text will be white
    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.