Cutting Edge

Owner-Drawing in .NET

Dino Esposito

Code download available at:CuttingEdge0402.exe(182 KB)

Contents

Customizing Menu Rendering
Overriding the Menu of a Form
The MeasureItem Event
The DrawItem Event
Painting the Menu Item
Context Menu and TextBoxes
Using Graphical Menus Seamlessly

Each new major release of a Microsoft product like Office or Visual Studio® is characterized, among other things, by a new menu style. As new menu styles make their way into products, third-party vendors begin to emulate them with a new wave of custom controls and components. If you're taking advantage of one of these products, you only have to upgrade to the new release. Otherwise, your applications will keep on showing the classic menu user interface that was last updated when Windows® 95 shipped almost a decade ago.

While Microsoft periodically refreshes the menu style in key applications, the menu API available to custom applications hasn't changed much since Windows 3.x and File Manager. Since Win16, applications have been able to customize their menu by claiming the right to paint it personally, one item after the next. This technique is known as owner-drawing and applies to a number of other system components and controls, including listboxes, buttons, and comboboxes.

Figure 1 Owner-drawn Controls

Figure 1** Owner-drawn Controls **

Customizing the drawing of controls is a rather boring task in Win16 and Win32®. It's not particularly complex in terms of functionality, but it's a little annoying to write and maintain. In this column, I'll delve into the owner-drawing mechanism exposed by the Microsoft® .NET Framework for window menus. The ultimate goal is to create a custom component that, once dropped onto the component tray of a Windows Form, allows you to customize the appearance of menus according to a given theme. As an example, I'll use a mix of the Visual Studio .NET and Office 2003 menu styles. Figure 1 gives you an idea of what you can accomplish once you understand owner-drawn controls in .NET.

Customizing Menu Rendering

In the .NET Framework, the classes involved with a window menu are MainMenu, ContextMenu, and MenuItem. All classes descend from a common base class—Menu. The top-level menu of a form is always an instance of the MainMenu class. The application's menu contains a list of popup menus, each composed of items and submenus. The ContextMenu class represents the context menu that any active visual object in the application can display. The context menu is an individual submenu and is in no way different from any of the submenus displayed by the top-level menu. Finally, a MenuItem object represents an individual item that is displayed within a MainMenu or ContextMenu object. The upshot is that any menu in the .NET Framework is a collection of MenuItem objects—no matter where or how it is displayed.

The underlying Win32 owner-draw functionality is conveniently exposed through a handful of properties and events on the MenuItem class. If you're an experienced Win32 programmer, you can appreciate the simplicity and the effectiveness of the .NET owner-draw programming model. If you've never worked with Win32 owner-drawn controls, you can't imagine how painful it really was. To turn on owner-draw for menus in .NET, you simply set the OwnerDraw property to true for each MenuItem object found in the MenuItems property. In addition, each MenuItem object can handle a couple of events, DrawItem and MeasureItem, that collect information for the underlying Win32 platform. The following code in Visual Basic® .NET shows what's needed to turn a menu item into an owner-drawn menu item.

Sub MakeItemOwnerDraw(ByVal item As MenuItem) item.OwnerDraw = True AddHandler item.DrawItem, AddressOf StdDrawItem AddHandler item.MeasureItem, AddressOf StdMeasureItem End Sub

The MeasureItem event occurs when the menu needs to know the size of a menu item before drawing it. The DrawItem event occurs when the menu needs to paint a particular item. Knowing the size of each contained item, Windows can make the menu large enough to accommodate the largest menu item. Typically, the MeasureItem event handler calculates and returns the size of the menu item text given the size of the menu font. The DrawItem handler paints some text onto the provided Graphics object, plus a bitmap, a dithered background, or whatever else you might want to render on the menu.

Overriding the Menu of a Form

The steps to override the menu of a form to make it look more colorful and appealing are straightforward: just set the OwnerDraw attribute of each MenuItem to true and write proper handlers for key events. What's the best way to implement this? Should you inherit a new class from Menu or from MainMenu? Or is it preferable to create a brand new component class that works side by side with the default set of menu objects?

