深度剖析消息反射机制
帐 号: 注册帐号
密 码:  找回密码
      记住用户名和密码
深度剖析消息反射机制

摘要:在前面我们分析了控件通知消息WM_NOTIFY,和WM_NOTIFY紧密联系的还有一个MFC新特性:消息反射。本文中,我想就这个问题作一个全面的论述,如果有错误,还望各路大虾批评指正。

  什么是消息反射?
  在windows里面,子控件经常向父控件发送消息,例如很多子控件要绘制自己的背景,就可能向父窗口发送消息WM_CTLCOLOR。对于从子控件发来的消息,父控件有可能在处理之前,把消息返还给子控件处理,这样消息看起来就想是从父窗口反射回来一样,故此得名:消息反射。

  消息反射的由来
  在windows和MFC4.0版本一下,父窗口(通常是一个对话框)会对这些消息进行处理,换句话说,自控件的这些消息处理必须在父窗口类体内,每当我们添加子控件的时候,就要在父窗口类中复制这些代码,我们可以想象这是多么的复杂,代码是多么的臃肿!
  我们可以想象,如果这些消息都让父窗口类去做,父窗口就成了一个万能的神,一个臃肿不堪的代码机,无论如何消息的处理都集中在父窗口类中,会使父窗口繁重无比,但是子控件却无事可做,并且代码也无法重用,这对于一个程序员来讲是多么痛苦的一件事?!
  在老版本的MFC中,设计者也意识到了这个问题,他们对一些消息采用了虚拟机制,例如:WM_DRAW99vEM,这样子控件就有机会控制自己的动作,代码的可重用性有了一定的提高,但是这还没有达到大部分人的要求,所以在高版本的MFC中,提出了一种更方便的机制:消息反射。 
  通过消息反射机制,子控件窗口便能够自行处理与自身相关的一些消息,增强了封装性,同时也提高了子控件窗口类的可重用性。不过需要注意的是:消息反射是MFC实现的,不是windows实现的;要让你的消息反射机制工作,你得类必须从CWnd类派生。

  Message-Map中的处理
  如果想要处理消息反射,必须了解相应的Message-Map宏和函数原型。一般来讲,Message-Map是有一定的规律的,通常她在消息的前面加上一个ON_ ,然后再消息的最后加上 _REFLECT。例如我们前面提到的WM_CTLCOLOR 经过处理后变成了ON_WM_CTLCOLOR_REFLECT;WM_MEASURE99vEM则变成了ON_WM_MEASURE99vEM_REFLECT。
  凡事总会有例外,这里也是这样,这里面有3个例外:
  (1) WM_COMMAND 转换成 ON_CONTROL_REFLECT;
  (2) WM_NOTIFY 转换成 ON_NOTIFY_REFLECT;
  (3) ON_UPDATE_COMMAND_UI 转换成 ON_UPDATE_COMMAND_UI_REFLECT;
  对于函数原型,也必须是以 afx_msg 开头。

  利用ClassWizard添加消息反射
  (1)在ClassWizard中,打开选择项Message Maps;
  (2)在下拉列表Class name中选择你要控制的类;
  (3)在Object IDs中,选中相应的类名;
  (4)在Messages一栏中找到前面带有=标记的消息,那就是反射消息;
  (5)双击鼠标或者单击添加按钮,然后OK!

  消息处理的过程
  (1)子窗口向父窗口发送通知消息,激发父窗口去调用它的虚函数CWnd::OnNotify。大致的结构如下
    BOOL CWnd::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult)
    {
     if (ReflectLastMsg(hWndCtrl, pResult)) file://hWndCtrl,为发送窗口
                  return TRUE; file://如果子窗口已处理了此消息,返回
     AFX_NOTIFY notify;
     notify.pResult = pResult;
     notify.pNMHDR = pNMHDR;
     return OnCmdMsg(nID, MAKELONG(nCode, WM_NOTIFY)? notify:NULL);     //这句调用父窗体的NOTIFY消息处理
   }
  (2)ReflectLastMsg声明如下:static BOOL PASCAL ReflectLastMsg(HWND hWndChild, LRESULT* pResult = NULL);
    它的主要任务就是调用发送窗口的SendChildNotifyLastMsg。
  (3)SendChildNotifyLastMsg声明如下:BOOL SendChildNotifyLastMsg(LRESULT* pResult = NULL);
    调用发送窗口的虚函数OnChildNotify函数,进行处理。 如果发送窗口没有进行重载处理,则调用ReflectChildNotify(...)函数进行标准的反射消息的消息映射处理。



  使用的一个例子
  这里面我们举一个简单的例子,希望大家能够更清晰的掌握消息反射机制。
  (1)创建一个基于对话框的工程。
  (2)利用向导创建一个新的类:CMyEdit,基类是CEdit。
  (3)在CMyEdit头文件中加入3个成员变量:
     COLORREF m_clrText ;
