MFC消息处理

一、消息分类

从不同的角度,有如下几种分类方式:

    • 从消息的发送途径上看,可以分为队列消息和非队列消息。
    • 从消息的来源上看,可以分为系统消息和自定义消息。
    • 从对消息的处理上看,可分为窗口消息,命令消息和控件通知。

  1、队列消息和非队列消息

队列消息是指由Windows放入程序的消息队列中的消息,在程序消息循环中,队列消息被重新传回并分配给窗口过程。队列消息大都是用户输入的结果,如击键(WM_KEYDOWN和WM_KEYUP)消息、字符(WM_CHAR)消息、移动鼠标(WM_MOUSEMOVE)消息以及单击鼠标消息(WM_LBUTTONDOWN和WM_LBUTTONUP)消息等。

非队列消息是指在Windows调用窗口时直接传送给窗口过程的消息。大部分消息都是非队列消息。此类消息大部分来自特定的Windows函数,如当调用UpdateWindow时,Windows将给调用此函数窗口的窗口过程发送WM_PAINT;当调用DestoryWindow时,Windows将给调用此函数窗口的窗口过程发送WM_DESTORY等。

综上,队列消息被“发送”给消息队列,而非队列消息则“发送”给窗口过程。

2、系统消息和自定义消息

系统消息ID的范围从0~WM_USER-1,或0X80000~0XBFFFF;应用程序消息从WM_USER(0X0400)~0X7FFF,或0XC000~0XFFFF;WM_USER~0X7FFF范围的消息是由应用程序自己使用:0XC000~0XFFFF范围的消息用来和其它应用程序通信,为了保证ID的唯一性,使用::RegisterWindowMessage来获取该范围的消息ID。

3、窗口消息、命令消息和控件通知

窗口消息(Window Message)是由操作系统和控制其他窗口的窗口所使用的消息,它一般与窗口的内部运作有关,如创建窗口

绘制窗口和销毁窗口等。通常,消息是从系统发送到窗口,或从窗口发送到窗口。此类消息的参数Message、wParam和lParam的格式表1所示。

表1      窗口消息参数
    Message     wParam    lParam
    WM_XXX    定义的命令    定义的命令

命令消息是一种特殊的窗口消息,它从一个窗口发送到另一个窗口,以处理来自用户的请求。当用户单击一个菜单项、工具栏或使用加速键是,将会产生命令消息,并被发送到能处理该请求的类对象。此类消息的参数Message、wParam和lParam的格式表2所示。

表2         命令消息参数
MessagewParamlParam
  WM_COMMAND       0      CommadnID      0

其中wParam的高字为0,低字为CommandID,这个命令ID要么是选中菜单项的ID,要么是被单击的工具栏按钮。需要注意的是,低字CommadnID不能大于一个字长,如果大于一个字长,则系统用0填充高位字。某些控件通知也用WM_COMMAND消息,区别这两种消息的唯一方法是lParam是否为NULL。

控件通知类似于命令消息,当用户与控件窗口交互时,这一类消息就从控件窗口发送到其中窗口。但是,这种消息的目的并不在于处理用户命令,而是为了让主窗口能够更新控件的状态,如加载并显示更多的数据。通常,在发生某些重要事件时,该消息有控件窗口发送到父窗口,它为父窗口进一步控制子窗口提供了机会。它所使用的消息形式有窗口消息形式、命令消息形式和WM_NOTIFY消息形式。

A.窗口消息形式

这种形式的控件通知是窗口消息的自己,因此,它的消息参数和窗口消息一致(表1)。

B.命令消息形式

这种形式的控件通知使用WM_COMMAND消息,虽然它与命令消息共享参数,但它的参数有另外的含义。此类消息的参数Message、wParam和lParam的格式表3所示

表3        命令消息参数
MessagewParamlParam
WM_COMMAND     XN_XXX    控件ID 窗口句柄

其中lParam用来标识是命令消息还是控件通知。如果是命令消息,则lParam为NULL,否则,lParam是一个句柄,用来表示发出的该通知的控件。

而wParam中的高字的XN_XXX值随发出通知控件的不同而变化,低字标识该通知的控件,即控件ID。

C.WM_NOTIFY消息形式

这种形式的控件通知使用WM_NOTIFY消息。标准的32位wParam和lParam消息参数所能提供的信息对于通用控件的需求来说是不够的,因此,通过引入WM_NOTIFY这种新的消息来解决“带宽”的问题。

在这种形式的消息所携带的参数中,wParam标识控件ID,而lParam是一个指向结构的指针,该结构可以是NMHDR或者是包含NMHDR的更大结构,但是后者以NMHDR作为它的第一个成员。需要注意的是,既然NMHDR是某个更大结构的第一个成员,那么指向这个结构的指针就可以用作NMHDR*类型或者指向那个更大结构的指针,当然,这取决于怎么去映射它。

二、消息的传送

消息的传输方式分为两种:寄送和发送。

1、消息的寄送

