Part 1 showed how to avoid owner draw menus by converting icons into bitmaps. In Part 2 we will show how to use the Visual Style APIs in your owner draw code.
Sample Custom Menu
The sample app demonstrates a simple custom behavior: A context menu that highlights the previously selected item. This was inspired by the "recently installed" highlighting that the Start Menu does. A dropdown menu lets you choose between standard menu rendering, owner draw with standard rendering, and owner draw using Visual Style APIs and the new Vista menu parts.
Visual Style API Basics
Before getting into the menu specifics there are a few concepts to touch on. A more complete discussion of the Visual Style API can be found here.
- Theme handles (HTHEME.) You call OpenThemeData() with your window handle and the visual class you are interested in (VSCLASS_MENU for our purposes.) You will get a NULL handle back if the current color scheme does not have visual styling enabled. You also need to watch WM_SETTINGCHANGE and regenerate your HTHEME. If you do get back a NULL handle you will need to fall back to non-styled APIs to do your rendering.
- Theme parts and states. A visual element is made up of parts and each part may have multiple states. For example, a button has a background part and different states to differentiate between pressed and non-pressed states. The sample code will show how to translate owner-draw fState flags into part and state IDs for menus. Vssym32.h contains the part and state IDs for every visual class.
- Measurement. You inquire about the size or margins for a part / state pair using GetThemePartSize() and GetThemeMargins() respectively. You use GetThemeTextExtent() to measure text strings.
- Drawing. You can use DrawThemeBackground() and DrawThemeText() to render according to the current color scheme.
Overall structure of Sample App
The MENUITEMDATA structure holds our owner draw data and the GetMenuItem() function returns a MENUITEM structure that contains all the info we need:
struct MENUITEM
{
MENUITEMINFO mii;
MENUITEMDATA mid;
WCHAR szItemText[256];
};
The test app has a dropdown to switch between standard, owner draw, and owner draw using Visual APIs. To make this easier an interface is defined and two implementors of the interface are provided (CClassicOwnerDrawMenu and CVistaOwnerDrawMenu):
interface IOwnerDrawMenu
{
virtual HRESULT Initialize(HWND hwndParent) = 0;
virtual void MeasureItem(__in MENUITEM *pmi, __inout MEASUREITEMSTRUCT *pmis) = 0;
virtual void DrawItem(__in MENUITEM *pmi, __in DRAWITEMSTRUCT *pdis) = 0;
virtual void SelectedItem(int id) = 0;
virtual HRESULT SettingChange() = 0;
virtual void Release() = 0;
};
The SelectedItem() method tells the class which item should receive special highlighting (The test app will call this with the previously selected menu item.)
CShellSimpleApp implements the outer shell of the program and is essentially a dialog box. The WM_MEASUREITEM, WM_DRAWITEM, and WM_SETTINGCHANGE message handlers are the relevant portion of this class, along with the _SetMenuType() method which switches between menu types.
The remainder of this article will focus on CVistaOwnerDraw menu and it’s helper class CMenuMetrics. CClassicOwnerDraw is left as an exercise to the reader.
Modifying owner-draw code – Initialization
Before you are able to measure or draw with the Visual APIs you need an HTHEME and, as I said earlier, the user may not be using a color scheme that uses Visual APIs. The Initialize method will attempt to get an HTHEME based on the parent window and return failure if no HTHEME is available. The test harness will fall back to one of the other menu types in this case.
The heart of the initialization is done here (this is not the complete function, it’s been pared down for illustration):
HRESULT CMenuMetrics::Initialize()
{
HRESULT hr = E_FAIL;
hTheme = OpenThemeData(hwndTheme, VSCLASS_MENU);
if (hTheme)
{
GetThemePartSize(hTheme, NULL, MENU_POPUPCHECK, 0, NULL, TS_TRUE, &sizePopupCheck);
GetThemeInt(hTheme, MENU_POPUPITEM, 0, TMT_BORDERSIZE, &iPopupBorderSize);
GetThemeMargins(hTheme, NULL, MENU_POPUPCHECK, 0, TMT_CONTENTMARGINS, NULL, &marPopupCheck);
hr = S_OK;
}
return hr;
}
An HTHEME is requested from the parent window which maps to the menu class. If it succeeds we then ask for the metrics we will need to properly measure and layout our menu items.
Modifying Owner-Draw code – Measurement
There are several “Get” functions that return information on a part/state:
- GetThemePartSize() will give you the dimensions of a part/state pair.
- GetThemeMargins() will give you the spacing around a part/state pair.
- GetThemeInt(…, TMT_BORDERSIZE, …) will give you the size of the border around a part/state pair
- GetThemeTextExtent() will give you the dimensions of the text you specify using the correct font for the part/state pair. The parameters are similar to the DrawText API, including a parameter that accepts DT_* flags.
This information will enable you to make the appropriate measurement and layout calculations. In the test app CMenuMetrics caches these metrics and provides some helper functions, like ToMeasureSize() which applies the specified margins to the tight bounding box you calculated for the menu item.
Modifying Owner-Draw code – Drawing
The first thing you need to do is convert the DRAWITEMSTRUCT’s itemState field into the correct state id (POPUPITEMSTATES for popup menus.) For example, ODS_HOTLIGHT gets translated to MPI_HOT and ODS_INACTIVE can get translated to MPI_DISABLED. See CMenuMetrics::ToItemStateId() for details.
The next thing you do is layout the items according to the metrics you calculated during WM_MEASUREITEM and draw the menu in layers using DrawThemeBackground(), starting from the bottom layer:
- MENU_POPUPBACKGROUND (if the background contains transparency)
- MENU_POPUPGUTTER (if you want a gutter)
- MENU_POPUPSEPARATOR (if the item is a separator)
- MENU_POPUPITEM
- MENU_POPUPCHECKBACKPGROUND (if you are rendering a checkmark)
- MENU_POPUPCHECK (if you are rendering a checkmark)
- DrawThemeText(…, MENU_POPUPITEM, …)
CVistaOwnerDraw::_DrawMenuItem() demonstrates this process.
Special considerations
Allowing the test app to switch between owner-draw and non-owner draw menus presented an interesting issue: USER does not issue new WM_MEASUREITEM messages when the MFT_OWNERDRAW bit is toggled so it continues to use the old metrics. This may be old news (it appears to have always worked this way) but it was a surprise to me.
Fortunately there is a simple workaround: make sure fMask has MIIM_BITMAP set when you call SetMenuItemInfo() and this will cause new WM_MEASUREITEM messages to be sent. The ResetMenuMetrics() helper function will clear out all the menu items of the specified hmenu. A more efficient method would be to set this bit when flipping the MFT_OWNERDRAW bit but I wanted to call this out clearly in the sample code. The MakeOwnerDraw() helper function is used by the test app to change between owner-draw and non-owner-draw.
Important details not covered in article
The sample code presents a simplified version of menu rendering in order to explain the basic concepts. It does not use every part and state (no submenu rendering), it does not cover the menu bar, and it will not necessarily line up to the pixel with the system’s menu rendering. If you are in the business of custom rendered menus then I don’t believe any of these simplifications will be an issue for you.
At this point someone may say “Why didn’t you make an API that would render portions of the menu to make all this easier?” The answer to that is a familiar one: time and resources. Vista was a big undertaking and we had plenty to do in order to get it wrapped up and out the door.
Conclusion
The Visual Style APIs provide all the mechanisms needed to measure, layout and render many of our visual elements, and Vista added menu visuals with VSCLASS_MENU. In spite of this owner-draw menus are still a significant amount of work to develop and maintain. Now that Windows draws nicer looking menus, and there’s a way to get good looking icons with standard menus, I hope that the amount of owner-draw menu code reduces dramatically, leaving all of you with more time for innovation of your core products.