As I see things, the simplest thing to do is create a new external class—I'll call it GraphicMenu—that contains an initialization method. This method takes a reference to each menu you want to customize and turns on the owner-draw flag for each child item. The drawback of this approach is that an explicit call to the initializer is always needed. Perhaps more importantly, you won't get any support from the Visual Studio .NET designer. As a result, the menu only has an owner-drawn appearance at run time. In this column, I'll fully describe this approach.

Like Menu, the GraphicMenu class inherits Component:

Public Class GraphicMenu : Inherits Component

This means that it is docked in the designer's component tray at design time (see Figure 2). The GraphicMenu class acts as a transformer of existing menus—both the application's main menus and context menus. You add an instance of the GraphicMenu class to the form's set of controls and components and the form uses the class to transform regular menus into owner-drawn menus.

Figure 2 Visual Basic .NET Designer

Figure 2** Visual Basic .NET Designer **

The code in Figure 3 shows the entry point method of GraphicMenu. The method Init transforms a gray, 3D, text-only standard menu into something more colorful. The method accepts a Menu object and configures each of its menu items to be owner-drawn. If the menu is a context menu—that is, if the type of the object is ContextMenu—then the owner-draw loop operates directly on it.

Figure 3 Graphic Menu Class

Public Class GraphicMenu : Inherits Component ' Appearance Properties ' TODO:: List properties ' Behavior Properties ' TODO:: List properties Public Sub Init(ByVal menu As Menu) ' Return if the menu is null If menu Is Nothing Then Return End If ' Initialize the font object used to render the menu items ItemFont = New Font(FontName, FontSize) ' Initialize the hashtable used to hold bitmap/item bindings If menuItemIconCollection Is Nothing Then menuItemIconCollection = New Hashtable End If ' Context menu requires a different treatment If TypeOf menu Is ContextMenu Then HandleChildMenuItems(menu) Return End If ' Iterate on all top-level menus and handle their items For Each popup As MenuItem In menu.MenuItems HandleChildMenuItems(popup) Next End Sub End Class

Why should you add more graphics to your menus? Well, other than improving the appearance of the application, one of the main reasons is to add explanatory icons alongside menu items. This means that some extra information must be added to each MenuItem in order to store the icon to be drawn next to the item. Ideally, you would replace the MenuItem class with a new derived class, say GraphicMenuItem, that looks like the parent but shows off a new Icon property. This approach works great if you always generate or manipulate the menu programmatically. Menus of many Visual Studio .NET-based applications are generated using the menu designer (see Figure 4), which automatically creates and registers MenuItem objects.

Figure 4 Menu Designer in Visual Studio .NET

Figure 4** Menu Designer in Visual Studio .NET **

To avoid writing a brand new visual designer and tweaking the auto-generated Visual Studio .NET code, I decided to take another route. The association between icons and menu items is stored within the GraphicMenu class in an internal hash table. The hash table is populated with an AddIcon method:

extendedMenu.AddIcon(FilePrint, "..\images\print.bmp") extendedMenu.AddIcon(FileNew, "..\images\new.bmp") extendedMenu.AddIcon(FileOpen, "..\images\open.bmp") extendedMenu.AddIcon(FileSave, "..\images\save.bmp")

AddIcon takes two parameters—the menu item object and the path to a small bitmap (or any other image file in a format supported by the System.Drawing.Image class). In the hash table, the instance of the menu item is the key and the icon file name is the value. While this might be less elegant from a theoretical point of view, this solution has the advantage of letting you work with your usual tools (Visual Studio .NET) in the traditional way. You just call an extra method to run some background code that reconfigures the menu for you.

The previous code assumes that the menu bitmaps are stored in external files—much like a Web app. If you plan to use images embedded in the app's assembly, you can overload the AddIcon method and make it accept an Image object instead of a file path:

Sub AddIcon(ByVal item As MenuItem, ByVal icon As Image)

The application would first extract the image from the assembly and then add it to the hash table.

The MeasureItem Event

