在COM/ActiveX中的无模式对话框中支持PreTranslateMessage(以及TAB + ARROW键)






4.63/5 (21投票s)
一个解决PreTranslateMessage在COM/Active-X中的无模式对话框中不被调用的问题。它还解决了Arrow和Tab键在COM/ActiveX中不起作用的问题。

引言
通常,处理PreTranslateMessage
并在其中执行某些操作 - 不被专家推荐。如果您认为自己是专家,并且认为PreTranslateMessage
处理很棒,那么您对专家的定义和我对专家的定义是不同的。然而,我们都必须同意的是,在某些实际情况和场景下,您无法离开PreTranslateMessage
。其中最好的例子是使用Enter键作为Tab键,其中PreTranslateMessage
处理提供了最简单、最快速的解决方案。但是,当您处于令人讨厌的COM工厂中时,您的生活会变得有些困难,因为无模式对话框的PreTranslateMessage
将不会被调用。本文试图提供一个优雅的解决方案来克服这个问题。
为什么在COM/ActiveX中不调用PreTranslateMessage
当您在COM中处理模态对话框时,不会遇到此类问题,但每当您的对话框是无模式的,就会发生这种区别。这是为什么?对此的回答是,通常会说COM或ActiveX控件不拥有消息泵。您可以在此链接上查找更多信息。提供的链接处理的内容与PreTranslateMessage
未被调用(换句话说,同一个故事,不同版本)几乎相同。在我看来,拥有消息泵意味着无论哪个模块正在运行,通过AfxInternalPumpMessage
调用(或类似调用)的Windows消息循环拥有消息泵。对于模态对话框,模态对话框本身通过运行消息循环来拥有消息泵,因此它们可以实现PreTranslateMessage
。无模式对话框遭受这种糟糕的命运,无法获得PreTranslateMessage
的祝福,因为它们不拥有消息泵。
对我来说,仅仅听到COM/ActiveX不是消息泵的拥有者还不够。所以我不得不深入研究这个问题。我的发现是,负责消息泵的人并不知道COM组件或ActiveX中的窗口和对话框对象(注意,我没说句柄,我说的是对象,意味着CWnd
实例)。因此,当负责的模块(通常是您的可执行文件)调用CWnd::FromHandlePermanent(...)
并将COM中的窗口句柄作为参数时,它会收到NULL
,并且无法调用与该窗口/对话框关联的PreTranslateMessage
。我现在知道为什么会返回NULL
,但讨论它超出了本文的范围。
尽管如此,那些好奇心强的人可以调试MFC的整个窗口创建过程(包括CCmdTarget
的创建),并查看通过AfxGetModuleState
在CCmdTarget
中获取的AFX_THREAD_STATE
实例的AFX_MODULE_STATE m_pModuleState
成员,以及线程状态变量的m_pmapHWND
成员以及afxMapHWND
... 我已经说了足够多的复杂术语来劝阻您 :) 深入研究了吗?
*小提示:驻留在COM/ActiveX中的窗口实例不包含在主应用程序线程状态的m_pmapHWND
成员中。
如何绕过它?
为了绕过这个问题,一个想法是使用SetWindowsHook(Ex)
将钩子过程安装到钩子链中,这个解决方案在一些Microsoft站点上也有提供。对我来说,这个解决方案不够优雅,因为您不能直接使用PreTranslateMessage
的功能,而必须编写一些新代码。这会很困难,如果您已经有现有的PreTranslateMessage
代码,并且现在希望它在COM中被调用。
另一种解决方案是在应用程序的GetMessage
/PeekMessage
执行后,以某种方式将消息结构MSG传递给COM。COM收到结构后,需要执行MFC执行的相同操作,将PreTranslateMessage
发送到相关的窗口及其父窗口以及更上层的窗口。对我来说,这似乎非常优雅。CWinApp
在这方面为您提供了一个幸运的帮助,因为在GetMessage
/PeekMessage
之后,CWinApp::PreTranslateMessage
肯定会被调用,并且它包含正确的结构和正确的窗口句柄,即使窗口驻留在COM/ActiveX中。所以,Windows操作系统做得很好。PreTranslateMessage
未在COM对话框/窗口中调用的原因是MFC未能根据窗口句柄找到合适的窗口指针(CWnd*
)。换句话说,当MFC内部机制遍历窗口层次结构以调用PreTranslateMessage
时,那个PreTranslate
...家伙永远不会接听电话,因为CWnd::FromHandlePermanent(...)
给了他一个致命的打击,让他彻底失败。
所以,这是我们必须完成的工作。我们需要在主应用程序和COM窗口(CWinApp
中PreTranslateMessage
的启动者)之间建立一个握手机制,以便PreTranslateWindow
中的MSG
结构能够以某种方式被传输并传播到父窗口层次结构的顶层。所以,让我们让应用程序和COM相互通信以建立连接。
让我们开始交谈
为了实现上面所说的,我们可以发送一个COM和主应用程序都通用的消息。因此,我们可以创建一个注册的用户消息,如下所示
const UINT RWM_PRETRANSLATEMSG = ::RegisterWindowMessage(_T("RWM_PRETRANSLATEMSG"));
此消息可以被视为COM及其宿主应用程序都能识别的握手机制。从应用程序方面,我们可以按照以下描述将MSG
结构传递给COM
BOOL CTestAppApp::PreTranslateMessage(MSG* pMsg)
{
HWND hWndParent = AppGetTopParent(pMsg->hwnd);
if(::SendMessage(hWndParent, RWM_PRETRANSLATEMSG, 0, (LPARAM)pMsg) == TRUE)
{
return TRUE;
}
return CWinApp::PreTranslateMessage(pMsg);
}
我们在这里做什么 - 从负责消息调用的窗口句柄开始,检索不具有子窗口样式的最顶层父窗口。显然,这个窗口是一个独立的对话框或弹出窗口,它将首先接收到注册消息和传递在LPARAM
中的相关MSG
结构。
这就是应用程序方面的全部内容。
现在是更有趣的部分。如何处理传递的参数以及如何在COM端进行处理。直观的用户已经可以从前面的讨论中猜到 - 我要做什么。很简单,只需在COM中捕获此消息,并执行类似于我们在BOOL PASCAL CWnd::WalkPreTranslateTree(HWND hWndStop, MSG* pMsg)
中看到的的操作,以遍历需要调用PreTranslateMessage
的窗口链,并简单地调用需要调用的内容。
让我们付诸行动
为了捕获这些,我们可以使用已故的Paul DiLascia简洁而设计精良的CSubclassWnd
类作为CPreTranslateMsgHook
的基础,它将为我们完成这项工作。它将捕获RWM_PRETRANSLATEMSG
消息,提取作为LPARAM
传递的MSG结构,并将消息传播到目标窗口,如下所示
LRESULT CPreTranslateMsgHook::WindowProc(UINT msg, WPARAM wp, LPARAM lp)
{
if(msg == RWM_PRETRANSLATEMSG)
{
BOOL bRet = FALSE;
MSG* pMsg = (MSG*)(lp);
ASSERT(pMsg);
CWnd* pWnd = CWnd::FromHandlePermanent(m_hWnd);
if(pWnd != NULL)
{
bRet = WalkPreTranslateMsg(pWnd->GetSafeHwnd(), pMsg);
}
return bRet;
}
return Default();
}
WalkPreTranslateMsg
是按照其名称所暗示的进行遍历的方法,其中的代码只是您在BOOL PASCAL CWnd::WalkPreTranslateTree(HWND hWndStop, MSG* pMsg)
中看到的内容的复制。我将省去您浏览乱七八糟的MFC代码,直接将代码粘贴在这里
ASSERT(hWndStop == NULL || ::IsWindow(hWndStop));
ASSERT(pMsg != NULL);
// walk from the target window up to the hWndStop window checking
// if any window wants to translate this message
for (HWND hWnd = pMsg->hwnd; hWnd != NULL; hWnd = ::GetParent(hWnd))
{
CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);
if (pWnd != NULL)
{
// target window is a C++ window
if (pWnd->PreTranslateMessage(pMsg))
return TRUE; // trapped by target window (eg: accelerators)
}
// got to hWndStop window without interest
if (hWnd == hWndStop)
break;
}
return FALSE; // no special processing
从代码中可以明显看出,遍历从消息队列中发布消息的窗口开始,直到达到顶层父窗口。在遍历过程中,遍历器还会调用CWnd::FromHandlePermanent
,而这一次,为了天哪,它成功了 - 因为COM本身就了解它包含的窗口。对于高级读者来说,这是另一种说法,COM模块线程状态的m_pmapHWND成员包含在句柄与永久窗口指针映射中的相应窗口指针。
不用说,只需要对顶层父窗口进行子类化/钩挂即可通过我们自己的CPreTranslateMsgHook
来启动遍历。
在代码中使用CPreTranslateMsgHook
我们已经说了很多,也付诸行动了。现在我们只需要使用代码,只需要两行。为了在COM对话框(当然是无模式的!)中启用pretranslatemessage
支持,您只需创建钩子并安装钩子。看,很简单
m_pHook = new CPreTranslateMsgHook();
m_pHook->HookWindow(GetSafeHwnd());
您可以在OnInitDialog
覆盖函数中调用这两行代码,或者在通过Create
API创建对话框后立即调用。
顺便说一句,实际上是三行(不是两行:),因为您需要在使用new
运算符实例化的钩子被销毁后delete
它。
关注点
这个PreTranslateMessage
调用解决方案也解决了COM/ACTIVE-X中无模式对话框中的TAB和ARROW键不起作用的问题,进一步证明了所提出的方法的有效性和优雅性。
所提出解决方案的缺点是,您自己必须拥有COM模块和主应用程序源代码的权限。所以,通常情况下,当您试图将一些现有代码移植到COM,并且可以修改主应用程序代码时,您会应用这个解决方案。然而,即使您没有主应用程序的源代码,您也可以通过在COM中安装消息钩子(WH_GETMESSAGE
)来使用一个简单的变通方法,并将钩子过程视为主应用程序的PretranslateMessage
。然后,您可以按照上面提到的相同过程发送一个注册消息(参见CTestAppApp::PreTranslateMessage
代码),让CPreTranslateMsgHook
类像以前一样处理其余的事情。这种修改应该只需要在COM部分增加几行代码。
致谢
所有的功劳都归于不太为人所知的Jacques Raphanel先生(也许他喜欢独自一人和安静),因为是他向我介绍了这种实现看似非常复杂的事情的简单技术。
也感谢已故的Paul DiLascia提供的CSubclassWnd
类。
历史
- 文章上传日期:2011年6月16日