目录
一、 Windows消息机制流程
(1) 消息
消息系统对于一个Windows程序来说十分重要,它是一个程序运行的动力源泉。一个消息,是系统定义的一个32位的值,他唯一的定义了一个事件。消息本身是作为一个记录传递给应用程序的,这个记录中包含了消息的类型以及其他信息。例如,对于单击鼠标所产生的消息来说,这个记录中包含了单击鼠标时的坐标。这个记录类型叫做MSG,MSG含有来自windows应用程序消息队列的消息信息,它在Windows中声明如下:
typedef struct tagMSG {
HWND hwnd; //消息所属窗口
UINT message; //消息标识符
WPARAM wParam; //指定消息的附加信息
LPARAM lParam; //指定消息的附加信息
DWORD time; //消息队列中的时间
POINT pt; //鼠标的当前位置
} MSG;
windows中的消息虽然很多,但是种类并不繁杂,大体上有3种:窗口消息、命令消息和控件通知消息。窗口消息是系统中最为常见的消息,它是指由操作系统和控制其他窗口的窗口所使用的消息。例如CreateWindow、DestroyWindow和MoveWindow等都会激发窗口消息。命令消息是一种特殊的窗口消息,他用来处理从一个窗口发送到另一个窗口的用户请求,例如按下一个按钮,他就会向主窗口发送一个命令消息。控件通知消息,是指一个窗口内的子控件发生了一些事情,需要通知父窗口。通知消息只适用于标准的窗口控件如按钮、列表框、组合框、编辑框,以及Windows公共控件如树状视图、列表视图等。例如,单击或双击一个控件、在控件中选择部分文本、操作控件的滚动条都会产生通知消息。其中窗口消息及控件通知消息主要由窗口类即直接或间接由CWND类派生类处理。相对窗口消息及控件通知消息而言,命令消息的处理对象范围就广得多,它不仅可以由窗口类处理,还可以由文档类,文档模板类及应用类所处理。其中,消息用消息标识符进行区分。
从消息的发送途径来看,消息可以分成2种:队列消息和非队列消息,队列消息送到系统消息队列,然后到线程消息队列;非队列消息直接送给目的窗口过程。从处理方式来看,消息队队列可以分成系统消息队列和线程消息队列。系统消息队列由Windows维护,线程消息队列则由每个GUI线程自己进行维护,为避免给non-GUI现成创建消息队列,所有线程产生时并没有消息队列,仅当线程第一次调用GDI函数时系统才给线程创建一个消息队列。
(2) Windows窗体的创建
Windows窗体的创建分为三步:声明WNDCLASS实例、窗体注册、创建窗体。
WNDCLASS是一个由系统支持的结构,用来储存某一类窗口的信息,如ClassStyle,消息处理函数,Icon,Cursor,背景Brush等。声明如下,其中窗体接收Windows消息函数需格外注意。
typedef struct _WNDCLASS {
UINT style; //样式
WNDPROC lpfnWndProc; //设置窗体接收windws消息函数
int cbClsExtra; //窗口类扩展
int cbWndExtra; //窗口实例扩展
HINSTANCE hInstance; //窗体实例名,由windows自动分发
HICON hIcon; //显示上面的图标titlte
HCURSOR hCursor; //窗口光标
HBRUSH hbrBackground; //背景刷
LPCTSTR lpszMenuName; //窗口菜单
LPCTSTR lpszClassName; //窗体类名
} WNDCLASS, *PWNDCLASS;
窗体注册函数是RegisterClass或RegisterClassEx,该函数注册后在调用CreateWindow()或CreatewindowEx函数中使用的窗口类。原型如下:
ATOM WINAPI RegisterClass( _In_ const WNDCLASS *lpWndClass);
//lpWndClass 指向一个 WNDCLASS 结构的指针
窗口创建用到的函数理所是CreateWindow()或CreatewindowEx。这个函数是基于窗口类的,所以还需要指定几个参数来制定特定的窗口。而且像一些不带边框的窗口是怎么创建的也是具有相当的技巧的,就是创建的是不带标题和边框的窗口,然后自己在客户区绘制程序的内容,能够制作个性化的应用程序。函数原型如下,注意函数返回值为创建窗体的句柄,这一点需格外关注。
HWND WINAPI CreateWindow(
_In_opt_ LPCTSTR lpClassName, // 窗口类名称
_In_opt_ LPCTSTR lpWindowName, // 窗口标题
_In_ DWORD dwStyle, // 窗口风格,或称窗口格式
_In_ int x, // 初始 x 坐标
_In_ int y, // 初始 y 坐标
_In_ int nWidth, // 初始 x 方向尺寸
_In_ int nHeight, // 初始 y 方向尺寸
_In_opt_ HWND hWndParent, // 父窗口句柄
_In_opt_ HMENU hMenu, // 窗口菜单句柄
_In_opt_ HINSTANCE hInstance, // 程序实例句柄
_In_opt_ LPVOID lpParam // 创建参数
);
(3).Windows消息的处理
简单讲完消息的定义以及窗体的创建过程,下面开始探究消息如何传递到窗体中。
应用程序中含有一段称作“消息循环”的代码,用来从消息队列中检索这些消息并把它们分发到相应的窗口函数中。Windows为当前执行的每个Windows程序维护一个消息队列。在发生输入事件之后,Windows将事件转换为一个消息并将消息放入程序的消息队列中。程序通过执行一块称之为消息循环的程序代码从消息队列中取出消息。具体执行流程分为三步:消息的发送、消息的接收以及消息的处理。
消息的发送有3种方式:发送、寄送和广播。发送消息的函数有SendMessage、SendMessageCallback、SendNotifyMessage、SendMessageTimeout;寄送消息的函数主要有PostMessage、PostThreadMessage、PostQuitMessage;广播消息的函数我知道的只有BroadcastSystemMessage、BroadcastSystemMessageEx。其中 SendMessage主要是向一个或多个窗口发送一条消息,一直等到消息被处理之后才会返回。不过需要注意的是,如果接收消息的窗口是同一个应用程序的一部分,那么这个窗口的窗口函数就被作为一个子程序马上被调用;如果接收消息的窗口是被另外的线程所创建的,那么窗口系统就切换到相应的线程并且调用相应的窗口函数,这条消息不会被放进目标应用程序队列中。函数的返回值是由接收消息的窗口的窗口函数返回,返回的值取决于被发送的消息。 PostMessage则把一条消息放置到创建hWnd窗口的线程的消息队列中,该函数不等消息被处理就马上将控制返回。
消息的接收主要有3个函数:GetMessage、PeekMessage、WaitMessage。其中GetMessage是从调用线程的消息队列里取得一个消息并将其放于指定的结构。此函数可取得与指定窗口联系的消息和由PostThreadMessage寄送的线程消息。此函数接收一定范围的消息值。GetMessage不接收属于其他线程或应用程序的消息。获取消息成功后,线程将从消息队列中删除该消息。函数会一直等待直到有消息到来才有返回值PeekMessage与GetMessage功能类似,不同之处在于GetMessage不将控制传回给程序,直到从程序的消息队列中取得消息,但是PeekMessage总是立刻传回,而不论一个消息是否出现。WaitMessage功能为线程的消息队列中无其它消息时,该函数就将控制权交给另外的线程,同时将该应用程序挂起,直到一个新的消息被放入应用程序的队列之中才返回。
消息的处理是整个Windows消息处理的重点。典型处理流程如下:
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消息中调用。通过DispatchMessage函数即将WNDCLASS中的窗体过程函数关联起来。
窗体过程函数是一个用于处理所有发送到这个窗口的消息的函数。任何一个窗口类都有一个窗口过程。同一个类或一个父类的窗口使用同样的窗口过程来响应消息。系统发送消息给窗口过程将消息数据作为参数传递给他,消息到来之后,按照消息类型排序进行处理,其中的参数则用来区分不同的消息,窗口过程使用参数产生合适行为。一个窗口过程不经常忽略消息,如果他不处理,它会将消息传回到执行默认的处理。窗口过程通过调用DefWindowProc来做这个处理。窗口过程被所有属于同一个类的窗口共享,能为不同的窗口处理消息。通常窗口过程函数是通过一个switch语句来实现的,利用HANDLE_MSG消息分流器则可以把switch语句分成更小的函数,每一个消息都对应一个小函数,这样做的好处就是对消息更容易管理。
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;
}
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);
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);
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;
}
二、一个简单实例
为分析Windows消息处理过程,首先写了一个最简单的窗体实例作分析。
#include <windows.h>
#include <mmsystem.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); //声名消息处理函数(处理windows和接收windows消息)
//hInstance:系统为窗口分配的实例号,2和3忘了.4是显示方式
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("Register"); //窗体名
HWND hwnd;//句柄
MSG msg;//消息体
WNDCLASS wndclass;//这义一个窗体类实例
//设置窗体参数
wndclass.style = CS_HREDRAW | CS_VREDRAW; //样式
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;//窗体实例名,由windows自动分发
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);//显示上面的图标titlte
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);//窗口光标
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);//背景刷
wndclass.lpszMenuName = NULL;
wndclass.lpfnWndProc = WndProc;//设置窗体接收windws消息函数
wndclass.lpszClassName = szAppName;//窗体类名
if (!RegisterClass(&wndclass))//注册窗体类
{
MessageBox(NULL, TEXT("This program requires Windows NT!"), szAppName, MB_ICONERROR);
return 0;
};
//创建一个窗体。已分配内存。返回一个窗体句柄
hwnd = CreateWindow(szAppName, // window class name
TEXT("The Hello Program"), // window caption
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT,// initial x position
CW_USEDEFAULT,// initial y position
CW_USEDEFAULT,// initial x size
CW_USEDEFAULT,// initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL);
ShowWindow(hwnd, iCmdShow);//显示窗口
UpdateWindow(hwnd);//更新窗体
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);//翻译消息并发送到windows消息队列
DispatchMessage(&msg);//接收信息
}
return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)//消息的处理程序
{
HDC hdc;
PAINTSTRUCT ps;
RECT rect;
switch (message)
{
case WM_CREATE:
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
TextOut(hdc, 0, 0, "TEST", strlen("TEST"));
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
这段代码实现的是一个最简单的窗体。效果如下: