两行代码实现所有者绘制菜单






4.78/5 (27投票s)
另一种实现所有者绘制菜单的方法,只需您作为编码人员编写两行代码。
目录
- 快速入门指南
- 引言
- 入门
- 问题,问题
- 多个
WM_INITMENUPOPUP
消息 - Shell 文件上下文菜单 - 例如,在“另存为”对话框中右键单击
- 其他注意事项
- TPM_NONOTIFY 风格
- 拦截操作系统
- 编译器问题
- 版本历史
快速入门指南
要开始将此功能用于您的应用程序,请按照以下步骤操作
- 将 ODMenu.h/.cpp、EnumerateLoadedModules.h/cpp 文件添加到您的应用程序中。
- 将这两行代码添加到您的
CMainFrame
类中#include "ODMenu.h" CODMenu m_ownerMenu; // member variable
- 将 PSAPI.lib 添加到您应用程序的链接信息中。
- 重新生成您的应用程序。
引言
我正在查看我的一份旧项目,该项目正在被重新开发,并且对它实现所有者绘制菜单的方式不太满意。在开发时,我对这个领域没有太多经验,并使用了 Brent Corkum 的 带位图的酷所有者绘制菜单 - 3.03 版 类。在修复了一些(小的)问题并与动态菜单项进行了大量斗争后,我让系统运行起来了。但这是应用程序中一个丑陋(且易于出错)的区域,现在很少有人敢于涉足。*颤抖*
我之前还为我的系列文章开发了一个插件式所有者菜单处理器。虽然我的应用程序使用了这项技术的非常老的变体,但将其转换为与新方法一起运行的工作量太大了,所以我想到:“如果我修改我的所有者绘制菜单方法,使其挂钩所有窗口并完成所有工作,这样我就可以将我的所有应用程序菜单代码恢复为 CMenu
?”。因此,我满怀热情地开始了我的新旅程。
入门
项目的第一个阶段是学习 Windows 挂钩。这是我以前从未接触过的领域,所以这将是一次学习经历。第一站是 MSDN,然后是 CodeProject VC++ 论坛,当我需要有关未正确工作或不如预期工作的目标区域的帮助时。
在初步查阅了文档后,我认为我只需要挂钩我的主应用程序 UI 线程,使用 WH_CALLWNDPROC
挂钩类型。从那里,我可以拦截 WM_INITMENUPOPUP
、WM_MEASUREITEM
和 WM_DRAWITEM
这三个消息,并将它们映射到我原来的插件式所有者绘制菜单代码。
void CODMenu::InstallHook() { m_hookHandle = SetWindowsHookEx( WH_CALLWNDPROC, (HOOKPROC)CODMenu::HookFunction, AfxGetResourceHandle(), GetCurrentThreadId()); } void CODMenu::UninstallHook() { VERIFY(::UnhookWindowsHookEx(m_hookHandle)); }
我想,“搞定”。
但生活并非如此简单,Windows 也不如此。
第一个明显(对我而言)的问题是,WH_CALLWNDPROC
挂钩在实际目标窗口处理消息之前被调用。如果查看代码,OnInitMenuPopup()
处理程序如下所示:
void CODMenu::OnInitMenuPopup(HMENU hMenu, UINT nIndex, BOOL bSysMenu) { UNREFERENCED_PARAMETER(nIndex); // 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 (hMenu != NULL) { m_bSysMenu = (bSysMenu != FALSE); m_menuBeingProcessed = hMenu; // only valid for measure item calls int itemCount = ::GetMenuItemCount(hMenu); for (int item = 0; item < itemCount; 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 | MIIM_TYPE; VERIFY(::GetMenuItemInfo(hMenu, item, TRUE, // by position &itemInfo)); int itemID = ::GetMenuItemID(hMenu, item); if ((itemInfo.fType & MFT_SEPARATOR) == 0) { ::ModifyMenu(hMenu, item, itemInfo.fState | MF_BYPOSITION | MF_OWNERDRAW, itemID, (LPCTSTR)item); } else { ::ModifyMenu(hMenu, item, MF_BYPOSITION | MF_OWNERDRAW | MF_SEPARATOR, itemID, (LPCTSTR)item); } } } }
此代码将所有者绘制样式 MF_OWNERDRAW
添加到每个菜单项,以便我们能够接收 WM_MEASUREITEM
和 WM_DRAWITEM
消息。还请注意,它确保我们不会清除 MF_SEPARATOR
样式,因为这样做会使分隔符可以通过键盘(但不能通过鼠标)选择。由于这发生在标准的 MFC 代码处理消息之前(由于挂钩样式),这意味着在处理 WM_INITMENUPOPUP
消息期间添加的任何项都不会设置所有者绘制样式。这将导致同一个菜单中出现所有者绘制和非所有者绘制的项的组合。这看起来会很难看。
通常,您会在“文件”菜单的“最近使用的文件”列表或“窗口”菜单的“打开文档”列表中看到这种情况。
这是我们问题的简单修复方法,我们只需切换到 WH_CALLWNDPROCRET
挂钩方法,因为它在应用程序完成处理消息后被调用。
这样第一个问题就解决了。下一个问题是,无论我做什么,WM_MEASUREITEM
处理程序虽然被调用,但从未设置实际的菜单项宽度/高度,因为 WH_CALLWNDPROC
/WM_CALLWNDPROCRET
挂钩函数旨在确保您无法修改消息及其任何参数。我得到的 LPMEASUREITEMSTRUCT
指针指向另一个对象,这意味着我的所有菜单项都未获得正确的宽度/高度,因此我的菜单项默认大小为 12 * 16 像素。这看起来很难看。
问题,问题
因此,我回到了 MSDN 文档,试图找到一个允许我修改消息参数的挂钩函数。唯一似乎允许我这样做并获得我想要的邮件类型的挂钩是 WH_GETMESSAGE
。所以我使用此挂钩设置了第二个应用程序 UI 线程挂钩。然后,我为 WM_MEASUREITEM
和 WM_DRAWITEM
添加了正确的映射。我期望取得辉煌的成果。我没想到 Windows 会从中作梗。
此挂钩几乎获取了所有消息,除了我想要的两个。从未收到,什么也没有,零。呜呜呜。
这时,我以为我的超级方法已经死了。无论我尝试哪种挂钩类型,都没有成功。我诉诸于 VC 论坛,开始提出一些看似困难的问题。我得到了一些回复,其中一个如下所示:
挂钩通常很糟糕。最好像这样对窗口进行子类化:
oldWndProc = (WNDPROC)SetWindowLong(hwnd, GWL_WNDPROC, (LONG)NewWndProc);
如果我使用这种方法,我实际上会子类化拥有菜单的窗口。我可以这样做足够长的时间来处理菜单的测量/绘制调用,然后一旦菜单不再显示就取消子类化。这样做可以避免我们必须保留 HWND
到旧 WNDPROC
指针的句柄映射。由于一个应用程序一次只能显示一个菜单,因此这不会有问题。
因此,在进行了一些快速工作后,我的挂钩函数如下所示:
LRESULT CALLBACK CODMenu::HookFunction(int code, WPARAM wParam, LPARAM lParam) { CWPRETSTRUCT * cwpretStruct = (CWPRETSTRUCT*)(lParam); ASSERT(m_activeObject != NULL); switch (cwpretStruct->message) { case WM_INITMENUPOPUP: { TRACE("WM_INITMENUPOPUP\n"); m_activeObject->OnInitMenuPopup( (HMENU)cwpretStruct->wParam, LOWORD(cwpretStruct->lParam), (BOOL)HIWORD(cwpretStruct->lParam)); if (m_oldWndProc == NULL) { // hook the window message queue so // we can handle the WM_DRAWITEM/WM_MEASUREITEM messages m_oldWndProc = (WNDPROC)SetWindowLong( cwpretStruct->hwnd, GWL_WNDPROC, (LONG)MenuWndProc); TRACE("Replaced WndProc\n"); } m_activeMenuLayers++; } break; case WM_UNINITMENUPOPUP: { TRACE("WM_UNINITMENUPOPUP\n"); m_activeMenuLayers--; if (m_activeMenuLayers == 0) { // restore the old wndProc m_oldWndProc = (WNDPROC)SetWindowLong( cwpretStruct->hwnd, GWL_WNDPROC, ( LONG)m_oldWndProc); m_oldWndProc = NULL; TRACE("Restored WndProc\n"); } } break; } return ::CallNextHookEx(m_hookHandle, code, wParam, lParam); }
一个有趣的地方是计数器 m_activeMenuLayers
的使用。我们需要它,因为我们不希望在具有许多弹出层菜单的窗口上进行多重子类化。对于用户进入的每个额外弹出窗口,我们都会对其进行计数。我们接收每一个1 WM_INITMENUPOPUP
消息,对应于每个显示出来的弹出级别,以及每一个1 WM_UNINITMENUPOPUP
消息,对应于每个隐藏的弹出级别。我们只需要跟踪当前有多少弹出层正在使用。当计数变为 0 时,我们可以取消子类化窗口。
1 我后来发现这是不正确的。有些菜单不遵循此规则,您会收到两个(或更多) WM_INITMENUPOPUP
消息,用于第一个菜单层,对应单个 WM_UNINITMENUPOPUP
消息。这会导致菜单代码因认为仍有活动的菜单层正在显示而无法取消子类化窗口。这会导致非常糟糕的屏幕重绘问题,并可能导致非常严重的崩溃。
多个 WM_INITMENUPOPUP 消息
为了解决这个多次接收 WM_INITMENUPOPUP
消息的问题,我首先需要找到一个每次显示菜单时都会发生这种情况的案例。我最终找到了一个在标准的 MFC “另存为”对话框中。如果您单击下拉箭头以更改列表控件中文件名显示方式,则会显示一个小型弹出菜单。对于此菜单,我收到了两个 WM_INITMENUPOPUP
消息,之后应用程序中的任何菜单都无法正确绘制。我还注意到此对话框中的另一个问题 - 如果您右键单击文件以获取上下文菜单,Windows 会提供一个带有预设图标的 shell 菜单。此菜单根本无法正确渲染,并且通常会导致 GPF2。
2 我将在下一节中展示我如何解决这个问题。
我对这个多次消息问题进行了初步分析,似乎没有找到任何解决办法的希望。代码需要子类化窗口,但似乎无法区分有效的 WM_INITMENUPOPUP
消息和无效的消息。由于我在一个即将发布的应用程序中使用了此代码的变体,因此被认为优先级非常高,如果找不到解决方案,所有 OD 菜单代码都将被禁用,无法发布。这似乎是最有可能的行动方案,直到我注意到一个细节。在两个初始化消息中传递的 HMENU
句柄是不同的,并且它们也不是任何包含的弹出菜单的 HMENU
。
一旦我意识到这一点,我就能设置一个测试来识别无效消息。我只需要构建一个当前和子菜单的有效 HMENU
句柄列表,用于正在显示的菜单,如果它不在列表中,那么它就是无效消息!
void CODMenu::BuildSubMenuList(HMENU hMenu) { m_subMenus.clear(); // iterate the menu and get the HMENUs for all popups upder the current one AddToList(hMenu); } void CODMenu::AddToList(HMENU hMenu) { int itemCount = ::GetMenuItemCount(hMenu); for (int item = 0; item < itemCount; item++) { MENUITEMINFO itemInfo; memset(&itemInfo, 0, sizeof(MENUITEMINFO)); itemInfo.cbSize = sizeof(MENUITEMINFO); itemInfo.fMask = MIIM_SUBMENU; ::GetMenuItemInfo(hMenu, item, TRUE, &itemInfo); if (itemInfo.hSubMenu != NULL) { // do a recursive call on this popup menu m_subMenus.push_back(itemInfo.hSubMenu); // recursive call to get any sub menus under this one AddToList(itemInfo.hSubMenu); } } } bool CODMenu::SubMenuPresentInList(HMENU hMenu) const { bool found = false; for (size_t index = 0 ; index < m_subMenus.size() && !found ; ++index) { if (m_subMenus[index] == hMenu) { // its present in the list, ok to go! found = true; } } return found; }
因此,我们在类中添加了三个新的私有函数,一个用于构建列表,另一个用于递归任何给定的菜单,还有一个用于检查给定的 HMENU
是否存在于列表中。这使得代码能够正确检查不良消息,并解决了第一个问题。
Shell 文件上下文菜单 - 例如,在“另存为”对话框中右键单击
正如您在上面的图片中看到的,Shell 上下文菜单包含由其他应用程序和 Shell 插件提供的图标。当我第一次遇到 ODMenu 代码绘制此菜单时遇到问题时,我试图让它正确渲染此菜单。有几个问题:
- 分隔符被绘制为菜单项 - 这是因为菜单为分隔符提供了非零 ID。ODMenu 代码检查 ID,仅当 ID 为 0 时才将其渲染为分隔符。这可以通过获取菜单项的样式并检查
MFT_SEPARATOR
标志来轻松修复。这将使它们正确渲染。 - 我无法保证菜单文本的 ASCII 版本 - Shell 以 UNICODE 运行。代码使用了
::GetMenuString
函数的 ANSI 版本,然后内部将其文本转换为 UNICODE。这导致菜单项的文本损坏或根本没有文本。我将所有代码转换为内部使用 UNICODE。这解决了 70% 的菜单文本问题。有些菜单项似乎使用回调来获取菜单文本,但我无法获取,因此没有文本被渲染。这尤其适用于“发送到”菜单项。 - 系统提供的图标 - 系统为许多菜单项旁边的图标提供支持,菜单 ID 也会随机匹配应用程序中使用的 ID 并显示不正确的图标。我最初试图渲染系统提供的图标,并且还重新编号了我的菜单 ID,以便它们不与系统菜单冲突。
MENUITEMINFO
对象应该能够返回要渲染在菜单项旁边的图标的HBITMAP
,但这需要您使用WINVER
定义为0x0500
的方式编译代码。这样做会导致编译器发出许多关于此类代码不允许向公众发布的警告。没有它,您得不到所需的HBITMAP
;有了它,您就无法发布代码。我找不到解决此问题的方法。
在识别并仅部分解决了 Shell 菜单的这些问题后,内部决定最好是将所有 Shell 菜单排除在所有者绘制之外。因此,必须找到一种识别这些类型菜单的方法。我认为唯一的方法是,分隔符具有非零菜单 ID!因此,我们只需要扫描菜单以查看它是否具有这些。如果有,我们就忽略它,不将其设置为所有者绘制。
bool CODMenu::IgnoreThisMenu(HMENU hMenu) { // if we receive a menu which has separators // which have a non-zero value, then its // a menu we should not make ownerdrawm // determine whether this is such a menu bool ignoreMenu = false; int itemCount = ::GetMenuItemCount(hMenu); for (int item = 0; item < itemCount && !ignoreMenu; item++) { MENUITEMINFO itemInfo; memset(&itemInfo, 0, sizeof(MENUITEMINFO)); itemInfo.cbSize = sizeof(MENUITEMINFO); itemInfo.fMask = MIIM_TYPE | MIIM_ID | MIIM_SUBMENU; ::GetMenuItemInfo(hMenu, item, TRUE, &itemInfo); int itemID = itemInfo.wID; if ((itemInfo.fType & MFT_SEPARATOR) != 0 && itemID != 0) { // this is a menu type we need to ignore ignoreMenu = true; } if (itemInfo.hSubMenu != NULL) { // do a recursive call on this popup menu ignoreMenu = IgnoreThisMenu(itemInfo.hSubMenu); } } return ignoreMenu; }
在对挂钩函数进行了所有更改后,它现在如下所示:
LRESULT CALLBACK CODMenu::HookFunction(int code, WPARAM wParam, LPARAM lParam) { CWPRETSTRUCT * cwpretStruct = (CWPRETSTRUCT*)(lParam); ASSERT(m_activeObject != NULL); switch (cwpretStruct->message) { case WM_INITMENUPOPUP: { if (!m_ignoredMenu) { m_ignoredMenu = IgnoreThisMenu((HMENU)cwpretStruct->wParam); if (!m_ignoredMenu) { m_activeObject->OnInitMenuPopup( (HMENU)cwpretStruct->wParam, LOWORD(cwpretStruct->lParam), (BOOL)HIWORD(cwpretStruct->lParam)); if (m_oldWndProc == NULL) { // hook the window message queue so we can handle // the WM_DRAWITEM/WM_MEASUREITEM messages m_oldWndProc = (WNDPROC)SetWindowLong( cwpretStruct->hwnd, GWL_WNDPROC, (LONG)MenuWndProc); m_activeObject->BuildSubMenuList( (HMENU)cwpretStruct->wParam); } } } // make sure its not a bogus WM_INITMENUPOPUP message if (m_activeMenuLayers == 0 || m_activeObject->SubMenuPresentInList((HMENU)cwpretStruct->wParam)) { // count the active layer m_activeMenuLayers++; } } break; case WM_UNINITMENUPOPUP: { m_activeMenuLayers--; if (m_activeMenuLayers == 0) { // restore the old wndProc if (!m_ignoredMenu) { m_oldWndProc = (WNDPROC)SetWindowLong( cwpretStruct->hwnd, GWL_WNDPROC, (LONG)m_oldWndProc); m_oldWndProc = NULL; } m_ignoredMenu = false; } } break; } return ::CallNextHookEx(m_hookHandle, code, wParam, lParam); }
其他注意事项
由于我希望此类非常易于使用,因此我不希望用户担心加载所有工具栏资源以获取所有者绘制菜单的正确图像。我需要代码能够自行获取这些。现在,我不太清楚目标应用程序环境会是什么样的。我必须做以下假设:
- 应用程序可能由 EXE 和其他 DLL 组成。(这就像我的工作项目。)
- 在我们需要枚举工具栏之前,所有 DLL 都已经加载。
因此,考虑到这两个要求,我着手开发一个辅助类,该类将为我提供当前应用程序所有已加载模块的列表。这很容易做到,因为我可以使用 PSAPI.DLL 提供的标准系统功能。这可能有一个问题,即它可能在旧的操作系统版本上不受支持。在这种情况下,有一个不同的 API 可用,类似于 TOOLWIN32(我忘记了实际名称)。
但对我来说,PSAPI 就足够了。
class EnumerateLoadedModules { public: EnumerateLoadedModules(); ~EnumerateLoadedModules(); int Count() const; HMODULE GetModuleHandle(int index) const; CString GetModuleFilename(int index) const; private: void Enumerate(); std::vector<HMODULE> m_loadedModuleHandles; std::vector<CString> m_loadedModuleFilenames; }; // EnumerateLoadedModules.cpp: implementation // of the EnumerateLoadedModules class. // ///////////////////////////////////////////////////////// #include "stdafx.h" #include "EnumerateLoadedModules.h" #include <Psapi.h> ///////////////////////////////////////////////////////// // Construction/Destruction ///////////////////////////////////////////////////////// EnumerateLoadedModules::EnumerateLoadedModules() { Enumerate(); } EnumerateLoadedModules::~EnumerateLoadedModules() { } void EnumerateLoadedModules::Enumerate() { DWORD currentProcessId = GetCurrentProcessId(); HANDLE hProcess = OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, currentProcessId); m_loadedModuleHandles.clear(); m_loadedModuleFilenames.clear(); if (hProcess) { HMODULE hMod; DWORD cbNeeded; if (EnumProcessModules( hProcess, &hMod, sizeof(hMod), &cbNeeded)) { int numModules = cbNeeded / sizeof(HMODULE); if (numModules > 0) { HMODULE * modules = new HMODULE[numModules]; EnumProcessModules( hProcess, modules, sizeof(HMODULE) * numModules, &cbNeeded); for (int moduleIndex = 0 ; moduleIndex < numModules ; ++moduleIndex) { char moduleFilename[MAX_PATH]; m_loadedModuleHandles.push_back(modules[moduleIndex]); if (GetModuleFileNameEx( hProcess, modules[moduleIndex], moduleFilename, MAX_PATH)) { m_loadedModuleFilenames.push_back(moduleFilename); TRACE("%s\n", moduleFilename); } } delete []modules; } } CloseHandle(hProcess); } } int EnumerateLoadedModules::Count() const { return m_loadedModuleHandles.size(); } HMODULE EnumerateLoadedModules::GetModuleHandle(int index) const { ASSERT(index >= 0 && index < m_loadedModuleHandles.size()); return m_loadedModuleHandles[index]; } CString EnumerateLoadedModules::GetModuleFilename(int index) const { ASSERT(index >= 0 && index < m_loadedModuleHandles.size()); return m_loadedModuleFilenames[index]; }
这是一个小的辅助类,ODMenu 代码在其构造函数中使用它。一旦我们有了模块列表,我们就可以枚举该模块中的各个工具栏并加载它们(如果它们大小正确)。
void CODMenu::EnumerateAndLoadToolbars() { // load all the toolbars from all loaded modules (exe files/dll files) EnumerateLoadedModules modules; for (int moduleIndex = 0 ; moduleIndex < modules.Count() ; ++moduleIndex) { TRACE("Enumerating file %s\n", modules.GetModuleFilename(moduleIndex)); EnumResourceNames( modules.GetModuleHandle(moduleIndex), RT_TOOLBAR, (ENUMRESNAMEPROC)EnumResNameProc, 0); } // 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); }
完成这些后,我们就拥有了所有需要的信息。
TPM_NONOTIFY 风格
在对该类进行进一步测试时,我注意到当使用 TPM_NONOTIFY
样式显示菜单时,我们不会收到 WM_INITMENUPOPUP
或 WM_UNINITMENUPOPUP
消息。这意味着任何控件(如编辑控件)使用的上下文菜单都无法设置为所有者绘制。这给用户界面带来不一致的感觉。所以我着手解决这个问题。
第一次尝试是使用已经使用的窗口挂钩方法。我们可以捕获窗口类 #32768
的 WM_CREATE
消息,这是大多数菜单类型(但不是全部)的标准类名。这种方法的缺点是您有一个 HWND
而不是 HMENU
句柄。我试图找到一种从菜单窗口句柄到其菜单句柄的方法,但无法从一个遍历到另一个。论坛和 Google 搜索的结果是寂静无声。我确实找到了一系列关于这个主题的帖子,但它们也没有解决问题。我放弃了这种方法,认为它是死胡同。
那么,我该如何继续呢?我唯一想到的是,在任何调用 TrackPopupMenu
/TrackPopupMenuEx
时,HMENU
句柄都将可用。那么,我能否拦截这些操作系统调用并有效地获取 HMENU
句柄?我认为这可能会奏效。
拦截操作系统
好吧,快速搜索互联网发现了一个非常 合适的技术,它允许我拦截操作系统调用,进行额外的菜单处理,然后将调用传回给操作系统函数。这种拦截方法使用了一种称为“蹦床”的技术。它通过在内存中找到目标函数,更改该内存区域的访问权限以便我们可以修改代码来工作。恐怖 - 自我修改代码! 然后,我们将前五个(或更多,取决于函数)字节复制到蹦床函数中,并插入一个相对跳转指令,使操作系统代码的开头跳转到我们的函数。
只要我们的拦截函数与操作系统函数具有相同的调用约定和参数列表,我们就可以做我们想做的事情,然后调用蹦床函数,该函数实际上运行那前五个指令字节并跳转回操作系统代码以继续执行。
编译器问题
这项技术看起来应该有效,但每次我拦截操作系统调用时都会失败。在查看我正在生成的函数的汇编源代码后,编译器似乎生成了错误的代码。如果您获取蹦床函数的地址并查看汇编源代码,它会以一个相对跳转到实际函数开头。为什么?这是实际函数的起始地址,为什么还要有这个额外的跳转阶段?
嗯,这意味着我必须将我的蹦床目标地址转换为实际跳转指令的目标。问题是蹦床函数至少需要 10 个字节的长度。这是为了拦截的操作系统函数的五个(或更多)原始指令字节,以及在末尾添加的相对跳转指令,以跳回到实际的操作系统代码(五个字节)。由于编译器给了我“伪造”的函数起始地址(五个字节长度),它跳转到实际函数,如果我将蹦床复制到那个位置,我就会破坏下一个五个字节的任何函数。
有了这个添加到 InterceptAPI
函数的转换代码,我们使用这段代码来拦截操作系统调用:
void CODMenu::InstallHook() { m_hookHandle = SetWindowsHookEx( WH_CALLWNDPROCRET, (HOOKPROC)CODMenu::HookFunction, AfxGetResourceHandle(), GetCurrentThreadId()); InterceptAPI( AfxGetResourceHandle(), "User32.dll", // module function is in "TrackPopupMenu", // function to intercept (DWORD)CODMenu::TrackPopupMenu, // our intercept function (DWORD)CODMenu::TrampolineTrackPopupMenu,// trampoline function 5); // target bytes to copy InterceptAPI( AfxGetResourceHandle(), "User32.dll", "TrackPopupMenuEx", (DWORD)CODMenu::TrackPopupMenuEx, (DWORD)CODMenu::TrampolineTrackPopupMenuEx, 5); } BOOL CODMenu::InterceptAPI( HMODULE hLocalModule, const char* c_szDllName, const char* c_szApiName, DWORD dwReplaced, DWORD dwTrampoline, int offset) { int i; DWORD dwOldProtect; DWORD dwAddressToIntercept = (DWORD)GetProcAddress( GetModuleHandle((char*)c_szDllName), (char*)c_szApiName); BYTE *pbTargetCode = (BYTE *) dwAddressToIntercept; BYTE *pbReplaced = (BYTE *) dwReplaced; BYTE *pbTrampoline = (BYTE *) dwTrampoline; // Change the protection of the trampoline region // so that we can overwrite the first 5 + offset bytes. if (*pbTrampoline == 0xe9) { // target function starts with an relative jump // change trampoline to the target of the jump pbTrampoline++; int * pbOffset = (int*)pbTrampoline; pbTrampoline += *pbOffset + 4; } VirtualProtect((void *) pbTrampoline, 5+offset, PAGE_WRITECOPY, &dwOldProtect); for (i=0;i<offset;i++) *pbTrampoline++ = *pbTargetCode++; pbTargetCode = (BYTE *) dwAddressToIntercept; // Insert unconditional jump in the trampoline. *pbTrampoline++ = 0xE9; // jump rel32 *((signed int *)(pbTrampoline)) = (pbTargetCode+offset) - (pbTrampoline + 4); VirtualProtect((void *) dwTrampoline, 5+offset, PAGE_EXECUTE, &dwOldProtect); // Overwrite the first 5 bytes of the target function VirtualProtect((void *) dwAddressToIntercept, 5, PAGE_WRITECOPY, &dwOldProtect); // check to see whether we need to translate the pbReplaced pointer if (*pbReplaced == 0xe9) { // target function starts with an relative jump // change to target of the jump pbReplaced++; int * pbOffset = (int*)pbReplaced; pbReplaced += *pbOffset + 4; } *pbTargetCode++ = 0xE9; // jump rel32 *((signed int *)(pbTargetCode)) = pbReplaced - (pbTargetCode +4); VirtualProtect((void *) dwAddressToIntercept, 5, PAGE_EXECUTE, &dwOldProtect); // Flush the instruction cache to make sure // the modified code is executed. FlushInstructionCache(GetCurrentProcess(), NULL, NULL); return TRUE; }
因此,我只需要将要添加到我的新拦截函数中的代码:
BOOL WINAPI CODMenu::TrackPopupMenu( HMENU hMenu, // handle to shortcut menu UINT uFlags, // options int x, // horizontal position int y, // vertical position int nReserved, // reserved, must be zero HWND hWnd, // handle to owner window CONST RECT *prcRect // ignored ) { bool hooked = false; if (uFlags & TPM_NONOTIFY) { m_activeObject->m_menuBeingProcessed = hMenu; CMenu menu; menu.Attach(hMenu); m_activeObject->OnInitMenuPopup(&menu, 0, FALSE); menu.Detach(); // hook the window message queue // so we can handle the WM_DRAWITEM/WM_MEASUREITEM messages m_oldWndProc = (WNDPROC)SetWindowLong(hWnd, GWL_WNDPROC, (LONG)MenuWndProc); hooked = true; } BOOL ret = TrampolineTrackPopupMenu(hMenu, uFlags, x, y, nReserved, hWnd, prcRect); if (hooked) { // restore the old wndProc m_oldWndProc = (WNDPROC)SetWindowLong(hWnd, GWL_WNDPROC, (LONG)m_oldWndProc); m_oldWndProc = NULL; } return ret; } BOOL WINAPI CODMenu::TrackPopupMenuEx( HMENU hMenu, // handle to shortcut menu UINT fuFlags, // options int x, // horizontal position int y, // vertical position HWND hwnd, // handle to window LPTPMPARAMS lptpm // area not to overlap ) { bool hooked = false; if (fuFlags & TPM_NONOTIFY) { m_activeObject->m_menuBeingProcessed = hMenu; CMenu menu; menu.Attach(hMenu); m_activeObject->OnInitMenuPopup(&menu, 0, FALSE); menu.Detach(); // hook the window message queue so we can handle // the WM_DRAWITEM/WM_MEASUREITEM messages m_oldWndProc = (WNDPROC)SetWindowLong(hwnd, GWL_WNDPROC, (LONG)MenuWndProc); hooked = true; } BOOL ret = TrampolineTrackPopupMenuEx(hMenu, fuFlags, x, y, hwnd, lptpm); if (hooked) { // restore the old wndProc m_oldWndProc = (WNDPROC)SetWindowLong(hwnd, GWL_WNDPROC, (LONG)m_oldWndProc); m_oldWndProc = NULL; } return ret; }
它奏效了!诸如 CEdit
之类的控件的上下文菜单被设置为所有者绘制。通过运行演示应用程序并使用“关于”框中编辑控件的上下文菜单可以看到这一点。
历史
V1.2 2005 年 2 月 1 日
- 图标透明度问题取决于操作系统版本。
- 无效的
WM_INITMENUPOPUP
消息。 - Shell 上下文菜单。
- 可能不正确地处理了未指向菜单的绘制/测量消息。
- 通用代码清理和合理化。
- 现在仅支持 XP 绘制样式 - 您需要自己添加其他绘制样式(抱歉)。
- 重复按钮 ID 时的图像索引问题。
V1.1 2004 年 11 月 29 日
- 已修复调用目标窗口的实际
WndProc
函数时可能发生的崩溃。 - 已升级以允许拦截上下文菜单并将其设置为所有者绘制。
- 修复了“关于”框中缺失的
ClientToScreen
函数(感谢 Dido2k)。 - 修复了分隔符可通过键盘选择的错误(感谢 David Simmonds)。
V1.0 2004 年 11 月 2 日
- 代码的初始发布。