window 消息传递机制【复杂版本】
一、消息概述
众人周知,window系统是一个消息驱动的系统, windows操作系统本身有自己的消息队列称做系统消息队列(操作系统队列),消息循环,它捕捉键盘,鼠标的动作生成消息,并将这个消息传给应用程序的消息队列称作线程消息队列(应用程序队列)。 余下的工作有应用程序处理消息, windows 消息机制在这儿就不再讲述,我们重点讲述应用程序的消息机制。 大家只要明白消息是由操作系统传递给应用程序的。 一副图更能详细说明:
窗口和窗口类
Windows UI 应用程序 (e) 具有一个主线程 (g)、一个或多个窗口 (a) 和一个或多个子线程 (k) [工作线程或 UI 线程]。
应用程序必须指定窗口类并向 Windows (d) 注册,然后才能创建窗口 (a) 并显示。窗口类是一种包含窗口属性的结构,例如窗口样式,图标,光标,背景颜色,菜单资源名称和窗口类名称等。注册窗口类会将窗口过程、类样式和其他类属性与类名相关联。
每个窗口类都有一个关联的窗口过程 (c),该过程由应用程序中同一类的所有窗口 (a) 共享。窗口过程处理该类的所有窗口的消息。
#include <stdio.h> #include <windows.h> #include <stdexcept> using namespace std; //回调函数原型声明,返回长整形的结果码,CALLBACK是表示stdcall调用LRESULT CALLBACK WinProc( HWND hwnd, // handle to window UINT uMsg, // message identifier WPARAM wParam, // first message parameter LPARAM lParam // second message parameter ); //(1) WinMain函数,程序入口点函数 int WINAPI WinMain( HINSTANCE hInstance, // handle to current instance HINSTANCE hPrevInstance, // handle to previous instance LPSTR lpCmdLine, // command line int nCmdShow // show state ){ //(2) //一.设计一个窗口类,类似填空题,使用窗口结构体 WNDCLASS wnd; wnd.cbClsExtra = 0; //类的额外内存 wnd.cbWndExtra = 0; //窗口的额外内存 wnd.hbrBackground = (HBRUSH)GetStockObject(NULL_BRUSH);//创建一个空画刷填充背景 //加载游标,如果是加载标准游标,则第一个实例标识设置为空 wnd.hCursor = LoadCursor(NULL, IDC_CROSS); wnd.hIcon = LoadIcon(NULL, IDI_ERROR); wnd.hInstance = hInstance;//实例句柄赋值为程序启动系统分配的句柄值 wnd.lpfnWndProc = WinProc;//消息响应函数 wnd.lpszClassName = "gaojun";//窗口类的名子,在注册时会使用到 wnd.lpszMenuName = NULL;//默认为NULL没有标题栏 wnd.style = CS_HREDRAW | CS_VREDRAW;//定义为水平和垂直重画 //二.注册窗口类 RegisterClass(&wnd); //三.根据定制的窗口类创建窗口 HWND hwnd;//保存创建窗口后的生成窗口句柄用于显示 //如果是多文档程序,则最后一个参数lParam必须指向一个CLIENTCREATESTRUCT结构体 hwnd = CreateWindow("gaojun", "WIN32应用程序", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, hInstance, NULL); //四.显示窗口 ShowWindow(hwnd, SW_SHOWDEFAULT); //五.更新窗口 UpdateWindow(hwnd); //(3).消息循环 MSG msg;//消息结构体 //如果消息出错,返回值是-1,当GetMessage从消息队列中取到是WM_QUIT消息时,返回值是0 //也可以使用PeekMessage函数从消息队列中取出消息 BOOL bSet; while((bSet = GetMessage(&msg, NULL, 0, 0)) != 0){ if (-1 == bSet) { return -1; } else{ TranslateMessage(&msg); DispatchMessage(&msg); } } return 0;//程序结束,返回0 } //消息循环中对不同的消息各类进行不同的响应 LRESULT CALLBACK WinProc( HWND hwnd, // handle to window UINT uMsg, // message identifier WPARAM wParam, // first message parameter LPARAM lParam // second message parameter ){ switch (uMsg) { case WM_CHAR://字符按键消息 char szChar[20]; sprintf(szChar, "char is %d;", wParam);//格式化操作,stdio.h MessageBox(hwnd, szChar, "gaojun", 0);//输出操作windows.h中 break; case WM_LBUTTONDOWN://鼠标左键按下消息 MessageBox(hwnd, "this is click event!", "点击", 0); HDC hdc; hdc = GetDC(hwnd);//获取设备上下文句柄,用来输出文字 //在x=0,y=50(像素)的地方输出文字 TextOut(hdc, 0, 50, "响应WM_LBUTTONDONW消息!", strlen("响应WM_LBUTTONDONW消息!")); ReleaseDC(hwnd, hdc);//在使用完DC后一定要注意释放 break; case WM_PAINT://窗口重给时报消息响应 HDC hDc; PAINTSTRUCT ps; hDc = BeginPaint(hwnd, &ps); TextOut(hDc, 0, 0, "这是一个Paint事件!", strlen("这是一个Paint事件!")); EndPaint(hwnd, &ps); break; case WM_CLOSE://关闭消息 if (IDYES == MessageBox(hwnd, "确定要关闭当前窗口?", "提示", MB_YESNO)) { DestroyWindow(hwnd);//销毁窗口 } break; case WM_DESTROY: PostQuitMessage(0);//在响应消息后,投递一个退出的消息使用程序安全退出 break; default: return DefWindowProc(hwnd, uMsg, wParam, lParam);//调用缺省的消息处理过程函数 } return 0; }
消息队列,每一个Windows应用程序开始执行后,系统都会为该程序创建一个消息队列,这个消息队列用来存放该程序创建的窗口(按钮 文本框等都是窗口)的消息。即应用程序队列,用来存放该程序可能创建的各种窗口的消息。应用程序中含有一段称作“消息循环”的代码,用来从消息队列中检索这些消息并把它们分发到相应的窗口函数中。
进队消息(队列消息)OS将产生的消息放在应用程序的消息队列中,让应用程序来处理
不进队消息(非队列消息)OS直接调用窗口的处理过程
应用程序的执行是通过消息驱动的。消息是整个应用程序的工作引擎,我们需要理解掌握我们使用的编程语言是如何封装消息的原理。
window消息来源
消息可以由系统或者应用程序产生。系统在发生输入事件时产生消息。举个例子, 当用户敲键, 移动鼠标或者单击控件。系统也产生消息以响应由应用程序带来的变化, 比如应用程序改变系统字体,改变窗体大小。应用程序可以产生消息使窗体执行任务,或者与其他应用程序中的窗口通讯。
1、消息
Windows中,消息使用统一的结构体(MSG)来存放信息,其中message表明消息的具体的类型,
而wParam,lParam是其最灵活的两个变量,为不同的消息类型时,存放数据的含义也不一样。
time表示产生消息的时间,pt表示产生消息时鼠标的位置。
Windows中消息MSG声明如下:
typedef struct tagMsg
{
HWND hwnd; // 接受该消息的窗口句柄 ,告诉操作系统,应该把消息发生给哪个应用 哪个窗口
UINT message; // 消息常量标识符,也就是我们通常所说的消息号
WPARAM wParam; // 32位消息的特定附加信息,确切含义依赖于消息值
LPARAM lParam; // 32位消息的特定附加信息,确切含义依赖于消息值
DWORD time; // 消息创建时的时间
POINT pt; // 消息创建时的鼠标/光标在屏幕坐标系中的位置
}MSG;
2、消息类型
(0) 消息ID范围
系统定义消息ID范围:[0x0000, 0x03ff]
用户自定义的消息ID范围:
WM_USER: 0x0400-0x7FFF (例:WM_USER+10)
WM_APP(winver> 4.0):0x8000-0xBFFF (例:WM_APP+4)
RegisterWindowMessage:0xC000-0xFFFF【用来和其他应用程序通信,为了ID的唯一性,使用::RegisterWindowMessage来得到该范围的消息ID 】
(1) 窗口消息:即与窗口的内部运作有关的消息,如创建窗口,绘制窗口,销毁窗口等。
可以是一般的窗口,也可以是MainFrame,Dialog,控件等。
如:WM_CREATE, WM_PAINT, WM_MOUSEMOVE, WM_CTLCOLOR, WM_HSCROLL等
(2) 当用户从菜单选中一个命令项目、按下一个快捷键或者点击工具栏上的一个按钮,都将发送WM_COMMAND命令消息。
LOWORD(wParam)表示菜单项,工具栏按钮或控件的ID;如果是控件, HIWORD(wParam)表示控件消息类型。
#define LOWORD(l) ((WORD)(l))
#define HIWORD(l) ((WORD)(((DWORD)(l) >> 16) & 0xFFFF))
(3) 随着控件的种类越来越多,越来越复杂(如列表控件、树控件等),仅仅将wParam,lParam将视为一个32位无符号整数,已经装不下太多信息了。
为了给父窗口发送更多的信息,微软定义了一个新的WM_NOTIFY消息来扩展WM_COMMAND消息。
WM_NOTIFY消息仍然使用MSG消息结构,只是此时wParam为控件ID,lParam为一个NMHDR指针,
不同的控件可以按照规则对NMHDR进行扩充,因此WM_NOTIFY消息传送的信息量可以相当的大。
注:Window 9x 版及以后的新控件通告消息不再通过WM_COMMAND 传送,而是通过WM_NOTIFY 传送,
但是老控件的通告消息, 比如CBN_SELCHANGE 还是通过WM_COMMAND 消息发送。
(4) windwos也允许程序员定义自己的消息,使用SendMessage或PostMessage来发送消息。
3、消息队列(Message Queues)
Windows中有两种类型的消息队列
1) 系统消息队列(System Message Queue)
这是一个系统唯一的Queue,设备驱动(mouse, keyboard)会把操作输入转化成消息存在系统队列中,然后系统会把此消息放到目标窗口所在的线程的消息队列(thread-specific message queue)中等待处理
2) 线程消息队列(Thread-specific Message Queue)
每一个GUI线程都会维护这样一个线程消息队列。(这个队列只有在线程调用GDI函数时才会创建,默认不创建)。然后线程消息队列中的消息会被送到相应的窗口过程(WndProc)处理.
注意: 线程消息队列中WM_PAINT,WM_TIMER只有在Queue中没有其他消息的时候才会被处理,WM_PAINT消息还会被合并以提高效率。其他所有消息以先进先出(FIFO)的方式被处理。
4、队列消息(Queued Messages)和非队列消息(Non-Queued Messages)
1)队列消息(Queued Messages)
消息会先保存在消息队列中,消息循环会从此队列中取出消息并分发到各窗口处理
如:WM_PAINT,WM_TIMER,WM_CREATE,WM_QUIT,以及鼠标,键盘消息等。
其中,WM_PAINT,WM_TIMER只有在队列中没有其他消息的时候才会被处理,
WM_PAINT消息还会被合并以提高效率。其他所有消息以先进先出(FIFO)的方式被处理。
2) 非队列消息(NonQueued Messages)
消息会绕过系统消息队列和线程消息队列直接发送到窗口过程被处理 如: WM_ACTIVATE, WM_SETFOCUS, WM_SETCURSOR, WM_WINDOWPOSCHANGED
注意: postMessage发送的消息是队列消息,它会把消息Post到消息队列中; SendMessage发送的消息是非队列消息, 被直接送到窗口过程处理.
队列消息和非队列消息的区别
从消息的发送途径来看,消息可以分成2种:队列消息和非队列消息。消息队列由可以分成系统消息队列和线程消息队列。系统消息队列由Windows维护,线程消息队列则由每个GUI线程自己进行维护,为避免给non-GUI现成创建消息队列,所有线程产生时并没有消息队列,仅当线程第一次调用GDI函数时系统才给线程创建一个消息队列。队列消息送到系统消息队列,然后到线程消息队列;非队列消息直接送给目的窗口过程。
对于队列消息,最常见的是鼠标和键盘触发的消息,例如WM_MOUSERMOVE,WM_CHAR等消息,还有一些其它的消息,例如:WM_PAINT、
WM_TIMER和WM_QUIT。当鼠标、键盘事件被触发后,相应的鼠标或键盘驱动程序就会把这些事件转换成相应的消息,然后输送到系统消息队列,由
Windows系统去进行处理。Windows系统则在适当的时机,从系统消息队列中取出一个消息,根据前面我们所说的MSG消息结构确定消息是要被送往那个窗口,然后把取出的消息送往创建窗口的线程的相应队列,下面的事情就该由线程消息队列操心了,Windows开始忙自己的事情去了。线程看到自己的消息队列中有消息,就从队列中取出来,通过操作系统发送到合适的窗口过程去处理。
一般来讲,系统总是将消息Post在消息队列的末尾。这样保证窗口以先进先出的顺序接受消息。然而,WM_PAINT是一个例外,同一个窗口的多个
WM_PAINT被合并成一个 WM_PAINT 消息, 合并所有的无效区域到一个无效区域。合并WM_PAIN的目的是为了减少刷新窗口的次数。
5、应用程序消息循环(message loop)
Windows为当前执行的每个Windows程序维护一个「消息队列」。在发生输入事件之后,Windows将事件转换为一个「消息」并将消息放入程序的消息队列中。程序通过执行一块称之为「消息循环」的程序代码从消息队列中取出消息:
消息循环代码是应用程序中主函数WinMain ( )中类似如下的程序段:
while(GetMessage(&msg,NULL,0,0)) //GetMessage的主要功能是从消息队列中“取出”消息,消息被取出以后,就从消息队列中将其删除;,接收到WM_QUIT消息时,才返回0 { TranslateMessage(&msg); //检索并生成字符消息WM_CHAR,转换消息格式 DispatchMessage(&msg); //分发消息给相应的窗口函数 } //msg变量是型态为MSG的结构,型态MSG在WINUSER.H中定义如下: typedef struct tagMSG { HWND hwnd ; UINT message ; WPARAM wParam ; LPARAM lParam ; DWORD time ; POINT pt ; } MSG, * PMSG ;
由此可见,所谓“消息循环”,其实就是一个While循环语句罢了。
-
GetMessage()函数每次从消息队列中取出一条消息,GetMessage的主要功能是从消息队列中“取出”消息,消息被取出以后,就从消息队列中将其删除;
-
TranslateMessage()函数主要用于将WM_KEYDOWN和WM_KEYUP消息转换WM_CHAR消息。
-
消息处理的关键是DispatchMessage()函数。这个函数根据取出的消息中所包含的窗体句柄,将这一消息转发给引此句柄所对应的窗体对象。
- 。
5.1窗体函数(WindowProc)
Windows 应用程序创建的每个窗口都在系统核心注册一个相应的窗口函数,窗口函数程序代码形式上是一个巨大的switch 语句,用以处理由消息循环发送到该窗口的消息,窗口函数由Windows 采用消息驱动的形式直接调用,而不是由应用程序显示调用的,窗口函数处理完消息后又将控制权返回给Windows。
//而窗体负责响应消息的函数称为“窗体过程(Window Procedure)”,窗体过程是一个函数,每个窗体一个,它大致拥有以下的“模样”(C++代码): LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { //…… switch (uMsg) //依据消息标识符进行分类处理 { case WM_CREATE: // 初始化窗体. return 0; case WM_PAINT: // 绘制窗体 return 0; // //处理其他消息 // default: //如果窗体没有定义处理此种消息的代码,则转去调用系统默认的消息处理函数 return DefWindowProc(hwnd, uMsg, wParam, lParam); } } //可以看到,“窗体过程”不过就是一个多分支语句罢了,在这个语句中,窗体对不同类型的消息进行处理。
PostMessage(异步)和SendMessage(同步)的区别:
a、 PostMessage 是异步的,SendMessage 是同步的。
PostMessage 只把消息放到队列,不管消息是不是被处理就返回,消息可能不被处理;
SendMessage等待消息被处理完了才返回,如果消息不被处理,发送消息的线程将一直处于阻塞状态,等待消息的返回。
b、 同一个线程内:
SendMessage 发送消息时,由USER32.DLL模块调用目标窗口的消息处理程序,并将结果返回,SendMessage 在同一个线程里面发送消息不进入线程消息队列;PostMessage 发送的消息要先放到消息队列,然后通过消息循环分派到目标窗口(DispatchMessage)。
c、不同线程:
SendMessage 发送消息到目标窗口的消息队列,然后发送消息的线程在USER32。DLL模块内监视和等待消息的处理结果,直到目标窗口的才处理返回,SendMessage在返回之前还需要做许多工作,如响应别的线程向它发送的SendMessage().PostMessge() 到别的线程的时候最好使用PostThreadMessage 代替。PostMessage()的HWND 参数可以为NULL,相当于PostThreadMessage() + GetCrrentThreadId.
d、系统处理消息。
系统只处理(marshal)系统消息(0--WM_USER),发送用户消息(用户自己定义)时需要用户自己处理。
使用PostMessage,SendNotifyMessage,SendMessageCallback等异步函数发送系统消息时,参数不可以使用指针,因为发送者不等待消息的处理就返回,接收者还没有处理,指针就有可能被释放了,或则内容变化了。
e、在Windows 2000/XP,每个消息队列最多只能存放一定数量的消息,超过的将不会被处理就丢掉。系统默认是10000;:[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows] USERPostMessageLimit
peekmessage和getmessage的区别:
PeekMessage:有消息时返回TRUE,没有消息返回FALSE
GetMessage:有消息时且消息不为WM_QUIT时返回TRUE,如果有消息且为WM_QUIT则返回FALSE,没有消息时不返回。
GetMessage:取得消息后,删除除WM_PAINT消息以外的消息。
PeekMessage:取得消息后,根据wRemoveMsg参数判断是否删除消息。PM_REMOVE则删除,PM_NOREMOVE不删除。
The PeekMessage function normally does not remove WM_PAINT messages from the queue. WM_PAINT messages remain in the queue until they are processed. However, if a WM_PAINT message has a null update region, PeekMessage does remove it from the queue.
不能用PeekMessage从消息队列中删除WM_PAINT消息,从队列中删除WM_PAINT消息可以令窗口显示区域的失效区域变得有效(刷新窗口),如果队列中包含WM_PAINT消息程序就会一直while循环了。
消息分类
-
窗口消息:WM_CREATE,WM_DESTROY,WM_CLOSE
与窗口的内部运作有关,如创建窗口,绘制窗口,销毁窗口等。
我们创建一个窗口对象的时候,这个窗口对象在创建过程中收到的就是WM_CREATE消息,对这个消息的处理过程一般用来设置一些显示窗口前的初始化工作,如设置窗口的大小,背景颜色等,WM_DESTROY消息指示窗口即将要被撤消,在这个消息处理过程中,我们就可以做窗口撤消前的一些工作。WM_CLOSE消息发生在窗口将要被关闭之前,在收到这个消息后,一般性的操作是回收所有分配给这个窗口的各种资源。在windows系统中资源是很有限的,所以回收资源的工作还是非常重要的。
-
键盘消息:WM_CHAR,WM_KEYDOWN,WM_KEYUP
这三个消息用来处理用户的键盘数据,当用户在键盘上按下某个键的时候,会产生WM_KEYDOWN消息,释放按键的时候又回产生WM_KEYUP消息,所以WM_KEYDOWN与WM_KEYUP消息一般总是成对出现的,至于WM_CHAR消息是在用户的键盘输入能产生有效的ASCII码时才会发生。这里特别提醒要注意前两个消息与WM_CHAR消息在使用上是有区别的。在前两个消息中,伴随消息传递的是按键的虚拟键码,所以这两个消息可以处理非打印字符,如方向键,功能键等。而伴随WM_CHAR消息的参数是所按的键的ASCII码,ASCII码是可以区分字母的大小写的。而虚拟键码是不能区分大小写的。
-
鼠标消息:WM_MOUSEMOVE,WM_LBUTTONDOWN, WM_LBUTTONUP,WM_LBUTTONDBCLICK,WM_RBUTTONDOWN, WM_RBUTTONUP,WM_RBUTTONDBCLICK
这组消息是与鼠标输入相关的,WM_MOUSEMOVE消息发生在鼠标移动的时候,剩余的六个消息则分别对应于鼠标左右键的按下、释放、双击事件,要指出的是WINDOWS系统并不是在鼠标每移动一个像素时都产生MOUSEMOVE消息,这一点要特别注意。
-
另一组窗口消息:WM_MOVE , WM_SIZE , WM_PAINT
当窗口移动的时候产生WM_MOVE 消息,窗口的大小改变的时候产生WM_SIZE消息,而当窗口工作区中的内容需要重画的时候就会产生WM_PAINT消息。
-
焦点消息WM_SETFOCUS,WM_KILLFOCUS
当一个窗口从非活动状态变为具有输入焦点的活动状态的时候,它就会收到WM_SETFOCUS消息,而当窗口失去输入焦点的时候它就会收到WM_KILLFOCUS消息。
-
命令消息(Command Message)
与处理用户请求有关, 如单击菜单项或工具栏或控件时, 就会产生命令消息。
WM_COMMAND, LOWORD(wParam)表示菜单项,工具栏按钮或控件的ID。如果是控件, HIWORD(wParam)表示控件消息类型
-
控件通知(Notify Message)
控件通知消息, 这是最灵活的消息格式, 其Message, wParam, lParam分别为:WM_NOTIFY, 控件ID,指向NMHDR的指针。NMHDR包含控件通知的内容, 可以任意扩展。
-
定时器消息:WM_TIMER
当我们为一个窗口设置了定时器资源之后,系统就会按规定的时间间隔向窗口发送WM_TIMER消息,在这个消息中就可以处理一些需要定期处理的事情。
最后要指出的一点是,在WINDOWS环境下,消息的来源是多方面的,最常见的是用户的操作产生消息,系统在必要的时候也会向程序发送系统消息,其他在运行中的程序也可以向程序发送消息。此外,在程序的内部,也可以根据需要在适当的时候主动产生消息,比如主动产生WM_PAINT消息以实现需要的重画功能。
按扭控件
-
BN_CLICKED 用户单击了按钮
-
BN_DISABLE 按钮被禁止
-
BN_DOUBLECLICKED 用户双击了按钮
-
BN_HILITE 用/户加亮了按钮
-
BN_PAINT 按钮应当重画
-
BN_UNHILITE 加亮应当去掉
组合框控件
-
CBN_CLOSEUP 组合框的列表框被关闭
-
CBN_DBLCLK 用户双击了一个字符串
-
CBN_DROPDOWN 组合框的列表框被拉出
-
CBN_EDITCHANGE 用户修改了编辑框中的文本
-
CBN_EDITUPDATE 编辑框内的文本即将更新
-
CBN_ERRSPACE 组合框内存不足
-
CBN_KILLFOCUS 组合框失去输入焦点
-
CBN_SELCHANGE 在组合框中选择了一项
-
CBN_SELENDCANCEL 用户的选择应当被取消
-
CBN_SELENDOK 用户的选择是合法的
-
CBN_SETFOCUS 组合框获得输入焦点
编辑框控件
-
EN_CHANGE 编辑框中的文本己更新
-
EN_ERRSPACE 编辑框内存不足
-
EN_HSCROLL 用户点击了水平滚动条
-
EN_KILLFOCUS 编辑框正在失去输入焦点
-
EN_MAXTEXT 插入的内容被截断
-
EN_SETFOCUS 编辑框获得输入焦点
-
EN_UPDATE 编辑框中的文本将要更新
-
EN_VSCROLL 用户点击了垂直滚动条消息含义
列表框控件
-
LBN_DBLCLK 用户双击了一项
-
LBN_ERRSPACE 列表框内存不够
-
LBN_KILLFOCUS 列表框正在失去输入焦点
-
LBN_SELCANCEL 选择被取消
-
LBN_SELCHANGE 选择了另一项
-
LBN_SETFOCUS 列表框获得输入焦点
-
- https://blog.csdn.net/xiaoyafang123/article/details/79220531