消息处理

消息被寄送或者发送之后,将会按照一定的路线寻找合适的处理函数,以便得到处理,或者交由默认的窗口过程进行处理。本节将讨论消息在MFC中的处理过程。
6.3.1  消息的接收
寄送的消息一般存放在消息队列中,此消息队列在应用程序初始化时由操作系统所建立。通常,鼠标和键盘单击产生寄送消息,然后应用程序逐一地将它们从消息队列中删除,并将它们发送到被鼠标单击的窗口,或者按键按下时接收输入的窗口。
Windows API提供了两个调用函数,即GetMessage()和PeekMessage(),它们允许应用程序从队列中删除消息。MFC类CWinThread 将这些函数调用封装到Run()函数中,Run()函数不断检查消息队列,以判断用户是否进行了键盘或者鼠标等操作;Run()函数还执行一些MFC类的 后台维护工作,并为程序员提供机会进行自己的维护工作。
因为寄送的消息要在应用程序的消息队列中花费一些时间,即在GetMessage()函数和PeekMessage()函数取出它之前,它要一 直在队列中。一旦从消息队列中删除一条寄送消息,并通过DispatchMessage()将其发送到一个窗口的窗口过程,对它的处理就与接下来所讨论的 发送消息一样了。
6.3.2  窗口过程
窗口过程是窗口消息的处理场所,在Windows中,所有的窗口都有自己的窗口过程,不过同一种窗口类都共享同一个窗口过程。每个窗口在系统中都有自己唯一的标志—窗口句柄,系统正是通过句柄来实现消息的正确分发。消息结构的第一个成员即是消息要发往的窗口句柄。
1.传统的设计方法
在传统SDK程序设计中,注册窗口类时,都会指定一个窗口过程—WndProc(),所有的窗口消息都将对此函数进行调用。如下代码为SDK中WinMain的摘录:
 
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                         PSTR szCmdLine, int iCmdShow)
{
      static TCHAR szAppName[] = TEXT ("") ;
      HWND    hwnd ;
      MSG     msg ;
      WNDCLAS  wndclass ;
      wndclass.style= CS_HREDRAW | CS_VREDRAW ;
      wndclass.lpfnWndProc= WndProc ;
      wndclass.cbClsExtra = 0 ;
      wndclass.cbWndExtra= 0 ;
      wndclass.hInstance= hInstance ;
      wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
      wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
      wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ;
      wndclass.lpszMenuNam= NULL ;
      wndclass.lpszClassName= szAppName ;
}
 
显然,窗口过程指定为WndProc。在WinMain中,必须指定一个窗口过程,否则消息将不知何去何从了。在SDK中,这个过程清晰可见,没有什么隐藏的细节。
2.MFC的设计方法
在MFC中,就不如SDK中简单了。上一章中关于主框架窗口的创建函数,简单列出如下:
 
BOOL CWnd::CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName,
    LPCTSTR lpszWindowName, DWORD dwStyle,
    int x, int y, int nWidth, int nHeight,
    HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam)
{
    …
    //调用PreCreateWindow,在其中进行类的注册,后文中将会对此分析
    if (!PreCreateWindow(cs))
    {
            PostNcDestroy();
            return FALSE;
    }
    …
    AfxHookWindowCreate(this);
    HWND hWnd = ::CreateWindowEx(cs.dwExStyle, cs.lpszClass,
            cs.lpszName, cs.style, cs.x, cs.y, cs.cx, cs.cy,
            cs.hwndParent, cs.hMenu, cs.hInstance, cs.lpCreateParams);
    return TRUE;
}
 
它主要负责窗口类的注册和窗口的创建,当然,这些工作都是通过调用另外的函数来完成的。不过,还有一点值得注意,在调用 了::CreateWindowEx之后,CWnd类的内部成员m_pfnSuper将得到有效值(也就是说将原来的窗口过程保存到CWnd的成员变量 m_pfnSuper中,但是从表面上却看不出来这一点),此变量的类型为WNDPROC(即指向窗口过程的指针),最后在消息的默认处理 时,m_pfnSupe将会派上用场。
 
说明
 MFC中m_pfnSuper获得有效值的过程不是很清晰,但可以通过跟踪调试手段很清楚地看到这一点。
 
 
在PreCreateWindow函数的调用中,完成了窗口类的注册,请回顾如下的调用过程:PreCreateWindow-> AfxDeferRegisterClass-> AfxEndDeferRegisterClass(从左至右为调用者与被调用者),在对AfxEndDeferRegisterClass的调用中才真 正完成窗口类的注册。在注册窗口类时,使用与SDK类似的方式指定了窗口类的窗口过程:
 
//缺省窗口过程
wndcls.lpfnWndProc = DefWindowProc;
 
类似地,可以推测,所有没有被正常处理(即由CCmdTarget::OnCmdMsg处理)的消息最后都应该由窗口过程DefWindowProc处理,但事实上呢?继续往下看。
3.钩子过程
注册完窗口类后,CWnd::CreateEx又调用了函数AfxHookWindowCreate(this),下面是该函数的定义:
 