Items marked as owner-draw fire a pair of events to allow for custom rendering. The first event is bound to the Win32 WM_MEASUREITEM message. When the form's window receives this message, it bubbles up a MeasureItem event to any owner-drawn MenuItem object. The event delegate is a class named MeasureItemEventHandler. This code demonstrates its prototype:

Sub StdMeasureItem(ByVal sender As Object, ByVal e As MeasureItemEventArgs)

Information about the event is stored in an instance of the MeasureItemEventArgs class passed to the event handler. Figure 5 lists and describes the properties of this class.

Figure 5 MeasureItemEventArgs

Property Description
Graphics Gets the Graphics object to measure against
Index Gets or sets the index of the item for which the height and width is needed
ItemHeight Gets or sets the height of the item
ItemWidth Gets or sets the width of the item

The purpose of the MeasureItem event is to determine how much space is required by a menu item. In order to compute this, you need to know the details of the surface on which the item will be painted. As such, the event data provides the GDI+ Graphics object representing the drawing surface on which the menu will be drawn. Typically, you compute the width and height based on the text that will be displayed and the desired font, which you get from the menu item. The MeasureItemEventArgs structure doesn't explicitly contain a reference to the MenuItem object, however you can easily grab it by casting the sender of the MeasureItem event to MenuItem. The code in Figure 6 shows how to do that. The text of the menu item plus any shortcut information (such as Ctrl+S) is measured against the current drawing surface and font using the MeasureString method. The resulting size is used to set the ItemWidth property. In the sample code in Figure 6, the height of the menu item is fixed for all items; this is the recommended approach. Note that the owner-draw mechanism is common to a few other controls like listbox and combobox, where items of different heights may sometimes be acceptable.

Figure 6 Define Size of Objects

' **************************************************************** ' HELPER: StdMeasureItem ' INPUT : menu item, data used for measurement ' NOTES : Event handler for the MeasureItem event typical of ' owner-drawn objects Private Sub StdMeasureItem(ByVal sender As Object, _ ByVal e As MeasureItemEventArgs) ' Grab a reference to the menu item being measured Dim item As MenuItem = CType(sender, MenuItem) ' If it is a separator, handle differently If (item.Text = "-") Then e.ItemHeight = SeparatorHeight Return End If ' Measure the item text with the current font. The text to ' measure includes keyboard shortcuts Dim stringSize As SizeF stringSize = e.Graphics.MeasureString(GetEffectiveText(item), ItemFont) ' Set the height and width of the item e.ItemHeight = MenuItemHeight e.ItemWidth = BitmapWidth + HorizontalTextOffset + _ CInt(stringSize.Width) + RightOffset End Sub ' **************************************************************** ' HELPER: StdDrawItem ' INPUT : menu item, ad hoc structure for custom drawing ' NOTES : Event handler for the DrawItem event typical of ' owner-drawn objects Private Sub StdDrawItem(ByVal sender As Object, ByVal e As _ DrawItemEventArgs) ' Grab a reference to the item being drawn Dim item As MenuItem = CType(sender, MenuItem) ' Saves helper objects for easier reference Dim g As Graphics = e.Graphics Dim bounds As RectangleF = MakeRectangleF(e.Bounds) Dim itemText As String = item.Text Dim itemState As DrawItemState = e.State ' Define bounding rectangles to use later CreateLayout(bounds) ' Draw the menu item background and text DrawBackground(g, itemState) ' Draw the bitmap leftmost area DrawBitmap(g, item, itemState) ' Draw the text DrawText(g, item) End Sub

The largest menu item determines the size of the menu window. When the measurement phase is completed, the menu begins the actual rendering of the items in which each item is called to paint its own area. Each item can paint whatever it needs to, including text of various fonts, dithered backgrounds, and bitmaps. This step is handled by the DrawItem event, which I'll cover next.

The DrawItem Event

As mentioned, a menu item receives the DrawItem event when it is configured as owner-draw and a request is made for it to be drawn. The event is raised by the Win32 WM_DRAWITEM message and passes an instance of the DrawItemEventArgs class to all registered handlers. Figure 7 describes the members of this class and the information available to the menu item at rendering time.

Figure 7 DrawItemEventArgs