寄送一个消息时,把消息发送到拥有那个窗口的应用程序的消息队列中。当应用发现自己有空闲时,它就搜索消息队列,并在消息队列中处理消息,即从队列中删除它们,并将它们发送到指定的窗口。

用MFC寄送一个消息的方法是:首先获取接收消息的CWnd类对象的指针;然后调用CWnd的成员函数PostMessage()。该函数寄送消息即刻返回,其返回值放在Res中,它仅标志寄送成功与否。

LRESULT Res = pWnd->PostMessage(UINT Msg, WPARAM wParam, LPARAM lParam);

发送消息到一个没有CWnd类对象的窗口,可以用下列目标窗口的句柄直接调用API:

LRESULT = ::PostMessage(HWND hWnd,UINT Msg, WPARAM wParam, LPARAM lParam );

其中hWnd是目标窗口的句柄。

2、消息的发送

发送一个消息时,消息不进入目标窗口的消息队列,本质上与直接调用窗口处理过程一样,就好象它是另一个函数,所以通信是即时的。直到窗口过程为调用函数返回一个结果后,应用程序才能继续运行。其具体的调用方式与寄送消息时类似,然而消息发送是调用SendMessage而不是PostMessage。另外,返回结果也不太一样,它返回ide是由目标窗口的窗口过程处理而得到的结果。

三、消息的处理

消息被寄送或者发送之后,将会按照一定的路线寻找合适的处理函数,以便得到处理,或者交由默认的窗口过程进行处理。

1、消息的接收

寄送消息一般存放在消息队列中,次消息队列在应用程序初始化时由操作系统所建立。通常,鼠标和键盘单击产生寄送消息,然后应用程序逐一地将它们从消息队列中删除,并将它们发送到被鼠标单击的窗口,或者按键按下时接收输入的窗口。

Windows API提供了两个调用函数,即GetMessage()和PeekMessage(),它们允许应用程序从队列中删除消息。MFC类CWinThread将这些函数调用封装到Run()函数中,Run()函数不断检查消息队列,以判断用户是否进行了键盘或者鼠标等操作;Run()函数还执行一些MFC类的后台维护工作,并未程序员提供机会进行自己的维护工作。

因为寄送的消息要在应用程序的消息队列中花费一些时间,即在GetMessage()函数和PeekMessage()函数取出它之前,它要一直在队列中。一旦从消息队列中删除一条寄送消息,并通过DispatchMessage()将其发送到一个窗口的窗口过程,对它的处理就与接下来所讨论的发消息一样了。

2、窗口过程

窗口过程是窗口消息的处理场所,在Windows中,所有的窗口都有自己的窗口过程,不过同一种窗口类都共享同一个窗口过程。每一个窗口在系统中都有自己唯一的标志——窗口句柄,系统正是通过句柄来实现消息的正确分发。消息结构的第一个成员即是消息要发往的窗口句柄。具体过程就不赘述了。

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


此函数首先调用OnWndMsg(),它试图在类中为该消息寻找一个处理函数;任何返回到WindowProc()而未被处理的消息都被传输到DefWindowProc();DefWindowProc()是缺省的窗口过程,所有不能或者没有被OnWndMsg处理的函数都将交由它处理。它将任何没有被处理的消息都视为一个窗口消息。OnWndMsg()搜索类的消息映射,以便找到一个能处理任何窗口消息的处理函数。

4、消息反射

Windows控件常常向它们的父窗口发送通知消息,例如,为了让画刷能够重画控件的背景颜色,控件一般都会向其父窗口发送WM_CTLCOLOR通知消息。但是这样违背了面向对象编程的意图,因为它要求每个对象都应该包括所有属于它自己的功能。“消息反射”就是MFC完成此项功能引入的一种新的机制。说明:因为消息反射是由MFC而非windows实现的,因此其父窗口必须派生自CWnd类,否则反射机制将不能正常工作。

如果在父窗口类中为某个消息提供了处理函数,且该处理函数没有调用基类的实现,那么,该消息将不能被反射到子窗口。

反射消息的传递过程如下:

// 反射最后一个消息给子窗口
CWnd::ReflectLastMsg()
// 该函数将来自父窗口的通知消息提供给子窗口,以便子窗口可以处理某些任务
CWnd::SendChildNotifyLastMsg()
// 当子窗口从父窗口那里收到适宜于自己的通知消息时,父窗口将调用此函数
CWnd::OnChildNotify()
CWnd::ReflectChildNotify()
请注意函数OnChildNotify,当窗口收到属于自身的消息时,由控件窗口的父窗口调用此函数,开发人员不要直接调用此函数,他的缺省实现返回0,即是说父窗口必须处理此消息。

对于ReflectChildNotify的实现,有以下几个问题:

它将消息分为四类:第一类主要包跨WM_HSCROLL、WM_VSCROLL、WM_PARENTNOTIFY、WM_DRAWITEM、WM_MEASUREITEM、WM_DELETEITEM、WM_VKEYROITEM、WM_CHARTOITEM以及WM_COMPAREITEM等;第二类是命令消息WM_COMMAND;第三类是通知消息WM_NOTIFY;最后一类是WM_CTLCOLOR族消息。

