Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

FlatMenusForm - A Class, which Makes the Default 3D Menus Look Flat

0.00/5 (No votes)
31 May 2003 1  
How to change the default 3D look in Windows 9x, NT and WinXP classic appearance menus

This is menu's default look:

The menu with its default look

And this is the menu with the new look:

The menu with its flat 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:

  1. BorderColor - use it to change the border color around the menus
  2. 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));
//WH_CALLWNDPROC is defined as 4 in winuser.h

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:  //HC_ACTION -> this means that the hook 
             //procedure should process the message contained in CWPSTRUCT
      switch(cwp.message)
      {
        case 0x0001://WM_CREATE - catch this before the window is created
          string s = string.Empty;
          char[] className = new char[10];
          //Get the window class name
          int length = Win32.GetClassName(cwp.hwnd,className,9);
          //Convert it to string
          for(int i=0;i<length;i++)
            s += className[i];
          //Now check if the window is a menu
          if(s == "#32768")//System class for menu
            //if true - subclass the window
            defaultWndProc = SetWindowLong(cwp.hwnd, (-4), subWndProc);
          break;
     }
     break;
  }
  return Win32.CallNextHookEx(hookHandle,code,wparam, ref cwp);
  //Let other applications use hook codes
}

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)
{
//tracer.Items.Add(msg.ToString());
 switch(msg)
 {
  case 0x0085:     //WM_NCPAINT
   IntPtr menuDC  = Win32.GetWindowDC(hwnd);                    
   Graphics g = Graphics.FromHdc(menuDC);
   DrawBorder(g);
   Win32.ReleaseDC(hwnd,menuDC);
   g.Dispose();
   return 0;                    
  case 0x0317:     //WM_PRINT
   int result = Win32.CallWindowProc(defaultWndProc,hwnd,msg,wparam,lparam);
   menuDC  = wparam;
   g = Graphics.FromHdc(menuDC);
   //Draw the border around the menu
   DrawBorder(g);
   Win32.ReleaseDC(hwnd,menuDC);
   g.Dispose();
   return result;
  }            
return Win32.CallWindowProc(defaultWndProc,hwnd,msg,wparam,lparam);//In all other cases 
                                                                   //use default wndproc
}

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:

The menu with its flat look

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.

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