Introduction
We all like the cool icons that adorn a modern application's menu items. You would have thought that .NET would have directly supported it out of the box. Well it does, sort of.
When a menu is constructed with the VS IDE menu editor, it's very easy to add menu items and set up the hierarchy of the menu structure. Using just a few mouse clicks and a few keystrokes, your whole menu structure can created in just a few minutes. "So", you may ask, "how do we get these cool icons like you promised?"
Background
To use the supplied code, you will need to know how to create menu items with the VS IDE menu editor
Using the Code
When you use the menu editor, you are creating individual MenuItem
s. As you can guess, MenuItem
s contains various properties, methods, and events. The primary property we are concerned with is the OwnerDraw
Property. The default setting for a MenuItem
for OwnerDraw
is False
. This causes the CLR to draw the text and checks on the item without any intervention, giving you the standard menu look and feel.
Setting the OwnerDraw
property to True
, however, puts all the control into your hands. When this property is true
, the CLR cause two events to be fired - MeasureItem
and DrawItem
. The help documentation defines these properties as follows:
MeasureItem | Occurs when the menu needs to know the size of a menu item before drawing it |
DrawItem | Occurs when the OwnerDraw property of a menu item is set to true and a request is made to draw the menu item |
When the MeasureItem
event is fired, you tell the CLR how big your MenuItem
should be as follows:
e.ItemWidth = 80
e.ItemHeight = 24
This would tell the CLR that the menu item is 80 pixels wide and 24 pixels high. This is important, because these are the dimensions used to construct a graphics area for you to draw into in the DrawItem
event, demonstrated below:
e.DrawBackground()
Dim TextPoint As PointF = New PointF(18, 0)
e.Graphics.DrawIcon(New Icon(New Icon("c:\icons\myicon.ico"), 16, 16), _
New Rectangle(0, 0, 16, 16))
e.Graphics.DrawString(sender.Text, MyFont, New SolidBrush(Black), TextPoint)
The above code will draw the icon on the menu item and print the text defined in your menu. This is a very simple example, but it demonstrates the power of owner draw menus.
Sample Class
The below class takes this example and goes a bit further. Not only does the class draw the menu item, but it also sets the OwnerDraw
property and handles the MeasureItem
and DrawItem
events for you, so that all you have to do is add a menu item to the class and it does all the work. The class creates an ArrayList
and populates it with items of the LocalMenuItem
structure type. This makes it easier to identify and work with the menu items.
Several Add Method constructors are provided which construct and add the individual LocalMenuItem
items to the ArrayList
. Some things are vitally important when adding items to the class:
- All
MenuItem
s should be added - the total menu will not be drawn correctly if items are skipped - The
MenuItemNumber
parameter determines the position of the item in the menu, starting at 0 - these must be sequential as this is used to determine the position and graphics area of the MenuItem
- The
IsSeparator
parameter flags the menu item as a separator - Pass
Nothing
as the icon if the item is a separator or you don't want an icon drawn for the item - Quick access keys (mnemonics) are not supported with the sample
The class only handles vertical (sub menu) items and does not handle the MainMenu
items, although it could be modified to draw these also. It also handles separators properly.
Imports System.Drawing
Imports System.Windows.Forms
Public Class OwnerDrawMenu
Private _MenuItems As New ArrayList()
Private _Font As Font = SystemInformation.MenuFont
Private _TextColor As Color = System.Drawing.SystemColors.MenuText
Private Const NORMALITEMHEIGHT As Integer = 20
Private Const SEPITEMHEIGHT As Integer = 12
Private Const EXTRAWIDTH As Integer = 30
Private Const ICONSIZE16 As Integer = 16
Private Structure LocalMenuItem
Dim MenuItemNumber As Integer
Dim MenuItem As Windows.Forms.MenuItem
Dim Icon As System.Drawing.Icon
Dim IconRectangle As System.Drawing.Rectangle
Dim TextLeft As Integer
Dim TextTopPosition As Integer
Dim Font As System.Drawing.Font
Dim TextColor As System.Drawing.Color
Dim Height As Integer
Dim Width As Integer
Dim IsSeperator As Boolean
End Structure
Public Sub New()
End Sub
Public Sub New(ByVal Font As System.Drawing.Font)
_Font = Font
If _Font.Size > 12 Then
_Font = New Font(_Font.Name, 12, _Font.Style)
End If
End Sub
Public Sub New(ByVal TextColor As System.Drawing.Color)
_TextColor = TextColor
End Sub
Public Sub New(ByVal Font As System.Drawing.Font, _
ByVal TextColor As System.Drawing.Color)
_TextColor = TextColor
_Font = Font
If _Font.Size > 12 Then
_Font = New Font(_Font.Name, 12, _Font.Style)
End If
End Sub
Public Sub Add(ByVal MenuItem As Windows.Forms.MenuItem, _
ByVal Icon As System.Drawing.Icon, _
ByVal MenuItemNumber As Integer, _
ByVal IsSeperator As Boolean)
Me.Add(MenuItem, Icon, MenuItemNumber, IsSeperator, _Font, _TextColor)
End Sub
Public Sub Add(ByVal MenuItem As Windows.Forms.MenuItem, _
ByVal Icon As System.Drawing.Icon, _
ByVal MenuItemNumber As Integer, _
ByVal IsSeperator As Boolean, _
ByVal Font As System.Drawing.Font)
Me.Add(MenuItem, Icon, MenuItemNumber, IsSeperator, Font, _TextColor)
End Sub
Public Sub Add(ByVal MenuItem As Windows.Forms.MenuItem, _
ByVal Icon As System.Drawing.Icon, _
ByVal MenuItemNumber As Integer, _
ByVal IsSeperator As Boolean, _
ByVal Font As System.Drawing.Font, _
ByVal TextColor As System.Drawing.Color)
Static LastTopPosition As Integer
Static LastLeftPosition As Integer
Dim li As New LocalMenuItem()
If MenuItemNumber = 0 Then
LastLeftPosition = 2
LastTopPosition = 0
Else
LastTopPosition = LastTopPosition + IIf(IsSeperator, _
SEPITEMHEIGHT, NORMALITEMHEIGHT)
LastLeftPosition = 2
End If
Const ICONWIDTH As Integer = ICONSIZE16
Const ICONHEIGHT As Integer = ICONSIZE16
Dim IconRect As Rectangle
If IsSeperator Then
IconRect = New Rectangle(LastLeftPosition, LastTopPosition, _
ICONWIDTH, ICONHEIGHT)
Else
IconRect = New Rectangle(LastLeftPosition, LastTopPosition + 2, _
ICONWIDTH, ICONHEIGHT)
End If
MenuItem.OwnerDraw = True
With li
.MenuItemNumber = MenuItemNumber
.Font = Font
.MenuItem = MenuItem
.Icon = Icon
.TextLeft = LastLeftPosition + ICONWIDTH
.TextTopPosition = LastTopPosition
.IconRectangle = IconRect
.TextColor = TextColor
.IsSeperator = IsSeperator
End With
_MenuItems.Add(li)
AddHandler MenuItem.DrawItem, AddressOf Me.DrawItemHandler
AddHandler MenuItem.MeasureItem, AddressOf Me.MesaureItemHandler
End Sub
Private Sub DoDraw(ByVal LI As LocalMenuItem, _
ByRef e As System.Windows.Forms.DrawItemEventArgs)
e.DrawBackground()
Const LastLeftPosition As Integer = 2
Const ICONWIDTH As Integer = ICONSIZE16
Dim ThisMenuItem As MenuItem = LI.MenuItem
Dim MenuItemGraphics As Graphics = e.Graphics
Dim bBypassString As Boolean
Dim SizeF As SizeF = e.Graphics.MeasureString(LI.MenuItem.Text, _Font)
Dim TextPoint As PointF = New PointF(LI.TextLeft, _
LI.TextTopPosition + ((NORMALITEMHEIGHT - SizeF.Height) / 2))
Dim RectHeight As Integer = SizeF.Height
If Not LI.Icon Is Nothing Then
MenuItemGraphics.DrawIcon(New Icon(LI.Icon, _
ICONSIZE16, ICONSIZE16), LI.IconRectangle)
ElseIf LI.IsSeperator Then
MenuItemGraphics.DrawLine(New Pen(LI.TextColor, 1), _
TextPoint.X, TextPoint.Y + 11, _
TextPoint.X + LI.Width + EXTRAWIDTH, TextPoint.Y + 11)
bBypassString = True
End If
If Not bBypassString Then
If LI.MenuItem.Enabled Then
MenuItemGraphics.DrawString(Replace(LI.MenuItem.Text, "&", ""), _
LI.Font, New SolidBrush(LI.TextColor), TextPoint)
Else
MenuItemGraphics.DrawString(Replace(LI.MenuItem.Text, "&", ""), _
LI.Font, New SolidBrush(Drawing.SystemColors.GrayText), TextPoint)
End If
End If
End Sub
Private Sub DoMeasure(ByVal LI As LocalMenuItem, _
ByRef e As System.Windows.Forms.MeasureItemEventArgs)
Dim ThisMenuItem_Strings As String() = LI.MenuItem.Text.Split(",")
Dim TextSize As SizeF = e.Graphics.MeasureString( _
ThisMenuItem_Strings(0).Replace("&", ""), LI.Font)
e.ItemWidth = TextSize.Width + EXTRAWIDTH
If LI.MenuItem.Text = "-" Then
e.ItemHeight = SEPITEMHEIGHT
Else
e.ItemHeight = NORMALITEMHEIGHT
End If
LI.Height = e.ItemHeight
LI.Width = e.ItemWidth
End Sub
Public Sub DrawItemHandler(ByVal sender As Object, _
ByVal e As System.Windows.Forms.DrawItemEventArgs)
Dim li As LocalMenuItem
For Each li In _MenuItems
If li.MenuItem Is sender Then
DoDraw(li, e)
Exit For
End If
Next
End Sub
Public Sub MesaureItemHandler(ByVal sender As Object, _
ByVal e As System.Windows.Forms.MeasureItemEventArgs)
Dim li As LocalMenuItem
For Each li In _MenuItems
If li.MenuItem Is sender Then
DoMeasure(li, e)
Exit For
End If
Next
End Sub
End Class
After adding a MainMenu
control, add a few menu items. Remember, the class handles setting the OwnerDraw
Property for you and also handles the MeasureItem
and DrawItem
events so you need do nothing else but add it to the class. To do this, in the Form_Load
event, add the following code:
Dim odm As New ODM.OwnerDrawMenu(New Font("Tahoma", 8), System.Drawing.Color.Red)
odm.Add(Me.MenuItem3, New Icon("new.ico"), 0, False)
odm.Add(Me.MenuItem2, New Icon("open.ico"), 1, False)
odm.Add(Me.MenuItem4, Nothing, 2, False)
odm.Add(Me.MenuItem5, Nothing, 3, True)
odm.Add(Me.MenuItem6, Nothing, 4, False)
That's all there is to it! You now have those "cool" icons.