void AFXAPI AfxHookWindowCreate(CWnd* pWnd)
{
      …
      if (pThreadState->m_hHookOldCbtFilter == NULL)
      {
             pThreadState->m_hHookOldCbtFilter = ::SetWindowsHookEx(WH_CBT,
                   _AfxCbtFilterHook, NULL, ::GetCurrentThreadId());
             if (pThreadState->m_hHookOldCbtFilter == NULL)
                    AfxThrowMemoryException();
      }
      …
}
 
SetWindowsHookEx函数向钩子链中安装了一个应用预定义的WH_CBT类型的钩子过程。该函数声明如下:
 
HHOOK SetWindowsHookEx(
  int idHook,                            //钩子类型
  HOOKPROC lpfn,                         //钩子过程地址
  HINSTANCE hMod,                        //应用实例的句柄
  DWORD dwThreadId                       //钩子所属的线程的标志
);
 
安装WH_CBT类型的钩子过程后,系统在下列情况下将会首先调用此函数:激活窗口,创建或销毁窗口,最大化或最小化窗口,移动或缩放窗口,完 成一个系统命令,从系统的消息队列里移除一个鼠标或者键盘事件,设置键盘,设置输入焦点或者同步系统消息队列事件等。安装钩子后,窗口的消息走向就要发生 变化。接下来观察其中钩子过程的地址_AfxCbtFilterHook的内容,其定义如下:
 
LRESULT CALLBACK _AfxCbtFilterHook(int code, WPARAM wParam, LPARAM lParam)
{
    _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
    if (code != HCBT_CREATEWND)
    {
           // 等待HCBT_CREATEWND通过其他的钩子
           return CallNextHookEx(pThreadState->m_hHookOldCbtFilter, code,
                  wParam, lParam);
    }
    …
    if (!afxData.bWin4 && !bContextIsDLL &&
            (pCtl3dState = _afxCtl3dState.GetDataNA()) != NULL &&
             pCtl3dState->m_pfnSubclassDlgEx != NULL &&
            (dwFlags = AfxCallWndProc(pWndInit, hWnd, WM_QUERY3DCONTROLS)) != 0)
    {
            // 窗口类注册是用AfxWndProc进行的吗
            WNDPROC afxWndProc = AfxGetAfxWndProc();
            BOOL bAfxWndProc = ((WNDPROC)
                GetWindowLong(hWnd, GWL_WNDPROC) == afxWndProc); 
            pCtl3dState->m_pfnSubclassDlgEx(hWnd, dwFlags);  
            //如果还没有编排到AfxWndProc则用AfxWndProc子类化窗口
            if (!bAfxWndProc)
            {
                   //用标准的AfxWndProc子类化窗口
                   oldWndProc = (WNDPROC)SetWindowLong(hWnd, GWL_WNDPROC,
                        (DWORD)afxWndProc);
                   ASSERT(oldWndProc != NULL);
                   //保存原窗口过程
                   *pOldWndProc = oldWndProc;
            }
    }
    …
}
 
说明
 钩子是Windows系统中非常重要的系统接口,通过它来监视系统或进程中的各种事件消息,截获发往目标窗口的消息并进行处理。 通过安装钩子,开发人员可以给Windows设置一个处理或过滤事件的回调函数(回调函数与一般函数的主要区别在于虽然它也由用户编写,但是用户并不直接 调用它而是由系统负责调用),该函数也叫做“钩子函数”。每当有监视范围内的事件发生时,Windows都将调用该函数,以便完成特定的功能。最后,当不 再使用钩子时,必须及时卸载,简单地调用函数UnhookWindowsHookEx即可。当然,上文所谈到的这个过程在MFC中是自动实现的。
 
 
从钩子函数中可以看到,如果它检测到注册窗口类时,使用的窗口过程不是AfxWndProc函数,则它将用此函数替代,并且通过这个设置,在调 用::CreateWindowEx后,将原窗口过程保存在窗口类的成员变量m_pfnSuper中,这样就形成了一个窗口过程链。在需要的时候,原窗口 过程地址可以通过窗口类成员函数GetSuperWndProcAddr获得。
这样,AfxWndProc就成为CWnd或其派生类的窗口过程。不论队列消息还是非队列消息,都将首先送到AfxWndProc窗口过程进行处理。经过消息分发之后仍没有被处理的消息,将送给原窗口过程处理。在后文中将会清楚地看到这一点。
正如前面所看到的,注册时所使用的窗口过程果真不是AfxWndProc,而是DefWindowProc,因此钩子函数将完成这一替换任务。而调用函数AfxGetAfxWndProc所返回的结果正是函数AfxWndProc的地址,从其定义便可知这一事实:
 
