Advanced Basics
Creating A Breadcrumb Control
Duncan Mackenzie
Code download available at:
AdvancedBasics0507.exe
(152 KB)
Browse the Code Online

Contents
Hansel and Gretel had the right idea when "they followed the pebbles that glistened there like newly minted coins, showing them the way." The deeper you get into the forest or into your data, the more likely you are going to need help to find your way back out again. On the MSDN® Web site, and many others, the way out is represented by a navigational element in the header of the page that shows your current position in the site. Clicking on any of the links to the left of your current position takes you to that location, providing a quick path to find more related information or to get back to the starting point of the site.
This type of navigation is often referred to as "breadcrumbs" and is most appropriate when your information is organized in a hierarchical structure, with many levels, and works well in the same situations in which a TreeView would be used. Data often maps well to a tree-like structure, so this style of data representation is often found in all sorts of apps, both Windows® and Web-based.
My own applications are no exception; I'm currently working on a new app in which the main method of navigation will be through a tree that displays the structure of a complete Web site, and another tree that represents a single page. Because I am so familiar with the breadcrumb control on MSDN, I decided that I would employ that functionality in my application.
Building a Breadcrumb Control
When the user selects a single page deep within the site, I would like to show them their current position and, at the same time, make it easy to move back up the hierarchy to any point. They will have a TreeView, of course, but I don't think that a tree is as clear and easy as a horizontal list of past locations. So, I decided that I would create a breadcrumb control in Windows Forms.
When you are looking at the code sample, you'll notice that my control is called an "Eyebrow" instead of a breadcrumb. Eyebrow is the name used in MSDN code, and it just stuck in my mind. You're probably wondering how this control relates to an eyebrow. So am I. I know my eyebrows don't have any information about my current position, and they certainly don't help me get around, but that's what they're called in the code, so that's how it'll be.
Back to the control. The control works by being associated with a TreeView. You can configure that association programmatically or through the property grid in Visual Studio® at design time. The control's rendering is then based on information in the associated TreeView, namely the currently selected node.
By hooking the tree's selection changed event (AfterSelect), the control is notified whenever it needs to be redrawn. The associated TreeView is accessed directly to find the currently selected node, to navigate up through all of the parent nodes, and to change the selected node when the user clicks on one of the hyperlinked items. By obtaining all of its navigational information from the TreeView, the breadcrumb control doesn't have to know anything about your particular application. As long as you set up and populate your TreeView and handle the tree's events, this control should work within your application.
Drawing and Clicking
The real core of the control falls into two areas: rendering the control and tracking which areas of the control should be clickable. The first task, the rendering, isn't doing anything really special if you are used to GDI+ coding. I build up a collection of tree nodes in the current node's path, then walk through them from top to bottom, and write out the appropriate text, as shown in Figure 1.

Figure 1 Rendering the Control
Dim bounds As Rectangle = Me.ClientRectangle()
If Me.ClickAreas Is Nothing Then
Me.ClickAreas = New ClickAreaCollection
Else
For Each ca As ClickArea In Me.ClickAreas
ca.Dispose()
Next
Me.ClickAreas.Clear()
End If
Dim nodes As New myTreeNodeCollection
Dim parentNode As TreeNode = m_tree.SelectedNode
Do While Not parentNode Is Nothing
nodes.Insert(0, parentNode)
parentNode = parentNode.Parent
Loop
Dim startingPos As Point = bounds.Location
For Each n As TreeNode In nodes
Dim p As String = n.Text
If Not n Is m_tree.SelectedNode Then
Dim r As Region = DrawText(p, startingPos, g)
Dim ca As New ClickArea
ca.Region = r
ca.Node = n
Me.ClickAreas.Add(ca)
DrawDelim(Me.m_delim, startingPos, g)
Else
DrawDelim(p, startingPos, g)
End If
Next
As I go through this drawing loop, I'm building up a collection of ClickArea objects, which associate a specific tree node with an area of the control. This process, and the collection it creates, is the key to properly handling mouse over and click events.
When the user brings the mouse over the control, a search through the collection of ClickArea objects is executed (shown in Figure 2), and each one in order is checked to see if the current mouse position is contained within the associated Region. If the mouse is over a clickable region, the pointer is changed to the Hand cursor, indicating to the user that something should happen if they click on this point (see Figure 3). The code that changes the cursor is shown in Figure 4.

