Windows消息机制

一、简介

Windows程序设计是一种事件驱动的程序设计模式,主要是基于消息的。

一个消息从产生到被一个窗口响应,其中有5个步骤:

  1. 系统中发生了某个事件。
  2. Windows把这个事件翻译为消息,然后把它放到消息队列中。
  3. 应用程序从消息队列中接收到这个消息,把它存放在Msg记录中。
  4. 应用程序把消息传递给一个适当的窗口的窗口过程。
  5. 窗口过程响应这个消息并进行处理。

消息本身是作为一个记录传递给应用程序的,这个记录中包含了消息的类型以及其他信息。这个记录类型叫做MSG,MSG含有来自windows应用程序消息队列的消息信息,在Windows中MSG结构体定义如下:

typedef struct tagMsg
{
       HWND    hwnd;       /* 接受该消息的窗口句柄 */
       UINT    message;    /* 消息常量标识符,也就是我们通常所说的消息号 */
       WPARAM  wParam;     /* 32位消息的特定附加信息,确切含义依赖于消息值 */
       LPARAM  lParam;     /* 32位消息的特定附加信息,确切含义依赖于消息值 */
       DWORD   time;       /* 消息创建时的时间 */
       POINT   pt;         /* 消息创建时的鼠标/光标在屏幕坐标系中的位置 */
}MSG;

本文主要就几个基本概念和消息收发过程来介绍Windows消息机制。

二、消息标识符的值

1、系统保留消息

0x0000~0x03ff(WM_USER-1)

这些值被系统定义消息使用,应用程序不能使用这些值给自己的消息。

2、应用程序消息

a)由应用程序自己使用

0X0400(WM_USER)~0X7FFF

b)用来和其他应用程序通信

0XC000~0XFFFF

3、具有标志性的消息

作用
0x0000( WM_NULL)空消息
0x0001~0x0087主要是窗口消息
0x00A0~0x00A9非客户区消息
0x0100~0x0108键盘消息
0x0111~0x0126菜单消息
0x0132~0x0138颜色控制消息
0x0200~0x020A鼠标消息
0x0211~0x0213菜单循环消息
0x0220~0x0230多文档消息
0x03E0~0x03E8DDE消息
0x0400WM_USER
0x8000WM_APP
0x0400~0x7FFF应用程序自定义私有消息

三、消息种类

1、窗口消息

系统中最为常见的消息,它是指由操作系统和控制其他窗口的窗口所使用的消息。例如CreateWindow、DestroyWindow和MoveWindow等都会激发窗口消息,单击鼠标所产生的消息也是一种窗口消息。

2、命令消息

一种特殊的窗口消息,他用来处理从一个窗口发送到另一个窗口的用户请求,例如按下一个按钮,他就会向主窗口发送一个命令消息。

3、控件通知消息

指这样一种消息,一个窗口内的子控件发生了一些事情,需要通知父窗口。
通知消息只适用于标准的窗口控件如按钮、列表框、组合框、编辑框,以及Windows公共控件如树状视图、列表视图等。例如,单击或双击一个控件、在控件中选择部分文本、操作控件的滚动条都会产生通知消息。她类似于命令消息,当用户与控件窗口交互时,那么控件通知消息就会从控件窗口发送到它的主窗口。但是这种消息的存在并不是为了处理用户命令,而是为了让主窗口能够改变控件,例如加载、显示数据。例如按下一个按钮,他向父窗口发送的消息也可以看作是一个控件通知消息;单击鼠标所产生的消息可以由主窗口直接处理,然后交给控件窗口处理。

常见控件通知消息

