Click here to Skip to main content
Licence 
First Posted 7 Aug 2007
Views 16,717
Downloads 0
Bookmarked 19 times

A higher fidelity mimic of flat menu to VS.NET 2003

By | 7 Aug 2007 | Article
Mimic the VS.NET 2003 menu style by improving the existing work
Title:       A higher fidelity mimic of flat menu to VS.NET 2003
Author:      rufei zhao
Email:       slimzhao@21cn.com
Member ID:   2308266
Language:    C#
Platform:    .NET 1.1
Technology:  .NET/C#
Level:       Beginner, Intermediate
Description: An article on howto implement VS.NET 2003's flat menu
Section      
SubSection   
Screenshot - vs2003_menu.png
Screenshot - vs2005_menu.png

Introduction

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

James T. Johnson's MenuItem Extender, http://www.codeproject.com/cs/menu/MenuExtender.asp

Georgi Atanasov's FlatMenuForm, http://www.codeproject.com/cs/miscctrl/flatmenuform.asp

Background

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

MenuItem Extender

MenuItem Extender do a sound work on menu processing when you working on .NET, especially before .NET 2.0. It provide several menu theme 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 it's Tag.

But the default effect of BetterMenu only draw the menu item itself, it do nothing about the menu window's non-client area, and it's looks that there's no way to do it in it's current architecture. So the menu's border retain 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 measure the menu item's width inappropriately if the main menu item itself is too long:
Screenshot - better_menu_bug.png

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

FlatMenu Form fill 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 mix the two excellent work.

  • Rectangle vs polygon border
    VS.NET 2003's border looks merge into the main menu item's border: While FlatMenu's implementation just call Graphics.DrawRectangle to draw the menu window's border.
    Screenshot - diff.png
  • Sub menu's window overlapped on the top of it's parent. Please also reference the above image.
  • Border width
    VS.NET 2003's menu border is 2 pixel in both X and Y direction.
  • 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 copied the image into photoshop to figure out the difference.

To make the overall menu looks like an integrated unit, the bottom border of the main menu should not be draw. But it's a little difficult to get the exact width of it. Graphics.MeasureString only return 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 info 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 that install these Event Handler all in the FlatMenu's code:

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 handler 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.

The another issue arise when the menu pops up a sub menu, because the sub menu window should draw the whole border. FlatMenu's implementation just process 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 should to skip and, if yes, the extend of the top-border.

The default behavior of menu window is: CreateWindow when it's pop 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 need to draw when the main menu item's window is still alive. Here comes the work-around:
/// <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 to be drawn.

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

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:

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:
            FlatMenu.FlatMenuFactory.MenuStyle = FlatMenu.MenuStyle.Flat;
            
  • at the end of your form's constructor, add the following line:
              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:
                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

int os_thread_id = AppDomain.GetCurrentThreadId();

For .NET 2003 and 2005, the system menu still retain 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's some difference 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

About the Author

ZhaoRuFei

Software Developer

China China

Member



Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board. (secure sign-in)
 
Search this forum  
 FAQ
    Noise  Layout  Per page   
  Refresh
GeneralNice Code. [modified] Pinmemberbenjamin239:36 21 Mar '08  

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

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

Permalink | Advertise | Privacy | Mobile
Web04 | 2.5.120517.1 | Last Updated 7 Aug 2007
Article Copyright 2007 by ZhaoRuFei
Everything else Copyright © CodeProject, 1999-2012
Terms of Use
Layout: fixed | fluid