Owner-Drawn Menus
If this is your first look at .NET menus, you may be disappointed to see that they don't support the common Windows convention of using embedded thumbnail bitmaps to help distinguish common items. To solve this problem, you could purchase a third-party control, but luckily the logic is easy to implement on your own. All it requires is a dash of GDI+.
The steps for creating an owner-drawn menu are as follows.
Set the OwnerDraw property for the menu items to true. Note that even when you do this, you can still see the menu item in the design environment and configure its Text property.
Handle the MeasureItem event. This is where you tell .NET how much space you need to display the menu item.
Handle the DrawItem event. This is where you actually write the output for the item to the screen. In our case, this output consists of text and a small thumbnail image.
The first step is easy enough. The second step requires a little bit more work. To specify the required size, you need to set the ItemHeight and ItemWidth properties of the MeasureItemEventArgs class with a value in pixels. However, the size required depends a great deal on the font and text you use. Fortunately, the MeasureItemEventArgs class also provides a reference to the graphics context for the menu, which provides a useful MeasureString() method. This method returns a Size structure that indicates the space required.
To keep your code manageable, you should use a single event handler to measure all the menu items. In our example, this includes a "New," "Open," and "Save" menu entry.
private void mnu_MeasureItem(object sender,
System.Windows.Forms.MeasureItemEventArgs e)
{
// Retrieve current item.
MenuItem mnuItem = (MenuItem)sender;
Font menuFont = new Font("Tahoma", 8);
// Measure size needed to display text.
// We add 30 pixels to the width to allow a generous spacing for the image.
e.ItemHeight = (int)e.Graphics.MeasureString(mnuItem.Text, menuFont).Height
+ 5;
e.ItemWidth = (int)e.Graphics.MeasureString(mnuItem.Text, menuFont).Width
+ 30;
}
When displaying a drop-down menu, Windows automatically measures the size of each item, and uses the greatest required width for all menu items.
The final step of displaying the menu item is similarly straightforward. You must find the appropriate picture, and write both the text and image to the screen using the graphics context provided by the DrawItem event handler and the DrawString() and DrawImage() methods. The result is shown in Figure 4-16.