对于第一类和第四类消息调用函数OnWndMsg,由于一般的Windows消息都是在此函数中与第二类和第三类消息开始区分的;而对于第二和第三类消息则直接调用OnCmdMsg即可。

反射的消息一般都是由原消息值与一个基值(WM_REFLECT_BASE)相加而得到的。基值在MFC中是一个预定义常量,其值为0XBC00。

5、消息的默认处理

在CWnd::WindowProc收到消息后,首先调用OnWndMsg,以常规方式为消息寻求一个处理函数,当不能够找到合适的处理函数是,再调用缺省处理函数DefWindowProc,DefWindowProc()是默认的窗口过程,所有不能或者没有被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);
}
CWnd::DefWindowProc首先检测m_pfnSuper是否有效,若有效则调用它;否则调用CallWindowProc,此函数主要用于调用窗口过程链中的指定的窗口过程。
四、重定向消息

MFC引入了重定向消息技术,实现消息的重定向的方法:消息反射、子类化、超类化、重载OnCmdMsg、使用SetWindowsHookEx以及创建自己的消息泵。这里主要介绍子类化、重载OnCmdMsg等比较常用的重定向技术。

1、子类化

子类化就是通过改变一个已经建立的窗口对象中的窗口过程的地址,来截取发往该对象的某些消息的方法。一般,用自己窗口进程的地址替换窗口对象的初始窗口过程的地址即可。这样,用户不必重载原来的窗口过程,只需要处理那些想用自己的窗口过程处理的消息,任何不想处理的消息可以发送到最初的窗口过程。

在MFC中,CWnd类为联结C++对象和Windows窗口对象提供了如下三种方法:

CWnd创建HWND,不过可以在派生类中修改这种行为。通过创建一个派生于CWnd的类,并且调用Create函数即可完成。

将CWnd直接绑定到已存在的HWND,但不能修改已存在的窗口的行为,这种方法一般是通过调用Attach,从而将现存的HWND联结到CWnd C++对象。

将CWnd直接绑定到已存在的HWND,并且可以在派生类中修改窗口的行为。这种方法即“动态子类化”,这意味着可以在运行时改变窗口对象的行为。

这里以CWnd::SubclassWindow为例具体讲述子类化的过程,下面是该函数的实现:

BOOL CWnd::SubclassWindow(HWND hWnd)
{
    if (!Attach(hWnd))
        return FALSE;
    // 允许别的子类化过程
    PreSubclassWindow();
    // 勾入AFX WndProc
    WNDPROC* lplpfn = GetSuperWndProcAddr();
    WNDPROC oldWndProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (DWORD)AfxGetAfxWndProc());
    ...
    return TRUE;
    
}
SubclassWindow函数通过调用Attach,从而实现C++对象和Windows对象的绑定,最后调用函数SetWindowLong,此函数的声明如下:

LONG SetWindowLong(HWND hWnd, int nIndex, LONG dwNewLong);

参数说明。

hWnd:子类化的窗口的句柄;

nIndex:设定值的偏移量;

dwNewLong:子类化的C++类的窗口过程的地址

此函数通过改变与指定窗口相关联的窗口过程,从而实现子类化,这样可以让系统调用新的窗口过程,而非原来的窗口过程。同时,通过调用CallWindowProc将所有为被新窗口处理的消息传递给原来的窗口过程。
2、重载OnCmdMsg

通常,框架自动地将命令消息提供给视图、文档、框架和应用程序类,不过还可以通过重载OnCmdMsg()将消息提供给其他类。然而从当前处理命令消息的类中,调用新类的OnCmdMsg()成员函数。

比如,一个无模式对话框不能自动接收来自主菜单或工具栏的任何命令消息。如果想在一个无模式对话框中处理一个主菜单命令,则需要重载CMainFrame的OnCmdMsg(),并在那里调用无模式对话框的OnCmdMsg(),当然无模式对话框与所有控制一个窗口的类一样,他是派生自CCmdTarget的。

五、自定义消息

用户可以为了特定的目的而定义自己的消息,这些消息也成为自定义消息。进行自定义消息的方法很简单,一般有两种:一种是利用ON_MESSAGE宏,另外一种就是创建自己的消息映射宏,这一种方法可以定制消息的参数类型,而不像前者只能使用固定的消息参数类型。但是一般情况下不需要定制消息的刹那胡类型,因此这里对此不予介绍。

预定义常量值WM_USER(0X0400)是系统专门用来实现处理用户自定义的消息而提供的接口,需要自定义消息是,只需要一如下形式定义即可:

WM_USER + X

其中X是一个非负整数值。另外,需要注意的是,用户自定义消息的值范围是有规定的,前面有提到过。自定义消息的参数内容和返回值都可以定制,根据需要的不同而定制不同的部分。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值