WNDPROC AFXAPI AfxGetAfxWndProc()
{
#ifdef _AFXDLL
      return AfxGetModuleState()->m_pfnAfxWndProc;
#else
      return &AfxWndProc;
#endif
}
 
说明
 为什么要对窗口过程进行这种复杂的替换过程呢?实际上这是为了兼容新的3D控件而采取的一种方法。在使用AfxHookWindowCreate之后,同时还要使用函数AfxUnhookWindowCreate卸载安装的滤网函数。
 
 
至此,相信读者能够明白DispatchMessage最终将消息发到了AfxWndProc函数里,而非DefWindowProc里。 AfxWndProc的作用是截获所有发送给窗口的消息(包括队列消息和非队列消息),所以实质上它是一个窗口消息分发器。下一节将详细讨论调用 DispatchMessage后,消息寻找其处理函数的过程。
6.3.3  消息的处理
通过上一小节的学习,相信读者已经掌握了窗口过程的含义及其变迁的过程,本节将详细讨论消息寻找其处理函数的具体细节。
如果发送一个消息,则SendMessage()实质上是直接调用AfxWndProc(),而如果寄送一个消息,则消息循环通过 DispatchMessage()来调用AfxWndProc()。在MFC的源码中,有这样一句说明:此函数是所有的CWnd类及其派生类的 WndProc。这再次说明了AfxWndProc()确实是变迁了以后的窗口过程。它的具体定义如下:
 
//所有的CWnd类及其派生类的WndProc
LRESULT CALLBACK
AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
    …
    //其他所有的消息路由消息循环
    CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);
    ASSERT(pWnd != NULL);
    ASSERT(pWnd->m_hWnd == hWnd);
    return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam);
}
 
AfxWndProc()要做的第一件事是找到目标窗口的CWnd对象。虽然一个窗口对它的CWnd对象的所有情况一无所知,但应用程序在映像中跟踪窗口和类的配对,一旦找到CWnd对象,就会立刻调用AfxCallWndProc()。
而AfxCallWndProc()则用来存储消息(消息标识符和参数)以供未来引用,然后调用WindowProc()。其具体定义如下:
 
//发送消息到CWnd的常规途径
LRESULT AFXAPI AfxCallWndProc(CWnd* pWnd, HWND hWnd, UINT nMsg,
     WPARAM wParam = 0, LPARAM lParam = 0)
{
         _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
     //存储标志符和参数,因为MFC内部需要这些参数和信息,但用户不需关心
     MSG oldState = pThreadState->m_lastSentMsg;  
     pThreadState->m_lastSentMsg.hwnd = hWnd;
     pThreadState->m_lastSentMsg.message = nMsg;
     pThreadState->m_lastSentMsg.wParam = wParam;
     pThreadState->m_lastSentMsg.lParam = lParam;
     …
     //委派到窗口的WindowProc
     lResult = pWnd->WindowProc(nMsg, wParam, lParam);
     …
     return lResult;
}
 
事实上,即使以后改变消息中的参数,当窗口过程用Default成员函数进行默认处理时,窗口进程也将引用保存在这里的参数;一旦保存了消息,就会调用WindowProc(),该函数的具体定义如下:
 
//主WindowProc实现
LRESULT CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
    //除了对DefWindowProc函数调用外,OnWndMsg做了大部分工作
    LRESULT lResult = 0;
    if (!OnWndMsg(message, wParam, lParam, &lResult))
             lResult = DefWindowProc(message, wParam, lParam);
    return lResult;
}
 
说明
 Default是CWnd的成员函数,此函数直接调用DefWindowProc以确保所有的消息都能被处理。
 
 
此函数首先调用OnWndMsg(),它试图在类中为该消息寻找一个处理函数;任何返回到WindowProc()而未被处理的消息都被传输到 DefWindowProc();DefWindowProc()是缺省的窗口过程,所有不能或者没有被OnWndMsg处理的函数都将交由它处理。下面 看看OnWndMsg是如何为消息寻找处理函数的,它的定义如下:
 
BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
    LRESULT lResult = 0;
    //对于WM_COMMAND消息的处理
    if (message == WM_COMMAND)
    {
           if (OnCommand(wParam, lParam))
           {
                 lResult = 1;
                 goto LReturnTrue;
           }
           return FALSE;
    }
    //对于WM_NOTIFY消息
    if (message == WM_NOTIFY)
    {
           NMHDR* pNMHDR = (NMHDR*)lParam;
           if (pNMHDR->hwndFrom != NULL && OnNotify(wParam, lParam, &lResult))
                  goto LReturnTrue;
           return FALSE;
    }
    //对于一般Windows及窗口消息的处理
    …
    return TRUE;
}
 
