65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.09/5 (4投票s)

2007年8月7日

6分钟阅读

viewsIcon

32698

downloadIcon

65

通过改进现有工作来模仿VS.NET 2003菜单样式

Screenshot - vs2003_menu.png

Screenshot - vs2005_menu.png

引言

CodeProject上有很多关于菜单效果的文章。其中有几篇是关于VS.NET Studio或某个Office版本的平面菜单的。本文基于该主题的两篇现有文章

背景

演示在WinXP SP2简体中文专业版上测试。VS.NET 2003

MenuItem Extender

在处理.NET菜单时,尤其是在.NET 2.0之前,MenuItem Extender做得很好。它提供了几种菜单主题以及一个集成到VS.NET Studio中的扩展器提供程序,这使得您只需编辑属性即可扩展菜单的功能。现在,每个菜单都可以将其任意对象关联为Tag

但是BetterMenu的默认效果只绘制菜单项本身,它不处理菜单窗口的非客户区,而且看起来它在当前架构中无法做到这一点。因此,菜单的边框保留了系统默认的3D效果,这看起来有点奇怪。

MenuItem Extender无法处理系统菜单,因此系统菜单保留默认效果。

如果主菜单项本身太长,MenuItem Extender会不恰当地测量菜单项的宽度

Screenshot - better_menu_bug.png

MenuItem Extender反过来也基于他人的工作。欲了解更多信息,请访问上述URL。

FlatMenu表单通过绘制菜单窗口的非客户区填补了MenuItem Extender留下的空白。但与VS.NET 2003的菜单仍有细微差别——当两者混合使用时,效果极佳。

  • 矩形与多边形边框
    VS.NET 2003的边框看起来与主菜单项的边框融合在一起:而FlatMenu的实现只是调用Graphics.DrawRectangle来绘制菜单窗口的边框。

    Screenshot - diff.png

  • 子菜单窗口在其父级上方重叠。另请参阅上图。
  • 边框宽度
    VS.NET 2003的菜单边框在X和Y方向上均为2像素。
  • 主菜单项Border的阴影。

修改和改进

调整边框宽度和颜色是一件简单的事情,对我来说,我只需用snagit捕获屏幕,然后将图像复制到photoshop中找出差异。

为了使整体菜单看起来像一个集成单元,主菜单的下边框不应被绘制。但是很难获得它的确切宽度。Graphics.MeasureString只返回菜单文本的宽度,我还没有找到获取菜单文本左/右边距的方法。SystemInformation中没有此类信息。我通过反复试验才获得正确的值(至少在我的电脑上)是(intGraphics.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,因此可能出现以下序列

  1. 创建主菜单项的窗口
  2. 绘制主菜单项窗口的边框
  3. 创建主菜单项的子菜单窗口
  4. 绘制子项窗口的边框
  5. 销毁主菜单项的子菜单窗口
  6. 绘制主菜单项窗口的边框

仅设置Base.MainMenuItem_Widthbase.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中,菜单项激活或非激活时没有区别。

许可证

本文没有明确的许可证附加到它,但可能包含在文章文本或下载文件本身中的使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.