Figure 4-16: An owner-drawn menu
private void mnu_DrawItem(object sender,
System.Windows.Forms.DrawItemEventArgs e)
{
// Retrieve current item.
MenuItem mnuItem = (MenuItem)sender;
// This defaults to the highlighted background if the item is selected.
// Otherwise, it is the default grey background.
e.DrawBackground();
// Retrieve the image from an ImageList control.
Image menuImage = imgMenu.Images[mnuItem.Index];
// Draw the image.
e.Graphics.DrawImage(menuImage, e.Bounds.Left + 3, e.Bounds.Top + 2);
// Draw the text with the supplied colors and in the set region.
e.Graphics.DrawString(mnuItem.Text, e.Font, new SolidBrush(e.ForeColor),
e.Bounds.Left + 25, e.Bounds.Top + 3);
}
Note that this code uses the Windows standard font and colors, which are provided in properties like e.Font and e.ForeColor. Alternatively, you could create your own Color or Font objects and use them. The next example shows more flexible custom formatting.
Tip
You don't need to worry about invisible menu items. If the Visible property is set to false, .NET will not fire the MeasureItem and DrawItem events.You would, however, have to add the drawing logic if you wanted to let your custom menu draw separator items (when the menu text is set to "-"), checkmarks, or greyed out text and images (when the menu item is disabled).
An Owner-Drawn Menu Control
Writing the correct code in the MeasureItem and DrawItem event handlers requires some tweaking of pixel offsets and sizes. Unfortunately, in our current implementation there is no easy way to reuse this logic for different windows (not to mention different applications). A far better approach is to perfect your menu as a custom control, and then allow this control to be reused in a variety of projects and scenarios.
The following example adopts this philosophy, and shows a menu control that provides Image, Font, and ForeColor properties. This custom menu item handles its own drawing logic. All the client code needs to do is set the appropriate properties. The code also extends the previous example by correctly drawing disabled menu items.
using System;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Text;
public class ImageMenuItem : MenuItem
{
private Font font;
private Color foreColor;
private Image image;
public Font Font
{
get
{
return font;
}
set
{
font = value;
}
}
public Image Image
{
get
{
return image;
}
set
{
image = value;
}
}
public Color ForeColor
{
get
{
return foreColor;
}
set
{
foreColor = value;
}
}
public ImageMenuItem(string text, Font font, Image image, Color foreColor)
: base(text)
{
this.Font = font;
this.Image = image;
this.ForeColor = foreColor;
this.OwnerDraw = true;
}
public ImageMenuItem(string text, Image image) : base(text)
{
// Choose a suitable default color and font.
this.Font = new Font("Tahoma", 8);
this.Image = image;
this.ForeColor = SystemColors.MenuText;
this.OwnerDraw = true;
}
protected override void OnMeasureItem(
System.Windows.Forms.MeasureItemEventArgs e)
{
base.OnMeasureItem(e);
// Measure size needed to display text.
e.ItemHeight = (int)e.Graphics.MeasureString(this.Text, this.Font).Height
+ 5;
e.ItemWidth = (int)e.Graphics.MeasureString(this.Text, this.Font).Width
+ 30;
}
protected override void OnDrawItem(System.Windows.Forms.DrawItemEventArgs e)
{
base.OnDrawItem(e);
// Determine whether disabled text is needed.
Color textColor;
if (this.Enabled == false)
{
textColor = SystemColors.GrayText;
}
else
{
e.DrawBackground();
if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
{
textColor = SystemColors.HighlightText;
}
else
{
textColor = this.ForeColor;
}
}
// Draw the image.
if (this.Image != null)
{
if (this.Enabled == false)
{
ControlPaint.DrawImageDisabled(e.Graphics, this.Image,
e.Bounds.Left + 3, e.Bounds.Top + 2, SystemColors.Menu);
}
else
{
e.Graphics.DrawImage(Image, e.Bounds.Left + 3, e.Bounds.Top + 2);
}
}
// Draw the text with the supplied colors and in the set region.
e.Graphics.DrawString(this.Text, this.Font, new SolidBrush(textColor),
e.Bounds.Left + 25, e.Bounds.Top + 3);
}
}
Because this class inherits from MenuItem, you can add instances of it to any MenuItems collection. Here's an example that creates the same menu you considered in your previous example by using the ImageMenuItem control:
mnuFile.MenuItems.Add(new ImageMenuItem("New", imgMenu.Images[0]));
mnuFile.MenuItems.Add(new ImageMenuItem("Open", imgMenu.Images[1]));
mnuFile.MenuItems.Add(new ImageMenuItem("Save", imgMenu.Images[2]));
Alternatively, you can use the other supplied constructor to configure an unusual font and color combination. For example, the code that follows creates a submenu that lists every font, with each name displayed in its own typeface (see Figure 4-17).

Figure 4-17: Displaying a list of installed fonts
InstalledFontCollection fonts = new InstalledFontCollection();
foreach (FontFamily family in fonts.Families)
{
try
{
mnuFonts.MenuItems.Add(new ImageMenuItem(family.Name,
new Font(family, 10), null, Color.CornflowerBlue));
}
catch
{
// Catch invalid fonts/styles and ignore them.
}
}
Unfortunately, there is no easy way to insert an ImageMenu object into a menu using the integrated Visual Studio .NET menu designer. If you want to bridge this gap, you would have to create a custom MainMenu and ContextMenu objects, and then develop custom designers for them that would allow ImageMenu objects to be inserted. Chapter 8 introduces custom designers.
Tip
You can mix owner-drawn ImageMenu objects and ordinary MenuItem objects in the same menu without any complications.Now that you have a grip on the basic suite of .NET controls, the remainder of the chapter dives into two more interesting topics: adding drag-and-drop ability, and creating advanced validation code.