(1)按扭控件
作用
BN_CLICKED用户单击了按钮
BN_DISABLE按钮被禁止
BN_DOUBLECLICKED用户双击了按钮
BN_HILITE用/户加亮了按钮
BN_PAINT按钮应当重画
BN_UNHILITE加亮应当去掉
(2)组合框控件
作用
CBN_CLOSEUP组合框的列表框被关闭
CBN_DBLCLK用户双击了一个字符串
CBN_DROPDOWN组合框的列表框被拉出
CBN_EDITCHANGE用户修改了编辑框中的文本
CBN_EDITUPDATE编辑框内的文本即将更新
CBN_ERRSPACE组合框内存不足
CBN_KILLFOCUS组合框失去输入焦点
CBN_SELCHANGE在组合框中选择了一项
CBN_SELENDCANCEL用户的选择应当被取消
CBN_SELENDOK用户的选择是合法的
CBN_SETFOCUS组合框获得输入焦点
(3)编辑框控件
作用
EN_CHANGE编辑框中的文本己更新
EN_ERRSPACE编辑框内存不足
EN_HSCROLL用户点击了水平滚动条
EN_KILLFOCUS编辑框正在失去输入焦点
EN_MAXTEXT插入的内容被截断
EN_SETFOCUS编辑框获得输入焦点
EN_UPDATE编辑框中的文本将要更新
EN_VSCROLL用户点击了垂直滚动条消息含义
(4)列表框控件
作用
LBN_DBLCLK用户双击了一项
LBN_ERRSPACE列表框内存不够
LBN_KILLFOCUS列表框正在失去输入焦点
LBN_SELCANCEL选择被取消
LBN_SELCHANGE选择了另一项
LBN_SETFOCUS列表框获得输入焦点

其中窗口消息及控件通知消息主要由窗口类即直接或间接由CWND类派生类处理。相对窗口消息及控件通知消息而言,命令消息的处理对象范围就广得多,它不仅可以由窗口类处理,还可以由文档类,文档模板类及应用类所处理。

四、队列消息和非队列消息

从消息的发送途径来看,消息可以分成2种:队列消息和非队列消息。
消息队列由可以分成系统消息队列和线程消息队列。系统消息队列由Windows维护,线程消息队列则由每个GUI线程自己进行维护,为避免给非GUI线程创建消息队列,所有线程产生时并没有消息队列,仅当线程第一次调用GDI函数时系统才给线程创建一个消息队列。队列消息送到系统消息队列,然后到线程消息队列;非队列消息直接送给目的窗口过程。

1、队列消息

最常见的队列消息是鼠标和键盘触发的消息,例如WM_MOUSERMOVE,WM_CHAR等消息,还有一些其它的消息,例如:WM_PAINT、 WM_TIMER和WM_QUIT。当鼠标、键盘事件被触发后,相应的鼠标或键盘驱动程序就会把这些事件转换成相应的消息,然后输送到系统消息队列,由 Windows系统去进行处理。Windows系统则在适当的时机,从系统消息队列中取出一个消息,根据前面我们所说的MSG消息结构确定消息是要被送往那个窗口,然后把取出的消息送往创建窗口的线程的相应队列,下面的事情就该由线程消息队列操心了,Windows开始忙自己的事情去了。线程看到自己的消息队列中有消息,就从队列中取出来,通过操作系统发送到合适的窗口过程去处理。
一般来讲,系统总是将消息Post在消息队列的末尾。这样保证窗口以先进先出的顺序接受消息。然而,WM_PAINT是一个例外,同一个窗口的多个 WM_PAINT被合并成一个 WM_PAINT 消息, 合并所有的无效区域到一个无效区域。合并WM_PAIN的目的是为了减少刷新窗口的次数。

2、非队列消息

非队列消息将会绕过系统队列和消息队列,直接将消息发送到窗口过程。系统发送非队列消息通知窗口,系统发送消息通知窗口。例如,当用户激活一个窗口系统发送WM_ACTIVATE, WM_SETFOCUS, and WM_SETCURSOR。这些消息通知窗口它被激活了。非队列消息也可以由当应用程序调用系统函数产生。例如,当程序调用SetWindowPos系统发送WM_WINDOWPOSCHANGED消息。一些函数也发送非队列消息,例如下面我们要谈到的函数。

