Creating A Breadcrumb Control
Code download available at:AdvancedBasics0507.exe(152 KB)
Building a Breadcrumb Control
Drawing and Clicking
All the Bells and Whistles
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 **
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 email@example.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.