Click here to Skip to main content
15,890,438 members
Articles / Web Development / HTML

A Higher Fidelity Mimic of Flat Menu to VS.NET 2003

Rate me:
Please Sign up or sign in to vote.
2.09/5 (4 votes)
7 Aug 20076 min read 31.9K   19   2
Mimic the VS.NET 2003 menu style by improving the existing work

Screenshot - vs2003_menu.png

Screenshot - vs2005_menu.png

Introduction

There are many article that address the menu effects on CodeProject. Several of them are about flatten menu in VS.NET Studio or some version of office. This article is based on the two existing articles about this topic:

Background

Demo tested on WinXP SP2, simplified Chinese Pro Edition. VS.NET 2003

MenuItem Extender

MenuItem Extender does a sound work on menu processing when you are working on .NET, especially before .NET 2.0. It provides several menu themes as well as an extender provider integrated into VS.NET Studio, which makes you extend the menu's ability by just editing the property. And the extra facility is now each menu can associate an arbitrary object as its Tag.

But the default effect of BetterMenu only draws the menu item itself, it does nothing about the menu window's non-client area, and it looks like there's no way to do it in its current architecture. So the menu's border retains the system default 3D effect, which looks a little strange.

MenuItem Extender cannot process system menu, so the system menu retains the default effect.

MenuItem Extender measures the menu item's width inappropriately if the main menu item itself is too long:

Screenshot - better_menu_bug.png

MenuItem Extender is in turn based on other's work. For further information, please visit the URL above.

FlatMenu Form fills the blank which MenuItem Extender left, by drawing the non-client area of the menu's window. But there's still little difference from VS.NET 2003's menu - when mixed, the two work excellent.

  • Rectangle vs polygon border
    VS.NET 2003's border looks merged into the main menu item's border: While FlatMenu's implementation just calls Graphics.DrawRectangle to draw the menu window's border.

    Screenshot - diff.png

  • Sub menu's window overlapped on the top of its parent. Please also reference the above image.
  • Border width
    VS.NET 2003's menu border is 2 pixel in both X and Y directions.
  • Shadow of main menu item's Border.

Modification and Improvement

Justifying the border width and color is a simple thing, for me, I just capture the screen by snagit then copy the image into photoshop to figure out the difference.

To make the overall menu look like an integrated unit, the bottom border of the main menu should not be drawn. But it's a little difficult to get the exact width of it. Graphics.MeasureString only returns the Menu Text's width, I haven't found a way to get the left/right margin of the menu text. There's no such information in SystemInformation. I struggled with it by try/error to get the correct value (at least on my pc) is (int) Graphics.MeasureString( "File " ... ) + 11; To make MeasureItem to take on, we need to set OwnerDraw to true, be aware that the "MenuItem Extender" will set OwnerDraw to true in the runtime even you set left it false in the property window.

And, we need to get the main menu item's width each time the menu pops up:

It's more convenient to install these Event Handlers all in the FlatMenu's code:

C#
public static void Register_Main_Flat_Menu(MainMenu flat_main_menu)
{
    foreach(MenuItem m in flat_main_menu.MenuItems)
    {
        m.Popup += new EventHandler(main_menu_item_Popup);
        m.MeasureItem += new MeasureItemEventHandler(main_menu_item_MeasureItem);
    }
}

private static void main_menu_item_Popup(object sender, EventArgs e)
{
    FlatMenu.FlatMenuFactory.IsMainMenuItemOpened = true;
    FlatMenu.FlatMenuFactory.MainMenuItem_Width = (int)main_menu_item_width[ sender ] + 11;
    Debug.WriteLine(string.Format("Main Menu Item Width:{0}",
        FlatMenu.FlatMenuFactory.MainMenuItem_Width) );
}

private static void main_menu_item_MeasureItem(object sender,
    System.Windows.Forms.MeasureItemEventArgs e)
{
    e.ItemWidth = (int)e.Graphics.MeasureString( ( sender as MenuItem).Text,
        SystemInformation.MenuFont).Width;

    e.ItemHeight = SystemInformation.MenuHeight;

    main_menu_item_width[sender] = e.ItemWidth;
}

Yes, the hardcoded decimal is evil, if you find a more elegant and portable way, please let me know.

The MainMenuItem_Width is a static Property I add to FlatMenu Form, IsMainMenuItemOpened is a static Property to indicate that one main menu window is opened.

Keep in mind that you need to re-initialize these event handlers if the application changes the main menu item.

And, there's a known trick that MeasureItem will be called only once, but you can force the system to call it again by adding, then removing a dummy menu item.

