Windows系统的消息机制
一个库函数(比如fopen),最终会调用操作系统的API来实现其功能,在Windows中,不仅库函数最终会调用系统函数,系统函数反过来也会调用用户函数,这种机制就是通过消息来实现的。
我们假设程序发生了一项鼠标点击“关闭”按钮的操作,系统会发现这次操作,并将这次操作包装成消息结构体发送到程序的消息列表中,每一个程序都有一个消息列表(后面会讲),消息列表中的消息一般会一个一个的执行,当程序执行到“关闭”的消息时,会调用程序的“关闭”回调函数——这就是上面说的Windows系统也会调用用户函数的底层逻辑。
Windows系统就是基于事件驱动的,消息就是其中比较重要的一种事件。
消息的结构体如下:
typedef struct tagMSG
{
HWND hwnd;//消息产生的窗口
UINT message;//消息的类型
WPARAM wParam;//消息的具体信息
WPARAM lParam;//消息的具体信息
DWORD time;//消息发生的时间
POINT pt;//鼠标的当前位置
}
消息队列
每一个Windows系统中的程序执行以后,系统都会为它分配一个消息队列,消息队列中保存有系统发给它的消息,比如说鼠标左键按下时,就会产生WM_LBUTTONDOWN的事件,系统检测到这个事件后,会把这个事件包装成一个消息发送给目标程序的消息列表中,消息列表中的消息是循环不断的进行处理的。
系统发送消息分为两种,一种是进队消息,还有一种是不进队消息,不进队消息就是系统直接将消息发送给程序,不需要进入队列等待,后面还会详细说。
不进队消息
发送消息可以用SendMessage函数,这个函数直接将消息发送给窗口,当窗口过程回调函数执行完毕后才会返回,这种方式发送的消息就是不进队消息。
进队消息
PostMessage将消息放进目标程序的消息队列后会立即返回。还有一个PostThreadMessage函数,用于向线程发送消息,对于线程消息,hwnd成员总是NULL。
Windows窗口程序的实现
上面介绍了Windows下的消息机制,系统发送消息到程序,程序接收到消息后的处理统称为窗口过程。
要实现窗口过程当然需要先创建一个窗口程序了。窗口程序的创建很简单,主要分为以下几个步骤:
- 注册窗口类
- 创建窗口及显示窗口
- 创建消息循环
- 编写窗口过程函数
接下来详细说下细节。
注册窗口类
注册窗口要做的事情主要是设定窗口的属性和特征,通过调用RegisterClass(&wndclass)完成注册,wndclass是一个叫作WNDCLASS的结构体,它可以设置窗口的很多属性,在注册之前有若干个字段是必须要赋值的,它的结构体如下:
style:指定窗口的样式
- CS_HREDRAW:当窗口的水平宽度发生变化时窗口进行重绘
- CS_VREDRAW:代表什么不用说了吧
- CS_NOCLOSE:没有关闭按钮
- CS_DBLCLKS:可以发送双击消息
去掉某个样式:style = style &~CS_XXXX
lpfnWndProc:窗口过程函数,Windows系统中每个窗口都可以有一个窗口过程函数,它的创建过程如下:
- 将窗口过程函数赋值给lpfnWndProc
- 注册窗口类,调用RegisterClass函数进行注册
- 主循环中调用DispatchMessage进行消息派发
窗口过程函数的声明如下:
typedef LRESULT (CALLBACK *WNDPROC)(HWND,UINT,WPARAM,WPARAM);
LRESULT实际上是long类型,CALLBACK实际上是__stdcall,在VC++开发环境中,默认的编译选项是__cdecl,这种调用约定适用参数可变的函数,因为它是在函数外进行栈桢平衡。如果需要使用__stdcall需要指定,Windows API都遵循__stdcall。在Windows NT4.0以后程序中要使用回调函数必须遵循__stdcall。
LoadIcon:这个字段用来指定图标,它的第二个参数用来指定一个资源名,Windows SDK的资源命名规范一般是ID+类型,比如说按钮就是IDB_XXX。
hbrBackground:当窗口发生重绘时,系统使用这个字段指定的背景色作为重绘的颜色
贴上一段参考代码:
WNDCLASSEXW wcex;
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.cbSize = sizeof(WNDCLASSEXW);
wcex.lpfnWndProc = (WNDPROC)WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInst;
wcex.hCursor = LoadCursor(nullptr, MAKEINTRESOURCE(IDI_ZZPEANALYZER));
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ZZPEANALYZER));
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
wcex.lpszMenuName = MAKEINTRESOURCE(IDC_ZZPEANALYZER);
RegisterClassExW(&wcex);
创建窗口及显示窗口
创建窗口没有什么特别需要讲解的地方,下面将要贴出的代码说明了一切,不过还是有需要注意的地方。
- 当调用CreateWindows函数时,传递参数hWndParent时,如果指定为WS_CHILD,说明创建的是子窗口,子窗口会被父窗口所影响,具体影响如下:
- 调用UpdateWindow函数会发送一个WM_PAINT消息来刷新窗口
HWND hWnd = CreateWindowEx(WS_EX_ACCEPTFILES,szWindowClass,szTitle,WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT,700,500,NULL,NULL,hInstance,NULL);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nShowCmd);
UpdateWindow(hWnd);
创建消息循环
每个程序都有一个消息队列,可以通过GetMessage函数获取队列中的消息。
GetMessage原型如下:
BOOL GetMessage(LPMSG lpMsg,//消息结构体
HWND hWnd,//接收指定窗口的消息
UINT wMsgFilterMin,//获取的消息最小值
UINT wMsgFilterMax);//获取的消息最大值
GetMessage只有在接收到WM_QUIT时才会返回0,如果出现错误会返回-1,主循环一般如下:
MSG msg;
while (GetMessage(&msg,nullptr,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
GetMessage获取到消息队列中的消息以后,会执行TranslateMessage(),这个函数是用来将虚拟按键转换成字符的,比如说用户按下某个按键会产生WM_KEYDOWN和WM_KEYUP两个虚拟键代码,TranslateMessage()会将这两个虚拟键码转为WM_CHAR,并将这个message放到消息队列中,当下次调用GetMessage时这个消息会被调用,TranslateMessage()并不会修改消息,只是会新增一个消息。
DispatchMessage()用来将消息回传给系统,系统会调用窗口过程的回调函数。
PeekMessage
和GetMessage()类似的函数还有PeekMessage,这个函数跟GetMessage()唯一的不同是有一个wRemoveMsg字段,当它是PM_REMOVE的时候在获取到消息后会将消息从消息队列中删除,跟GetMessage一致,当它是PM_NOREMOVE时不会将消息从消息队列中移除。
编写窗口过程函数
还记得在注册窗口时绑定的回调函数吗?它就是窗口过程函数的入口。
wcex.lpfnWndProc = (WNDPROC)WndProc;
这个回调函数的声明如下:
参数在上面的注册窗口部分讲解过,就不赘述了。这个回调函数内部的实现主要是通过switch的方式来分类不同的消息处理函数,下面的图可供参考:
WM_CHAR:当用户按下键盘上的一个字符键,这个分支会被调用
WM_PAINT:当窗口的一部分或全部变为无效时就会调用这个分支,具体有以下几种情况触发:
- 窗口刚创建时
- 调用UpdateWindow时
- 窗口大小变化时(前提需要注册窗口时设置了CS_HREDRAW和CS_VREDRAW标志)
- 窗口被遮盖再显示时
注意,只有在WM_PAINT分支内部才可以使用BeginPaint(对应EndPaint),在外部只能通过GetDC函数来获取DC(对应ReleaseDC)。
DC全称Device Context,它包含了显示器、图形设备驱动器的一些信息,要在窗口上显示文字或者显示图形都需要用到DC,如果没有DC,我们就需要了解图形设备和它的驱动程序,通过调用驱动程序的接口来完成图形的显示,而图形设备有很多种,每一种都是不一样的,如果真要去了解这些驱动程序再作画,那工作量也太大了,因此微软提供了DC,由它去跟图形设备驱动程序打交道,我们只要使用它就可以直接画图了
WM_CLOSE:当用户单击窗口上的关闭按钮时,系统会发送一条WM_CLOSE消息。
DestroyWindow函数会向窗口过程发送WM_DESTROY消息,DestroyWindow函数执行完窗口就已经被销毁了,但是程序没还没有退出,因此需要在WM_DESTROY分支里面进行最后的处理。
如果程序没有响应WM_CLOSE消息,系统就会调用DefWindowProc函数,这个函数会调用DestroyWindow函数来响应这条WM_CLOSE消息。
WM_DESTROY:在这个分支里调用了PostQuitMessage,它会向消息队列中发送一条WM_QUIT消息,之前我们说过GetMessage在收到WM_QUIT会返回0,当它返回0时,主循环就停止了。传递给PostQuitMessage的参数会作为WM_QUIT消息的wParam参数,这个值通常作为WinMain函数的返回值。
就先介绍到这里。