MFC 扩展库 - 处理拥有者绘制菜单的插件






4.41/5 (7投票s)
插件架构系列文章的续篇
目录
引言
本文是对文章 “使用 MESSAGE_MAPs 为您的应用程序启用 DLL 插件技术的 MFC 扩展库” 的后续。如果您不熟悉该库本身,我建议您先阅读主文章。
当我首次发布 MFC 插件架构时,我一直打算编写一系列标准的插件,为所有应用程序添加功能。这是该系列中的第二篇,第一篇是主文章中的示例插件。
本文介绍了一个新插件,它使用 Brent Corcums 的 “带位图的酷拥有者绘制菜单 - V3.03” 的修改版本来处理拥有者绘制菜单。我将在本文中介绍我是如何开发该插件以及在此过程中遇到的问题的。

初步工作
当我开始开发这个插件时,我意识到插件库本身存在一个问题。那就是你必须提供插件使用的类名。对于拥有者绘制菜单,我们需要成为许多类的插件,但我事先并不知道所有这些类的名称。
// old method LPCTSTR CMFPlugIn::GetClass() { // return the name of the class which this is // a plug in map for, e.g. "CMyApp" return L"CMainFrame"; }
我需要改变逻辑,让插件自己询问“你是否是这个类的插件?”并返回 true
/false
,而不是提供类名。所以,在我们的情况下,我们只为所有启用了插件的对象返回 true
,以便我们可以处理使任何菜单显示为拥有者绘制。
// new method bool CMFPlugIn::IsPlugInFor(CRuntimeClass *pClass) { return (_tcscmp(pClass->m_lpszClassName, "CMainFrame") == 0); }
这也迫使我重新处理了在主应用程序中抑制消息处理的方法,因为以前你必须将 m_pPlugInFor
指针强制转换为正确的对象并设置一个成员变量。当您不知道您是哪个类的插件时,这实际上是不可能的!
由于此插件仅与库 V1.2 或更高版本一起使用(V1.2 包含了对库的必要修改以正确支持此插件),因此现有库用户应访问主文章并升级到最新的源代码版本。您还必须更新您自己的现有插件以符合所做的更改。此过程的说明已包含在主文章中。
我们需要实现哪些消息?
所以,我们想要创建一个拥有者绘制菜单插件,这个插件应该处理哪些消息来实现其行为?嗯,这归结为 3 个,它们是:
WM_INITMENUPOPUP
当菜单即将显示时,在显示任何弹出菜单之前或当用户在多个顶层菜单之间切换时(例如,在“文件”和“编辑”之间),都会发送此消息。在此消息处理程序中,我们需要为即将显示的菜单中的每个菜单项设置拥有者绘制样式位 MF_OWNERDRAW
。
// early version of the OnInitMenuPopup() function void CODMenu::OnInitMenuPopup(CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu) { if (IsPostCall()) { // iterate any menu about to be displayed and make sure // all the items have the ownerdrawn style set // We receive a WM_INITMENUPOPUP as each menu // is displayed, even if the user // switches menus or brings up a sub menu. This means we only need to // set the style at the current popup level. // we also set the user item data to the HMENU handle to allow // us to measure/draw the item correctly later if (pPopupMenu != NULL) { int itemCount = pPopupMenu->GetMenuItemCount(); for(int item = 0; item < itemCount; item++) { pPopupMenu->ModifyMenu(item, MF_BYPOSITION | MF_OWNERDRAW, itemID, (LPCTSTR)pPopupMenu->m_hMenu); } } } }
由于我们是“按需”设置 MF_OWNERDRAW
样式,因此我们无需子类化或替换任何现有的 CMenu
成员函数。程序员不必担心将他们修改的菜单保持在正确的拥有者绘制状态。
由于库支持插件消息的“预”调用和“后”调用,并且我们不希望多次执行此标志设置,因此我们需要选择是在标准 MFC 消息处理之前(预)还是之后(后)执行。我选择在之后,原因如下:
- MFC 调用菜单项的
ON_UPDATE_COMMAND_UI
处理程序 - MFC 会扩展一些菜单选项(例如,最近使用的文件列表,窗口中的打开文档),而我们需要菜单中的所有项都拥有者绘制。
WM_MEASUREITEM
一旦菜单项为其设置了 MF_OWNERDRAW
样式位,应用程序将开始接收 WM_MEASUREITEM
消息,以便操作系统在菜单项即将显示时知道每个显示元素的大小。此消息是“预”消息处理程序,因为我们需要抑制在常规 MFC 调用列表中处理该消息。抑制此消息可以阻止 MFC 在调试时显示警告消息(因为它认为应用程序未处理该消息,但实际上是我们的插件在处理)。
在此消息中,我们还必须返回要测量的菜单选项的大小。Brent 的 BCMenu
代码的修改提供了所需的功能。此处不涵盖该代码,请查阅源代码以获取更多信息。
WM_DRAWITEM
一旦菜单项为其设置了 MF_OWNERDRAW
样式位,应用程序将开始接收这些消息,以便在需要时绘制项。这通常是实现拥有者绘制菜单的复杂部分。再次,Brent 的 BCMenu::DrawItem
代码的修改拯救了我!
我最初曾尝试自己实现拥有者绘制菜单代码,但这太耗时了。:(
这些消息只应在对象类型为 ODT_MENU
时处理。因为 WM_MEASUREITEM
和 WM_DRAWITEM
可以针对许多不同的控件类型接收。
void CODMenu::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct) { // check that the call is for an owner drawn menu if (IsPreCall() && lpMeasureItemStruct->CtlType == ODT_MENU) { MeasureItem(lpMeasureItemStruct); // we have handled it, stop it flowing through regular MFC SuppressThisMessage(); } } void CODMenu::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct) { if (IsPreCall() && lpDrawItemStruct->CtlType == ODT_MENU) { DrawItem(lpDrawItemStruct); // we have handled it, stop it flowing through regular MFC SuppressThisMessage(); } }
设置菜单的图像列表
BCMenu
类通常会在应用程序初始化期间调用 LoadToolbar
。这是此插件无法自动完成的任务,因此它需要自动查找并使用应用程序和其他插件中的所有工具栏资源。那么我们如何做到这一点呢?
嗯,当库完全初始化并加载所有插件 DLL 后,每个插件 DLL 都会收到对其 IntialiseDLL()
函数(如果已实现)的调用。我们需要在此函数中编写一些代码来扫描 exe/dll 以查找任何工具栏资源。通过 MSDN
快速搜索,我找到了 EnumResourceNames
函数,这看起来是完成此任务的理想选择。
extern "C" void InitialiseDLL() { CODMenu::EnumerateAndLoadToolbars(); }
如果您查看 EnumResourceNames
函数原型
// ANSI WINBASEAPI BOOL WINAPI EnumResourceNamesA( HMODULE hModule, LPCSTR lpType, ENUMRESNAMEPROC lpEnumFunc, LONG lParam); // UNICODE WINBASEAPI BOOL WINAPI EnumResourceNamesW( HMODULE hModule, LPCWSTR lpType, ENUMRESNAMEPROC lpEnumFunc, LONG lParam);
您可以看到它在每次调用时都需要一个 HMODULE
。这将是应用程序的 HINSTANCE
或已加载插件 DLL 的 HINSTANCE
。
但是我们如何获取已加载 DLL 的 HINSTANCE
呢?这是库中的另一个问题,我通过向主 CPlugInApp
类添加两个额外函数来解决。
int CPlugInApp::GetPlugInDLLCount()
返回插件 DLL 的数量。CDLLWrapper* CPlugInApp::GetDLL(int index)
返回指向包装插件 DLL 的对象的指针。
CDLLWrapper
类也额外添加了一个接口函数
HINSTANCE CDLLWrapper::GetHInstance()
返回插件 DLL 的HINSTANCE
。
如果我们拥有每个需要搜索的模块的 HINSTANCE
,我们可以使用系统函数 EnumResourceNames()
并将 RT_TOOLBAR
作为我们要枚举的资源类型传递。系统然后调用一个 static
成员 CALLBACK
过程,我们在其中获取关于每个模块中找到的每个工具栏的信息!
void CODMenu::EnumerateAndLoadToolbars() { // This procedure is called by the InitialiseDLL function // When we intialise, we need to enumerate all the toolbar // resources in all the plug-in DLLs and the application // this allows us to generate a map of menu items which have toolbar // images available for them // First enumerate and use the appplications toolbar(s) CPlugInApp *pApp = static_cast<CPlugInApp*>(AfxGetApp()); if (pApp) { TRACE("Enumerating application\n"); EnumResourceNames(pApp->m_hInstance, RT_TOOLBAR, (ENUMRESNAMEPROC)EnumResNameProc, 0); // now enumerate all the plug-in DLL toolbars int dllCount = pApp->GetPlugInDLLCount(); for(int dll = 0 ; dll < dllCount; dll++) { TRACE("Enumerating DLL %1d\n", dll); EnumResourceNames(pApp->GetDLL(dll)->GetHInstance(), RT_TOOLBAR, (ENUMRESNAMEPROC)EnumResNameProc, 0); } }
在回调中,我们加载工具栏资源条目,该条目指定了每个工具栏按钮的 WM_COMMAND
id。我们还可以加载同名的位图资源,并将它们批量添加到我们的图像列表中,该列表用于绘制菜单显示时使用的实际工具栏图像。
但是我们如何从工具栏资源中获取按钮 ID 呢?我们不知道它的结构!
工具栏资源结构
标准的 MFC MDI 工具栏在 .rc 文件中定义如下:
IDR_MAINFRAME TOOLBAR DISCARDABLE 16, 15 BEGIN BUTTON ID_FILE_NEW BUTTON ID_FILE_OPEN BUTTON ID_FILE_SAVE SEPARATOR BUTTON ID_EDIT_CUT BUTTON ID_EDIT_COPY BUTTON ID_EDIT_PASTE SEPARATOR BUTTON ID_FILE_PRINT SEPARATOR BUTTON ID_APP_ABOUT BUTTON ID_CONTEXT_HELP END
当我们枚举每个工具栏资源并加载它时,我们会得到一个指向资源的指针,如下所示:
BOOL CALLBACK CODMenu::EnumResNameProc(HMODULE hModule, LPCTSTR lpszType, LPTSTR lpszName, LONG lParam) { TRACE("Toolbar found, module %x, Type %1d, Name %1d\n", hModule, lpszType, lpszName); // There should be 2 resources,one of type RT_TOOLBAR, // which enumerates the command IDs // the other of type RT_BITMAP, which are the images for each button // load in the RT_TOOLBAR button indexes HRSRC hrsrcToolbar = ::FindResource(hModule, lpszName, lpszType); HGLOBAL hToolbar = ::LoadResource(hModule, hrsrcToolbar); int size = ::SizeofResource(hModule, hrsrcToolbar); WORD* pToolbarData = (WORD*)::LockResource(hToolbar);
这给了我们一个指向工具栏资源的 WORD*
指针,但是数据是什么意思呢?经过一点调查,我们发现它是一个指向如下结构的指针:
struct ToolbarResource { int version; // version of the resource (always 1) int imageSizeX; // the X size of the toolbar images int imageSizeY; // the Y size of the toolbar images int tableEntries; // the number of toolbar button entries int buttons[tableEntries]; // button indexes (0 is separator) };
因此,学习了这一点后,我们按如下方式加载并构建映射:
// from examination of a toolbar resource, the layout it: // WORD : Version number(usually 1) // WORD : X_SIZE of image(16) // WORD : Y_SIZE of image(15) // WORD : NUMBER OF TOOLBAR BUTTONS // x * WORD : TOOLBAR BUTTON ID's // NOTE THAT A TOOLBAR BUTTON ID OF 0 IS A SEPARATOR if (pToolbarData[0] == 0x0001) { // we recognise this toolbar version, go ahead if (pToolbarData[1] == m_iconX && pToolbarData[2] == m_iconY) { // buttons are the correct size for a menu, // lets load these images CBitmap toolbarBitmap; // make sure we load the bitmap image from the correct module HINSTANCE old = AfxGetResourceHandle(); AfxSetResourceHandle(hModule); toolbarBitmap.LoadBitmap(lpszName); AfxSetResourceHandle(old); // extract the IDs of each toolbar button for(int buttonIndex = 0 ; buttonIndex < pToolbarData[3] ; buttonIndex++) { if (pToolbarData[4 + buttonIndex] != 0) { // this is not a separator // make sure there is not already a map entry for this button if (m_commandToImage.find(pToolbarData[4 + buttonIndex]) == m_commandToImage.end()) { // does not exist, add it m_commandToImage[pToolbarData[4 + buttonIndex]] = m_commandToImage.size(); } else { TRACE("Warning, duplicate toolbar button" " %1d found, using first\n", pToolbarData[4 + buttonIndex]); } } } // add the toolbar images on mass m_buttonImages.Add(&toolbarBitmap, ::GetSysColor(COLOR_3DFACE)); toolbarBitmap.DeleteObject(); } } // return TRUE to keep enumerating any other toolbar // resources in the same module return TRUE; }
在枚举工具栏的过程中,我们还构建了一个 std::map<int, int>
对象,它将 WM_COMMAND
id 与图像列表索引位置(图像在列表中的位置)关联起来。
一旦所有工具栏都被枚举并且映射构建完毕,我们就可以创建在渲染菜单时使用的禁用位图图像。
// we now have all the toolbars loaded into the // main image list m_buttonImages // generate the disabled versions of the images used CBitmap disabledImage; CWindowDC dc(NULL); dc.SaveDC(); disabledImage.CreateCompatibleBitmap(&dc, m_iconX, m_iconY); dc.SelectObject(&disabledImage); for(int image = 0 ; image < m_buttonImages.GetImageCount() ; image++) { CBitmap bmp; GetBitmapFromImageList(&dc, &m_buttonImages, image, bmp); DitherBlt3(&dc, bmp, ::GetSysColor(COLOR_3DFACE)); m_disabledImages.Add(&bmp, ::GetSysColor(COLOR_3DFACE)); } dc.RestoreDC(-1);
在开始时执行此操作可以节省菜单显示期间的处理时间,因为 Brent 最初的 BCMenu
代码会在每次需要渲染禁用位图图像时都生成它。
系统菜单图标
为了使系统菜单图标能够正确渲染,我在拥有者绘制菜单插件中添加了一个工具栏资源,其定义如下:
IDR_TOOLBAR TOOLBAR DISCARDABLE 16, 15 BEGIN BUTTON 61472 BUTTON 61488 BUTTON 61536 BUTTON 61728 END
显示的数字是 SC_CLOSE, RC_RESTORE...
等的实际代码值。我还添加了工具栏图像。

遇到的问题
在我编写此插件时,我遇到了以下问题:
我正在测量哪个项目?
为了能够正确测量一个项目,我最初将菜单项的“用户数据”设置为它所属的 HMENU
。这是因为 LPMEASUREITEMSTRUCT
不包含您实际测量哪个菜单项的信息。最初这看起来是好的。我使用提供的 itemID
值通过 MF_BYCOMMAND
获取菜单信息,这对于普通菜单项来说效果很好,但一旦遇到弹出菜单或分隔符,您就会得到一个 ID 为 -1
或 0
的值,GetMenuItemInfo()
会在此失败。因此,我无法测量弹出菜单/分隔符菜单项!
经过进一步思考,我不再将 HMENU
保存在“用户数据”中,而是保存了菜单中的位置。这使得 GetMenuItemInfo()
函数在弹出菜单时通过 MF_BYPOSITION
检索信息时能够正常工作。但是现在我不知道该在哪个菜单上调用!因此,我引入了一个类成员变量 HMENU m_hMenuBeingProcessed
,并在 OnInitMenuPopup
处理程序中设置它。这效果很好,因为我们在测量该菜单中的所有项目之间从未收到 WM_INITMENUPOPUP
消息。
将“用户数据”保存为菜单中的位置也有助于我们在 DrawItem
处理程序中完全处理菜单项,尽管我们在此处提供了要绘制的项的正确 HMENU
。
所有菜单项始终启用
当我想要启用拥有者绘制的禁用/已选中项时,我在菜单中添加了一些项,设置了正确的 ON_UPDATE_COMMAND_UI
处理程序,并设置了相关的状态。但在我的 DrawItem
代码中,项的状态始终是启用的且未选中。事实上,在调用之间唯一不同的标志是 ODS_SELECTED
,它表示当前高亮显示的是哪个项。我在 DrawItem
过程中尝试了各种方法来获取项的正确状态,但都没有成功。所以我回去睡了一觉。
醒来后,我有了解决方案:问题出在 OnInitMenuPopup
函数中,我当时正在迭代即将显示的菜单并设置 MF_OWNERDRAW
标志。由于这是一个“后”调用,MFC 已经完成了它对 ON_UPDATE_COMMAND_UI
处理程序的所有调用,我的 MenuModify
调用只是设置了 MF_OWNERDRAW
标志,从而覆盖了由 ON_UPDATE_COMMAND_UI
处理程序设置的项的实际状态。解决方案是在此时读取菜单项的状态,并使用 |
(或)运算 MF_OWNERDRAW
标志,而不是丢失状态。
// a fixed version of the OnInitMenuPopup() function void CODMenu::OnInitMenuPopup(CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu) { if (IsPostCall()) { // iterate any menu about to be displayed and make sure // all the items have the ownerdrawn style set // We receive a WM_INITMENUPOPUP as each menu is //displayed, even if the user // switches menus or brings up a sub menu. This means we only need to // set the style at the current popup level. // we also set the user item data to the index into the menu to allow // us to measure/draw the item correctly later if (pPopupMenu != NULL) { m_menuBeingProcessed = pPopupMenu->m_hMenu; // only valid for measure item calls int itemCount = pPopupMenu->GetMenuItemCount(); for(int item = 0; item < itemCount; item++) { int itemID = pPopupMenu->GetMenuItemID(item); // make sure we do not change the state of the menu items as // we set the owner drawn style MENUITEMINFO itemInfo; memset(&itemInfo, 0, sizeof(MENUITEMINFO)); itemInfo.cbSize = sizeof(MENUITEMINFO); itemInfo.fMask = MIIM_STATE; pPopupMenu->GetMenuItemInfo(item, &itemInfo, TRUE); // by position pPopupMenu->ModifyMenu(item, itemInfo.fState | MF_BYPOSITION | MF_OWNERDRAW, itemID, (LPCTSTR)item); } } } }
系统菜单显示不正确
在调试此插件的过程中,我注意到当我们渲染系统菜单时,我们绘制的项不正确。经过进一步研究,我发现系统菜单似乎从来就没有正确工作过!代码正确地将 MF_OWNERDRAWN
位设置在系统菜单的每个项上,我们正确地收到了所有项的 WM_DRAWITEM
消息,但我们只收到了 2 条 WM_MEASUREITEM
消息,而菜单中有 7 个项。

我们收到消息的项是:恢复和移动。这些菜单字符串比 关闭\tAlt + F4 短得多,这是唯一一个渲染不正确的项(在未修改的系统菜单中)。
嗯,我能找到的唯一解决这个问题的方法是一个小技巧!那就是我们在测量系统菜单项之前,在类中设置一个成员变量标志。
m_bSysMenu = (bSysMenu != FALSE);
然后,在 MeasureItem()
过程结束时,我们检查该标志。如果为 true
,并且我们刚刚完成测量的项的宽度小于具有 关闭\tAlt + F4 文本的项的宽度,那么我就将其宽度设置为该值,以便系统菜单能够正确渲染。
if (m_bSysMenu) { // solve problem with system menu items which we // do not receive a WM_MEASUREITEM for if (lpMIS->itemWidth < m_minSystemMenuWidth) { // set to minimum width for correct draw lpMIS->itemWidth = m_minSystemMenuWidth; } }

默认项目测量不正确

当我添加默认菜单项时(这些项的状态成员中具有 ODS_DEFAULT 样式),这些项从未返回这些项文本的正确长度。我用来测量项的代码是这样的:
if (state & ODS_DEFAULT) pFont = pDC->SelectObject(&m_menuFontBold); else pFont = pDC->SelectObject(&m_menuFont);// Select menu font in...
经过一些调查,结果是我们从未在 OnMeasureItem
函数中收到或能够获得菜单项的正确状态信息。处理此问题的最简单方法是始终使用**粗体**字体来测量所有项。这将使标准菜单项显得略大,但会使默认项正确渲染,我认为类的用户会更喜欢这样。
// always measure using the bold version of the font //if (state & ODS_DEFAULT) pFont = pDC->SelectObject(&m_menuFontBold); //else // pFont = pDC->SelectObject(&m_menuFont); // Select menu font in...
结论
这个插件展示了插件架构的一些强大功能,并提供了一些开发者喜欢在应用程序中拥有的标准功能。它加速了最终产品的开发周期,并以模块化的方式实现。
在此过程中,我阐述了一些我遇到的问题以及如何解决它们。
好了,这个插件就到这里了。这是一次有趣的菜单探索之旅,我也学到了一些不错的枚举函数,并且对菜单有了更好的理解。我曾与 Brent 的拥有者绘制菜单代码搏斗——从我的角度来看,主要问题是他的编码风格。总的来说,整个项目集成得非常好。
希望您喜欢阅读(并使用)这篇文章。
参考文献
以下是制作此插件时使用的一些相关文章列表:
版本历史
- V1.0 2004年5月10日 - 初始发布