Icons In Menus Using OwnerDraw Methods In VB.Net

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

Page Updated 23/02/09