![]() |
Desktop Development »
Menus »
Custom menus
Advanced
A Revolutionary New Approach to Custom Drawn MenusBy .dan.g.A new and unique approach to the perennial problem of how to change the default appearance of Windows menus |
VC6Win2K, WinXP, MFC, Dev
|
|
Advanced Search |
|
|
|
||||||||||||||||


Changing the default appearance of elements of the Windows GUI has been a never ending challenge for Windows programmers since the first release of the Windows API. And anyone who has seriously tried to change the appearance of menus (beyond simple owner-draw techniques) will know that it represents one of holy grails of Windows GUI programming.
Note: To reduce the clutter in the article I am going to use the word 'skin' in place of 'changing the default appearance'.
What I want to present here is an entirely new and original approach to skinning menus which will allow you to skin every menu that your application produces, including (but not restricted to):
Windows menus have always been a black box to developers, with all of the implementation hidden away inside user32.dll. All that Windows has allowed a developer to do is to define the menu as owner-draw and then handle the WM_MEASUREITEM and WM_DRAWITEM messages for drawing one or more menu items.
This is fine if your interest in owner-draw menus is to replace the standard text interface of, say, a 'select color' menu with menu items displaying the colors themselves, but it falls way short of an architecture to replace all menus with a different style to the gray (or COLOR_MENU) default.
In particular:
WM_MEASUREITEM and WM_DRAWITEM messages
Much more fundamentally however, there is no way for you to handle menus that someone else (like Windows itself) has already defined as owner-draw, because generally you will have no idea what memory structures have been used for storing the information needed for the drawing stage.
And it is this last point which ultimately kills off owner-draw as a possible solution to the problem.
How the 'solution' occurred to me, I have no idea. I may have been taking a bath, or possibly sitting under a tree when an apple fell on my head, I don't know. But it did. 'What if', I thought, 'I could redirect all of the painting of a menu away from the screen and onto a memory DC so that i could post-process it. 'Then I could do anything I liked to it before finally rendering the menu on the screen.' And the rest as they say, was hard slog.
The implementation of this solution can be broken down into 2 distinct tasks which needed solving:
The solution to the former was definitely the easier of the two. All I needed to do was to install a Windows Hook to detect the creation of all menu windows.
Note: For those of you who don't know, menu windows have a special class name (#32768) which makes their detection no more difficult that any other window.
Having caught the menu window just before its creation, however, still leaves the question, 'What to do with it?' to which the answer is (of course) subclassing. And this is where I must give due thanks to Paul DiLascia and his now legendary CSubclassWnd implementation. CSubclassWnd is an MFC class that Paul wrote to allow the interception and overriding of all Windows messages for a given window, even when you did not create the window itself. So that's essentially it: Catch all menu windows just before they are shown and subclass them so that we can override the drawing.
Its often the case in programming (and certainly the case in engineering) that if the design is sound then the implementation will generally fall into place. Unfortunately, this was not exactly one of those occasions. I've found with Windows GUI programming that there are just too many quirks and variations between the various flavours of Windows (95 -> XP) not to have to resort to using undocumented tweaks and fudge factors to achieve a reasonable solution.
Moreover, in this particular situation, there was no documentation to either support or suggest that I stood any reasonable chance of being successful. My first task was to determine what Windows messages prompted menus to (re)draw themselves so that I could replace these with my custom implementations. Fortunately, what I found was that menu drawing is quite simple (if undocumented). This is a summary of the Windows messages which needed to be handled:
WM_PRINT - requests the menu to draw its non-client and/or client areas in the DC supplied in wParam (Windows 9x, 2K, XP)
WM_PRINTCLIENT - requests the menu to draw its client area in the DC supplied in wParam (Windows 9x, 2K, XP)
WM_PAINT - requests the menu to draw the foreground of its invalid area (Windows 9x)
WM_ERASEBKGND - requests the menu to draw the background of its invalid area (Windows 9x)
0x01e5 (undocumented) - requests the menu to redraw the item supplied in the message wParam using an internal method (Windows 9x, 2K, XP) This last message (0x01e5) is the most interesting and the most crucial discovery I made on this project. Windows sends it to the menu every time you twitch the mouse inside or outside the menu, so that the menu can redraw the specified item. At first it might seem like a gross inefficiency since no other window controls behave that way, but if you've got menu animation turned on you'll see why its necessary.
Normally when GUI events happen in Windows its usual to wait until they are complete and then redraw the control as required. Not so with menu fading; in this case, the menu fading is carried out after the event has happened using a system timer (I believe). So if you only redraw the menu once after the event triggers then you get drawing artifacts all over the place. Luckily, it was simply a matter of calling SetRedraw() before and after the default implementation of this message to ensure that the menu did not do its 'under the cover' drawing, followed by invalidating the rectangle of the menu item in question.
Once I had control of the redrawing, I still needed to work out how to replace the system colors with the users choices.
As I had already been working extensively on a application skinning system and had been using TransparentBlt() to good effect there, I hit on the idea of using it to do multi-pass post-processing of the menu image, replacing one system color in each pass.
I had anticipated a fair performance hit with this idea but in practice it seems to be quite reasonable. Even under Windows 98 where I have substituted my own implementation of TransparentBlt() (the default version has a well documented resource leak) it still performs quite acceptably, although I have not done nearly enough testing on lower end machines.
I have also gone to some lengths to ensure that I carry out the minimum number of TransparentBlt() in thsoe case where some system colors resolve to the same COLORREF.
Some of you sharper readers may have noticed an implicit reference to a menu handle (HMENU) in the previous paragraph ('...rectangle of the menu item...') which I had not mentioned before. Whilst its true that the menu skinning can be achieved without having the menu handle, the implementation can be made more efficient by only invalidating the menu items which need redrawing rather than the whole window.
Determining the menu handle, however, is a real pain because Windows provides no explicit mapping between a menu window and its associated menu handle. This is where the fudge I referred to earlier comes in. To retrieve the menu handle, I wait until I detect a WM_INITMENUPOPUP and then either attach it to the menu window if it has already been created, or hold on to it until the menu window is subclassed and add it then. There's no guarantee that the match-up will correct but it seems to work okay. A real fudge but without an alternative as far as I can see.
Note: in my demo project, these files are in a separate 'skinwindows' folder because they form a subset of a much larger skinning system, but there is no need for you to do the same.
CHookMgr (hookmgr.h) - template class for simplifying hooking
CSkinBase (skinbase.h/.cpp) - some hard core helper methods
CSkinGlobals (skinglobals.h/.cpp, skinglobalsdata.h) - helper class for overriding default Windows colors
CSkinMenu (skinmenu.h/.cpp) - menu window overriding
CSkinMenuMgr (skinmenumgr.h/.cpp) - menu window hooking and management
CSubclassWnd (subclass.h/.cpp) - subclassing helper class (heavily modified from Paul DiLascia's original)
CWinClasses (winclasses.h/.cpp) - helper class for retrieving and testing window classes
#defines for all window classes (and some others)
NO_SKIN_INI to the preprocessor definitions in your project settings. This is to avoid compilation problems due to missing files, because this project forms part of a larger system which supports loading color information from a file, which is not included here.
CWinApp derived application InitInstance() method as follows: #include "skinmenumgr.h" // assumes files are in same folder // as rest of the project BOOL CMyApp::InitInstance() { : : CSkinMenuMgr::Initialize(); : : }
Have a look at the implementation of CSkinMenuMgr::Initialize() for more detail on the options available. In particular you can elect to display a sidebar of a given width and give the menu border a flat or beveled edge.
For total control over how the menus are drawn, including the menu background, derive a class from ISkinMenuRender which is defined in skinmenu.h and pass to CSkinMenu::SetRenderer() which is a static method. Then during the drawing process, the CSkinMenu class will call back into your derived class giving you the opportunity to drawn whichever portions of the menu you choose to. (see CSkinMenuTestDlg for an example implementation)
The code is supplied here for you to use and abuse without restriction, except that you may not modify it and pass it off as your own. The concept and design, however, remains my intellectual property in perpetuity.
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 26 May 2003 Editor: Chris Maunder |
Copyright 2003 by .dan.g. Everything else Copyright © CodeProject, 1999-2009 Web09 | Advertise on the Code Project |