Another issue arises when the menu pops up a sub menu, because the sub menu window should draw the whole border. FlatMenu's implementation just processes the menu's window by subclass and hook and PInvoke, it has no knowledge about the menu window's semantics: Main menu or context menu or system menu or submenu. So the host application should notify FlatMenu whether to skip and, if yes, the extend of the top-border.

The default behavior of menu window is: CreateWindow when it pops up, and DestroyWindow when it closed, so it's possible for the following sequence:

  1. Create the main menu item's window
  2. Draw the border of main menu item's window
  3. Create the main menu item's sub-menu's window
  4. Draw the border of sub-item's window
  5. Destroy the main menu item's sub-menu window
  6. Draw the border of main menu item's window

It's not sufficient just setting Base.MainMenuItem_Width, and base.IsMainMenuItemOpened, because a border of sub-menu's window needs to draw when the main menu item's window is still alive. Here comes the work-around:

C#
/// <summary>
/// Key: IntPtr.ToString() window handle
/// Value: bool: Whether the only one menu (main menu, context menu, system menu)
/// </summary>
private static Hashtable menu_win = new Hashtable();

private static 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
        string s = string.Empty;
        char[] className = new char[10];
        int length = 0;
        switch(cwp.message)
        {
            case Win32.WM_CREATE:    // - catch this before the window is created
                s = string.Empty;
                Array.Clear(className, 0, className.Length);
                //Get the window class name
                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[ cwp.hwnd.ToString() ] = 
                        SetWindowLong(cwp.hwnd, (-4), subWndProc);

                    menu_win[ cwp.hwnd.ToString() ] = (menu_win.Count == 0);
                }
                break;
            case Win32.WM_DESTROY:
                s = string.Empty;
                Array.Clear(className, 0, className.Length);
                //Get the window class name
                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
                    menu_win.Remove( cwp.hwnd.ToString() );
                }
                if( menu_win.Count == 0)
                {
                    IsMainMenuItemOpened = false;
                }
                break;
        }
            break;
    }
    return Win32.CallNextHookEx( (IntPtr)hookHandle[ AppDomain.GetCurrentThreadId().ToString() ],
        code,wparam, ref cwp);
}

There's no Menu Close event so we can set IsMainMenuItemOpened to true in Menu's Popup Event Handler, but must set it to false in the this hook. This will make sure the whole border of context menu and system menu is drawn.

