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

FlatMenusForm - 一个类,它使默认的 3D 菜单看起来很平坦

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.27/5 (19投票s)

2003年6月1日

4分钟阅读

viewsIcon

120059

downloadIcon

675

如何更改 Windows 9x、NT 和 WinXP 经典外观菜单的默认 3D 外观

这是菜单的默认外观

The menu with its default look

这是菜单的新外观

The menu with its flat look

引言

我想学习如何更改 Windows XP 经典外观下菜单的默认 3D 外观,因为在实现属主绘制菜单时,Windows 只提供菜单窗口的客户区供我绘制。当使用 Windows XP 外观时,它的平坦菜单效果很好,但切换到经典外观时——这太糟糕了,3D 边框根本不适合菜单项。然后,我开始在 .NET Framework SDK 中搜索解决我问题的方法,最终我放弃了——那里没有任何类或 enum 或其他东西……然后(我有点固执,想学习其中的诀窍),我深入研究了 Platform SDK 和 Win32 API……

基本思想

简而言之,这个想法是子类化 Windows 为其菜单提供的默认窗口类。这种子类化是通过使用 P/Invokes 并调用本机 API 来完成的。

Using the Code

首先,如何让您的菜单拥有平坦的外观——只需继承自 Base 类即可!我在 FlatMenuForm 中添加了两个额外的属性

  1. BorderColor - 用它来改变菜单周围的边框颜色
  2. MenuStyle - 这是一个枚举,包含两个字段——FlatDefault。使用 MenuStyle.Default 使用默认菜单外观,而使用另一个字段实现平坦外观。

现在让我们从头开始——我进行菜单窗口自定义绘制的第一次尝试。起初,我尝试了这样的代码

IntPtr hdc = GetWindowDC(mainMenu1.Handle);
Graphics g = Graphics.FromHdc(hdc);
Rectangle r = new Rectangle(0,0,(int)g.VisibleClipBounds.Width-1, 
                (int)g.VisibleClipBounds.Height-1);
g.DrawRectangle(new Pen(borderColor),r);
Win32.ReleaseDC(mainMenu1.Handle,hdc);
g.Dispose();

嗯,这根本不起作用,因为我总是收到一个“Out of Memory”异常(这是因为,当我跟踪原因时,在调用 GetWindowDC 时会抛出一个 INVALID_WINDOW_HANDLE win32 错误)。我尝试使用 MenuItem.Handle 属性运行这段代码,但得到了相同的异常。然后我开始阅读关于子类化窗口和更改其默认 WndProc 的内容。然后一个想法出现了——为什么不先尝试子类化菜单窗口呢……到目前为止一切似乎都很好,但是当我没有有效的窗口句柄时,如何子类化窗口呢?现在,SetWindowsHookEx API 出现了,并指定了 WH_CALLWNDPROC 挂钩类型——它安装一个挂钩过程,该过程在系统将消息发送到目标窗口过程之前监视这些消息。

hookHandle = SetWindowsHookEx(4,hookProc,IntPtr.Zero,Win32.GetWindowThreadProcessId(Handle,0));
//WH_CALLWNDPROC is defined as 4 in winuser.h

该函数的第二个参数非常重要——它是我的 HookProc 的地址,它将监视特殊消息(顺便说一句,当您需要在托管代码中声明 API 并且有一个函数指针时,您可以使用 delegate)。这是 HookProc 委托的声明

delegate int HookProc(int code, IntPtr wparam, ref Win32.CWPSTRUCT cwp);

CWPSTRUCT 结构定义了传递给 WH_CALLWNDPROC 挂钩过程的消息参数

[StructLayout(LayoutKind.Sequential)]
public struct CWPSTRUCT
{
  public IntPtr lparam;
  public IntPtr wparam;
  public int message;
  public IntPtr hwnd;
}

由于我在窗体构造函数中放置了 SetWindowsHookEx 调用,以便在窗体构建时挂钩窗口(我的意思是主窗体窗口)。这是 Hook 过程的实现

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
      switch(cwp.message)
      {
        case 0x0001://WM_CREATE - catch this before the window is created
          string s = string.Empty;
          char[] className = new char[10];
          //Get the window class name
          int 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 = SetWindowLong(cwp.hwnd, (-4), subWndProc);
          break;
     }
     break;
  }
  return Win32.CallNextHookEx(hookHandle,code,wparam, ref cwp);
  //Let other applications use hook codes
}

另一个巨大的困难是获取正确的窗口类。这是一个系统定义的类,仅供系统使用,但在 Platform SDK 文档中给出了它的名称——它是“#32768”。当我获得正确的类时——使用 SetWindowLong API 和 GWL_WNDPROC 值来子类化它,该值设置窗口过程的新地址((-4)代表 GWL_WNDPROC)。subWndProc 参数是类型为 MyWndProc 的委托

delegate int MyWndProc(IntPtr hwnd,int msg,IntPtr wparam,IntPtr lparam);

SubclassWndProc 的实现

int SubclassWndProc(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam)
{
//tracer.Items.Add(msg.ToString());
 switch(msg)
 {
  case 0x0085:     //WM_NCPAINT
   IntPtr menuDC  = Win32.GetWindowDC(hwnd);                    
   Graphics g = Graphics.FromHdc(menuDC);
   DrawBorder(g);
   Win32.ReleaseDC(hwnd,menuDC);
   g.Dispose();
   return 0;                    
  case 0x0317:     //WM_PRINT
   int result = Win32.CallWindowProc(defaultWndProc,hwnd,msg,wparam,lparam);
   menuDC  = wparam;
   g = Graphics.FromHdc(menuDC);
   //Draw the border around the menu
   DrawBorder(g);
   Win32.ReleaseDC(hwnd,menuDC);
   g.Dispose();
   return result;
  }            
return Win32.CallWindowProc(defaultWndProc,hwnd,msg,wparam,lparam);//In all other cases 
                                                                   //use default wndproc
}

这可能看起来很奇怪,但是 Windows 在 WM_NCPAINT 之后发送一个 WM_PRINT 消息。我花了几个小时试图理解我的代码为什么出了问题并且不起作用,直到我往 SubclassWndProc 中添加了一个简单的追踪器,并找出发送到菜单窗口的消息。在处理 WM_PRINT 消息时,一切都正常——BINGO!终于,我改变了菜单窗口的默认外观!

我还想处理 WM_NCCALCSIZE 消息,以便减小菜单的非客户区,但失败了……在托管代码中如何做到这一点(我在 MFC 中实现了它)将非常感激!另外,我无法覆盖 WM_WINDOWPOSCHANGINGWM_WINDOWPOSCHANGED 的默认实现——我丢失了默认的系统动画……

还有一件事——我没有在其他平台上测试过这段代码(我的平台是 Windows XP),所以如果您发现任何错误,请告诉我!!!

关注点

我还添加了属主绘制菜单的平坦外观实现,在某种程度上,它们现在看起来确实像 Visual Studio .NET 的菜单!我还没有实现顶部菜单项右侧的阴影。也许可以通过获取桌面窗口 DC,在其上绘制,然后使该矩形失效来完成——如果我有足够的时间,我会尝试的。

这是我最终得到的结果

The menu with its flat look

好了,就这样!

再次声明——任何评论、建议甚至批评都欢迎!

历史

  • 2003 年 6 月 1 日:第一次修订

许可证

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

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

© . All rights reserved.