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);
for (HWND hWnd = pMsg->hwnd; hWnd != NULL; hWnd = ::GetParent(hWnd))
{
CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);
if (pWnd != NULL)
{
if (pWnd->PreTranslateMessage(pMsg))
return TRUE; }
if (hWnd == hWndStop)
break;
}
return FALSE;
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