从OnWndMsg()函数的实现代码可以看出,它或者为WM_COMMAND消息调用OnCommand(),或者为WM_NOTIFY消息 调用OnNotify()。它将任何没有被处理的消息都视为一个窗口消息。OnWndMsg()搜索类的消息映射,以便找到一个能处理任何窗口消息的处理 函数。
为了讲述方便,首先创建一个简单的工程。工程命名Chapter06,除了Step 1中选择单文档外,其余全部设置为默认即可。接着,在“帮助”菜单项中添加“测试”子菜单,其ID设为ID_TEST,然后通过类向导在 CMainFrame类中为其添加处理函数OnTest(),并添加如下代码:
 
void CMainFrame::OnTest()
{
      // TODO: Add your command handler code here
      AfxMessageBox("This is a test!");
      TRACE("route through CMainFrame");
}
 
然后,再利用类向导重载函数CFrameWnd::OnCommand并在此函数内部设置断点即可。如下所示:
 
BOOL CMainFrame::OnCommand(WPARAM wParam, LPARAM lParam)
{
    // TODO: Add your specialized code here and/or call the base class
    return CFrameWnd::OnCommand(wParam, lParam);
}
 
然后调试运行,并选择“帮助”菜单项的“测试”子菜单,此时,就可以看到函数运行至CMainFrame::OnCommand,而通过调用堆栈窗口可以看到,程序的运行次序如下:
 
CMainFrame::OnCommand()
CWnd::OnWndMsg()
CWnd::WindowProc()
AfxCallWndProc()
AfxWndProc()
 
这和前面所讨论的内容完全一致。不过需要记住的是,此时this是指向CMainFrame对象的。因为OnCommand函数是CWnd类的 虚函数,它在CFrameWnd类中已经被重载,仍然是以虚函数的身份出现,在工程中也重载了此函数,因此,在CWnd::OnWndMsg中调用时,依 据当前的this指针,它所调用的OnCommand版本应该是CMainFrame::OnCommand(),但为了能够处理消息映射,必须调用它的 基类版本,下面是它的基类版本的实现:
 
BOOL CFrameWnd::OnCommand(WPARAM wParam, LPARAM lParam)
{
    …
    //继续路由
    return CWnd::OnCommand(wParam, lParam);
}
 
此函数又将调用其基类版本,接着看:
 
// CWnd命令处理
BOOL CWnd::OnCommand(WPARAM wParam, LPARAM lParam)
{
    UINT nID = LOWORD(wParam);
    HWND hWndCtrl = (HWND)lParam;
    int nCode = HIWORD(wParam);
    // 对于命令消息,则进行默认的路由
    if (hWndCtrl == NULL)
    {
           …
           //确信命令在路由前没有被禁止,若被禁止则直接返回
           OnCmdMsg(nID, CN_UPDATE_COMMAND_UI, &state, NULL);
    }
    //对于控件通知
    else
    {
           …
           //反射通知到子窗口控件
           if (ReflectLastMsg(hWndCtrl))
                  //如果被子窗口处理则返回TRUE
                  return TRUE;
           …
    }
    …
    return OnCmdMsg(nID, nCode, NULL, NULL);//注意此时的this指针的指向
}
 
在前面曾经提到WM_COMMAND消息的一种特殊情况,即:它既可以标识命令消息,也可以标识一个控件通知。在CWnd类的 OnCommand()中就可以明显地看到这一点,它检查参数lParam是否为一个有效的窗口句柄,如果不是,则将以默认的消息路径继续寻找它的归宿; 如果是,则OnCommand()以控件通知处理该消息,此时,lParam是控件的句柄,并且将消息反射到发送它的窗口。
 
注意
 这里出现了一个新的概念“消息反射”,在§6.3.4中将对此概念详细介绍。
 
 
可以注意到,调用OnCmdMsg()的次数最多可达两次。当该消息为命令消息时,第一次调用OnCmdMsg(),此时应用程序可以自己启用 或禁用菜单项或控件;如果不能为请求的行为提供一个特定的用户界面处理函数,则OnCmdMsg()被第二次调用(也就是代码实现中的最后一行),以检查 菜单项或控件究竟有没有一个消息处理函数,如果没有,则OnCmdMsg()将自动禁用菜单项或控件,当然,两次调用的目的不同,参数自然也就不同了。
注意,此时this指针指向主框架CMainFrame,但是主框架类没有重载OnCmdMsg()函数,所以应该调用的是其基类的版本,即:
 