Property Description
BackColor Gets the background color of the item being drawn
Bounds Gets the rectangle that represents the bounds of the item being drawn
Font Gets the font assigned to the item being drawn
ForeColor Gets the foreground color of the item being drawn
Graphics Gets the graphics surface to draw the item on
Index Gets the index value of the item being drawn
State Gets the state of the item being drawn

Three members of DrawItemEventArgs are particularly important: Graphics, Bounds, and State. Graphics represents the device context for any incoming GDI+ activity. All painting must take place on that Graphics object. The Bounds property is a Rectangle object that represents the drawing area whose width and height have been determined by the MeasureItem event. In addition, the Bounds rectangle provides the x,y coordinates of the item based on the height of previous items and the item's index. If you want to paint the background or to draw a bitmap or some text, this is the region in which it is safe to work.

Finally, the State property is a bit mask of DrawStateItem values and represents the state of the menu item. The DrawStateItem enumerated type contains binary flags denoting states like disabled, checked, hot-tracked, and selected. Each of these states influences the way a menu item is rendered. For example, a selected menu item would render with a different background color, and a disabled item would appear grayed out.

Things are a little more complex for items configured as check or radio items. A checked item is a menu item that represents a Boolean state rather than an action. When clicked, the application typically toggles an internal variable instead of or in addition to executing an action. Checked items are graphically rendered with a checkmark icon. Radio items render mutually exclusive options and are rendered with a bullet icon. Be aware that once you embark on the customization of a menu, you're totally responsible for consistently handling all of these situations.

Painting the Menu Item

The following code snippet shows a typical DrawItem event handler. The method receives the menu item and its related drawing parameters and goes through a pipeline of four steps.

Sub StdDrawItem(ByVal sender As Object, ByVal e As DrawItemEventArgs) Dim item As MenuItem = CType(sender, MenuItem) Dim g As Graphics = e.Graphics Dim itemState As DrawItemState = e.State CreateLayout(bounds) DrawBackground(g, itemState) DrawBitmap(g, item, itemState) DrawText(g, item, itemState) End Sub

First, the code creates some helper rectangles used to draw the various parts of the graphical outline of each menu item. Next, it paints the background and then the bitmap. Finally, the text is rendered. Of course, the steps outlined here depend on the layout I've designed for the menu which, in this case, is similar to that of Visual Studio .NET, as illustrated in Figure 4.

My menu item consists of a bitmap area on the left and a text area on the right. The idea is to give each area different settings for foreground and background colors.

The DrawBackground method examines the state of the menu item and creates foreground and background brushes (optionally, gradient brushes). Next, it fills the bitmap area and the text area. The background colors of both areas are controlled through properties on GraphicMenu. In GDI+, you fill an area using a Brush object. Interestingly enough, the Brush object is the base class of the SolidBrush and LinearGradientBrush types so that having a solid or a dithered background for the menu requires no significant change to the code. The complete source used to paint a menu item is available in the code download (see the link at the top of this article), but I'll examine it in detail here.

The DrawBitmap method also looks at the state of the menu item. If the state requires the use of an embedded bitmap (such as a check or radio item), the necessary image is extracted from the control's assembly and rendered. Otherwise, the DrawBitmap method searches the internal hash table for an icon associated with the MenuItem, using the MenuItem as the key. If an image reference is found and the image exists, it is rendered.