五、消息的发送

讲完上面这些基础理论后,我们就来介绍下简单的消息发送和接收。把一个消息发送到窗口有3种方式:发送、寄送和广播。

1、发送消息

发送消息的函数,主要有SendMessage、SendMessageCallback、SendNotifyMessage、 SendMessageTimeout。
SendMessage原型如下:

LRESULT SendMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam)

这个函数主要是向一个或多个窗口发送一条消息,一直等到消息被处理之后才会返回。不过需要注意的是,如果接收消息的窗口是同一个应用程序的一部分,那么这个窗口的窗口函数就被作为一个子程序马上被调用;如果接收消息的窗口是被另外的线程所创建的,那么窗口系统就切换到相应的线程并且调用相应的窗口函数,这条消息不会被放进目标应用程序队列中。函数的返回值是由接收消息的窗口的窗口函数返回,返回的值取决于被发送的消息。

2、寄送消息

寄送消息的函数,主要有PostMessage、PostThreadMessage、 PostQuitMessage。

PostMessage原型如下:

BOOL PostMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam)

该函数把一条消息放置到创建hWnd窗口的线程的消息队列中,该函数不等消息被处理就马上将控制返回。需要注意的是,如果hWnd参数为 HWND_BROADCAST,那么,消息将被寄送给系统中的所有的重叠窗口和弹出窗口,但是子窗口不会收到该消息;如果hWnd参数为NULL,则该函数类似于将dwThreadID参数设置成当前线程的标志来调用PostThreadMEssage函数。

发送方式和寄送方式的区别:被发送的消息是否会被立即处理,函数是否立即返回。被发送的消息会被立即处理,处理完毕后函数才会返回;被寄送的消息不会被立即处理,他被放到一个先进先出的队列中,一直等到应用程序空线的时候才会被处理,不过函数放置消息后立即返回。你可以要求操作系统截获所有被发送的消息,但是不能够截获对窗口处理过程的直接调用。以寄送方式发送的消息通常是与用户输入事件相对应的,因为这些事件不是十分紧迫,可以进行缓慢的缓冲处理,例如鼠标、键盘消息会被寄送,而按钮等消息则会被发送。

3、广播消息

广播消息用得比较少,函数主要有BroadcastSystemMessage、 BroadcastSystemMessageEx。

BroadcastSystemMessage函数原型如下:

long BroadcastSystemMessage(DWORD dwFlags,LPDWORD lpdwRecipients,UINT uiMessage,WPARAM wParam,LPARAM lParam)

该函数可以向指定的接收者发送一条消息,这些接收者可以是应用程序、可安装的驱动程序、网络驱动程序、系统级别的设备驱动消息和他们的任意组合。需要注意的是,如果dwFlags参数是BSF_QUERY并且至少一个接收者返回了BROADCAST_QUERY_DENY,则返回值为0,如果没有指定BSF_QUERY,则函数将消息发送给所有接收者,并且忽略其返回值。

六、消息的接收

消息的接收主要有3个函数:GetMessage、PeekMessage、WaitMessage。

1、GetMessage

原型如下:

BOOL GetMessage(LPMSG lpMsg,HWND hWnd,UINT wMsgFilterMin,UINT wMsgFilterMax)

该函数用来获取与hWnd参数所指定的窗口相关的且wMsgFilterMin和wMsgFilterMax参数所给出的消息值范围内的消息。需要注意的是,如果hWnd为NULL,则GetMessage获取属于调用该函数应用程序的任一窗口的消息,如果 wMsgFilterMin和wMsgFilterMax都是0,则GetMessage就返回所有可得到的消息。函数获取之后将删除消息队列中的除 WM_PAINT消息之外的其他消息,至于WM_PAINT则只有在其处理之后才被删除。

2、PeekMessage

原型如下:

