Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

PreTranslateMessage (and TAB + ARROW key) Support in Modeless Dialogs inside COM/ActiveX

0.00/5 (No votes)
21 Jun 2011 1  
A solution to the problem of PreTranslateMessage not being called for modeless dialogs inside COM/Active-X. It also addresses the problem of Arrow and Tab keys not working inside COM/ActiveX
screenshot.JPG

Introduction

Usually, handling PreTranslateMessage and doing something inside it - is not recommended by experts. If you think you are an expert and think PreTranslateMessage handling is cool, then your definition of expert and my definition of the same is varied. However, what we both must agree upon is, there are some practical cases and situations where you cannot live without PreTranslateMessage. The best example of it is using Enter as Tab where PreTranslateMessage handling offers the easiest of solutions in quickest quanta of time. But, when you are inside the dreaded COM factory, your life becomes a little difficult since PreTranslateMessage for modeless dialogs will not be called. This article tries to provide an elegant solution to overcome the problem.

Why PreTranslateMessage is not called inside COM/ActiveX

When you are dealing with Modal dialogs inside COM, you don't face such problems, but this discrimination happens whenever your dialog is modeless. Why is that? In answer to that, it is often said, the COM or the ActiveX control doesn't own the message pump. You might look up at this link for more information. The link provided deals with nearly the same thing as PreTranslateMessage not being invoked (in other words same story, different version). In my understanding, what this owning of the message pump means is whatever module is running, the windows message loop through AfxInternalPumpMessage call (or a similar call) is owning the message pump. Incase of modal dialogs, the modal dialogs themselves own the message pump by running a message loop, so they got away with implementing PreTranslateMessage. Modeless dialogs suffer this terrible fate of not having the blessings of PreTranslateMessage as they don't own the message pump.

For me, just hearing the COM/ActiveX is not the owner of the message pump wasn't enough. So, I had to dig deep inside the problem. My findings were, whoever is in charge of the message pump is not aware of the window and dialog objects inside the COM component or ActiveX (mind it, I didn't say handle, I said object, meaning CWnd instance). Therefore whenever that module in charge (usually your executable) calls CWnd::FromHandlePermanent(...) with a window handle from COM as argument, it receives NULL and cannot invoke the corresponding PreTranslateMessage associated with that window/dialog. I now know why that returning of NULL happens but discussing that is beyond the scope of this article.

Still, those who are curious as cat can debug the entire window creation process of MFC (which includes also the creation of CCmdTarget) and look at AFX_MODULE_STATE m_pModuleState member of AFX_THREAD_STATE instance obtained by AfxGetModuleState inside CCmdTarget and look at m_pmapHWND member of the thread state variable as well as afxMapHWND ..... Haven't I already thrown enough complicated terms to discourage :) into going deep?

*Small hint: The window instance residing in COM/ActiveX is not contained inside the m_pmapHWND member of the main application thread state.

How To Get Around It?

In order to get around the problem, one idea is installing a hook procedure into a chain of hooks using SetWindowsHook(Ex)and this solution is offered in some of the Microsoft sites as well. To me, the solution is not elegant because you cannot directly use the feature of PreTranslateMessage and have to write up some new codes. This becomes difficult, if you already have existing codes with PreTranslateMessage and now want it to get invoked inside COM.

The alternate solution is to somehow pass the message structure MSG to COM after a GetMessage/PeekMessage of the application gets executed. After COM receives the structure, it needs to perform the same things MFC does to transmit the PreTranslateMessage to the relevant window and its parent and above. This, to me, seems pretty elegant. CWinApp gives you a lucky break in this regard because CWinApp::PreTranslateMessage will definitely be called after a GetMessage/PeekMessage and it contains the proper structure with the proper window handles even if the window resides in COM/ActiveX. So, windows O/S does its job alright. The reason why PreTranslateMessage is not called for a COM dialog/window is due to the failure of MFC in finding the appropriate window pointer (CWnd*) given a window handle. In other words, when the hierarchy of windows is traversed by MFC internal mechanizm to call up PreTranslateMessage, that guy PreTranslate... never picks up the phone because CWnd::FromHandlePermanent(...) gave him a heart attack by failing miserably.

