Icons In Menus Using OwnerDraw Methods In VB.Net
Here's how I managed to get my VB.Net application to display icons in the menus using OwnerDraw methods. It's quite a fiddle, but very flexible as you can draw any colours, text and images across each MenuItem as you want. While I was at it, I made it look like the VB.Net development studio which looks quite nice. You can paint anything, even images across the menus to make it look how you want, but the example to the right shows how the code below worked for me. It works for menus and sub-menus.
Update: It now displays any shortcut keys you might have assigned to the menu items, and also shortcut accelerator keys.
First, here are the variables that define the appearance of the menus. Put these at the top of the form's code so their scope covers the whole form:
Dim m_topLevelMenuFont As New Font("Verdana", 8)
Dim m_subMenuFont As New Font("Verdana", 8)
Dim m_subMenuFontSelected As New Font("Verdana", 8)
Dim m_iMenuWidth As Integer = 200
Dim m_iMenuItemHeight As Integer = 20
Dim m_iMenuItemSeparatorHeight As Integer = 4
Dim m_iTopLevelMenuItemHeight As Integer = 25
Dim m_iMenuStripeWidth As Integer = 25
Dim m_iSubMenuTextColour As Integer = SystemColors.ControlText.ToArgb
Dim m_iSubMenuTextColourSelected As Integer = SystemColors.ControlText.ToArgb
Dim m_iSubMenuBackColour As Integer = Color.White.ToArgb
Dim m_iSubMenuBackColourSelected As Integer = Color.CornflowerBlue.ToArgb
Dim m_iTopMenuTextColour As Integer = SystemColors.ControlText.ToArgb
Dim m_iTopMenuTextColourSelected As Integer = SystemColors.ControlText.ToArgb
Dim m_iTopMenuBackColour As Integer = SystemColors.Control.ToArgb
Dim m_iTopMenuBackColourSelected As Integer = Color.CornflowerBlue.ToArgb
Dim m_iMenuStripeColour As Integer = SystemColors.Control.ToArgb
Now, add this sub into your form's code. It saves you having to code each event into each MenuItem.
Private Sub drawMenus()
' Add MeasureItem and DrawItem events for all menus and submenus on this form
Try
For Each mi As MenuItem In Me.Menu.MenuItems
mi.OwnerDraw = True
AddHandler mi.MeasureItem, AddressOf topLevelMenu_MeasureItem
AddHandler mi.DrawItem, AddressOf topLevelMenu_DrawItem
For Each msi As MenuItem In mi.MenuItems
msi.OwnerDraw = True
AddHandler msi.MeasureItem, AddressOf submenuitem_MeasureItem
AddHandler msi.DrawItem, AddressOf submenuitem_DrawItem
For Each mssi As MenuItem In msi.MenuItems
mssi.OwnerDraw = True
AddHandler mssi.MeasureItem, AddressOf subMenuItem_MeasureItem
AddHandler mssi.DrawItem, AddressOf submenuitem_DrawItem
Next
Next
Next
Catch ex As Exception
Throw
End Try
End Sub
And call that sub from the Form's New sub:
Public Sub New()
MyBase.New()
'This call is required by the Windows Form Designer.
InitializeComponent()
'Add any initialization after the InitializeComponent() call
' Colour and draw menus
Call drawMenus()
End Sub
Add an ImageList control to your form. I've called mine ilMenu. Add your icons to the Images collection of the ImageList, noting the index of each one as you go. You'll need to refer to the correct index in the code below.
Paste in the following four functions:
Private Sub topLevelMenu_DrawItem(ByVal sender As Object, _
ByVal e As System.Windows.Forms.DrawItemEventArgs)
' This should be called to draw the colour and text on the top level menu items
Dim mi As MenuItem = CType(sender, MenuItem)
Dim sText As String
Dim rect As Rectangle
Dim sf As New StringFormat
Try
' Get the area of the menu item
rect = New Rectangle(e.Bounds.X + 1, e.Bounds.Y + 1, _
e.Bounds.Width - 5, e.Bounds.Height - 1)
' Get the text for the menu item
sText = mi.Text
' Position the text within the menu item area
sf.Alignment = StringAlignment.Near
sf.LineAlignment = StringAlignment.Center
sf.HotkeyPrefix = sf.HotkeyPrefix.Show
' Paint the background rectangle and draw the text on. Different
' colours depending on whether the item is selected with the mouse
If e.State = (DrawItemState.NoAccelerator Or DrawItemState.Selected) OrElse _
e.State = (DrawItemState.NoAccelerator Or DrawItemState.HotLight) Then
' Selected
e.Graphics.FillRectangle( _
New SolidBrush(Color.FromArgb(m_iTopMenuBackColourSelected)), rect)
e.Graphics.DrawString(sText, m_topLevelMenuFont, _
New SolidBrush(Color.FromArgb(m_iTopMenuTextColourSelected)), _
RectangleF.op_Implicit(rect), sf)
Else
' Normal
e.Graphics.FillRectangle( _
New SolidBrush(Color.FromArgb(m_iTopMenuBackColour)), rect)
e.Graphics.DrawString(sText, m_topLevelMenuFont, _
New SolidBrush(Color.FromArgb(m_iTopMenuTextColour)), _
RectangleF.op_Implicit(rect), sf)
End If
e.DrawFocusRectangle()
Catch ex As Exception
m_errors.handle(ex)
Finally
sf = Nothing
rect = Nothing
mi = Nothing
End Try
End Sub
Private Sub topLevelMenu_MeasureItem(ByVal sender As Object, _
ByVal e As System.Windows.Forms.MeasureItemEventArgs)
Dim mi As MenuItem = CType(sender, MenuItem)
Dim g As Graphics = CreateGraphics()
Try
' Make the top level menu item the width of the text within it
e.ItemWidth = CInt(g.MeasureString(mi.Text, m_topLevelMenuFont).Width) - 12
e.ItemHeight = m_iTopLevelMenuItemHeight
Catch ex As Exception
m_errors.handle(ex)
Finally
g = Nothing
mi = Nothing
End Try
End Sub
Private Sub submenuitem_DrawItem(ByVal sender As Object, _
ByVal e As System.Windows.Forms.DrawItemEventArgs)
' Event called by menu items that should paint the background
' colour and the text onto the menu item area. Should be called
' not for the top level menu items, but the sub items
Dim mi As MenuItem = CType(sender, MenuItem)
Dim rc, rcText As RectangleF
Dim sf, sfShortcut As StringFormat
Dim img As Image
Dim sText, sShortcut, sLeftPortion As String
Try
' Get the areas covered by the menu item, and the area
' covered by the menu text
rc = New RectangleF(e.Bounds.X, e.Bounds.Y, _
e.Bounds.Width, e.Bounds.Height)
rcText = New RectangleF(e.Bounds.X + m_iMenuStripeWidth + 5, e.Bounds.Y, _
e.Bounds.Width - (e.Bounds.X + m_iMenuStripeWidth + 5), e.Bounds.Height)
' Get the text and positioning within for the text
sText = mi.Text
sf = New StringFormat
sf.HotkeyPrefix = sf.HotkeyPrefix.Show
sf.Alignment = StringAlignment.Near
sf.LineAlignment = StringAlignment.Center
' Get the shortcut text and positioning
sShortcut = CStr(IIf(mi.Shortcut.ToString.ToUpper = "NONE", "", mi.Shortcut.ToString))
sfShortcut = New StringFormat
sfShortcut.Alignment = StringAlignment.Far
sfShortcut.LineAlignment = StringAlignment.Center
' Paint background and write text differently, depending on
' whether the item is selected or not
If mi.Text = "-" Then
' Separator
e.Graphics.FillRectangle( _
New SolidBrush(Color.FromArgb(m_iSubMenuBackColour)), rc)
e.Graphics.DrawLine(New Pen(Color.FromArgb(m_iMenuStripeColour)), _
e.Bounds.X + 5 + m_iMenuStripeWidth, e.Bounds.Y, _
e.Bounds.X + e.Bounds.Width, e.Bounds.Y)
' Left stripe
e.Graphics.FillRectangle( _
New SolidBrush(Color.FromArgb(m_iMenuStripeColour)), _
New Rectangle(e.Bounds.X, e.Bounds.Y, e.Bounds.X + m_iMenuStripeWidth, _
e.Bounds.Height))
ElseIf e.State = (DrawItemState.NoAccelerator Or DrawItemState.Selected) Then
' Item is selected
e.Graphics.FillRectangle( _
New SolidBrush(Color.FromArgb(m_iSubMenuBackColourSelected)), rc)
' Write the text
e.Graphics.DrawString(sText, m_subMenuFontSelected, _
New SolidBrush(Color.FromArgb(m_iSubMenuTextColourSelected)), rcText, _
sf)
' Write the shortcut text
e.Graphics.DrawString(sShortcut, m_subMenuFontSelected, _
New SolidBrush(Color.FromArgb(m_iSubMenuTextColourSelected)), rcText, _
sfShortcut)
Else
' Normal
e.Graphics.FillRectangle( _
New SolidBrush(Color.FromArgb(m_iSubMenuBackColour)), rc)
' Draw a stripe along the left of the menu item
e.Graphics.FillRectangle( _
New SolidBrush(Color.FromArgb(m_iMenuStripeColour)), _
New Rectangle(e.Bounds.X, e.Bounds.Y, e.Bounds.X + m_iMenuStripeWidth, _
e.Bounds.Height))
' Write the text
e.Graphics.DrawString(sText, m_subMenuFont, _
New SolidBrush(Color.FromArgb(m_iSubMenuTextColour)), rcText, sf)
' Write the shortcut text
e.Graphics.DrawString(sShortcut, m_subMenuFontSelected, _
New SolidBrush(Color.FromArgb(m_iSubMenuTextColourSelected)), rcText, _
sfShortcut)
End If
e.DrawFocusRectangle()
' Decide which if any, icon to draw next to the text
If mi Is mnuStockMaint Then
img = ilMenu.Images(0)
ElseIf mi Is mnuEditCut Then
img = ilMenu.Images(1)
ElseIf mi Is mnuEditCopy Then
img = ilMenu.Images(2)
ElseIf mi Is mnuEditPaste Then
img = ilMenu.Images(3)
ElseIf mi Is mnuEditDelete Then
img = ilMenu.Images(4)
End If
If Not img Is Nothing Then
e.Graphics.DrawImage(img, CInt(e.Bounds.X + 5), _
CInt((e.Bounds.Bottom + e.Bounds.Top) / 2 - _
img.PhysicalDimension.Height / 2))
End If
Catch ex As Exception
m_errors.handle(ex)
Finally
img = Nothing
sf = Nothing
rc = Nothing
rcText = Nothing
mi = Nothing
End Try
End Sub
Private Sub subMenuItem_MeasureItem(ByVal sender As Object, _
ByVal e As System.Windows.Forms.MeasureItemEventArgs)
Dim mi As MenuItem = CType(sender, MenuItem)
Try
e.ItemWidth = m_iMenuWidth
If mi.Text = "-" Then
e.ItemHeight = m_iMenuItemSeparatorHeight
Else
e.ItemHeight = m_iMenuItemHeight
End If
Catch ex As Exception
m_errors.handle(ex)
Finally
mi = Nothing
End Try
End Sub
You will need to amend the code I've highlighted in red so the that indexes of the images correspond to the Name property of each MenuItem you want to add an icon to.
