
Add Icons to Your Menus
by Andrew Barfield
Level: Intermediate
January 2003 Issue
Technology Toolbox: VB.NET, GDI+
Menu items' OwnerDraw property and events let you leverage the power of GDI+ in dropdown and context menus. Menu icons help users associate menu items and their equivalent commands quickly. With a little creativity, you can build your own Favorites menu or emulate the Windows Start menu, for example.
Begin by creating a Windows application and adding a MainMenu from the Visual Studio toolbox. Add a top-level menu item such as &File to it and fill it in with &New, &Open, and so on. Change the OwnerDraw property of each menu item except File to True in the Properties window. This tells the OS you'll lay out and draw your items.
Use a comma-delimited method to specify each menu item's text label and the icon to display. For example, change the &New menu item's text to &New,New. The text before the comma will be drawn as the menu text, and the text after the comma is the item's icon name in your resource file. When your code measures and draws your menu items, the menu labels will split at the comma and be used separately.
If you ran your application now, you'd see the File item on the menu bar, but no items would appear in the dropdown menu, and the dropdown menu would be the wrong size. You must use the MeasureItem event to specify the width and height for each item in your menu(s). Change to the code editor in Visual Studio and select the first OwnerDraw menu item (for instance, New) in the Class Name combo box. Then select the MeasureItem event in the Method Name combo box. This creates the MeasureItem event you'll use for all the menu items. Add its name to the handles list for the other OwnerDraw menu items:
Private Sub Menu_MeasureItem(...) Handles _
File_New.MeasureItem, File_Open.MeasureItem, _
File_Save.MeasureItem, _
File_SaveAs.MeasureItem, _
File_PageSetup.MeasureItem, _
File_Print.MeasureItem, _
File_Exit.MeasureItem, _
File_Sep1.MeasureItem, File_Sep2.MeasureItem
Examine the code inside the MeasureItem event:
Dim ThisMenuItem As MenuItem = Sender
Dim ThisMenuItem_Strings As String() = _
ThisMenuItem.Text.Split(",")
Dim TextSize As SizeF = _
e.Graphics.MeasureString(_
ThisMenuItem_Strings(0).Replace("&", ""), _
SystemInformation.MenuFont)
e.ItemWidth = TextSize.Width + 30
If ThisMenuItem.Text = "-" Then
e.ItemHeight = 3
Else
e.ItemHeight = 22
End If
Two parameters are passed in to the event. Sender is the OwnerDraw item that needs to be measured. You'll use Sender to get this item's text label, split it, and calculate the text's width using the MeasureString method. You use the Replace method in the MeasureString call to remove any ampersands that would cause a width miscalculation. Then add a hard-coded value of 30 because the icons in this example are always 16 pixels wide with seven pixels of padding on each side.
Next, measure the menu-item height. Menu separators have a hyphen for their text label, so this item's height is three pixels if it's a separator; icon items' height is 22 (16 pixels tall with three pixels of padding top and bottom). Put the preceding code in your MesureItem event.
Now you can draw the menu items on screen. First you must hook the DrawItem event to each item. Choose the first OwnerDraw item and select DrawItem from the Method Name combo box, then add all the other menu items to the handles list:
Private Sub Menu_DrawItem(...) Handles _
File_New.DrawItem, File_Open.DrawItem, _
File_Save.DrawItem, File_SaveAs.DrawItem, _
File_PageSetup.DrawItem, _
File_Print.DrawItem, File_Exit.DrawItem, _
File_Sep1.DrawItem, File_Sep2.DrawItem
The DrawItem event handler works this way: You draw a selection rectangle, its icon with a drop shadow, and its text if an item is selected and enabled. If an item is disabled, the selection rectangle isn't drawn and the icon and text are grayed. For enabled but not selected items, the selection rectangle isn't drawn but the icon and text are drawn normally. Menu separators are drawn as black, horizontal lines:
On Error Resume Next
Dim ThisMenuItem As MenuItem = sender
Dim ThisMenuItem_Brush As Brush
Dim ThisMenuItem_Strings As String() = _
ThisMenuItem.Text.Split(",")
Dim ThisMenuItem_Icon As Icon = _
CType(res.GetObject( _
ThisMenuItem_Strings(1)), System.Drawing.Icon)
Dim ThisMenuItem_Rect As RectangleF = New _
RectangleF(30, e.Bounds.Top, _
e.Bounds.Width - 30, e.Bounds.Height)
If ((e.State And DrawItemState.Selected) = _
DrawItemState.Selected) And ((e.State And _
DrawItemState.Disabled) <> _
DrawItemState.Disabled) Then
' DRAW THE "SELECTED ITEM" BOX
ThisMenuItem_Brush = New _
SolidBrush( _
SystemColors.InactiveCaptionText)
e.Graphics.FillRectangle(ThisMenuItem_Brush, _
e.Bounds)
ThisMenuItem_Brush.Dispose()
e.Graphics.DrawRectangle( _
SystemPens.Highlight, e.Bounds.X, _
e.Bounds.Y, e.Bounds.Width - 1, _
e.Bounds.Height - 1)
' DRAW THE ICON WITH DROPSHADOW
Dim img As Image = _
ThisMenuItem_Icon.ToBitmap()
ControlPaint.DrawImageDisabled( _
e.Graphics, img, e.Bounds.Left + 4, _
e.Bounds.Top + 4, _
SystemColors.InactiveCaptionText)
e.Graphics.DrawIcon(ThisMenuItem_Icon, _
e.Bounds.Left + 2, e.Bounds.Top + 2)
Else
' DRAW THE BACKGROUND
e.Graphics.FillRectangle( _
SystemBrushes.Control, e.Bounds.Left, _
e.Bounds.Top, 22, e.Bounds.Height)
e.Graphics.FillRectangle(SystemBrushes.Menu, _
e.Bounds.Left + 22, e.Bounds.Top, _
e.Bounds.Width, e.Bounds.Height)
' IF THIS IS A SEPARATOR, DRAW IT AND BAIL OUT
If ThisMenuItem.Text = "-" Then
e.Graphics.DrawLine( _
SystemPens.ControlDark, 30, _
e.Bounds.Top + 1, e.Bounds.Right, _
e.Bounds.Top + 1)
Exit Sub
End If
' DRAW THE ICON
If ((e.State And DrawItemState.Disabled) <> _
DrawItemState.Disabled) Then
e.Graphics.DrawIcon(ThisMenuItem_Icon, _
e.Bounds.Left + 3, e.Bounds.Top + 3)
Else
Dim img As Image = _
ThisMenuItem_Icon.ToBitmap()
ControlPaint.DrawImageDisabled( _
e.Graphics, img, e.Bounds.Left + 3, _
e.Bounds.Top + 3, _
SystemColors.InactiveCaptionText)
End If
End If
' DRAW THE TEXT
If (e.State And DrawItemState.Disabled) = _
DrawItemState.Disabled Then
ThisMenuItem_Brush = New _
SolidBrush(SystemColors.GrayText)
e.Graphics.DrawString( _
ThisMenuItem_Strings(0), _
SystemInformation.MenuFont, _
ThisMenuItem_Brush, ThisMenuItem_Rect, _
sfMenuItem)
ThisMenuItem_Brush.Dispose()
Else
e.Graphics.DrawString( _
ThisMenuItem_Strings(0), _
SystemInformation.MenuFont, _
SystemBrushes.ControlText, _
ThisMenuItem_Rect, sfMenuItem)
End If
This event draws menus that look similar to Visual Studio's menus. Selected items appear in light blue with a dark-blue border (the border varies with Windows themes), and selected icons seem to jump up and cast a shadow. The menus you can create are limited only by your imagination.
About the Author
Andrew Barfield is a founder and partner of Xcalibre Technologies, an Internet-oriented technology company. He writes for Visual Studio Magazine and other programming magazines. Contact him at .
|