// CFrameWnd命令/消息路由
BOOL CFrameWnd::OnCmdMsg(UINT nID, int nCode, void* pExtra,
    AFX_CMDHANDLERINFO* pHandlerInfo)
{
    …
    // 首先给视view一个机会
    CView* pView = GetActiveView();
    if (pView != NULL && pView->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
           return TRUE;
    //接着给自己一个机会
    if (CWnd::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
          return TRUE;
    // last but not least, pump through app
    CWinApp* pApp = AfxGetApp();
    if (pApp != NULL && pApp->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
           return TRUE;
    return FALSE;
}
 
函数OnCmdMsg将消息按照视、文档、框架和应用的顺序,如果在前一个类中没有找到相应的处理函数,则将消息交给后一个类进行处理,依次类推,如果最终没找到的话,则消息将会交给默认的窗口过程进行处理。
下面来逐步分析消息的处理过程。
1.视图对消息的处理
// 命令路由
BOOL CView::OnCmdMsg(UINT nID, int nCode, void* pExtra,
    AFX_CMDHANDLERINFO* pHandlerInfo)
{
    //首先路由视图
    if (CWnd::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
          return TRUE;
    //接着路由文档
    if (m_pDocument != NULL)
    {
          …
          return m_pDocument->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo);
    }
    return FALSE;
}
 
在视图消息的分发实现中,它先调用CWnd::OnCmdMsg给自己一个处理的机会。由于CWnd并没有重载OnCmdMsg函数,因此它调用的是其基类CCmdTarget的版本,其定义如下:
 
BOOL CCmdTarget::OnCmdMsg(UINT nID, int nCode, void* pExtra,
    AFX_CMDHANDLERINFO* pHandlerInfo)
{
    …
    //判定消息号和代码
    const AFX_MSGMAP* pMessageMap;
    const AFX_MSGMAP_ENTRY* lpEntry;
    UINT nMsg = 0;
    …
    //查看消息映射是否自己所需
    for (pMessageMap = GetMessageMap(); pMessageMap != NULL;
      pMessageMap = pMessageMap->pBaseMap)
    {
         //注意:捕捉宏BEGIN_MESSAGE_MAP(CMyClass, CMyClass)
       …
         lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries, nMsg, nCode, nID);
         if (lpEntry != NULL)
         {
                //如果发现的话则进行处理
                return _AfxDispatchCmdMsg(this, nID, nCode,
                      lpEntry->pfn, pExtra, lpEntry->nSig, pHandlerInfo);
         }
    }
    //如果没有被处理则返回FALSE
    return FALSE;
}
 
CCmdTarget是MFC消息映射体系结构的基类。正是通过这个体系结构,才将命令或者消息映射到开发人员所写的命令处理函数或者消息响应函数。
 
注意
 因为OnCmdMsg()实际上是CCmdTarget的成员函数,而不是CWnd的成员函数,所以它允许任何从CCmdTarget派生的类接收一个命令消息,即使那些没有一个窗口的类也可以。
 
 
消息映射就是通过几个宏(如BEGIN_MESSAGE_MAP、END_MESSAGE_MAP等)实现的一种消息传送的机制。通过调用全局 函数_AfxDispatchCmdMsg,来调用具体的消息处理函数。这样便完成了从产生消息到调用消息响应函数的全过程。接下来了解一下 _AfxDispatchCmdMsg函数的实现。
 
AFX_STATIC BOOL AFXAPI_AfxDispatchCmdMsg(CCmdTarget*pTarget,UINT nID,int nCode,
    AFX_PMSG pfn, void* pExtra, UINT nSig, AFX_CMDHANDLERINFO* pHandlerInfo)
        // return TRUE to stop routing
{
    …
    union MessageMapFunctions mmf;
    mmf.pfn = pfn;
    BOOL bResult = TRUE; // default is ok
 
    if (pHandlerInfo != NULL)
    {
           // just fill in the information, don't do it
           pHandlerInfo->pTarget = pTarget;
           pHandlerInfo->pmf = mmf.pfn;
           return TRUE;
    }
    switch (nSig)
    {
    case AfxSig_vv:
          //一般的命令或者控件通知
          ASSERT(CN_COMMAND == 0); // CN_COMMAND 和BN_CLICKED一样
          ASSERT(pExtra == NULL);
          //在这里将会调用具体的消息处理函数了
          (pTarget->*mmf.pfn_COMMAND)();
          break;
          …
    default: // 非法消息
           ASSERT(FALSE);
           return 0;
    }
    return bResult;
}
 
其参数分别介绍如下。
    参数pTarget:该参数是指向处理消息的对象。
    参数nID:该参数是命令ID。
    参数nCode:该参数是通知消息等,对于一个命令消息,该变量将赋值为CN_COMMAND(相当于0)。
    参数pfn:该参数是消息处理函数地址。
    参数pExtra:该参数用于存放一些有用的信息,它取决于当前正被处理的消息类型。如果是控件通知WM_NOTIFY,则是指向NMHDR的 AFX_NOTIFY结构的指针;如果是菜单项和工具栏更新,则它是指向CCmdUI派生类的指针;如果是其他类型,则为空。
    参数nSig:该参数定义消息处理函数的调用变量。在AFXMSG_.H中,为nSig预定义了60多个值,例如,nSig值为iww,则在调用消息处理函数前,使OnWndMsg()格式化wParam和lParam为两个UINT变量,返回值为整型。
    参数pHandlerInfo:该参数是一个指针,指向AFX_CMDHANDLERINFO结构。
前6个参数(除了pExtra以外)都是输入参数,而参数pExtra和pHandlerInfo既可以用作输出参数,也可以用作输入参数。
该函数主要完成的任务是:首先,它检查参数pHandlerInfo是否空,如果不空,则用pTarget和pfn填充它所指向的结构,并且返 回TRUE;其次,如果pHandlerInfo空,则进行消息处理函数的调用。它根据参数nSig的值,把参数pfn的类型转换为要调用的消息处理函数 的类型。
如果在视图中没有找到相应的消息处理函数,则将会交由文档类来进行处理。接下来讨论这个过程。
2.文档对消息的处理
在CView::OnCmdMsg的调用中,当在视类中没有发现消息处理函数时,它将会有如下调用:
 
m_pDocument->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo);
 
文档类重载了此函数,它具体的实现如下:
 
// 命令路由
BOOL CDocument::OnCmdMsg(UINT nID, int nCode, void* pExtra,
    AFX_CMDHANDLERINFO* pHandlerInfo)
{
    //调用基类版本
    if (CCmdTarget::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
           return TRUE;
    //如果仍然没有被处理则调用模板类的同名函数
    if (m_pDocTemplate != NULL &&
         m_pDocTemplate->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
           return TRUE;
    return FALSE;
}
 
CCmdTarget是CDocument的直接基类,它重载的目的只有一个,即对自己没有处理的消息交给文档模板类去处理。关于消息在CCmdTarget类中的走向,前一小节已经描述得比较清楚了,这里不再赘述。
3.框架和应用对消息的处理
如本节开始部分所讲的,当CFrameWnd::OnCmdMsg控制消息流向时,首先给了视图一个机会, 一旦视图不能找到合适的消息处理函数,则返回CFrameWnd::OnCmdMsg,接下来给自己处理消息的机会。具体代码如下:(摘自 CFrameWnd::OnCmdMsg)
 
    if (CWnd::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
          return TRUE;
    CWinApp* pApp = AfxGetApp();
    if (pApp != NULL && pApp->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
           return TRUE;
 
由于CWnd和CChapter06App、CWinApp以及CwinThread均没有重载该函数,则直接调用CCmdTarget的实现,即CCmdTarget::OnCmdMsg,关于此函数前面已经详细讨论了,因此这里不再赘述。
从上文CWnd::OnWndMsg的代码中可以发现,当此函数识别出消息是WM_NOTIFY时,它将会调用CWnd::OnNotify,下面给出此函数的定义:
 
BOOL CWnd::OnNotify(WPARAM, LPARAM lParam, LRESULT* pResult)
{
    ASSERT(pResult != NULL);
    NMHDR* pNMHDR = (NMHDR*)lParam;
    HWND hWndCtrl = pNMHDR->hwndFrom;
 
    //从窗口本身获取发送此消息的子窗口ID
    UINT nID = _AfxGetDlgCtrlID(hWndCtrl);
    //获取通知代码
    int nCode = pNMHDR->code;
    //确证子窗口
    ASSERT(hWndCtrl != NULL);
    ASSERT(::IsWindow(hWndCtrl));
    …
    //将通知消息反射回子窗口控件
    if (ReflectLastMsg(hWndCtrl, pResult))
            //如果被子窗口处理掉这里则返回TRUE
            return TRUE;
    …
    //调用本类的OnCmdMsg
    return OnCmdMsg(nID, MAKELONG(nCode, WM_NOTIFY), &notify, NULL);
}
 
此函数主要进行了两处调用:第一,反射消息到子窗口;第二,调用OnCmdMsg函数,由于此时this指针指向CMainFrame,因此这 里对OnCmdMsg的调用应该是对CMainFrame::OnCmdMsg,但是因为CMainFrame并没有重载此函数,因此它调用其基类 CFrameWnd重载过的该函数,即CFrameWnd:: OnCmdMsg。而此后的过程前面已经讨论过,这里不再赘述。下一节将详细讨论消息反射。
6.3.4  消息反射
Windows控件常常向它们的父窗口发送通知消息,例如,为了让画刷能够重画控件的背景颜色,控件一般都会向 其父窗口发送WM_CTLCOLOR通知消息。在Windows以及低于MFC4.0的版本中,往往都是父窗口来处理这些消息,这就意味着处理这些消息的 代码必须放在父窗口类中,而如果要在别的类中处理这些消息,则必须把这些代码拷贝到相应的类中。显然,这种方法违背了面向对象编程的意图,因为它要求每个 对象都应该包括所有属于它自己的功能。
而如果将控件通知处理功能交给控件,则就刚好符合面向对象编程的特点。这样,每当把控件移动到一个新的父窗口时,不必考虑把那些响应消息的代码拷贝到新的父窗口。“消息反射”就是MFC为完成此项功能,而在4.0以后的版本引入的一种新的机制,不过原有的机制仍然存在。
 
说明
 因为消息反射是由MFC而非Windows实现的,因此其父窗口必须派生自CWnd类,否则反射机制将不能正常工作。
 
 
如果在父窗口类中为某个消息提供了处理函数,且该处理函数没有调用基类的实现,那么,该消息将不再被反射到子窗口。下面做一个简单的例子,可以说明这个问题。
1.编程实例
通过类向导在工程Chapter06中添加一个从CEdit派生的类CRedEdit,然后为该类添加如下几个共有成员变量:
 
public:
      COLORREF m_ctrlText;              //文本颜色
      COLORREF m_ctrlBk;                //背景颜色
      CBrush   m_brBk;                  //画刷,用来刷新背景
 
接下来,在该类的构造函数中添加代码,添加后结果如下:
 
CRedEdit::CRedEdit()
{
     m_ctrlText=RGB(0,255,0);           //创建文本颜色,绿色
     m_ctrlBk=RGB(255,0,0);             //创建背景颜色,红色
     m_brBk.CreateSolidBrush(m_ctrlBk); //创建红色画刷
}
 
然后,将在CAboutDlg 类的声明前添加如下代码:
 
#include "RedEdit.h"
 
并且打开“关于”对话框模板,拖放一个编辑控件到对话框,其ID保持默认值即可,再次使用类向导为该控件映射CEdit类型成员变量,变量名为m_strRed。完成后,再把m_strRed的类型CEdit手工改为CRedEdit类型。
最后,仍然使用类向导,为CRedEdit类添加反射WM_CTLCOLOR消息函数。这样,类向导会自动生成一个名为CtlColor的函数和如下的骨架:
 
注意
 在类向导的消息列表中消息名前带有“=”的消息才是当前类被反射的消息。
 
 
BEGIN_MESSAGE_MAP(CRedEdit, CEdit)
     //{{AFX_MSG_MAP(CRedEdit)
     ON_WM_CTLCOLOR_REFLECT()
     //}}AFX_MSG_MAP
END_MESSAGE_MAP()
 
其中的ON_WM_CTLCOLOR_REFLECT()是框架自动添加的反射宏。对函数CtlColor编辑如下:
 
HBRUSH CRedEdit::CtlColor(CDC* pDC, UINT nCtlColor)
{
    // TODO: Change any attributes of the DC here
    pDC->SetTextColor(m_ctrlText);
    pDC->SetBkColor(m_ctrlBk);
    return m_brBk;
}
运行该工程并打开“关于”对话框,将会看到该对话框上一个红色背景的编辑控件,在其中随意输入文本,将会看到文本的颜色正是CtlColor中 设定的绿色。这种变化完全是由CRedEdit完成的,而与其父窗口没有任何关系。但是,如果在CAboutDlg类中提供了此消息的处理函数,那么结果 会如何呢?请再用类向导为CAboutDlg类响应WM_CTLCOLOR消息,函数名默认OnCtlColor,向导生成的代码如下:
 
HBRUSH CAboutDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
    HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
    // TODO: Change any attributes of the DC here
    // TODO: Return a different brush if the default is not desired
    return hbr;
}
 
此时再编译一下工程,并观察编辑控件有没有变化,应该是没有的,因为它调用了基类的实现,所以消息仍然被反射到子窗口。如果在该类中不调用基类 的实现,即如果把其中的第一行代码屏蔽掉,同时让return语句随便返回一个具体值,再编译一次并观察编辑控件状态,则有何变化呢?结果正如前文所 述,WM_CTLCOLOR消息就会直接被CAboutDlg类处理掉,而不会再反射给CRedEdit了,那么开发人员对控件的文本颜色和背景颜色的操 作就失去了控制。
 
技巧
 有时在类向导中并不能看到自己所要映射的消息,譬如CDialog中的WM_NCHITTEST消息等,其实这是类向导的消息过 滤器在作怪。只需要打开“ClassWizard”对话框,然后切换到“Calsslnfo”属性页,将“Message filter”设置为“Window”则就能在“Message Maps”属性页的“Messages”列表中看到WM_NCHITEST消息。
 
 
对于WM_NOTIFY消息,如果开发人员在父窗口类中为它提供了处理函数,则此函数只有在发送该消息的子控件没有提供反射消息处理函数(通过 映射宏ON_NOTIFY_REFLECT实现)时才被调用。同样,如果在消息映射中使用了宏ON_NOTIFY_REFLECT_EX,则用户提供的消 息处理函数既可以允许父窗口处理此消息,也可以拒绝父窗口处理此消息,这有赖于消息处理函数的返回值,如果为TRUE,则可以被父窗口处理,否则拒绝父窗 口处理。
 
说明
 ON_NOTIFY_REFLECT、ON_NOTIFY_REFLECT_EX等是MFC准备的一组用于消息反射的宏,它们也是一种映射消息的机制。
 
 
2.反射消息的传递
本小节的开始部分提到了函数ReflectLastMsg,其实消息被反射后,经历了一系列的调用,以寻找消息的最终归宿,下面简要列出这个调用过程:
 
//反射最后一个消息给子窗口
CWnd::ReflectLastMsg()
//该函数将来自父窗口的通知消息提供给子窗口,以便子窗口可以处理某些任务
CWnd::SendChildNotifyLastMsg()
//当子窗口从父窗口那里收到适宜于自己的通知消息时,父窗口将调用此函数
CWnd::OnChildNotify()
CWnd::ReflectChildNotify()
 
请注意函数OnChildNotify,当窗口收到属于自身的消息时,由控件窗口的父窗口调用此函数,开发人员不要直接调用此函数,它的缺省实现返回0,即是说父窗口必须处理此消息。
接下来,详细了解一下消息被再次分发前最后调用的一个函数ReflectChildNotify,它的定义如下:
 
BOOL CWnd::ReflectChildNotify(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT*
pResult)
{
     switch (uMsg)
     {
     //常规消息
     case WM_HSCROLL:
     …
     case WM_COMPAREITEM:
           //以WM_REFLECT_BASE+uMsg作为反射消息进入消息映射
           return CWnd::OnWndMsg(WM_REFLECT_BASE+uMsg, wParam, lParam, pResult);
     //对于WM_COMMAND的特定情形
     case WM_COMMAND:
           {
               //以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;
     //对于WM_NOTIFY的特定情形
     case WM_NOTIFY:
           {
               //以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), &notify, NULL);
          }
     //其他特定的情形(一般为WM_CTLCOLOR族的通知消息)
     default:
          if (uMsg >= WM_CTLCOLORMSGBOX && uMsg <= WM_CTLCOLORSTATIC)
          {
               …
               //以OCM_CTLCOLOR作为反射消息进入消息映射
               BOOL bResult = CWnd::OnWndMsg(WM_REFLECT_BASE+WM_CTLCOLOR, 0,
               (LPARAM)&ctl, pResult);
               …
          }
          break;
     }
     //子窗口如果没有处理,则返回FALSE,让其父窗口处理
     return FALSE;
}
 
从ReflectChildNotify的实现中,可以发现以下几个问题。
    它将消息消息分为四类:第一类主要包括WM_HSCROLL、WM_VSCROLL、WM_PARENTNOTIFY、WM_DRAWITEM、 WM_MEASUREITEM、WM_DELETEITEM、WM_VKEYTOITEM、WM_CHARTOITEM以及WM_COMPAREITEM 等;第二类是命令消息WM_COMMAND;第三类是通知消息WM_NOTIFY;最后一类是WM_CTLCOLOR族的消息。
    对于第一类和第四类消息调用函数OnWndMsg,由于一般的Windows消息都是在此函数中与第二类和第三类消息开始区分的(见7.3.3);而对于第二和第三类消息则直接调用函数OnCmdMsg即可。
    反射的消息一般都是由原消息值与一个基值(WM_REFLECT_BASE)相加而得到的。基值在MFC中是一个预定义常量,其值为0xBC00。
    消息被OnWndMsg或者OnCmdMsg接收后,后续的传输过程已和前文所讨论的没什么差异了。
消息反射在自定义控件时非常有用,因为它为开发人员提供给了定制控件的机会,允许给控件类添加任何功能,而且它也是重定向消息的一种重要方法。至此,有关消息反射的内容已讲述完毕,6.5节将详细介绍讨论重定向消息的其他方法。
6.3.5  消息的默认处理
在CWnd::WindowProc收到消息后,首先调用OnWndMsg,以常规方式为消息寻求一个处 理函数,当不能够找到合适的处理函数时,再调用缺省处理函数DefWindowProc,DefWindowProc()是默认的窗口过程,所有不能或者 没有被OnWndMsg处理的函数都将交由它处理。关于OnWndMsg的调用前文已经详细讨论过,本节简要的说明一下消息的默认处理过程。首先看一下默 认处理的实现:
 
// 窗口消息的默认处理
LRESULT CWnd::DefWindowProc(UINT nMsg, WPARAM wParam, LPARAM lParam)
{
    if (m_pfnSuper != NULL)
           return ::CallWindowProc(m_pfnSuper, m_hWnd, nMsg, wParam, lParam);
 
    WNDPROC pfnWndProc;
    if ((pfnWndProc = *GetSuperWndProcAddr()) == NULL)
            return ::DefWindowProc(m_hWnd, nMsg, wParam, lParam);
    else
            return ::CallWindowProc(pfnWndProc, m_hWnd, nMsg, wParam, lParam);
}
 
在6.3.2小节中曾提到m_pfnSuper,它在调用::CreateWindowEx后,获取有效值,即原来的窗口过程的指针,它的类型 为WNDPROC。CWnd::DefWindowProc首先检测此值是否有效,若有效则调用它。否则,调用CallWindowProc,此函数主要 用于调用窗口过程链中的指定的窗口过程。不过这些过程一般都是自动完成的。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值