Creating an Owner Drawn Menu
by Jon Wojtowicz

The base menu items that come with .Net are pretty bland menus as you can see in Figure 1. While working on a project I had a requirement to use menus with icons on them. You know, those cool looking menus used on modern application. My final result can be seen in Figure 2.
Figure 1.
Standard Menus
Figure 2.
Owner Drawn Menus
For various reasons the client did not want to use third party controls. So what I had was an interesting lesson in creating owner drawn menus.
The first thing was to create a class that inherits from MenuItem. This allowed me to use the base properties and extend where I needed to.
Then I had to change was creating the OwnerDraw property to true. This property indicates whether the system should draw the menu or not. It is defaulted to false which indicates the system will draw the menu. Setting this to true tells the system that the menu creating will handle the drawing.


The other item I had to manage was the two events the system uses when handling menus, OnMeasureItem and OnDrawItem.
 
protected override void OnMeasureItem(MeasureItemEventArgs e)				
			
The OnMeasureItem is fired when the system needs to determine the bounding rectangle around the menu item. In this method you need to tell the system the size of your menu item. One thing to keep in mind is that you must take into account all of your drawn items. If you return a size that is too small or much too large your menu items will not look good. You specify the size of the bounding rectangle in the MeasureItemEventArgs ItemHeight and ItemWidth properties to notify the system of the size.
 
protected override void OnDrawItem(DrawItemEventArgs e)				
			
The OnDrawItem is called when the menu item is actually rendered. In this method you need perform the actual drawing inside the menu items rectangle. The rectangle bounds are provided in the DrawItemEventArgs Bounds property. You can let your creativity go wild at this point.
The drawing was not as difficult as I had thought. Using the new GDI+ objects simplified the drawing process. One word of caution is that the drawing objects need to be disposed of properly to release the underlying unmanaged resources they consume. If this is forgotten your application can start having performance and memory issues over time.
The menu item I created accomplished my purpose at the time. It could definitely be enhanced but it does a good job of demonstrating the basics.

The code sample is fairly straight forward and gives the basic use of the menu item. Feel free to use this code as is or extend it and give your Windows application a modern look.

Key Code:

using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Text;
using System.Windows.Forms;

//-----------------------------------------------------------------------------
//Copyright �2002-2005 Jon Wojtowicz, All Rights Reserved.
// Some code may also contain other copyrights by the author.
// jwojtowicz@codemeister.net
//-----------------------------------------------------------------------------
// Distribution: You can freely use this code in your own
//               applications provided that credit is given 
//     to the author.
//               
//-----------------------------------------------------------------------------

namespace Persephone.Windows.Control
{
 /// <summary>
 /// A menu item with an associated icon.
 /// </summary>
 public class IconMenuItem : System.Windows.Forms.MenuItem
 {
  private const int defaultGraphicDimension = 20;

  private Font _font = new Font("Arial", 8);
  private Icon _icon;
  private int _graphicWidth = defaultGraphicDimension;
  private int _graphicHeight = defaultGraphicDimension;
  private bool _drawGraphic = true;

  #region Properties

  /// <summary>
  /// The actual text without the accelerator & in it.
  /// </summary>
  public string RealText
  {
   get{ return GetRealText(); }
  }

  /// <summary>
  /// Whether the icon and icon area should be drawn.
  /// </summary>
  public bool DrawGraphic
  {
   get{ return _drawGraphic; }
   set{_drawGraphic = value; }
  }

  /// <summary>
  /// The icon to use with this menu item.
  /// </summary>
  public Icon Icon
  {
   get{ return _icon; }
   set
   { 
    if( _icon != null)
     _icon.Dispose();
    _icon = value; 
    if( _icon != null)
    {
     _graphicWidth = (_icon.Width > _graphicWidth? _icon.Width:_graphicWidth);
     _graphicHeight = (_icon.Height > _graphicHeight? _icon.Height:_graphicHeight);
    }
    else
    {
     _graphicWidth = defaultGraphicDimension;
     _graphicHeight = defaultGraphicDimension;
    }
   }
  }

  /// <summary>
  /// The height of the graphic area for the icon.
  /// </summary>
  public int GraphicHeight
  {
   get { return _graphicHeight; }
   set{ _graphicHeight = value; }
  }

  /// <summary>
  /// The width of the graphic area for the icon.
  /// </summary>
  public int GraphicWidth
  {
   get{ return _graphicWidth; }
   set{ _graphicWidth = value; }
  }

  /// <summary>
  /// The font to use for the menu.
  /// </summary>
  public Font Font
  {
   get{ return _font; }
   set
   { 
    if(_font != null)
     _font.Dispose();
    _font = value; 
   }
  }

  #endregion

  #region Constructors

  /// <summary>
  /// Default constructor
  /// </summary>
  public IconMenuItem(): base()
  {
   base.OwnerDraw = true;
  }

  /// <summary>
  /// Constructor specifying the menu text.
  /// </summary>
  /// <param name="text">The menu text.</param>
  public IconMenuItem(string text): base(text)
  {
   base.OwnerDraw = true;
  }

  /// <summary>
  /// Constructor specifying the menu text and te click event handler.
  /// </summary>
  /// <param name="text">The menu text.</param>
  /// <param name="onClick">The click event handler.</param>
  public IconMenuItem(string text, EventHandler onClick): base(text, onClick)
  {
   base.OwnerDraw = true;
  }

  /// <summary>
  /// Constructor specifying the text and child menu items.
  /// </summary>
  /// <param name="text">The menu text.</param>
  /// <param name="items">The child menu items.</param>
  public IconMenuItem(string text, MenuItem[] items): base(text, items)
  {
   base.OwnerDraw = true;
  }

  #endregion

  #region Drawing

