This is menu's default look:
And this is the menu with the new look:
Introduction
I wanted to learn how to change the default 3D look of the menus in Windows XP classic appearance, because when implementing owner-draw menus windows gives you only the client area of the menu window for drawing. And when I have Windows XP appearance it is just fine with its flat menus, but when switching to classic - it is awful, the 3D border isn't fitting the menu items at all. Then I started looping through the .NET Framework SDK searching solution of my problem and finally I gave up - there was not such a class or enum or whatever... Then ( as I am a little bit stubborn and wanted to learn the know-how ) I went deeper in the Platform SDK and Win32 API's...
Basic Idea
In a few words the idea is subclassing the default window class Windows provides for its menus. This subclassing is made by using P/Invokes and calling native API's
Using the code
First - how to give your menus flat look - simply inherit from Base class and that's it! I added two extra properties to FlatMenuForm:
- BorderColor - use it to change the border color around the menus.
- MenuStyle - it is enumaration which consists of two fields - Flat and Default. Use
MenuStyle.Default to use the default menu look and the other for the flat look.
Now let's start from the very beginning - my first efforts in doing custom painting on the menu window. At first I tried something like this:
IntPtr hdc = GetWindowDC(mainMenu1.Handle);
Graphics g = Graphics.FromHdc(hdc);
Rectangle r = new Rectangle(0,0,(int)g.VisibleClipBounds.Width-1,
(int)g.VisibleClipBounds.Height-1);
g.DrawRectangle(new Pen(borderColor),r);
Win32.ReleaseDC(mainMenu1.Handle,hdc);
g.Dispose();
Well, it did not worked at all, because I always got an exception "Out of Memory" ( it is because , as I traced why is that, a
INVALID_WINDOW_HANDLE win32 error is thrown when calling
GetWindowDC ). I tried that code with a
MenuItem.Handle property but got the same exception. And then I started reading for subclassing a window and changing its default
WndProc. And then an idea arise - why not trying to subclass the menu window first... Everything seems OK till now, but how to subclass a window when I don't have a valid window handle? Now in help comes the
SetWindowsHookEx API with
WH_CALLWNDPROC hook type specified - it installs a hook procedure that monitors messages before the system sends them to the destination window procedure.
hookHandle = SetWindowsHookEx(4,hookProc,IntPtr.Zero,Win32.GetWindowThreadProcessId(Handle,0));
The secont parameter of that function is of great importance - it is the address of my
HookProc which will monitors for special messages ( by the way when you have to declare API in managed code and you have function pointer you use
delegate ). Here is the
HookProc delegate declaration :
delegate int HookProc(int code, IntPtr wparam, ref Win32.CWPSTRUCT cwp);
The
CWPSTRUCT structure defines the message parameters passed to a
WH_CALLWNDPROC hook procedure:
[StructLayout(LayoutKind.Sequential)]
public struct CWPSTRUCT
{
public IntPtr lparam;
public IntPtr wparam;
public int message;
public IntPtr hwnd;
}
As I needed the window ( I mean the main form window ) to be hooked when constructed I put this
SetWindowsHookEx call in the form's constructor. And here is the implementation of the Hook procedure:
int Hooked(int code, IntPtr wparam, ref Win32.CWPSTRUCT cwp)
{
switch(code)
{
case 0: switch(cwp.message)
{
case 0x0001: string s = string.Empty;
char[] className = new char[10];
int length = Win32.GetClassName(cwp.hwnd,className,9);
for(int i=0;i<length;i++)
s += className[i];
if(s == "#32768") defaultWndProc = SetWindowLong(cwp.hwnd, (-4), subWndProc);
break;
}
break;
}
return Win32.CallNextHookEx(hookHandle,code,wparam, ref cwp);
}
Another great difficulty was to get the appropriate window class. This is system defined class for use only by the system but its name is given in the Platform SDK documentation - it is "#32768". And when get the right class - subclass it using
SetWindowLong API with
GWL_WNDPROC value which sets a new address for the window procedure ( (-4) stands for
GWL_WNDPROC ). The
subWndProc parameter is delegate of type
MyWndProc :
delegate int MyWndProc(IntPtr hwnd,int msg,IntPtr wparam,IntPtr lparam);
The implementation of the SubclassWndProc:
int SubclassWndProc(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam)
{
switch(msg)
{
case 0x0085: IntPtr menuDC = Win32.GetWindowDC(hwnd);
Graphics g = Graphics.FromHdc(menuDC);
DrawBorder(g);
Win32.ReleaseDC(hwnd,menuDC);
g.Dispose();
return 0;
case 0x0317: int result = Win32.CallWindowProc(defaultWndProc,hwnd,msg,wparam,lparam);
menuDC = wparam;
g = Graphics.FromHdc(menuDC);
DrawBorder(g);
Win32.ReleaseDC(hwnd,menuDC);
g.Dispose();
return result;
}
return Win32.CallWindowProc(defaultWndProc,hwnd,msg,wparam,lparam);}
It may seems strange but Windows sends a WM_PRINT message AFTER WM_NCPAINT. I spent hours and hours trying to understand what is wrong with my code and why it is not working until I put a simple tracer to the SubclassWndProc and found out what messages are sent to the menu window. And when processing the WM_PRINT message it all worked fine - BINGO! Finally I changed the default appearance of the menu window!
I wanted also to process the WM_NCCALCSIZE message in order to reduce the non-client area of the menu but failed... Any suggestion on how this might be done in managed code ( I achieved it in MFC ) would be very much appreciated! Also I couldn't override the default implementation of WM_WINDOWPOSCHANGING and WM_WINDOWPOSCHANGED - I lost the default system animation...
And yet another thing - I haven't tested this code on other platforms (mine is Windows XP) so if you find some bugs in it please, let mi know!!!
Points of Interest
I have also added an implementation of owner-draw menus with flat look ant to some extent they now really look like the Visual Studio .NET ones! I haven't implemented the shadow on the right side of the top menu items. May be it might be achieved by getting the desktop window DC, draw on it and then invalidate that rectangle - if I have enough time I will try it.
Here is what I finally got :

Well, that's it.
Once again - any comments or suggestions or even criticism are welcome !
History
- June 1st, 2003: First revision
.NET & C# addicted. Win8 & WinRT enthusiast and researcher @Telerik.