带图标、标题和阴影的自定义绘制菜单






4.95/5 (198投票s)
轻松使用自定义绘制菜单,支持新的 Office 产品风格,包括标题、阴影和图标。
STYLE_XP | STYLE_ORIGINAL |
![]() |
![]() |
![]() |
![]() |
STYLE_XP_2003 | STYLE_ICY |
![]() |
![]() |
![]() |
![]() |
引言
此类 CNewMenu
实现了一个从 CMenu
类派生的自定义绘制菜单。此类目标是实现与 Microsoft 新产品菜单相同的外观,并支持 图标
和 标题
。我从 Brent Corkum 的优秀类 BCMenu 中获得了灵感。我借鉴了一些想法和几乎所有函数,但重新实现了几乎所有代码,并扩展了一些其他有用的功能。改变应用程序的整个菜单外观是一项艰巨的工作——改变边框和添加阴影。此外,在 Windows 2000 下或在使用主题的 Windows XP 下更改所有菜单也很棘手。总的来说,我希望现在您使用此类开发新应用程序会很容易。
此新菜单可在 Windows 95/98/Me 和 Windows NT4.0/2000/XP 下运行。如果您想使用中文或其他特殊字符,可以以 Unicode 支持编译您的项目。但请记住,您必须已安装 Visual Studio 的 Unicode 库,否则会发生链接错误。
此外,如果您希望程序能在所有平台上运行,则必须使用 Visual Studio 6.0 进行编译。我尚未弄清楚为什么在 Windows NT 4.0 下使用 Visual Studio 7.0 编译时,绘制效果不正确。此外,如果您不喜欢扁平边框,可以使用 STYLE_XXX_NOBORDER
,这样菜单将具有标准的系统菜单边框。
特别适用于旧版 Windows,如 Win95 或 WinNT 4.0
CNewMenu
也能在这些系统上运行,但您不会拥有所有功能。因此,新的 Office 2003 菜单样式只有标准的菜单栏颜色,因为这些系统不支持更改菜单栏或菜单背景。当您使用新的 Visual Studio 7.0 / 7.1 时,必须 #define WINVER 0x0400
,这样就不会出现运行时错误。
菜单中的精美标题
![]() |
![]() |
![]() |
![]() |
还可以为菜单添加标题。您可以将标题添加到菜单顶部或左侧。背景可以是渐变色或纯色。使用 SetMenuTitle
函数,您可以添加或删除菜单的标题。渐变绘制也适用于 Windows 95。
// Set a title to the system menu
m_SystemNewMenu.SetMenuTitle(_T("New Menu Sample"),
MFT_GRADIENT|MFT_LINE|MFT_CENTER);
图标的新效果:增亮、发光和灰度
![]() ![]() |
在 |
如何在项目中 O使用 CNewMenu
- 最简单的方法是将项目中的所有
CDialog, CFrameWnd, CMDIFrameWnd, CMDIChildWnd, CMiniFrameWnd
和CMultiDocTemplate
替换为新的类CNewDialog, CNewFrameWnd, CNewMDIFrameWnd, CNewMDIChildWnd, CNewMiniFrameWnd
和CNewMultiDocTemplate
。 - 在从
CWinApp
派生的应用程序实现中,在InitInstance
函数中创建框架或对话框窗口之前设置菜单样式。// Set drawing style of all menus to XP-Mode or STYLE_ORIGINAL CNewMenu::SetMenuDrawMode(CNewMenu::STYLE_XP);
- 当您有多个文档模板时,在创建模板后立即添加加载菜单图标的代码。
CNewMultiDocTemplate* pDocTemplate; pDocTemplate = new CNewMultiDocTemplate(IDR_MDINEWTYPE, RUNTIME_CLASS(CMDINewMenuDoc), // custom MDI child frame RUNTIME_CLASS(CChildFrame), RUNTIME_CLASS(CMDINewMenuView)); AddDocTemplate(pDocTemplate); // Loading toolbar icons into the menu pDocTemplate->m_NewMenuShared.LoadToolBar(IDR_DRAWBAR);
- 在主框架的 OnCreate 函数末尾插入加载图标的代码。例如:
m_DefaultNewMenu.LoadToolBar(IDR_MAINFRAME);
- 最后,别忘了在 stdafx.h 中包含 NewMenu.h 的定义,并将 NewMenu.cpp 添加到项目中。
- 现在您已准备好使用此类了。
如何在基于对话框的应用程序中使用 CNewMenu
- 在应用程序中,在
InitInstance
函数中创建对话框之前设置菜单样式。// Set drawing style of all menus to XP-Mode CNewMenu::SetMenuDrawMode(CNewMenu::STYLE_XP);
- 在您的项目中,将所有
CDialog
替换为CNewDialog
。 - 加载位图到菜单的最佳位置是调用基类函数后的
OnInitDialog
函数。// CDialogDlg could be your Dialog. BOOL CDialogDlg::OnInitDialog() { // Call the baseclass CNewDialog::OnInitDialog(); // Load icons to the menu m_DefaultNewMenu.LoadToolBar(IDR_MAINFRAME); }
- 别忘了在 stdafx.h 中包含 NewMenu.h 的定义,并将 NewMenu.cpp 添加到项目中。
- 特殊功能:我添加了菜单的自动更新。您可以像在框架窗口中一样为更新菜单项添加
OnUpdate
处理程序。如果您想在对话框中拥有标准的菜单处理,您应该重写OnInitMenuPopup
,就像调用更新处理程序一样,在您的// For disable automatically void CDialogDlg::OnInitMenuPopup(CMenu* pMenu, UINT nIndex, BOOL bSysMenu) { CNewFrame<CDialog>::OnInitMenuPopup(pMenu,nIndex,bSysMenu); }
如何在特殊框架窗口中使用 CNewMenu
- 通常,您应该重写
measureitem
、menuchar
和initmenupopup
。为此,我使用了CNewFrame
模板类。因此,您可以在项目中将CYourSpecialBase
替换为CNewFrame<CYourSpecialBase>
。 - 不要在
IMPLEMENT_DYNAMIC
、IMPLEMENT_SERIAL
或IMPLEMENT_DYNACREATE
中进行更改以进行子类化。IMPLEMENT_DYNAMIC(CYourNewSpecialFrame, CYourSpecialBase)
- 别忘了在
BEGIN_MESSAGE_MAP
中更改基类。BEGIN_MESSAGE_MAP(CYourNewSpecialFrame, CNewFrame<CYourSpecialBase>) //{{AFX_MSG_MAP(CYourNewSpecialFrame) //}}AFX_MSG_MAP END_MESSAGE_MAP()
- 要将位图加载到菜单中,您可以在
OnCreate
函数中添加代码。 - 最后,别忘了在 stdafx.h 中包含 NewMenu.h 的定义,并将 NewMenu.cpp 添加到项目中。
如何在 MDI 应用程序中使用 CNewMenu
- 在应用程序的基类中,将
BOOL InitInstance()
函数中的CMultiDocTemplate
更改为CNewMultiDocTemplate
。CNewMultiDocTemplate* pDocTemplate; pDocTemplate = new CNewMultiDocTemplate(IDR_MDINEWTYPE, RUNTIME_CLASS(CMDINewMenuDoc), // custom MDI child frame RUNTIME_CLASS(CChildFrame), RUNTIME_CLASS(CMDINewMenuView)); AddDocTemplate(pDocTemplate);
- 现在插入以下代码,用于从工具栏加载附加的位图到菜单中。
// Loading toolbar icons into the menu pDocTemplate->m_NewMenuShared.LoadToolBar(IDR_DRAWBAR); // Set drawing style of all menus to XP-Mode CNewMenu::SetMenuDrawMode(CNewMenu::STYLE_XP);
- 在主框架类(
CMainFrame
)的定义和实现文件中,将基类从CMDIFrameWnd
更改为CNewMDIFrame
。// Change the base class in the following makro IMPLEMENT_DYNAMIC(CMainFrame, CNewMDIFrameWnd)
// don't forget to change here the baseclass BEGIN_MESSAGE_MAP(CMainFrame, CNewMDIFrameWnd) //{{AFX_MSG_MAP(CMainFrame) // NOTE - the ClassWizard will add and remove mapping macros here. // DO NOT EDIT what you see in these blocks of generated code ! ON_WM_CREATE() //}}AFX_MSG_MAP END_MESSAGE_MAP()
- 在
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
末尾添加代码,用于在默认菜单中加载位图。m_DefaultNewMenu.LoadToolBar(IDR_MAINFRAME);
- 最后,将子框架的基类从
CMDIChildWnd
更改为CNewMDIChildWnd
。对所有派生类执行相同操作。 - 别忘了在 stdafx.h 中包含 NewMenu.h 的定义,并将 NewMenu.cpp 添加到项目中。
如何在 SDI 应用程序中使用 CNewMenu
- 在应用程序中,在
InitInstance
函数中创建对话框之前设置菜单样式。// Set drawing style of all menus to XP-Mode CNewMenu::SetMenuDrawMode(CNewMenu::STYLE_XP);
- 在您的项目中,将所有
CDialog
替换为CNewDialog
。 - 在主框架类(
CMainFrame
)的定义和实现文件中,将基类从CFrameWnd
更改为CNewFrameWnd
。 - 在
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
末尾添加代码,用于在默认菜单中加载位图。m_DefaultNewMenu.LoadToolBar(IDR_MAINFRAME);
- 别忘了在 stdafx.h 中包含 NewMenu.h 的定义,并将 NewMenu.cpp 添加到项目中。
如何在系统对话框中替换系统菜单
有时更改消息框或 CFileDialog
中的菜单会很有用。不幸的是,MessageBox 没有用于重写的基类。另一个问题是文件对话框。至少有两个对话框,而 CFileDialog
只对显示的 FileDialog
的子对话框进行子类化。对于这些以及其他对话框,我添加了一个特殊的子类化机制。
// Subclass dialog with system menu
CNewMenuHelper myHelper(NEW_MENU_DIALOG_SUBCLASS|NEW_MENU_DIALOG_SYSTEM_MENU);
AfxMessageBox(_T("You must restart the application"), MB_OK);
当您不想在下一个显示的对话框中替换菜单边框时,该怎么办?这非常简单!只需在调用之前放置 CNewMenuHelper
,如下面的示例所示。
// Subclass dialog with normal border painting
CNewMenuHelper myHelper(NEW_MENU_DEFAULT_BORDER);
AfxMessageBox(_T("You must restart the application"), MB_OK);
16 色位图的颜色替换
MFC 库会根据选择的系统颜色替换某些颜色。当您在位图中添加黑色线条时,黑色将被替换为 COLOR_BTNTEXT
系统颜色。遵循颜色映射表。顺便说一下,这与工具栏的颜色映射相同。
-
颜色
RGB(0x00,0x00,0x00)
将被更改为COLOR_BTNTEXT
(黑色)。 -
颜色
RGB(0x80,0x80,0x80)
将被更改为COLOR_BTNSHADOW
(深灰色)。 -
颜色
RGB(0xC0,0xC0,0xC0)
将被更改为COLOR_BTNFACE
(亮灰色)。 -
颜色
RGB(0xFF,0xFF,0xFF)
将被更改为COLOR_BTNHIGHLIGHT
(白色)。
为菜单添加图标
有几种为菜单添加图标的可能方法。一种简单的方法是使用工具栏资源。但 Developer Studio 只支持 16 色的工具栏位图。您可以这样做:首先定义您的工具栏,然后手动用高彩色位图替换保存的工具栏位图。此外,您可以为每个图标定义不同的位图并将其分配给菜单项。我的方法是定义一个像图像列表中的位图,制作一个包含位图资源 ID 和图标大小的辅助表,后面跟着图标的 ID。表的末尾必须用 NULL
ID 标记。
// Define the table
static WORD ToolId[] =
{ IDR_NEWMENU256, // Resource id of the bitmap-icons
16, 15, // Dimension of each icon
ID_FILE_NEW, // First icon's ID
ID_FILE_OPEN, // Second icon's ID
ID_FILE_SAVE, // and so on...
NULL}; // Marker for end of table
//Load the definition into menu with transparent color
m_DefaultNewMenu.LoadToolBar(ToolId,RGB(192,192,192));
如何为子菜单添加图标
如果您知道菜单文本,为子菜单条目添加或更改标题非常简单。可以使用辅助函数 ModifyODMenu
。
// set the icon IDB_MORE_C to the submenu "More A" m_NewMenuShared.ModifyODMenu(0,_T("More A"),IDB_MORE_C);
Windows 2000 下的菜单
奇怪的是,在 Windows 2000 下,有一个句柄为 0x10012 的窗口菜单,它被所有应用程序共享。(我认为在 Windows 98 下,菜单是 0x0090)。我不知道为什么,但这也被解释为某些子类化菜单效果的特殊原因。首先,当您禁用菜单效果时,菜单窗口会交替创建或显示。这意味着第一个显示的菜单通常是特殊菜单——从未创建或销毁——只显示和禁用,第二个是正常创建并在关闭后销毁。第三次,有时,特殊窗口会再次显示,然后下一个菜单正常创建。因此,当您子类化特殊菜单时,您负责在取消子类化之前恢复所有值。更改某些标志或样式时要小心,因为其他应用程序喜欢标准菜单。如果您忘记取消子类化或更改属于该菜单的绘制区域,您甚至可能遇到蓝屏。这真的很糟糕!其次,当您启用菜单效果时,每个显示的菜单都会从头开始创建。因此,是否恢复所有标志或样式并不重要!
让我解释一下我是如何实现子类化所有菜单的目标的。首先,您无法子类化应用程序的系统菜单类 #32768;我也在寻找其他可能性。我决定为所有窗口消息创建一个 Windows 挂钩,并捕获所有创建窗口的消息。在检查了窗口类之后,当找到菜单类时,我就对其进行了子类化。但如何子类化那个从未被创建的特殊菜单呢?我发现发送到该菜单的早期消息之一是 0x01E2,而最近的消息是 WM_SHOWWINDOW
。因此,我捕获了这些消息来子类化和取消子类化特殊菜单。只有在这两条消息之间,您才能访问特殊窗口!我不知道为什么,所以请小心。
如何绘制边框
在子类化菜单窗口后,我可以通过处理 WM_NCPAINT
消息来绘制边框。但阴影效果是如何产生的呢?在菜单显示在屏幕上之前,我将菜单区域保存在一个位图中。这就是为什么当背景被更改时它不起作用的原因。然后我将其与阴影绘制结合起来。另一种方法是使用分层窗口,但这在 Windows 95/98 等所有系统上都行不通。
渐变菜单栏
自 Windows 98 或 Windows 2000 起,就可以为菜单和子菜单设置画笔。因此,更改颜色很容易。但我们如何在菜单栏中实现渐变呢?好吧,您可以定义一个屏幕宽度的位图画笔,并将其设置为框架菜单的菜单。还有其他线索吗?哦,是的,在 Windows98/Me 等非 NT 系统上,您必须设置画笔原点,以避免在重绘菜单栏项的部分时位图出现偏移。
Windows XP 下的系统菜单
我曾经认为自定义绘制菜单一直都很容易,但在 Windows XP 上,当启用主题时,这会成为一个棘手的问题——尤其是当显示系统菜单的主框架。与自定义绘制不同,菜单项是由框架绘制的。框架在菜单项的位置绘制了菜单宽度的窗口按钮,而不是绘制它,所以我找到了一个解决方法。我在显示菜单之前增加了菜单项的标识符,并在关闭后恢复它。因此,它在 Windows 2000 和 XP 下都有效。
在 Windows 2000 下调试新菜单的重要信息
在绘制菜单的过程中,您绝对不应该停止调试,因为特殊菜单将不会被恢复或取消子类化。此外,还会出现许多奇怪的效果。这并非禁止,但请谨慎行事,并在调试器停止后出现问题时准备好重启系统。
如何使用 CNewToolBar
CNewToolBar
只能与 CNewMenu
一起使用。在您的项目中,您需要将 CToolBar
替换为 CNewToolBar
。此外,如果您希望停靠栏具有与菜单栏相同的颜色,则必须在包含 newmenu.h 之前定义 USE_NEW_DOCK_BAR
。
历史
2003 年 11 月 30 日 - 版本 1.18
CNewToolBar
扩展了 ICY / 2003 样式
2003 年 9 月 11 日 - 版本 1.17
- 改进了
STYLE_XP
和STYLE_XP_2003
的菜单栏画笔处理 - 修复了 WinNT 4.0 上菜单图标的绘制
2003 年 7 月 12 日 - 版本 1.16
- 为 Office 2003 样式添加了渐变停靠栏(
CNewDockBar
) - 修复了带位图的菜单复选标记
- 绘制到内存 DC 以最小化闪烁
- 现在可以更好地处理没有“&”的菜单项宽度
2003 年 6 月 22 日 - 版本 1.15
- 为
ModifyODMenu
和InsertODMenu
添加了快捷键处理 - 将 OS 标识符更改为 WinXP (OS>5.0)
- 更改了 Office 菜单的颜色方案并更新了菜单栏颜色
- Office 2003 栏的渐变颜色
- XP 更改设置边框样式 => 修复了边框绘制,无需重启
2003 年 4 月 23 日 - 版本 1.14
- 修复了极端宽度下的窗口菜单
- 使用
AppendMenu
添加的菜单项的快捷键处理现在更好 - 修正了默认项的
Measureitem
- 新的 Office 2003 菜单样式(Win95 & WinNT 4.0 上不支持更改菜单栏颜色)
- 修正了 Win95 & WinNT 4.0 下非活动应用程序的菜单栏绘制
2003 年 1 月 23 日 - 版本 1.13
- 修复了
SetMenuText
。 - 修复了带主题的菜单栏颜色。
- 为非
CNewMenu
的边框处理添加了一些辅助功能
2002 年 11 月 10 日 - 版本 1.11
- 修正了 Win 98 下带菜单动画的边框绘制。
- 修复了子菜单关闭时的边框绘制/重绘。
- 新的 ICY 菜单样式。
- 用于更改 CommonDialog 系统菜单的辅助类。
2002 年 7 月 30 日 - 版本 1.10
- 菜单之间共享图标以节省 GDI 资源。
STYLE_XPxx
下绘制图标的新效果。- 通过更改系统颜色重新着色菜单图标。
- 稍微改进了极端大菜单的滚动按钮绘制。
- 现在支持在菜单中混合不同大小的图标,但它们不会缩放到相同大小。
已知错误
- 菜单边框带阴影时,背景更改后不会更新。
- 高对比度模式不支持所有正确的颜色。