Introduction
Whilst waiting for Visual Studio 2005 to come along together with rather more comprehensive support for decent menus, I decided that I wanted to add full colour and image support to my own menus. Obviously, I could write a component using IExtenderProvider
to intercept the measuring and painting of menu items, but I also wanted to be able to paint the menu bar, and to provide a structure which would allow me to use different menu painters for different circumstances.
Using the Code
I decided that the support for painting the menu items and menu bar should be implemented via an interface, so that anyone could write their own component to implement the interface. This makes it very easy to add your own custom menu painter. The interface looks like this:
public interface IMenuPainter {
void PaintBar(Graphics g, Rectangle r);
void PaintItem(MenuItem item, DrawItemEventArgs e, Image image,
MenuImageSize imageSize);
void MeasureItem(MenuItem item, MeasureItemEventArgs e, MenuImageSize imageSize);
}
The main MenuEx
component exposes two properties: ImageList
and MenuPainter
. The former allows you to select the ImageList
which will provide the images for the menus, the latter allows you to attach an object which implements the IMenuPainter
interface in order to handle the drawing of menus. The MenuEx
component also adds an ImageIndex
property to each MenuItem
on the form so that you can specify the image to be associated with each MenuItem
. At run-time, it intercepts the MeasureItem
and DrawItem
events for each MenuItem
and passes on the relevant information to the attached IMenuPainter
implementation in order to allow owner-drawing of the menu items.
In addition, the MenuEx
component intercepts the FormResize
event for the form in order to implement painting support for the Menu Bar. I tried a number of ways to implement this, but settled on using the following method when painting the Menu Bar background:
private void SetBrush() {
BarBrush = SafeNativeMethods.CreatePatternBrush(barBitmap.GetHbitmap());
MENUINFO mi = new MENUINFO(form);
mi.fMask = MIM_BACKGROUND;
mi.hbrBack = BarBrush;
SafeNativeMethods.SetMenuInfo(mainMenu.Handle, ref mi);
SafeNativeMethods.SendMessage(form.Handle, WM_NCPAINT, 0, 0);
}
The relevant Windows API struct
and method is shown below:
internal struct MENUINFO {
internal int cbSize;
internal int fMask;
internal int dwStyle;
internal int cyMax;
internal IntPtr hbrBack;
internal int dwContextHelpID;
internal int dwMenuData;
internal MENUINFO(Control owner) {
cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(MENUINFO));
fMask = 0;
dwStyle = 0;
cyMax = 0;
hbrBack = IntPtr.Zero;
dwContextHelpID = 0;
dwMenuData = 0;
}
}
[DllImport("user32.dll")]
internal static extern int SetMenuInfo(IntPtr hmenu, ref MENUINFO mi);
This method allows the MenuEx
component to create a bitmap matching the size of the Menu Bar area, allows the IMenuPainter
implementation to paint it, and then register it as a PatternBrush
with the Windows API SetMenuInfo
method. Effectively, this provides user control over painting of the menu bar; there are limitations with this method, however - see Points of Interest below.
With the work complete on MenuEx
, I decided to write a few sample IMenuPainter
implementations, each written as a Component
. I've chosen to make them inherit from the same ancestor: BaseMenuPainter
, but you don't have to use it unless you want to. A series of virtual
methods allows inherited components to override painting or measuring of particular menu attributes as required. The components are:
BaseMenuPainter | Base implementation of IMenuPainter |
Office2003MenuPainter | Inherits from BaseMenuPainter and paints in the style of Office 2003 |
VisualStudioMenuPainter | Inherits from BaseMenuPainter and paints in the style of Visual Studio 2003 |
PlainMenuPainter | Inherits from BaseMenuPainter and paints in a simple style |
SkinMenuPainter | Inherits from BaseMenuPainter and paints using one of a number of "Skins" |
ImageMenuPainter | Inherits from BaseMenuPainter and paints using a background image for menu items. |
Using the Components
Using the components is very simple:
- Firstly, ensure you have built the libraries and added all items from the "ControlVault.MenuSuite.dll" assembly to your toolbox.
- Add a
MainMenu
component to your form and define your menu items in the normal way. - Add an
ImageList
and select the images you need for your menu items. - Add a
MenuEx
component to your form, and set the ImageList
property to the image list added in the previous step. - Add one of the
MenuPainter
components to your form. - Set the
MenuPainter
property of the MenuEx
component to the MenuPainter
you added to the form. - Go through each
MenuItem
in your form, setting the ImageIndex
property as appropriate.
That's it! If you run your project, you will find the menu bar and items are drawn using the selected MenuPainter
. Now you could try writing your own implementation of IMenuPainter
.
Points of Interest
I encountered a couple of problems along the way:
- The "arrow" used when painting a menu item containing sub-items seems to be painted by Windows whether you like it or not. I decided not to interfere! Suggestions for handling this would be welcomed.
- Whilst the documentation for
CreatePatternBrush
indicates that in Windows XP, you can use any size bitmap you like, I've encountered some strange painting behaviour. Some images work better than others. Again, any suggestions for improving image support are encouraged and welcomed.
History
- 30 June 2005 - Initial article submitted
- 11 July 2005 - Updated code to improve painting of text. Changed the
GetTextColor
method to pass the DrawItemState
in order to allow BaseMenuPainter
descendant classes to determine the state before drawing the text. - 11 July 2005 - Corrected a bug in
ImageMenuPainter
where the background of the MenuItem
objects was being painted with DrawImage
rather than by using a TextureBrush
.
Acknowledgements
Some of the ground covered in this article has inevitably been trodden before. Whilst all the code here is my own, the following article proved illuminating:
Thanks also to innumerable Usenet contributors who gave me ideas!