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