自定义绘制菜单的革命性新方法






4.95/5 (113投票s)
2003 年 2 月 22 日
11分钟阅读

626095

13224
一种新颖独特的方法来解决如何更改 Windows 菜单默认外观的长期存在的问题。
修订版 1.2
引言
自 Windows API 发布以来,改变 Windows GUI 元素默认外观一直是 Windows 程序员永无止境的挑战。任何认真尝试过改变菜单外观(除了简单的属主绘制技术)的人都知道,这代表了 Windows GUI 编程的圣杯之一。
注意:为了减少文章的冗余,我将使用“皮肤”一词来代替“改变默认外观”。
我想在这里介绍一种全新的、原创的菜单皮肤技术,它允许您为应用程序生成的所有菜单应用皮肤,包括(但不限于):
- 菜单栏(例如,应用程序主菜单)
- 所有右键单击(上下文)弹出菜单(包括您自己的以及 Internet Explorer 和标准文件对话框的菜单)
- 系统菜单(包括窗口标题栏和任务栏上的菜单)
- 工具栏下拉菜单
- 您自己或第三方属主绘制的菜单(查看文件对话框中的“新建”和“发送到”子菜单)
挑战
对于开发人员来说,Windows 菜单一直是一个黑匣子,所有实现都隐藏在 user32.dll 内部。Windows 允许开发人员做的只是将菜单定义为属主绘制,然后处理 WM_MEASUREITEM
和 WM_DRAWITEM
消息来绘制一个或多个菜单项。
如果您只对属主绘制菜单感兴趣,目的是用显示颜色本身的菜单项替换“选择颜色”菜单的标准文本界面,那么这可以接受。但如果想要一种可以替换所有菜单,使其外观不同于默认的灰色(或 COLOR_MENU
)的架构,则远远不够。
特别是
- 这需要您能够访问应用程序中的每一个菜单句柄,以便将它们修改为属主绘制(可以做一个实验:尝试获取编辑框上下文菜单的句柄)。
- 这需要您能够将代码插入到显示菜单的每个窗口或控件中,以处理
WM_MEASUREITEM
和WM_DRAWITEM
消息。 - 它没有提供一种机制来替换非客户区(边框)的绘制。
更根本的是,您无法处理别人(例如 Windows 本身)已经定义为属主绘制的菜单,因为通常您不知道在绘制阶段所需的信息存储在什么内存结构中。
而最后这一点最终使得属主绘制无法成为解决问题的方案。
解决方案
第一部分
我不知道“解决方案”是怎么想出来的。也许我当时正在洗澡,或者可能坐在树下被苹果砸中了头,我不知道。但它就是发生了。“如果,”我想,“我能将所有菜单的绘制从屏幕重定向到一个内存 DC,这样我就可以对其进行后处理了。然后我就可以随心所欲地修改它,最后再将菜单渲染到屏幕上。” 剩下的就是艰苦的工作了。
第二部分
此解决方案的实现可以分为两个需要解决的任务:
- 一种检测菜单即将显示的方法。
- 一种覆盖特定菜单绘制方式的方法。
前者问题的解决方案无疑是两者中最容易的。我需要做的就是安装一个 Windows Hook 来检测所有菜单窗口的创建。
注意:对于不知道的人来说,菜单窗口有一个特殊的类名(#32768
),这使得检测它们与检测其他窗口一样简单。
在菜单窗口创建之前捕获它,然而,仍然留下了“该怎么处理它?”的问题,答案当然是子类化。在这里我必须感谢 Paul DiLascia 和他现在传奇的 CSubclassWnd
实现。CSubclassWnd
是 Paul 编写的一个 MFC 类,它允许拦截和覆盖给定窗口的所有 Windows 消息,即使您不是窗口的创建者。所以基本上就是这样:在菜单窗口显示之前捕获所有菜单窗口,并对它们进行子类化,以便我们可以覆盖绘制。
覆盖默认绘制
在编程中(在工程中也一样),如果设计是健全的,实现通常会迎刃而解。不幸的是,情况并非如此。我发现 Windows GUI 编程存在太多 Windows 版本(95 -> XP)之间的细微差别和变化,不得不依赖于未公开的技巧和折衷方案来实现一个合理的解决方案。
此外,在这种特定情况下,没有任何文档支持或表明我有可能成功。我的第一个任务是确定哪些 Windows 消息会触发菜单(重新)绘制,以便我可以用我的自定义实现来替换它们。幸运的是,我发现菜单绘制非常简单(尽管未公开)。这是需要处理的 Windows 消息摘要:
WM_PRINT
- 请求菜单在wParam
提供的 DC 中绘制其非客户区和/或客户区(Windows 9x、2K、XP)WM_PRINTCLIENT
- 请求菜单在wParam
提供的 DC 中绘制其客户区(Windows 9x、2K、XP)WM_PAINT
- 请求菜单绘制其无效区域的前景(Windows 9x)WM_ERASEBKGND
- 请求菜单绘制其无效区域的背景(Windows 9x)0x01e5
(未公开)- 请求菜单使用内部方法重新绘制消息wParam
提供的项(Windows 9x、2K、XP)
最后一个消息(0x01e5
)是我在此项目中做出的最有趣、最关键的发现。每次您在菜单内或外移动鼠标时,Windows 都会向菜单发送此消息,以便菜单可以重新绘制指定的项。起初这似乎是一种巨大的效率低下,因为没有其他窗口控件会这样做,但如果您打开了菜单动画,您就会明白为什么这是必要的。
通常,在 Windows 中发生 GUI 事件时,通常的做法是等待事件完成,然后根据需要重绘控件。菜单淡入淡出不是这样;在这种情况下,菜单淡入淡出是在事件发生后使用系统计时器(我相信)进行的。因此,如果您仅在事件触发后重绘一次菜单,则会出现各种绘图瑕疵。幸运的是,只需在消息的默认实现之前和之后调用 SetRedraw()
即可确保菜单不会在其“幕后”进行绘制,然后使有问题菜单项的矩形无效。
替换系统颜色
一旦我控制了重绘,我仍然需要弄清楚如何用用户的选择替换系统颜色。
由于我一直在深入研究应用程序皮肤系统,并有效使用了 TransparentBlt()
,我想到可以使用它来进行菜单图像的多通道后处理,每次替换一种系统颜色。
我曾预料到这个想法会带来相当大的性能影响,但实际上它相当不错。即使在 Windows 98 上,我替换了 TransparentBlt()
的自定义实现(默认版本有众所周知的资源泄漏),它的性能仍然可以接受,尽管我在低端机器上的测试还远远不够。
我还尽力确保在某些系统颜色解析为相同的 COLORREF
时,我执行的 TransparentBlt()
次数最少。
其他实现细节
一些敏锐的读者可能会注意到上一段中对菜单句柄(HMENU
)的隐含引用(“...菜单项的矩形...”),而我之前从未提及过。虽然菜单皮肤技术确实可以在没有菜单句柄的情况下实现,但通过仅使需要重绘的菜单项无效而不是整个窗口,可以使实现更有效。
然而,确定菜单句柄非常困难,因为 Windows 没有提供菜单窗口与其关联菜单句柄之间的显式映射。这就是我之前提到的“折衷”的用武之地。要检索菜单句柄,我会等到检测到 WM_INITMENUPOPUP
,然后要么在菜单窗口已创建时将其附加到菜单窗口,要么等到菜单窗口被子类化时持有它,然后再添加。不能保证匹配是正确的,但似乎效果还不错。这是一个真正的折衷,但据我所见没有其他选择。
Using the Code
- 将以下源文件添加到您的项目中
注意:在我的演示项目中,这些文件位于一个单独的“skinwindows”文件夹中,因为它们是一个更大皮肤系统的一个子集,但您没有必要这样做。
CHookMgr
(hookmgr.h) - 简化钩子的模板类CSkinBase
(skinbase.h/.cpp) - 一些硬核辅助方法CSkinGlobals
(skinglobals.h/.cpp, skinglobalsdata.h) - 用于覆盖默认 Windows 颜色的辅助类CSkinMenu
(skinmenu.h/.cpp) - 菜单窗口覆盖CSkinMenuMgr
(skinmenumgr.h/.cpp) - 菜单窗口钩子和管理CSubclassWnd
(subclass.h/.cpp) - 子类化辅助类(Paul DiLascia 的原始版本进行了大量修改)CWinClasses
(winclasses.h/.cpp) - 用于检索和测试窗口类的辅助类- wclassdefines.h - 所有窗口类(和其他一些)的便捷
#defines
- skincolors.h - 颜色映射
- 在项目设置中将
NO_SKIN_INI
添加到预处理器定义中。这是为了避免因缺少文件而导致的编译问题,因为本项目是一个更大的支持从文件加载颜色信息的系统的组成部分,而该文件此处未包含。 - 在派生自
CWinApp
的应用程序的InitInstance()
方法中,按如下方式初始化皮肤菜单管理器:#include "skinmenumgr.h" // assumes files are in same folder // as rest of the project BOOL CMyApp::InitInstance() { : : CSkinMenuMgr::Initialize(); : : }
有关可用选项的更多详细信息,请查看 CSkinMenuMgr::Initialize()
的实现。特别是,您可以选择显示指定宽度的侧边栏,并为菜单边框设置平面或斜角边缘。
为了完全控制菜单的绘制方式,包括菜单背景,请从 skinmenu.h 中定义的 ISkinMenuRender
派生一个类,并将其传递给静态方法 CSkinMenu::SetRenderer()
。然后,在绘制过程中,CSkinMenu
类将调用您的派生类,让您有机会绘制您选择绘制的菜单的任何部分。(请参阅 CSkinMenuTestDlg
示例实现)
进一步工作
- 它不支持滚动菜单(感谢 saltynuts2002)
- 它在 Windows CE 下不起作用(感谢 João Paulo Figueira)
- 在 XP 下速度有点慢。
- 通过更多地考虑剪辑框,可以普遍提高渲染速度。
警告
- 在使用调试器进行键盘导航时,请准备好会出现一个内部 Windows 断点;这似乎在调试器之外是一个问题。
- 直言不讳地说,应尽可能避免在 Windows 98 和 Me 下进行调试。Windows 不喜欢在这些平台上进行菜单皮肤操作,如果您在菜单重绘代码中设置断点,您应该预期会频繁重启。
- 虽然演示应用程序在 Windows 98 和 Me 上运行正常,但这些平台更脆弱的本质意味着最轻微的错误都可能导致系统崩溃(不是蓝屏,但您仍然需要重启)。
- 在进行非客户区绘制时,完成前恢复 DC 状态至关重要。似乎 Windows 会重用同一个 DC 来渲染所有菜单,一旦您对其进行了修改,它就会保持修改状态。
- 在 XP 下速度有点慢,原因我正在调查。
- 在提出任何关于代码的问题之前,请花时间详细阅读代码。
Copyright
代码在此提供供您随意使用和滥用,但您不得修改它并冒充为自己的作品。然而,概念和设计在永久性的情况下仍是我的知识产权。
历史
- 1.0 初始发布
- 1.1 错误修复
- 添加了对键盘导航的支持,包括多列菜单
- 修正了禁用项和分隔线的颜色替换
- 增加了在菜单隐藏时取消皮肤支持(Win 9x)。这修复了 Windows 报告的关于菜单栏的资源泄漏问题。
- 1.2 错误修复 + 功能
- 更新以支持 CHookMgr
- 更好地处理滚动菜单
- 在 95/NT4 下禁用客户区回调,但仍支持非客户区绘制
- 在 CSkinMenuMgr::Initialize() 中添加了一个选项来禁用 XP 下的菜单皮肤