Figure 4 Changing the Cursor
Protected Overrides Sub OnMouseMove(ByVal e As MouseEventArgs)
Dim pos As New Point(e.X, e.Y)
Dim node As TreeNode
node = FindNode(pos)
If Not node Is Nothing Then
Me.Cursor = Cursors.Hand
Else
Me.Cursor = Cursors.Default
End If
End Sub
Private Function FindNode(ByVal pos As Point)
For Each ca As ClickArea In Me.ClickAreas
If ca.Region.IsVisible(pos) Then
Return ca.Node
End If
Next
Return Nothing
End Function

Figure 2 Defining the ClickArea Collection
Public Class ClickArea
Implements IDisposable
Private m_node As TreeNode
Private m_region As Region
Public Property Node() As TreeNode
Get
Return m_node
End Get
Set(ByVal Value As TreeNode)
m_node = Value
End Set
End Property
Public Property Region() As Region
Get
Return m_region
End Get
Set(ByVal Value As Region)
m_region = Value
End Set
End Property
Public Sub Dispose() Implements System.IDisposable.Dispose
m_node = Nothing
If Not m_region Is Nothing Then
m_region.Dispose()
m_region = Nothing
End If
End Sub
End Class
Figure 3 Hovering Over a Link
In the OnClick procedure of the control, the same search is performed, but in this case if a node is found, the current selection of the tree is updated to point to the clicked item:
Protected Overrides Sub OnClick(ByVal e As System.EventArgs)
Dim pos As Point = Me.PointToClient(Me.MousePosition())
Dim node As TreeNode
node = FindNode(pos)
If Not node Is Nothing Then
Me.m_tree.SelectedNode = node
End If
End Sub
Updating the SelectedNode of the tree will in turn cause the tree's AfterSelect event to fire, causing the Eyebrow control to be redrawn.
All the Bells and Whistles
Never content to leave something in its simplest state, I added some additional properties to the control: AutoSize, Wrap, and ShowImages. The first two are closely related, because AutoSize only works if Wrap is set to true. Setting both these properties to true causes the control to wrap text to a new line if necessary (Wrap) and to increase its height as required to display all the text (AutoSize). If a single node's text is wider than the control, then it is trimmed instead of wrapped because my code only supports wrapping between complete items. This is shown in Figure 5.

Figure 5 AutoSizing and Wrapping
'within the DrawItem routine
Dim sz As SizeF = g.MeasureString(p, f)
If Not img Is Nothing Then
sz.Width += img.Width + Spacer
sz.Height = Math.Max(sz.Height, img.Height)
End If
Dim w As Integer = sz.Width
Dim lineSize, remainingSpace As Integer
lineSize = w + Spacer
remainingSpace = clientBounds.Width - startingPos.X
If (lineSize > remainingSpace) AndAlso _
(lineSize <= clientBounds.Width) Then
If m_wrap Then
startingPos.X = clientBounds.Left
startingPos.Y += sz.Height + Spacer
End If
End If
ShowImages may be useful if you use images in your tree and if those images are meaningful to your users. If you set it to true, then when drawing out the individual item, the image is pulled from the TreeView's associated ImageList and drawn next to its text on the Eyebrow control, as shown here:
'grabbing the image from a TreeNode in OnPaint
Dim img As Image
If m_ShowImages AndAlso Not Me.Tree.ImageList Is Nothing Then
img = Me.Tree.ImageList.Images(n.ImageIndex)
End If
'drawing the image next to each item
If Not img Is Nothing Then
g.DrawImageUnscaled(img, startingPos)
End If
Including support for images complicates the sizing and wrapping code, but the visual effect will be worth it in most applications. The end result is shown in Figure 6.
Figure 6 AutoSizing, Wrapping, and Images
Conclusion
As usual, I haven't built everything into this control that you will probably want; it is a sample after all. I can think of three key additions that you may eventually want: the ability to control the image position (upper left, middle right, and so on), an option to use a separator image between items, and more complex display options for when space is limited. The control in its present state is a good place to start, though, and should open up some interesting navigation options for your tree-based apps.
Send your questions and comments for Duncan to basics@microsoft.com.
Duncan Mackenzie is a developer for MSDN and the author of the Coding 4 Fun column on MSDN online. He can be reached through his personal site at
www.duncanmackenzie.net.