COLORREF m_clrBkgnd ;
CBrush  m_brBkgnd;
  (4)利用向导在其中加入WM_CTLCOLOR(看到了么,前面是不是有一个=?),并且将它的函数体改为:
     HBRUSH CMyEdit::CtlColor(CDC* pDC, UINT nCtlColor)
    {
 pDC->SetTextColor( m_clrText );  // text
 pDC->SetBkColor( m_clrBkgnd );  // text bkgnd
 return m_brBkgnd;        // ctl bkgnd
    }
    同时我们在.cpp文件中会看到ON_WM_CTLCOLOR_REFLECT(),这就是我们所说的经过处理的宏,是不是很符合规则?
  (5)在对话框中加入一个Edit,增加一个关联的变量,选择Control属性,类别为CMyEdit。
  (6)在对话框.cpp文件中加入#

 

 

那么,消息反射是怎样实现的呢?源代码说明一切!父窗口在接收到子控件的通知消息时调用虚的消息响应函数CWnd::OnNotify(),代码如下:
BOOL CWnd::OnNotify(WPARAM, LPARAM lParam, LRESULT* pResult)
{
 ASSERT(pResult != NULL);
 NMHDR* pNMHDR = (NMHDR*)lParam;
 HWND hWndCtrl = pNMHDR->hwndFrom;

 // get the child ID from the window itself
 UINT nID = _AfxGetDlgCtrlID(hWndCtrl);
 int nCode = pNMHDR->code;

 ASSERT(hWndCtrl != NULL);
 ASSERT(::IsWindow(hWndCtrl));

 if (_afxThreadState->m_hLockoutNotifyWindow == m_hWnd)
  return TRUE;        // locked out - ignore control notification

 // reflect notification to child window control
 if (ReflectLastMsg(hWndCtrl, pResult))
  return TRUE;        // eaten by child

 AFX_NOTIFY notify;
 notify.pResult = pResult;
 notify.pNMHDR = pNMHDR;
 return OnCmdMsg(nID, MAKELONG(nCode, WM_NOTIFY), ¬ify, NULL);
}
接着看ReflectLastMsg()函数:
BOOL PASCAL CWnd::ReflectLastMsg(HWND hWndChild, LRESULT* pResult)
{
 // get the map, and if no map, then this message does not need reflection
 CHandleMap* pMap = afxMapHWND();
 if (pMap == NULL)
  return FALSE;

 // check if in permanent map, if it is reflect it (could be OLE control)
 CWnd* pWnd = (CWnd*)pMap->LookupPermanent(hWndChild);
 ASSERT(pWnd == NULL || pWnd->m_hWnd == hWndChild);
 if (pWnd == NULL)
 {
#ifndef _AFX_NO_OCC_SUPPORT
  // check if the window is an OLE control
  CWnd* pWndParent = (CWnd*)pMap->LookupPermanent(::GetParent(hWndChild));
  if (pWndParent != NULL && pWndParent->m_pCtrlCont != NULL)
  {
   // If a matching control site exists, it's an OLE control
   COleControlSite* pSite = (COleControlSite*)pWndParent->
   m_pCtrlCont->m_siteMap.GetValueAt(hWndChild);
   if (pSite != NULL)
   {
    CWnd wndTemp(hWndChild);
    wndTemp.m_pCtrlSite = pSite;
    LRESULT lResult = wndTemp.SendChildNotifyLastMsg(pResult);
    wndTemp.m_hWnd = NULL;
    return lResult;
   }
  }
#endif //!_AFX_NO_OCC_SUPPORT
  return FALSE;
 }

 // only OLE controls and permanent windows will get reflected msgs
 ASSERT(pWnd != NULL);
 return pWnd->SendChildNotifyLastMsg(pResult);
}
注意红色代码!此时调用的是子控件的SendChildNotifyLastMsg() 。继续看SendChildNotifyLastMsg():
BOOL CWnd::SendChildNotifyLastMsg(LRESULT* pResult)
{
 _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
 return OnChildNotify(pThreadState->m_lastSentMsg.message,
  pThreadState->m_lastSentMsg.wParam, pThreadState->m_lastSentMsg.lParam, pResult);
}
调用子控件的虚函数OnChildNotify函数,进行处理。 如果没有处理,则调用ReflectChildNotify(...)函数进行标准的反射消息的消息映射处理。  如果在ReflectChildNotify(...)中此消息还没被处理,就返回到CWnd::OnNotify(...)中调用OnCmdMsg(...)处理,这样,父窗口就可以响应此消息了。

