窗口和消息
见钱眼开 于2005-3-21
在Windows中,我们创建的应用程序都包含有“窗口”这一对象,它在屏幕上显示为一块矩形区域,含有显示程序名称的标题栏、菜单,甚至还有工具栏、滚动条等,用户可以通过鼠标和键盘直接与它进行交互操作。
下图描述了一个典型窗口的构成部件:
我们可以拖动标题栏(Title Bar)在屏幕上移动窗口;可以单击最大化按钮(Maximize button)使整个窗口充满整个屏幕;可以最小化按钮(Minimize button)使整个窗口缩为一个图标;可以单击关闭按钮(Close button)关闭窗口甚至应用程序;可以标题栏最左边的系统菜单(System menu)中执行前面这些选项;可以按住边框(Sizing border)改变窗口大小;可以选择菜单栏(Menu bar)的菜单项执行各种预定义操作;可以拖动滚动栏(Scroll bar)浏览客户区不同区域内容。
我们调用MessageBox函数创建的对话框其实是一个功能有限的窗口。不光如此,对话框表面的按钮和其他类型按钮如单选钮、复选框、列表框、滚动条、文本框其实都是窗口,只不过通常我们习惯称之为“子窗口”或“控件窗口”或“子窗口控件”。
所有窗口都是在“窗口类”的基础上创建的。“窗口类”是一个抽象概念,用以定义某一类型窗口对象的基本外观和行为。使用窗口类可以创建多个窗口基于同一个窗口类,并且都使用同一个窗口过程,都有基本一致的窗口外观。
在Windows中,一般使用一种叫做“匈牙利表示法”的变量命名约定,变量名以一个或多个小写字母开始,这些字符表示变量的数据类型。了解这点,有助于我们更好理解Windows API函数的参数含义。
调用RegisterClass注册一个窗口类,该函数只需一个参数,一个指向类型为WNDCLASS的结构指针。结构原型声明如下:
typedef struct {
UINT style; //类风格
WNDPROC lpfnWndProc; //窗口过程
int cbClsExtra; //额外类空间
int cbWndExtra; //额外窗口空间
HINSTANCE hInstance; //实例句柄
HICON hIcon; //图标
HCURSOR hCursor; //光标
HBRUSH hbrBackground; //背景画刷
LPCTSTR lpszMenuName; //菜单
LPCTSTR lpszClassName; //类名称
} WNDCLASS, *PWNDCLASS;
WNDCLASS结构中最重要的两个参数是第二个和最后一个。第二个参数(lpfnWndProc)是所有基于该类创建窗口的窗口过程地址。最后一个参数(lpszClassName)是窗口类的文本名。
窗口类定义窗口的一般特征,可以在已定义窗口类基础上调用CreateWindow创建具有不同特征的新窗口。CreateWindow函数原型声明如下:
HWND CreateWindow(
LPCTSTR lpClassName, //类名
LPCTSTR lpWindowName, //窗口名称
DWORD dwStyle, //窗口风格
int x, //左上角x坐标
int y, //左上角y坐标
int nWidth, //宽度
int nHeight, //高度
HWND hWndParent, //父窗口句柄
HMENU hMenu, //菜单句柄
HINSTANCE hInstance, //程序实例句柄
LPVOID lpParam //WM_CREATE消息结构中的参数值
);
创建一般的应用程序主窗口,指定dwStyle风格为WS_OVERLAPEDWINDOW即可。创建一个“顶级”窗口时,hWndParent参数设置为NULL。CreateWindow调用返回一个窗口句柄,每个创建的窗口都对应一个句柄。
句柄在Windows中使用非常频繁。它是一个32位整数,代表一个对象。程序通过Windows 函数获取句柄,在其他Window函数中使用句柄,以引用它代表的对象。一般无需关注句柄实际值。
调用CreateWindow只是创建了一个窗口,要想使创建窗口在屏幕上显示,必须调用ShowWindow和UpdateWindow函数。这两个函数都需要使用CreateWindow返回的窗口句柄,ShowWindow使窗口显示在屏幕上,UpdateWindow使窗口客户区被绘制。
有一点很让人迷惑,无论用户进行何种操作,窗口都会及时作出反应,这一切是如何实现的呢?
原来用户每次操作之后,Windows都给程序发送了一条消息。这条消息描述了特定的内容。程序接收该消息后,就调用目标窗口关联的窗口过程进行处理,最后返回结果。程序创建的每一个窗口都关联一个窗口过程。窗口过程其实就是一个函数,它有固定原型,声明如下:
LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);
当一个Windows程序执行后,Windows为该程序创建一个“消息队列”,这个队列用来存放发送给窗口的各种消息。在调用CreateWindow函数创建完窗口后,开始进入消息处理循环,将消息队列中的消息分发给目标窗口关联窗口过程。
程序通过执行一块被称之为“消息循环”的代码从消息队列中取出消息:
while(GetMessage(&Msg,NULL,0,0))
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
消息循环以GetMessage调用开始,它从消息队列取出一个消息,存放在一个名为Msg的MSG结构中。MSG结构原型如下:
typedef struct tagMSG
{
HWND hwnd; //目标窗口句柄
UINT message; //消息ID
WPARAM wParam; //消息辅助参数
LPARAM lParam; //消息辅助参数
DWORD time; //消息放入消息队列时间
POINT pt; //消息放入消息队列时的鼠标坐标
} MSG,*PMSG;
只要从消息队列中取出消息的message值不为WM_QUIT(0x0012),GetMessage就返回一个非0值。WM_QUIT消息使GetMessage返回0,导致消息循环的结束,进而结束应用程序。TranslateMessage主要进行一些键盘转换。DispatchMessage负责将消息传送给窗口过程,并调用窗口过程。在窗口过程调用结束之后,进行下一个GetMessage调用。
窗口过程一般由系统本身调用。程序一般不需要调用窗口过程。通过SendMessage函数,程序也可以直接调用窗口过程。窗口过程中忽略的消息由DefWindowProc提供缺省处理。
WM_PAINT消息在Windows中是个很重要的消息。当窗口客户区的部分或全部变得“无效”,以至于必须刷新,系统将发送这个消息给程序。以下几种情况将发送WM_PAINT消息:
1. 窗口最初创建时;
2. 窗口移动后或大小改变后;
3. 窗口隐藏后重新显示或被其他窗口遮掩的部分重新可见;
4. 调用InvalidateRect、InvalidateRgn函数;
5. 调用ScrollWindow或ScrollDC函数滚动客户区;
下列情况一般不发送WM_PAINT消息:
1. 光标穿越客户区;
2. 图标拖过客户区;
3. 显示对话框;
4. 下拉菜单后释放;
对于WM_PAINT的处理几乎总是从一个BeginPaint调用开始:
Hdc = BeginPaint(hwnd,&ps);
而以一个EndPaint调用结束:
EndPaint(hwnd,&ps);
在BeginPaint调用中,如果客户区背景未被清除,则由系统使用注册窗口类的WNDCLASS结构的hBackGround参数中指定的画刷负责清除。BeginPaint调用使客户区有效。无法使用从BeginPaint返回的设备描述表句柄在客户区之外绘图。EndPaint释放设备描述表句柄,使之不再有效。
Windows程序所做的一切都是响应发送给窗口过程的消息。
如果用户单击“关闭”按钮,DefWindowProc在处理这一鼠标输入后,它向窗口过程发送一个WM_SYSCOMMAND消息。窗口过程又将该消息传送给DefWindowProc处理,之后DefWindowProc又给窗口过程发送一个WM_CLOSE消息。窗口过程又将该消息传送给DefWindowProc处理,之后DefWindowProc又给窗口过程发送一个WM_Destroy消息。窗口过程接收到该消息后,调用PostQuitMessage发送一个WM_QUIT到消息队列。下次调用GetMessage后返回0。最后程序结束。
消息可分为“进队消息”和“不进队消息”。进队消息是由Windows放入程序消息队列,在程序的消息循环中,重新返回并分配给窗口;不进队消息是Windows直接调用窗口过程。
一般进队消息都是用户输入的结果。以击键(如WM_KEYDOWN和WM_KEYUP消息)、击键产生的字符(WM_CHAR)、鼠标移动(WM_MOUSEMOVE)、鼠标击键(WM_LBUTTONDOWN)的形式给出。进队消息还包括时钟消息(WM_TIMER)、刷新消息(WM_PAINT)、退出消息(WM_QUIT)。
不进队消息许多情况都来自调用特定的Windows函数。调用CreateWindow后发送一个WM_CREATE消息;调用ShowWindow后发送WM_SIZE和WM_SHOWWINDOW消息;调用UpdateWindow发送WM_PAINT消息。
应用程序中的消息处理必须以一种有序的同步的方式进行,无法并发执行。在一个窗口过程中处理某个消息时,程序不会被其他消息突然中断。必须处理完一个消息后,才能处理另一个消息。
窗口过程可重入,就是说窗口过程可嵌套调用。