高度还原VS.NET 2003平面菜单






2.09/5 (4投票s)
2007年8月7日
6分钟阅读

32698

65
通过改进现有工作来模仿VS.NET 2003菜单样式
引言
CodeProject上有很多关于菜单效果的文章。其中有几篇是关于VS.NET Studio或某个Office版本的平面菜单的。本文基于该主题的两篇现有文章
- James T. Johnson的MenuItem Extender,https://codeproject.org.cn/cs/menu/MenuExtender.asp
- Georgi Atanasov的FlatMenuForm,https://codeproject.org.cn/cs/miscctrl/flatmenuform.asp
背景
演示在WinXP SP2简体中文专业版上测试。VS.NET 2003
MenuItem Extender
在处理.NET菜单时,尤其是在.NET 2.0之前,MenuItem Extender做得很好。它提供了几种菜单主题以及一个集成到VS.NET Studio中的扩展器提供程序,这使得您只需编辑属性即可扩展菜单的功能。现在,每个菜单都可以将其任意对象关联为Tag
。
但是BetterMenu
的默认效果只绘制菜单项本身,它不处理菜单窗口的非客户区,而且看起来它在当前架构中无法做到这一点。因此,菜单的边框保留了系统默认的3D效果,这看起来有点奇怪。
MenuItem Extender无法处理系统菜单,因此系统菜单保留默认效果。
如果主菜单项本身太长,MenuItem Extender会不恰当地测量菜单项的宽度
MenuItem Extender反过来也基于他人的工作。欲了解更多信息,请访问上述URL。
FlatMenu
表单通过绘制菜单窗口的非客户区填补了MenuItem
Extender留下的空白。但与VS.NET 2003的菜单仍有细微差别——当两者混合使用时,效果极佳。
- 矩形与多边形边框
VS.NET 2003的边框看起来与主菜单项的边框融合在一起:而FlatMenu
的实现只是调用Graphics.DrawRectangle
来绘制菜单窗口的边框。 - 子菜单窗口在其父级上方重叠。另请参阅上图。
- 边框宽度
VS.NET 2003的菜单边框在X和Y方向上均为2像素。 - 主菜单项
Border
的阴影。
修改和改进
调整边框宽度和颜色是一件简单的事情,对我来说,我只需用snagit捕获屏幕,然后将图像复制到photoshop中找出差异。
为了使整体菜单看起来像一个集成单元,主菜单的下边框不应被绘制。但是很难获得它的确切宽度。Graphics.MeasureString
只返回菜单文本的宽度,我还没有找到获取菜单文本左/右边距的方法。SystemInformation
中没有此类信息。我通过反复试验才获得正确的值(至少在我的电脑上)是(int
)Graphics.MeasureString("文件 " ... ) + 11;
要使MeasureItem
生效,我们需要将OwnerDraw
设置为true
,请注意,“MenuItem
Extender”将在运行时将OwnerDraw
设置为true
,即使您在属性窗口中将其设置为false
。
而且,每次菜单弹出时,我们都需要获取主菜单项的宽度
将这些事件处理程序全部安装在FlatMenu
的代码中会更方便
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;
}
是的,硬编码的十进制是邪恶的,如果您找到更优雅和可移植的方法,请告诉我。
MainMenuItem_Width
是我添加到FlatMenu
Form的一个static
属性,IsMainMenuItemOpened
是一个static
属性,用于指示一个主菜单窗口已打开。
请记住,如果应用程序更改主菜单项,则需要重新初始化这些事件处理程序。
而且,有一个已知的技巧是MeasureItem
只会被调用一次,但是您可以通过添加然后删除一个虚拟菜单项来强制系统再次调用它。
当菜单弹出子菜单时,会出现另一个问题,因为子菜单窗口应该绘制整个边框。FlatMenu
的实现只是通过子类、钩子和PInvoke处理菜单窗口,它不了解菜单窗口的语义:主菜单、上下文菜单、系统菜单或子菜单。因此,宿主应用程序应该通知FlatMenu
是否跳过,如果是,则跳过顶部边框的范围。
菜单窗口的默认行为是:弹出时CreateWindow
,关闭时DestroyWindow
,因此可能出现以下序列
- 创建主菜单项的窗口
- 绘制主菜单项窗口的边框
- 创建主菜单项的子菜单窗口
- 绘制子项窗口的边框
- 销毁主菜单项的子菜单窗口
- 绘制主菜单项窗口的边框
仅设置Base.MainMenuItem_Width
和base.IsMainMenuItemOpened
是不够的,因为当主菜单项的窗口仍然存在时,需要绘制子菜单窗口的边框。以下是解决方法
/// <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);
}
没有菜单Close
事件,所以我们可以在菜单的Popup事件处理程序中将IsMainMenuItemOpened
设置为true
,但必须在this
钩子中将其设置为false
。这将确保绘制上下文菜单和系统菜单的整个边框。
确定如何绘制边框(在菜单窗口的窗口过程D中)
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);
}
以上代码只是对原始void DrawBorder(Graphics g)
的修复
它非常类似于将重叠的子菜单窗口稍微向右和向下移动。请参阅以下SubclassWndProc
函数
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);
}
注释
最初的实现要求您的表单继承自Base表单,这太受限制了,因为在您必须继承自另一个表单的情况下,而.NET不支持多重继承。所以我将Base
类更改为FlatMenuFactory
,一个static
类,现在它的使用非常简单
- 在主函数的开头,添加以下行
FlatMenu.FlatMenuFactory.MenuStyle = FlatMenu.MenuStyle.Flat;
- 在表单构造函数的末尾,添加以下行
FlatMenu.FlatMenuFactory.Register_Main_Flat_Menu(m_mainMenu);
其中m_mainMenu
是主菜单的变量名。 - 就是这样。
- 而且,您甚至可以通过以下方式在运行时更改菜单样式
FlatMenu.FlatMenuFactory.MenuStyle = FlatMenu.MenuStyle.Flat;
钩子作用于线程,也就是说,在同一线程中创建的UI将自动获得平面菜单效果,包括系统菜单。但您仍然需要注册应用程序中的每个主菜单。当您的应用程序是多线程时,您需要多次钩取它。
而且,上面提到的线程是操作系统线程,而不是.NET的Thread
类,它不等同于os-thread
。您可以通过以下方式获取当前正在运行的os-thread
int os_thread_id = AppDomain.GetCurrentThreadId();
对于.NET 2003和2005,系统菜单仍保留默认的3D效果,而不是平面菜单。
仍不完美之处
- VS.NET 2003中,分隔线一直绘制到外边框,此解决方案比其短1像素。
- 此解决方案中,主菜单项本身的小矩形没有阴影
如何模仿VS.NET 2005
2003和2005之间存在一些差异
- 渐变颜色,包括菜单窗口的左侧颜色条和主菜单的条。
- 在2003中,菜单项文本前的小图像在菜单项激活时看起来会弹出,并出现阴影。在2005中,菜单项激活或非激活时没有区别。
许可证
本文没有明确的许可证附加到它,但可能包含在文章文本或下载文件本身中的使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。