文章出处:http://www.diybl.com/course/3_program/c++/cppsl/2008520/117221.html

 

下面是我抄下来的源码,作补充

BOOL CWnd::OnChildNotify(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
#ifndef _AFX_NO_OCC_SUPPORT
 if (m_pCtrlSite != NULL)   //这里表示没有作MAP表映射
 {
  // first forward raw OCM_ messages to OLE control sources
  LRESULT lResult = SendMessage(OCM__BASE+uMsg, wParam, lParam);
  if (uMsg >= WM_CTLCOLORMSGBOX && uMsg <= WM_CTLCOLORSTATIC &&
   (HBRUSH)lResult == NULL)
  {
   // for WM_CTLCOLOR msgs, returning NULL implies continue routing
   return FALSE;
  }
  if (pResult != NULL)
   *pResult = lResult;
  return TRUE;
 }
#endif

 return ReflectChildNotify(uMsg, wParam, lParam, pResult);  //使用默认的反射处理
}

BOOL CWnd::ReflectChildNotify(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
 // Note: reflected messages are send directly to CWnd::OnWndMsg
 //  and CWnd::OnCmdMsg for speed and because these messages are not
 //  routed by normal OnCmdMsg routing (they are only dispatched)

 switch (uMsg)
 {
 // normal messages (just wParam, lParam through OnWndMsg)
 case WM_HSCROLL:
 case WM_VSCROLL:
 case WM_PARENTNOTIFY:
 case WM_DRAWITEM:
 case WM_MEASUREITEM:
 case WM_DELETEITEM:
 case WM_VKEYTOITEM:
 case WM_CHARTOITEM:
 case WM_COMPAREITEM:
  // reflect the message through the message map as WM_REFLECT_BASE+uMsg
  return CWnd::OnWndMsg(WM_REFLECT_BASE+uMsg, wParam, lParam, pResult);

 // special case for WM_COMMAND
 case WM_COMMAND:
  {
   // reflect the message through the message map as OCM_COMMAND
   int nCode = HIWORD(wParam);
   if (CWnd::OnCmdMsg(0, MAKELONG(nCode, WM_REFLECT_BASE+WM_COMMAND), NULL, NULL))
   {
    if (pResult != NULL)
     *pResult = 1;
    return TRUE;
   }
  }
  break;

 // special case for WM_NOTIFY
 case WM_NOTIFY:
  {
   // reflect the message through the message map as OCM_NOTIFY
   NMHDR* pNMHDR = (NMHDR*)lParam;
   int nCode = pNMHDR->code;
   AFX_NOTIFY notify;
   notify.pResult = pResult;
   notify.pNMHDR = pNMHDR;
   return CWnd::OnCmdMsg(0, MAKELONG(nCode, WM_REFLECT_BASE+WM_NOTIFY), ¬ify, NULL);  //这里表示默认情况下给窗体发WM_NOTIFY反射消息, 而CWnd默认是不处理WM_NOTIFY消息的,所以最终返回FALSE,这样WM_NOTIFY就交给父窗体去处理了
  }

 // other special cases (WM_CTLCOLOR family)
 default:
  if (uMsg >= WM_CTLCOLORMSGBOX && uMsg <= WM_CTLCOLORSTATIC)
  {
   // fill in special struct for compatiblity with 16-bit WM_CTLCOLOR
   AFX_CTLCOLOR ctl;
   ctl.hDC = (HDC)wParam;
   ctl.nCtlType = uMsg - WM_CTLCOLORMSGBOX;
   //ASSERT(ctl.nCtlType >= CTLCOLOR_MSGBOX);
   ASSERT(ctl.nCtlType <= CTLCOLOR_STATIC);

   // reflect the message through the message map as OCM_CTLCOLOR
   BOOL bResult = CWnd::OnWndMsg(WM_REFLECT_BASE+WM_CTLCOLOR, 0, (LPARAM)&ctl, pResult);
   if ((HBRUSH)*pResult == NULL)
    bResult = FALSE;
   return bResult;
  }
  break;
 }

 return FALSE;   // let the parent handle it
}

 