You can embed images into a .NET assembly by adding the bitmap as an embedded resource. To do so, simply change the Build Action setting in the Properties box that appears when the bitmap is selected in the Solution box. (Note that Embedded Resource is not the default build action for a graphic, so this step is crucial to successfully using the image as a resource.) Likewise, it is important that you apply a particular naming convention to the image. The name of the image must be prefixed by the class namespace in Visual Basic .NET (if you're using C#, this works slightly differently). If the GraphicMenu class belongs to the MsdnMag namespace, any embedded image must be named MsdnMag.FileName.bmp. How can you retrieve these images at run time? Here's an example:

If item.RadioCheck Then bmp = ToolboxBitmapAttribute.GetImageFromResource( _ Me.GetType(), "Bullet.bmp", False) Else bmp = ToolboxBitmapAttribute.GetImageFromResource( _ Me.GetType(), "Checkmark.bmp", False) End If

You can use a shared member on the ToolboxBitmapAttribute class named GetImageFromResource. Don't be fooled by the name of the class—it is an attribute class defined in the System.Drawing assembly, but it can be used programmatically in a traditional way (as opposed to the attribute-based programming model). The GetImageFromResource method is just a helper method built around this code:

' t is the type whose namespace is used to scope ' the resource name (GraphicMenu in this context) Dim img As Image Dim str As Stream str = t.Module.Assembly.GetManifestResourceStream(t, "Bullet.bmp"); If Not (str Is Nothing) Then img = new Bitmap(str) End If

When applied to a control, the ToolboxBitmapAttribute attribute tells containers like the Visual Studio Form Designer to retrieve an icon that represents the control. The bitmap is normally embedded in the assembly that contains the control (but it doesn't have to be). The size of the menu bitmap is set by the programmer and should be the same for all bitmaps. In the sample code being discussed here, it is assumed to be 16 × 16.

The DrawBitmap method also manages to render any image transparently, irrespective of the native format, using the MakeTransparent method on the Bitmap class:

bmp.MakeTransparent()

The MakeTransparent method configures the Bitmap object to use the color of the first pixel in the image (0,0 coordinates) as the transparent color. By using a different overload of the method, you can define the transparency color. Finally, to render grayed bitmaps, you have two options. One is to use a totally different set of icons, much like toolbars which use separate image lists. I chose the second—render the image with a different gamma color so that the same image looks grayed out (see Figure 8).

Figure 8 Graying an Image

If (disabled) Then Dim imageAttr As New ImageAttributes imageAttr.SetGamma(0.2F) Dim tmpRect = New Rectangle( BitmapBounds.X + 2, _ BitmapBounds.Y + 2, BitmapBounds.Width - 2, _ BitmapBounds.Right - 2) g.DrawImage(bmp, tmpRect, 0, 0, _ bmp.Width, bmp.Height, GraphicsUnit.Pixel, imageAttr) ImageAttr.ClearGamma() Else g.DrawImage(bmp, BitmapBounds.X + 2, BitmapBounds.Y + 2) End If

When you render an image, you can control the color gamma correction for a few categories of graphic tools, including pens and brushes. You set and reset gamma correction using the ImageAttributes class, an instance of which can be passed to the DrawImage method of the Graphics class. By using a gamma value near zero, DrawImage pales colors out. As Figure 9 shows, this is a fairly good technique for automatically graying images (see the Print menu item). A better approach entails using a transformation matrix. (See the GDI+ SDK documentation for more information on this specific point.)

Figure 9 Greying Out the Print Menu Item

Figure 9** Greying Out the Print Menu Item **

The final step consists of text rendering in the DrawText method. The text to paint can optionally include the textual representation of the shortcut—a string like Shift+Ctrl+F. If the Shortcut property on the menu item is set to true, the text to render is composed of two parts—the text and the shortcut. The code in the DrawText method uses an internal method—GetEffectiveText—to retrieve the item text and the shortcut. It is worth noting that the shortcut text is not immediately available in the typical display format—that is, Key+Key. Keyboard shortcuts are grouped in the Shortcut enum type, whose ToString method returns unseparated expressions such as "CtrlO." However, there's a common pattern in all of these expressions. Key names are camel-cased with the first letter uppercased and all the others lowercased. So if you insert a + separator whenever the occurrence of an uppercase character is found (except the first character), you can create the shortcut string you see in Figure 9.

Handling submenus is easy, regardless of the level of nesting. The trick is in the recursive algorithm I wrote to propagate the OwnerDraw setting in the GraphicMenu's Init method. No matter how many cascading popup menus you have, all of those items will correctly receive the handlers for the MeasureItem and DrawItem events. In addition, the MainMenu class supplies the glyph that indicates that an item has a child menu.

Context Menu and TextBoxes

Customizing a context menu involved the same code with one extension. Bear in mind that in the .NET Framework a context menu is just a main menu with exactly one child menu. The parent of a context menu, though, never appears. In light of this hierarchy, the code I built so far didn't require any changes to work.

A context menu can have a default item. When the user double-clicks a submenu that contains a default item, the default item is automatically selected. You can use the DefaultItem property on the MenuItem class to indicate the default action that is expected in a context menu. If not told otherwise, Windows renders default menu items in boldface. An owner-drawn menu must provide for this feature, too. The DrawItemState enumeration has a specific value—Default—that lets you know when you're rendering a default item. At that point, you just change the font in use and make it boldfaced. This code shows how:

Dim tmpFont As Font Dim defaultItem As Boolean defaultItem = (itemState And DrawItemState.Default) If (defaultItem) Then tmpFont = New Font(ItemFont, FontStyle.Bold) Else tmpFont = ItemFont End If

The Font.Bold property is read-only, meaning that you can't add or remove the bold typeface from a font once it is created. For this reason, if you're going to render a default menu item, you must create a temporary font object (with the bold attribute on) and use that to render. Remember that it's good programming practice to dispose of font objects and other GDI+ objects when you're finished using them.

In a Windows Forms application, you create one or more context menus and bind them to individual controls using the ContextMenu property available on each control. The various context menu objects must be preprocessed using the GraphicMenu object and its Init method to ensure a customized look. Figure 10 shows an example. The button is bound to a context menu where Order is the default item.

Figure 10 Creating Context Menus

Figure 10** Creating Context Menus **

The Textbox is the only control in Windows Forms that has a built-in context menu. The control exposes a ContextMenu property, but it doesn't return an instance of the context menu that appears when you right-click. Why? The code for the textbox's context menu is hardcoded in the Win32 API. In particular, the context menu is generated on the fly each time the user right-clicks. The menu is then released and destroyed once the selection is made. You can replace this menu with your own by setting the ContextMenu property, but there's no chance to manipulate the original menu. Since the menu has a very short (and unmanaged) life, how can you easily subclass it and mark it as owner-draw?

The simplest trick that I've found is the following: create a TextboxContextMenu class that represents a custom context menu with all of the items of a standard textbox context menu (Undo, Cut, Copy, and the like). Implementing these methods is relatively simple and samples can be found in the MSDN documentation. Once you hold an instance of a managed class that works like the typical context menu of a TextBox, you first make it owner-draw and then you bind it to any TextBox you want:

Dim txtMenu As New TextBoxContextMenu gMenu.AddIcon(txtMenu.MenuItemCut, "..\images\cut.bmp") gMenu.AddIcon(txtMenu.MenuItemCopy, "..\images\copy.bmp") gMenu.AddIcon(txtMenu.MenuItemPaste, "..\images\paste.bmp") gMenu.AddIcon(txtMenu.MenuItemDelete, "..\images\delete.bmp") TextBox1.ContextMenu = txtMenu gMenu.Init(TextBox1.ContextMenu)

The TextBoxContextMenu class lives in the same assembly that contains the GraphicMenu class. You should be aware that owner-drawn menus don't work correctly when attached to a NotifyIcon component. For more information on this problem see Knowledge Base article 827043.

Using Graphical Menus Seamlessly

The .NET Framework supports graphical menus using a programming interface that is the managed counterpart to the Win32 owner-draw mechanism. The managed API adds some abstraction, but doesn't redesign the underlying model. This means that a graphical menu can only be obtained with some custom code, as demonstrated in this column. Is there a way to make this process automatic so that no code must be written other than setting properties in the IDE? The code needed to set up GraphicMenu is minimal but necessary when the parent form loads. One way to hide it completely is to create a form class that contains an instance of the GraphicMenu class and in the Load event set up the main menu for owner-draw. If you add this class to the collection of inheritable forms, when you add a menu to the form it will automatically display with a custom look. So go ahead and give it a try.

Send your questions and comments for Dino to  cutting@microsoft.com.

Dino Esposito is an instructor and consultant based in Rome, Italy. Author of Programming ASP.NET (Microsoft Press, 2003), he spends most of his time teaching classes on ADO.NET and ASP.NET and speaking at conferences. Get in touch with Dino at cutting@microsoft.com.