65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.63/5 (21投票s)

2011年6月16日

CPOL

8分钟阅读

viewsIcon

68297

downloadIcon

1331

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

screenshot.JPG

引言

通常,处理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窗口(CWinAppPreTranslateMessage 的启动者)之间建立一个握手机制,以便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日
© . All rights reserved.