  /// <summary>
  /// Draw the menu.
  /// </summary>
  /// <param name="e">The event args specifying where to draw.</param>
  protected virtual void DrawItems(DrawItemEventArgs e)
  {
   Rectangle rcBk = e.Bounds;
   Brush br = null;
   Pen nPen = null;
  
   if (_drawGraphic)
   {
    rcBk.X += _graphicWidth;
   }

   if ((e.State & DrawItemState.Selected) != 0 && this.Enabled) 
   {
    br = new LinearGradientBrush(rcBk, SystemColors.ActiveCaption, 
SystemColors.InactiveCaption, LinearGradientMode.Horizontal); } else { br = new SolidBrush(System.Drawing.SystemColors.Control); } using(br) { e.Graphics.FillRectangle(br, rcBk); } if (_drawGraphic) { rcBk.X -= _graphicWidth; rcBk.Width = _graphicWidth + 4; using(br = new SolidBrush(System.Drawing.SystemColors.ControlLight)) { e.Graphics.FillRectangle(br, rcBk); } } using(StringFormat sf = new StringFormat()) { sf.HotkeyPrefix = HotkeyPrefix.Show; sf.SetTabStops(60, new float[] {0}); int left = 4; if (_drawGraphic) { left = e.Bounds.Left + _graphicWidth + 4; } if (String.Compare("-", this.Text) == 0 ) { using(Pen lineBasePen = new Pen(System.Drawing.SystemColors.ControlDark, 1)) { using(Pen lineHighlightPen = new Pen(System.Drawing.SystemColors.ControlLightLight, 1)) { int lineY = ((e.Bounds.Bottom - e.Bounds.Top) / 2) + e.Bounds.Top; e.Graphics.DrawLine(lineBasePen, left, lineY, e.Bounds.Right, lineY); e.Graphics.DrawLine(lineHighlightPen, left, lineY + 1, e.Bounds.Right, lineY + 1); } } } else { if (this.Enabled) { br = new SolidBrush(e.ForeColor); } else { br = new SolidBrush(System.Drawing.SystemColors.GrayText); } using(br) { e.Graphics.DrawString(GetRealText(), _font, br, left, e.Bounds.Top + 2, sf); } } } if (_icon != null && _drawGraphic) { if (! this.Checked) { e.Graphics.DrawIcon(_icon, e.Bounds.Left, e.Bounds.Top); } else { e.Graphics.DrawIcon(_icon, e.Bounds.Left + 2, e.Bounds.Top + 2); if (! this.Enabled) { nPen = new Pen(SystemColors.GrayText); } else { nPen = new Pen(SystemColors.ControlDarkDark); } using(nPen) { e.Graphics.DrawRectangle(nPen, 1, e.Bounds.Top, _graphicWidth, _graphicHeight); } } } else { if (this.Checked) { if (! this.Enabled) { nPen = new Pen(SystemColors.GrayText); } else { nPen = new Pen(SystemColors.ControlDarkDark); } using(nPen) { e.Graphics.DrawRectangle(nPen, 1, e.Bounds.Top, 20, 20); } Point[] Pnts = new Point[3]; Pnts[0] = new Point(15, e.Bounds.Top + 6); Pnts[1] = new Point(8, e.Bounds.Top + 13); Pnts[2] = new Point(5, e.Bounds.Top + 10); if (this.Enabled) { nPen = new Pen(Color.Black); } else { nPen = new Pen(Color.Gray); } using(nPen) { e.Graphics.DrawLines(nPen, Pnts); } } } } #endregion #region Helper Methods /// <summary> /// Measure the size of the menu item. Return the value in the event args. /// </summary> /// <param name="e">The event args for measuring this item.</param> protected virtual void MeasureItems(MeasureItemEventArgs e) { using(StringFormat sf = new StringFormat()) { sf.HotkeyPrefix = HotkeyPrefix.Show; sf.SetTabStops(60, new float[] {0}); if (String.Compare("-", this.Text) == 0) { e.ItemHeight = 3; } else { int textHeight = (int)(e.Graphics.MeasureString(GetRealText(), _font, 10000, sf).Height); e.ItemHeight = (textHeight > _graphicHeight? textHeight: _graphicHeight); } e.ItemWidth = (int)(e.Graphics.MeasureString(GetRealText(), _font, 10000, sf).Width) + _graphicWidth + 10; } } /// <summary> /// Returns the actual menu text that will be displayed in the menu. /// </summary> /// <returns>The actual text of the menu.</returns> protected string GetRealText() { string s = this.Text; if (this.ShowShortcut && this.Shortcut != Shortcut.None) { Keys k = (Keys)this.Shortcut; s = s + Convert.ToChar(9) + TypeDescriptor.GetConverter(typeof(Keys)).ConvertToString(k); } return s; } #endregion #region Overrides /// <summary> /// Triggered by the system to get the menu item measurements. /// </summary> /// <param name="e">The event args for measuring this item.</param> protected override void OnMeasureItem(MeasureItemEventArgs e) { base.OnMeasureItem(e); MeasureItems(e); } /// <summary> /// Triggered by the system to get the menu item drawn. /// </summary> /// <param name="e">The event args specifying where to draw.</param> protected override void OnDrawItem(DrawItemEventArgs e) { e.Graphics.CompositingQuality = CompositingQuality.HighQuality; base.OnDrawItem(e); DrawItems(e); } /// <summary> /// Clean up any contained objects that need to be disposed. /// </summary> /// <param name="disposing">True if dicpose is called from the client, /// false if called from the finalizer.</param> protected override void Dispose(bool disposing) { base.Dispose (disposing); if(disposing) { if(_font != null) _font.Dispose(); if(_icon != null) _icon.Dispose(); } } #endregion } }

Download the Visual Studio.Net 2003 Solution

Article Discussion: