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 APIs...
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 APIs.
Using the Code
First, how to give your menus a 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 enumeration 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 work at all, because I always got an exception "Out of Memory
" (it is because, as I traced why that is so, an 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 arose - why not try 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 second parameter of that function is of great importance - it is the address of my HookProc
which will monitor for special messages (by the way, when you have to declare API in managed code and you have a 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 seem 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 me know!!!
Points of Interest
I have also added an implementation of owner-draw menus with flat look and 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
- 1st June, 2003: First revision
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.