BOOL PeekMessage(LPMSG lpMsg,HWND hWnd,UINT wMsgFilterMin,UINT wMsgFilterMax,UINT wRemoveMsg)。

该函数用于查看应用程序的消息队列,如果其中有消息就将其放入lpMsg所指的结构中,不过,与GetMessage不同的是,PeekMessage函数不会等到有消息放入队列时才返回。同样,如果hWnd为NULL,则PeekMessage获取属于调用该函数应用程序的任一窗口的消息,如果hWnd=-1,那么函数只返回把hWnd参数为NULL的PostAppMessage函数送去的消息。如果 wMsgFilterMin和wMsgFilterMax都是0,则PeekMessage就返回所有可得到的消息。函数获取之后将视最后一个参数来决定是否删除消息队列中的除 WM_PAINT消息之外的其他消息,至于WM_PAINT则只有在其处理之后才被删除。

3、WaitMessage

原型如下:

BOOL WaitMessage();

当一个应用程序无事可做时,该函数就将控制权交给另外的应用程序,同时将该应用程序挂起,直到一个新的消息被放入应用程序的队列之中才返回。

七、消息的处理

接下来我们谈一下消息的处理,首先我们来看一下VC中的消息泵:

while(GetMessage(&msg, NULL, 0, 0))
{
       if(!TranslateAccelerator(msg.hWnd, hAccelTable, &msg))
      { 
            TranslateMessage(&msg);
            DispatchMessage(&msg);
       }
}

首先,GetMessage从进程的主线程的消息队列中获取一个消息并将它复制到MSG结构,如果队列中没有消息,则GetMessage函数将等待一个消息的到来以后才返回。如果你将一个窗口句柄作为第二个参数传入GetMessage,那么只有指定窗口的的消息可以从队列中获得。GetMessage也可以从消息队列中过滤消息只接受消息队列中落在范围内的消息。这时候就要利用GetMessage/PeekMessage指定一个消息过滤器。这个过滤器是一个消息标识符的范围或者是一个窗体句柄,或者
两者同时指定。当应用程序要查找一个后入消息队列的消息是很有用。WM_KEYFIRST 和 WM_KEYLAST 常量用于接受所有的键盘消息。 WM_MOUSEFIRST 和 WM_MOUSELAST 常量用于接受所有的鼠标消息。

然后TranslateAccelerator判断该消息是不是一个按键消息并且是一个加速键消息,如果是,则该函数将把几个按键消息转换成一个加速键消息传递给窗口的回调函数。处理了加速键之后,函数TranslateMessage将把两个按键消息WM_KEYDOWN和WM_KEYUP转换成一个 WM_CHAR,不过需要注意的是,消息WM_KEYDOWN,WM_KEYUP仍然将传递给窗口的回调函数。

处理完之后,DispatchMessage函数将把此消息发送给该消息指定的窗口中已设定的回调函数。如果消息是WM_QUIT,则 GetMessage返回0,从而退出循环体。应用程序可以使用PostQuitMessage来结束自己的消息循环。通常在主窗口的 WM_DESTROY消息中调用。

消息泵的运用例子

if (::PeekMessage(&msg, m_hWnd, WM_KEYFIRST,WM_KEYLAST, PM_REMOVE))
{
          if (msg.message == WM_KEYDOWN && msg.wParam == VK_ESCAPE)
          {
                  \* ...  *\
           }
}

这里我们接受所有的键盘消息,所以就用WM_KEYFIRST 和 WM_KEYLAST作为参数。最后一个参数可以是PM_NOREMOVE 或者 PM_REMOVE,表示消息信息是否应该从消息队列中删除。
所以这段小代码就是判断是否按下了Esc键,如果是就进行处理。

八、窗口过程