BOOL CWnd::OnWndMsg(....)

{

....

 for (pMessageMap = GetMessageMap(); pMessageMap->pfnGetBaseMap != NULL;
   pMessageMap = (*pMessageMap->pfnGetBaseMap)())    //这里是查消息映射表

 {
  // Note: catches BEGIN_MESSAGE_MAP(CMyClass, CMyClass)!
  ASSERT(pMessageMap != (*pMessageMap->pfnGetBaseMap)());
  lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries, nMsg, nCode, nID);
  if (lpEntry != NULL)
  {
   // found it
#ifdef _DEBUG
   if (nCode == CN_COMMAND)
    TRACE(traceCmdRouting, 1, "SENDING command id 0x%04X to %hs target./n", nID,
     GetRuntimeClass()->m_lpszClassName);
   else if (nCode > CN_COMMAND)
    TRACE(traceCmdRouting, 1, "SENDING control notification %d from control id 0x%04X to %hs window./n",
     nCode, nID, GetRuntimeClass()->m_lpszClassName);
#endif //_DEBUG
   return _AfxDispatchCmdMsg(this, nID, nCode,
    lpEntry->pfn, pExtra, lpEntry->nSig, pHandlerInfo);  //调用子窗体自身的WndProc过程的WM_NOTIFY响应部分

  }
 }

....

 

总结:消息反射并不是反射消息(父窗体向子窗体返回消息),而是当父窗体收到子窗体通知消息后(如WM_COMMAND、WM_NOTIFY),DISPATCH会调用父窗体的处理函数(一般CWnd等基类会定义如CWnd::OnCommand CWnd::OnNotify),在这里,父窗体会试图调用子窗体的ReflectChildNotify()函数,而ReflectChildNotify给了子窗体自己一个扫描反射消息映射表的机会,有则调用AfxDispatchMessage进行处理,并返回TRUE,通知父窗体OnNotify过程不用处理通知了。

 

另外,MFC基类默认已经书写了一些消息处理函数,如CWnd::OnNotify等,在这些函数里,它们搜索自己关心的消息MAP表,并进行处理。这就是消息MAP的本质。如果我们不用MAP表,完全可以在OnWndMsg或是WndProc里进行处理。关于这些函数的调用级别是:

WndProc()

        -> OnWndMsg()

                    -> OnNotify()

                                ->OnCmdMsg()

                                        ->消息MAP表

                                                ->AfxDispatchMessage 即 直接调用处理函数

        DefWindowProc()

copyright:2016-2020|邮箱:imalib@vip.163.com

蜀ICP备16020986号