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

- 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:
- Create the main menu item's window
- Draw the border of main menu item's window
- Create the main menu item's sub-menu's window
- Draw the border of sub-item's window
- Destroy the main menu item's sub-menu window
- 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:
private static Hashtable menu_win = new Hashtable();
private static int Hooked(int code, IntPtr wparam, ref Win32.CWPSTRUCT cwp)
{
switch(code)
{
case 0: string s = string.Empty;
char[] className = new char[10];
int length = 0;
switch(cwp.message)
{
case Win32.WM_CREATE: s = string.Empty;
Array.Clear(className, 0, className.Length);
length = Win32.GetClassName(cwp.hwnd,className,9);
for(int i=0;i < length;i++)
s += className[i];
if(s == "#32768") {
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);
length = Win32.GetClassName(cwp.hwnd,className,9);
for(int i=0;i < length;i++)
s += className[i];
if(s == "#32768") {
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() ] );
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);
if(border_pen == null)
{
border_pen = new Pen( Color.FromArgb(102, 102, 102) ); }
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) ); }
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);
}
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; }
if( (bool)menu_win[ hwnd.ToString()] == false &&
(pos.flags & Win32.SWP_NOMOVE) == 0 )
{
pos.x += 3; pos.y += 2; }
System.Runtime.InteropServices.Marshal.StructureToPtr( pos, lparam, true );
return 0;
case 0x0085: 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));
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.