窗口过程是一个用于处理所有发送到这个窗口的消息的函数。任何一个窗口类都有一个窗口过程。同一个类的窗口使用同样的窗口过程来响应消息。系统发送消息给窗口过程将消息数据作为参数传递给他,消息到来之后,按照消息类型排序进行处理,其中的参数则用来区分不同的消息,窗口过程使用参数产生合适行为。

一个窗口过程不经常忽略消息,如果他不处理,它会将消息传回到执行默认的处理。窗口过程通过调用DefWindowProc来做这个处理。窗口过程必须 return一个值作为它的消息处理结果。大多数窗口只处理小部分消息和将其他的通过DefWindowProc传递给系统做默认的处理。窗口过程被所有属于同一个类的窗口共享,能为不同的窗口处理消息。

代码示例:

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
 int wmId, wmEvent;
 PAINTSTRUCT ps;
 HDC hdc;
 TCHAR szHello[MAX_LOADSTRING];
 LoadString(hInst, IDS_HELLO, szHello, MAX_LOADSTRING);

 switch (message) 
 {
  case WM_COMMAND:
         wmId    = LOWORD(wParam); 
         wmEvent = HIWORD(wParam); 
         /* Parse the menu selections: */
         switch (wmId)
         {
          case IDM_ABOUT:
             DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About);
             break;
          case IDM_EXIT:
             DestroyWindow(hWnd);
             break;
          default:
             return DefWindowProc(hWnd, message, wParam, lParam);
         }
   break;

  case WM_PAINT:
         hdc = BeginPaint(hWnd, &ps);
         /* TODO: Add any drawing code here */
         RECT rt;
         GetClientRect(hWnd, &rt);
         DrawText(hdc, szHello, strlen(szHello), &rt, DT_CENTER);
         EndPaint(hWnd, &ps);
         break;

  case WM_DESTROY:
         PostQuitMessage(0);
         break;
  default:
         return DefWindowProc(hWnd, message, wParam, lParam);
  }
  return 0;
}

九、消息分流器

利用消息分流器,我们可以把switch语句分成更小的函数,每一个消息都对应一个小函数,这样做的好处就是对消息更容易管理。之所以被称为消息分流器,就是因为它可以对任何消息进行分流。
代码示例:

int id,HWND hWndCtl,UINT codeNotify)
{
      switch(id)
      {
     case ID_A:
                  if(codeNotify==EN_CHANGE)
                  break;
     case ID_B:
                  if(codeNotify==BN_CLICKED)
                  break;
             .
       }
}

然后修改一下窗口过程:

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
       switch(message)
      {
             HANDLE_MSG(hWnd,WM_COMMAND,MsgCracker);
             HANDLE_MSG(hWnd,WM_DESTROY,MsgCracker);
           default:
                    return DefWindowProc(hWnd, message, wParam, lParam);
   }
  return 0;
}

在WindowsX.h中定义了如下的HANDLE_MSG宏:

#define HANDLE_MSG(hwnd,msg,fn) \
             switch(msg): return HANDLE_##msg((hwnd),(wParam),(lParam),(fn));

实际上,HANDLE_WM_XXXX都是宏,例如:

HANDLE_MSG(hWnd,WM_COMMAND,MsgCracker);

将被转换成如下定义:

#define HANDLE_WM_COMMAND(hwnd,wParam,lParam,fn)\ 
             ((fn)((hwnd),(int)(LOWORD(wParam)),(HWND)(lParam),(UINT)HIWORD(wParam)),0L);

在windowsx.h里面还有一个宏:FORWARD_WM_XXXX,还是以WM_COMMAND为例进行分析:

#define FORWARD_WM_COMMAND(hwnd, id, hwndCtl, codeNotify, fn) \
     (void)(fn)((hwnd), WM_COMMAND, MAKEWPARAM((UINT)(id),(UINT)(codeNotify)), (LPARAM)(HWND)(hwndCtl))

所以实际上,FORWARD_WM_XXXX将消息参数进行了重新构造,生成了wParam && lParam,然后调用了我们定义的函数。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值