Determine how to draw the border(In menu window's window procedure):

C#
DrawMenuWinBorder(g, IsMainMenuItemOpened && (bool)Base.menu_win[ hwnd.ToString() ] );

/// <summary>
/// Avoid overlaid the main menu's bottom line
/// </summary>
/// < param name="g">
/// < param name="is_main_menu">
protected void DrawMenuWinBorder(Graphics g, bool is_main_menu)
{
    Rectangle r = new Rectangle(0,0,(int)g.VisibleClipBounds.Width - 1, 
        (int)g.VisibleClipBounds.Height - 1);
    Rectangle r1 = new Rectangle(1,1, r.Width -2, r.Height - 2);
    //            Rectangle r2 = new Rectangle(2,2,(int)g.VisibleClipBounds.Width-5, 
    //                (int)g.VisibleClipBounds.Height-5);

    if(border_pen == null)
    {
        border_pen = new Pen( Color.FromArgb(102, 102, 102) ); //Surprise? not Black for VS2003;
    }

    if(is_main_menu)
    {
        System.Diagnostics.Debug.Assert(MainMenuItem_Width > 0, 
                                 "IsMainMenuItemOpened = true, width = 0");
        g.DrawLine(border_pen, MainMenuItem_Width, r.Top, r.Right, r.Top);
        g.DrawLine(border_pen, r.Right, r.Top, r.Right, r.Bottom);
        g.DrawLine(border_pen, r.Right, r.Bottom, r.Left, r.Bottom);
        g.DrawLine(border_pen, r.Left, r.Bottom, r.Left, r.Top);
        Debug.WriteLine(string.Format("Draw Main Border item-width:{0}",
            FlatMenuForm.Base.MainMenuItem_Width) );
    }
    else
    {
        Debug.WriteLine(string.Format("Draw non-main menu item") );
        g.DrawRectangle(border_pen, r);
    }

    if(margin_pen == null)
    {
        margin_pen = new Pen(Color.FromArgb(249, 248, 247) ); //Cracked from VS2003 
                                                              //by Photoshop + Snagit
    }

    g.DrawRectangle(margin_pen, r1);
    if(left_strip_pen == null)
    {
        left_strip_pen = new Pen( SystemColors.Menu );
    }
    g.DrawLine(left_strip_pen, r1.Left, r1.Top + 1, r1.Left, r1.Bottom -1);
    //            g.DrawRectangle(new Pen(SystemColors.Menu),r2);
}

The above code is just a fix of the original void DrawBorder(Graphics g)

It's very similar to move the overlapped sub-menu window a little right and down. See the following SubclassWndProc function:

C#
int SubclassWndProc(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam)
{
    switch(msg)
    {
        case Win32.WM_WINDOWPOSCHANGING:
            Win32.WINDOWPOS pos = (Win32.WINDOWPOS)
                                   System.Runtime.InteropServices.Marshal.PtrToStructure
                                   (lparam,typeof(Win32.WINDOWPOS));
            if( (pos.flags & Win32.SWP_NOSIZE) == 0 ) 
            {
                pos.cx -= 2;
                pos.cy -= 2; //bugfix: -= 3 will make the last menu item's bottom border invisible.
            }
            if( (bool)menu_win[ hwnd.ToString()] == false &&
                (pos.flags & Win32.SWP_NOMOVE) == 0 ) 
            {
                pos.x += 3; //Move the sub-menu's window right
                pos.y += 2; //Move the sub-menu's window down
            }
            System.Runtime.InteropServices.Marshal.StructureToPtr( pos, lparam, true );
            return 0;   // !!! try to replace with "break;" It's very funny!

        case 0x0085://WM_NCPAINT
            IntPtr menuDC  = Win32.GetWindowDC(hwnd);                    
            Graphics g = Graphics.FromHdc(menuDC);
            try
            {
                DrawMenuWinBorder(g, IsMainMenuItemOpened &&
                    (bool)Base.menu_win[ hwnd.ToString() ] );
            }
            finally
            {
                g.Dispose();
                Win32.ReleaseDC(hwnd,menuDC);
            }
            return 0;
            
        case Win32.WM_NCCALCSIZE:
            Win32.NCCALCSIZE_PARAMS calc = (Win32.NCCALCSIZE_PARAMS)
                System.Runtime.InteropServices.Marshal.PtrToStructure(lparam,typeof
                (Win32.NCCALCSIZE_PARAMS));
            //http://www.vckbase.com/document/viewdoc/?id=1302
            calc.rgc0.left += 2;
            calc.rgc0.top += 2;
            calc.rgc0.right -= 2;
            calc.rgc0.bottom -= 2;
            System.Runtime.InteropServices.Marshal.StructureToPtr( calc, lparam, true );
            return Win32.WVR_REDRAW;
    }            
    return Win32.CallWindowProc(defaultWndProc,hwnd,msg,wparam,lparam);
}

Notes

The original implementation requires your form inherit the Base form, it's too limited because in the case that you must inherit from another form, while multi-inherit NET is not supported in dotnet. So I change the Base class to FlatMenuFactory, a static class, and use of it is very simple now:

  • At the beginning of the main function, add the following line:
    C#
    FlatMenu.FlatMenuFactory.MenuStyle = FlatMenu.MenuStyle.Flat;        
  • at the end of your form's constructor, add the following line:
    C#
    FlatMenu.FlatMenuFactory.Register_Main_Flat_Menu(m_mainMenu);        
    While m_mainMenu is the variable name of your main menu.
  • That's it.
  • And, you can even change the Menu Style at runtime by:
    C#
    FlatMenu.FlatMenuFactory.MenuStyle = FlatMenu.MenuStyle.Flat;        

The hook works on thread, that's to say, UI created in the same thread will get the flat menu effect automatically, including the system menu. But you still need to register every main menu in your application. When your application is multi-thread, you need to hook it more than once.

And, the above mentioned thread is os-thread, not .NET's Thread class, which is not equivalent to os-thread. You can get the current running os-thread by:

C#
int os_thread_id = AppDomain.GetCurrentThreadId();

For .NET 2003 and 2005, the system menu still retains the default 3D effect, not the flat menu.

Things Still Not Perfect

  • The separator line is drawn up to the outer border in VS.NET 2003, this solution is 1-pixel shorter than that.
  • There's no shadow in main menu item itself small rectangle in this solution

How to Mimic the VS.NET 2005

There are some differences between 2003 and 2005:

  • Gradient color, both in menu window's left color bar and main menu's bar.
  • In 2003, the small image before the menu item text looks popped up when the menu item is active, and a shadow is presented. No difference in 2005 when menu item is active or inactive.

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.


Written By
Software Developer
China China
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionThe source code is not available Pin
snipersun23-May-22 1:48
snipersun23-May-22 1:48 
GeneralNice Code. [modified] Pin
benjamin2321-Mar-08 9:36
benjamin2321-Mar-08 9:36 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.