So, it's on us to get the job done. What we need do is establish a handshaking mechanizm between the main app and the COM window (initiator of the PreTranslateMessage in CWinApp) so that somehow the MSG structure in PreTranslateWindow gets transferred and propagates up to the top level in the hierarchy of parent windows. So, let's make the App and COM talk to each other to make the connection.

LET'S TALK THE TALK

In order to implement what has just been said above, we can send a message which is both common to the COM and the main Application. Hence, we may create a registered user message as such:

const UINT RWM_PRETRANSLATEMSG = ::RegisterWindowMessage(_T("RWM_PRETRANSLATEMSG"));

This message can be considered as the handshaking element that COM and its host Application both can recognize. From the application side, we can transfer the MSG structure to COM as described below:

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);
}

What we are doing here is - from the handle of the window responsible for the message invocation, retrieve the topmost parent which doesn't have the child style set. Obviously this window is an independant dialog or popup window and it gets the first crack at the registered message and relavant MSG structure that has been passed in the LPARAM.

And that is all there is to it in the application side.

Now comes the more interesting part. What to do with the passed argument and how to process it in the COM side. Intuitive users can already guess from the forementioned discussions - what I am about to do. Simple, just catch this message in COM and do something similar like we see in BOOL PASCAL CWnd::WalkPreTranslateTree(HWND hWndStop, MSG* pMsg) to walk the chain of windows for whom PreTranslateMessage needs to be invoked and simply invoke what needs to be invoked.

LET'S WALK THE WALK

For this catching stuff, we can use the late great Paul DiLascia's simplistic yet beautifully designed CSubclassWnd class as a base for CPreTranslateMsgHook which will do the job for us. What it will do is catch the RWM_PRETRANSLATEMSG message and extract the MSG structure passed as LPARAM and propagate to the desired windows as follows:

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 is the method which does the walking as its name suggests and the code inside it is just a replica of what you see inside BOOL PASCAL CWnd::WalkPreTranslateTree(HWND hWndStop, MSG* pMsg). I will spare you the browsing of dirty MFC stuffs and just paste the code here:

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

From the code, it's obvious, the walk starts from the window for which the message is posted in message queue and ends up till the top parent is reached. While doing the walking, the traverser is also calling CWnd::FromHandlePermanent and this time, for heavens sake, it succeeds - since COM itself is aware of the windows contained inside it. For advanced readers, this is another way of saying, the m_pmapHWND member of the thread state of COM module contains the appropriate window pointer in the handle verses permanent window pointer map.

Needless to say, only the top parent needs to be subclassed/hooked to initiate the walking through our very own CPreTranslateMsgHook.

USING THE CPreTranslateMsgHook in the CODE

We have talked the talk, and walked the walk. Now all we need to do is use the code and it is only two lines. In order to enable the pretranslatemessage support in a COM dialog (modeless ofcourse!!), all you have to do is create the hook and install the hook. See it's simple:

m_pHook = new CPreTranslateMsgHook();
m_pHook->HookWindow(GetSafeHwnd());

You can call these two lines of code inside the OnInitDialog override or just after the creation of the dialog through the Create API.

By the way, it's actually three lines (not two :)), because you need to delete the hook (instantiated with the new operator) after dialog destruction.

Points of Interest

This solution to PreTranslateMessage invocation also solves the problem of TAB and ARROW key not working in modeless dialogs inside COM/ACTIVE-X, further justifying the effectiveness along with the elegance of the proposed method.

The drawback of the proposed solution is, you yourself must have the authority over both the COM module and the main application source codes. So, usually you apply this solution when you are trying to port some existing codes to COM and you also have the provision to change the main application code. However, even if you don't have the main application in your pocket, you can use a simple workaround by installing a message hook (WH_GETMESSAGE) inside your COM and consider the Hook Procedure to be the PretranslateMessage of the main application. From there, you can send a registered message following the same procedure mentioned above (refer to the CTestAppApp::PreTranslateMessage codes) and let the CPreTranslateMsgHook class to take care of the rest just like before. This alteration should only be a few lines of additional codes in the COM part.

Acknowledgements

And all credit goes to the little known greatness of Mr. Jacques Raphanel (probably he enjoys his bit of solitude and quietness), since he is the one who introduced me to this simple technique to achieve what seemed very complex.

Thanks also to late Paul DiLascia for his CSubclassWnd class.

History

  • Article uploaded: 16th June, 2011

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here