窗口是Windows程序设计的一大特点,也是很多应用程序的操作区域。消息则是应用程序与Windows系统、用户与应用程序之间互相沟通的通道。今天我们一起来学习这两个很重要的部分,为今后的程序设计打下良好的基础。
1.窗口的创建
1.1 系统结构概述
窗口最常见于应用程序的窗口,它基本上由标题栏、菜单栏、工具栏、客户区、滚动条、状态栏组成。还有就是对话框,这种窗口可以不带标题栏。其他不明显的窗口包括各种各样的按钮、单选按钮、复选框、列表框、滚动条等,这些对象都是窗口,更精确的描述是“子窗口”或“控件窗口”或“窗口控件”。
应用程序创建的每一个窗口都有一个与之关联的窗口过程,这个窗口过程可以是应用程序的某一个函数,也可以位于一个动态链接库里。Windows正是通过调用该窗口过程来向窗口传递消息的,窗口过程则依据这些消息作出相应的处理,然后将控制权返还给Windows。更准确来说,窗口总是依据“窗口类”来创建,窗口类标识了用于处理传递给窗口的消息的窗口过程。窗口类的使用允许多个窗口共享同一个窗口类,因而多个窗口可以使用相同的窗口过程。
当Window程序开始执行时,Windows首先为该程序创建一个“消息队列”(Message queue)。该消息队列中存放着应用程序可能创建的所有窗口的消息。Windows应用程序中一般都包含一小段称为“消息循环”的代码,该段代码用于从消息队列中检索消息,并将其分发给相应的窗口过程。其他消息则不经过消息队列直接发送给窗口过程。
1.2 HELLOWIN程序
这个时候或许用书本上的例子来介绍窗口和消息是比较合适的方式,虽然初次看起来有很多地方存在迷惑,但后面的介绍会一一解决。
HELLOWIN程序代码如下:
#include <windows.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
//主函数
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPreInstance, PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("HelloWin");
HWND hwnd;
MSG msg;
WNDCLASS wndclass;
//设置窗口类属性
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szAppName;
//注册窗口类
if ( !RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("This program requires Windows NT!"), szAppName, MB_ICONERROR);
return 0;
}
//创建、显示与更新窗口
hwnd = CreateWindow(szAppName,
TEXT("The Hello Program"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL);
ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);
//做消息循环
while( GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
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:
MessageBox(hwnd, TEXT("Creating window..."), TEXT("HelloWin"), 0);
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
GetClientRect(hwnd, &rect);
DrawText(hdc, TEXT("Hello, Windows!"), -1, &rect,
DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
HELLOWIN程序的功能:显示一个窗口,在客户区显示文本“Hello, Windows”,该窗口可大小缩放、移动、关闭,可以在标题栏右击弹出系统菜单进行大小缩放、移动、关闭操作。
HELLOWIN程序的实现部分由WinMain主函数和WndProc窗口过程共两个函数构成,接下来将根据代码部分分别介绍该程序的具体实现细节。
1.3 窗口类的注册
窗口总是基于窗口类来创建的,窗口类确定了处理窗口消息的窗口过程。在创建应用程序窗口之前,必须调用函数RegisterClass来注册窗口类。该函数唯一的参数是一个指向窗口类(WNDCLASS)类型的结构的指针。
窗口类的定义方式有ASCII版本的WNDCLASSA和Unicode版本的WNDCLASSW,两者的差别是字符的长度。以ASCII版本WNDCLASSA为例,其结构体声明如下:
typedef struct tagWNDCLASSA {
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCSTR lpszMenuName;
LPCSTR lpszClassName;
} WNDCLASSA, *PWNDCLASSA, NEAR *NPWNDCLASSA, FAR *LPWNDCLASSA;
第一个参数style是窗口类的风格,通过位或运算符“|”方式,组合两个或者多个窗口类类型标识。本程序中使用的CS_HREDRAW表示窗口宽度的变化会引起整个窗口的重画,CS_VREDRAW表示窗口高度的变化会引起整个窗口的重画。
第二个参数lpfnWndProc,是指向窗口过程的指针。本程序中将WndProc窗口过程传递给该参数。
第三个参数cbClsExtra,表示窗口类结构的额外字节数,默认为0。
第四个参数cbWndExtra,表示窗口实例的额外字节数,默认为0。
第五个参数hInstance,表示包含窗口过程的实例,本程序中是该程序对应的实例hInstance。
第六个参数hIcon,表示指向窗口类图标的句柄,本程序中通过LoadIcon函数获取IDI_APPLICATION类型图标(系统预定义图标)。
第七个参数hCursor,表示指向窗口类鼠标的句柄,本程序中通过LoadCursor函数获取IDC_ARROW类型图标(系统箭头鼠标)。
第八个参数hbrBackground,表示指向窗口类背景画刷的句柄,本程序中通过GetStockObject函数获取WHITE_BRUSH白色画刷(该画刷属于系统标准画刷之一)。
第九个参数lpszMenuName,表示窗口类菜单资源的字符串名称,本程序中使用NULL,表示窗口类无菜单。
第十个参数lpszClassName,表示窗口类的名称,本程序中使用字符串TEXT("HelloWin")作为窗口类的名称。
当对该窗口类初始化完成后,然后在调用RegisterClass注册该窗口类,注册失败时返回值为0.
1.4 窗口的创建
窗口的创建通过调用函数CreateWindow完成,其11个参数指定了窗口的各个特征,包括窗口的窗口类、窗口风格、大小、位置、菜单、标题名称、父窗口等等。其函数声明如下:
HWND CreateWindow( LPCTSTR lpClassName,
LPCTSTR lpWindowName,
DWORD dwStyle,
int x,
int y,
int nWidth,
int nHeight,
HWND hWndParent,
HMENU hMenu,
HINSTANCE hInstance,
LPVOID lpParam
);
第一个参数lpClassName,表示新建窗口对应的窗口类名称,将特定名称的窗口类与新建窗口关联起来,本程序中窗口类名称为"HelloWin"。
第二个参数lpWindowName,表示新建窗口的标题名称,在标题栏显示,本程序中标题名称为"The Hello Program"。
第三个参数dwStyle,表示新建窗口的窗口显示风格,可以通过位或运算符"|"将特定风格组合起来,本程序中窗口风格为一个普通的层叠窗口(Overlapped),该窗口有一个标题栏、一个位于标题栏左边的系统菜单栏按钮、一个窗口尺寸调整边框以及位于标题栏右方的三个按钮(分别用于最小化、最大化和关闭窗口)。
第四个、五个参数x,y分别是窗口左上角相对于屏幕左上角的初始水平方向、垂直方向位置,本程序中均使用CW_USEDEFAULT设置初始化位置默认。默认值0x80000000表示WIndows将连续新建的窗口的左上角位置沿水平方向和垂直方向分别作步长为1的偏移。
第六个、七个参数nWidth、nHeight分别表示新建窗口的宽度和高度,本程序中使用CW_USEDEFAULT设置窗口默认大小。
第八个参数hWndParent,表示新建窗口的父窗口句柄,当新建窗口为顶级窗口时,父窗口句柄为NULL,否则需要指出新建窗口所属的父窗口对应的句柄,本程序中使用NULL表示新建窗口为顶级窗口。
第九个参数hMenu,表示新建窗口的菜单句柄,本程序中没有指定菜单,则设置该项为NULL。
第十个参数hInstance,表示新建窗口关联的实例句柄,本程序中使用WinMain函数的参数hInstance作为实例句柄。
第十一个参数lpParam,表示指向通过CREATESTRUCT结构传递给窗口的结构数据指针,该结构指向WM_CREATE消息中的lParam,后续可以在WM_CREATE消息处理中使用,本程序中设置该指针为NULL。
其他:很多新手很容易对窗口类和窗口之间的区别以及为什么窗口的特征不能被一次性指定完毕而感到疑惑。实际上,按照这种方式对信息进行划分会带来很多便利。例如,所有的下压按钮都基于相同的窗口类,与该窗口类关联的窗口过程位于Windows内部,并负责处理鼠标和键盘对按钮的输入,以及定义阿牛在屏幕上的视觉外观。从这个方面看,所有下压按钮的工作方式都是一样的,但所有的下压按钮又都是不同的,他们尺寸各异,在屏幕上的位置也不尽相同,而且所带的文本字符串也有差别,后面得这几种特征都是窗口定义的一部分,而非属于窗口类定义。因此可以看出窗口类定义和窗口定义缺一不可,互为补充。
1.5 窗口的显示
当CreateWindow调用返回后,窗口已经在Windows内部被创建。这句话的基本意思是,Windows已经分配了一块内存来保存CreateWindow调用中指定的窗口信息以及一些其他消息。Windows可通过窗口句柄来获取这些信息。但是,要将窗口显示在屏幕上,仅仅这样还是不够的,还需要调用另外两个函数。
第一个函数是ShowWindow(hwnd, iCmdShow)。该函数的一个参数是指向刚才由CreateWindow所创建的窗口的句柄。第二个参数WinMain函数所接收的iCmdShow值,该参数决定着窗口在屏幕上的初始显示方式,即是正常显示(SW_SHOWNORMAL),还是显示为最小化窗口(SW_SHOWMINIMIZED)或最大化窗口(SW_SHOWMAXIMIZED),或者只显示在任务栏(SW_SHOWMINNOACTIVE)。本程序中iCmdShow使用SW_SHOWNORMAL,同时该窗口的客户区将被在窗口类中指定的画刷擦除。
第二个函数是UpdateWindow(hwnd);该函数是窗口客户区重绘,这是通过向窗口过程发送WM_PAINT消息而完成的。
1.6 消息循环
在UpdateWindow被调用后,新建窗口在屏幕中便完全可见了。此时,该程序必须能够接收来自用户的键盘输入和鼠标输入。Windows为当前在其中运行的每一个Windows程序都维护一个“消息队列”。当输入事件发生后,Windows会自动将这些事件转化为“消息”,并将其放置在消息队列中。该部分详见程序中的如下代码:
//做消息循环
while( GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
开启消息循环的GetMessage函数用于从消息队列中对消息进行检索。该函数声明如下:
BOOL GetMessage( LPMSG lpMsg,
HWND hWnd,
UINT wMsgFilterMin,
UINT wMsgFilterMax
);
第一个参数lpMsg,指向MSG消息结构的指针。Windows将消息队列即将处理的对应消息信息填充到该结构中。
第二个参数hWnd,指向从哪些窗口获取消息的所对应的窗口句柄。该窗口必须属于当前线程。该参数为NULL时,表示窗口消息和线程消息都会被处理。当该参数为-1时,仅处理当前线程的特定消息(消息hwnd为NULL,即通过PostMessage(hwnd参数为NULL),或者PostThreadMessage函数发送的消息)。本程序中使用NULL表示该参数。
第三个、四个参数wMsgFilterMin,wMsgFilterMax分别表示获取消息值得最小值和最大值。Windows使用WM_KEYFIRST来指定第一个键盘消息,使用WM_KEYLAST来制定最后一个键盘消息,使用WM_MOUSEFIRST来指定第一个鼠标消息,使用WM_MOUSELAST来指定最后一个鼠标消息。其中当这两个参数均使用0值时,表示获取所有可以使用的消息(即无上下限限制)。
另外,需要先介绍下MSG消息结构。该结构声明如下:
typedef struct {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG, *PMSG;
第一个参数hwnd,表示接收消息所指向的窗口句柄。
第二个参数message,表示消息标识符,用于表示消息的数字,以WM_开头。
第三个参数wParam,表示一个32位的“消息参数”,该参数的含义和取值取决于具体的消息。
第四个参数lParam,表示一个32位的“消息参数”,该参数的含义和取值取决于具体的消息。
第五个参数time,表示消息进入消息队列的时间。
第六个参数pt,表示消息进入消息队列中的鼠标指针的位置坐标。
当从消息队列中检索到的消息的message字段不等于WM_QUIT时,GetMessage函数返回一个非0值,否则返回0.
接下来,调用TranslateMessage将某些键盘消息进行转换,然后将MSG结构返回给Windows。
然后,DispatchMessage再次将MSG结构返回给Windows。
最后,Windows将这些消息发送给合适的窗口过程来处理,当窗口过程处理完毕后,WIndows还将为DispatchMessage调用服务,然后消息循环又进入下一轮的GetMessage调用。
1.7 窗口过程
前面一次涉及到了注册窗口类、创建窗口、显示窗口、程序进入消息循环、从消息队列中检索消息,咋看起来感觉已经很繁琐了,但是不用担心,这些只是Windows程序的常规步骤,一旦掌握后就会感觉到很容易。我们经常要做的程序控制发生在窗口过程中,它决定了窗口客户区的显示内容以及窗口如何对用户的输入输出相应。
一个Windows程序可以包含多个窗口过程,一个窗口过程总是与一个通过调用RegisterClass注册的特定窗口类相关联,CreateWindow函数基于特定的窗口类创建窗口,而基于同一个窗口类则可创建多个窗口。
窗口过程函数的定义格式如下:
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
窗口过程的4个参数与MSG结构的前4个字段是一一对应的。
窗口过程几乎总是由Windows自身调用,应用程序如果希望调用自身的窗口过程,则可通过调用函数SendMessage来实现。
1.8 消息处理
窗口过程所接受的每条信息都有一个数字标识,即窗口过程的message参数,Windows头文件WINUSER.H中为各种类型的消息定义以WM为前缀的标识符。
通常,WIndows程序员会使用switch-case结构来确定窗口过程所收到的消息的类型以及相应的处理方法。当窗口过程对消息进行处理后,返回0。所有窗口过程不进行处理的消息都必须传给名称为DefWindowProc的Windows函数,DefWindowProc的返回值必须从窗口过程返回。
在本程序中,WndProc选择了只对三个消息进行处理,即WM_CREATE、WM_PAINT、WM_DESTROY。处理结构如下所示:
switch (message)
{
case WM_CREATE:
[处理WM_CREATE消息过程]
return 0;
case WM_PAINT:
[处理WM_PAINT消息过程]
return 0;
case WM_DESTROY:
[处理WM_DESTROY消息过程]
return 0;
}
用DefWindowProc函数来对所有窗口过程中没有处理的消息进行默认处理非常重要,否则其他的正常行为(如结束程序)将无法进行。
下面我们将依次介绍WM_CREATE、WM_PAINT、WM_DESTROY三个消息处理。
第一个处理的消息是WM_CREATE,其实这也是窗口过程接收的第一个消息,当Windows在WinMain函数中处理CreateWindow函数调用时,WndProc将接收到WM_CREATE消息。本程序中在WM_CREATE消息处理中弹出了一个提示对话框,显示文本为"Creating window..."。
第二个处理的消息是WM_PAINT,当窗口客户区的部分或者全部“无效”且必须“更新”时,应用程序将得到此消息,这也意味着窗口必须被“重绘”。我们将介绍下什么情况下客户区会变为无效:
场景1:当窗口被首次创建时,整个客户区都是无效的。第一条WM_CREATE消息(通常在应用程序中调用WinMain中的UpdateWindow时出现)将知识窗口过程在窗口客户区进行绘制。
场景2:在调整窗口尺寸时,客户区会变为无效。本程序中窗口类wndclass结构的style字段设置为CS_HREDRAW和CS_VREDRAW,指示Windows当窗口尺寸发生变化时,整个窗口都应该宣布无效。当窗口尺寸调整时,都会收到WM_PAINT消息。
场景3:先最小化窗口,然后再将窗口恢复到原先的尺寸,Windows并不会保存客户区的内容。在图形环境中,这种情况下保存的数据太多了。对此,Windows采取的策略是宣布窗口无效,窗口过程接收到WM_PAINT消息后,会自行恢复窗口的内容。
场景4:在屏幕上拖动窗口导致窗口之间发生重叠,Windows并不负责保存被另一个窗口覆盖的区域,当被覆盖的区域在后来不再被遮挡时,窗口被标记为无效。窗口过程会收到一条WM_PAINT消息,并对窗口的内容进行重绘。
场景5:当程序调用RedrawWindow函数(需要设置RDW_INTERNALPAINT标记),这时窗口更新区域可能不存在,此时需要调用GetUpdateRect函数来确定窗口是否有更新区域,返回0表示无更新区域,这时不能够调用BeginPaint和EndPaint函数。
对WM_PAINT消息的处理几乎都是从调用BeginPaint函数开始:
hdc = BeginPaint(hwnd, &ps);
而以调用EndPaint函数结束:
EndPaint(hwnd, &ps);
在这两个函数调用中,第一个参数均为接收消息的窗口句柄,而第二个参数均为指向一个类型为PAINTSTRUCT结构的指针,它包括了一些窗口过程用来对客户区进行绘制的信息。在BeginPaint调用期间,如果客户区的背景尚未被擦除,则
Windows会对其进行擦除,使用窗口类结构中的hbrBackground字段。BeginPaint调用将使整个客户区有效,并返回一个“设备环境句柄”,设备环境指物理输出设备及其设备驱动程序。在调用BeginPaint函数后,调用GetClientRect和DrawText函数
在客户区输出文本内容,然后调用
EndPaint函数释放设备环境句柄,以使设备环境句柄无效,结束WM_PAINT消息处理过程。
关于WM_PAINT消息,有如下特点:当消息队列中没有其他消息时,才会发送WM_PAINT消息(WM_PAINT消息优先级低)。当WM_PAINT消息被处理后发送一次,Windows不会再发送WM_PAINT,除非有窗口无效发生或者RedrawWindow函数(需要设置RDW_INTERNALPAINT标记)被调用。
第三个处理的消息是WM_DESTROY,表示Windows正处在依照用户的命令销毁窗口的过程中。当用户单击【关闭】按钮,或者从系统菜单中选择【关闭】时,该消息发出。本程序中通过调用PostQuitMessage来对WM_DESTROY消息做出响应,这是一种标准的响应方式:PostQuitMessage(0);该函数的功能是将一个WM_QUIT消息插入到程序的消息队列中,当程序检索到WM_QUIT消息时,GetMessage函数返回为0,程序退出消息循环,然后执行return msg.wParam。其中msg结构的wParam字段是传递给PostQuitMessage函数的值(通常为0)。这时程序从WinMain中退出并将程序结束。
2. Windows编程中的若干难点
2.1 究竟是谁调用谁
我们很熟悉的调用方式是应用程序调用Windows系统提供的操作函数,比如打开文件、关闭文件等等。但Windows系统可以调用用户的程序,尤其是窗口过程。Windows对窗口过程的调用都是以消息的形式出现。
2.2 队列消息和非队列消息
HelloWin程序中的消息循环处理代码是否能够处理所有的消息?答案是否定的。
消息分为“队列消息”和“非队列消息”。队列消息是指那些由WIndows放入程序的消息队列中的消息。在程序的消息循环中,消息被检索,然后被投递到窗口过程中。非队列消息则是由Windows对窗口过程的直接调用而产生。我们一般说队列消息是被“投递”(post)到消息队列中,而非队列消息是被“发送”(send)到窗口过程。无论在哪种情形下,窗口过程都会为窗口获取所有消息——无论是队列消息还是非队列消息。因此,窗口过程实际上是窗口的“消息中心”。
队列消息主要是由用户的输入产生,主要形式为按键消息(WM_KEYDOWN/WM_KEYUP)、由按键产生的字符消息(WM_CHAR)、鼠标移动(WM_MOUSEMOVE)、鼠标单击(WM_LBUTTONDOWN)等。此外,队列消息还包括定时器消息(WM_TIMER)、重回消息(WM_PAINT)和退出消息(WM_QUIT)。
非队列消息则包括队列消息意外的其他所有消息。非队列消息通常由调用特定的Windows函数引起,例如WinMain调用CreateWindow函数产生WM_CREATE消息,调用ShowWindow函数产生WM_SIZE和WM_SHOWWINDOW消息,调用UpdateWindow函数产生WM_PAINT消息。表明键盘或者鼠标输入的队列消息也会产生非队列消息,例如当用键盘或者鼠标选择某个菜单项时,键盘或鼠标消息会进入队列消息,而最终表明有某菜单项被选中的WM_COMMAND消息却是一个非队列消息。
在窗口过程中处理某一个消息的过程中,程序不会被其他消息突然中断。另外当一个消息循环从其自身的消息队列中检索消息,并调用DispatchMessage函数将检索到的消息发送给窗口过程时,只有在窗口过程将控制权返还给Windows后,DispatchMessage才会返回。
但是,窗口过程可以调用为其发送其他消息的函数。在这种情形下,在该函数调用返回之前,窗口过程必须将第二个消息处理完毕,此时窗口过程才处理前一条消息。例如,当一个窗口过程调用UpdateWindows时,Windows会以一条WM_PAINT消息来调用窗口过程,当窗口过程处理完WM_PAINT消息后,UpdateWindow调用才将控制权返还给窗口过程。
这就意味着窗口过程必须是可重入的(reentrant)。在大多数情形下,这并不会带来什么问题,但是对此必须做到心中有数。例如,假定在窗口过程中处理某条消息期间,你对一个静态变量进行了设置,接着由调用一个Windows函数。当该函数返回时,你能确保这个变量仍然跟之前一样么?我们的确无法保证这一点,因为如果你调用的特定Windows函数产生了另外一条消息,且窗口过程在处理第二条消息期间对该变量进行了修改,则该变量的状态一定会发生改变。而这也是我们在编译Windows程序时需要将某些编译优化关闭的原因之一。
2.3 速战速决(不要让用户等程序响应太久)
当程序处理一条特定消息,需要花费一到二分钟的时间,这个时候用户可以切换到其他程序,但是用户无法对你的程序做任何操作。用户不能对你的程序窗口进行移动,也不能对窗口采取调整尺寸、最小化或者关闭等操作。这是因为你的窗口过程正在执行一项非常耗时的任务,表面看上来窗口过程并没有执行移动或者尺寸调整的工作,但实际上它确实在做,这部分工作由DefWindowProc函数负责。如果我们的应用程序真出现这种情况,那只能说我们的应用程序出现了大问题,这对用户造成的影响与因程序缺陷、行为异常以及帮助文件不完成而产生的麻烦没有什么两样。所以,请不要给用户制造不必要的麻烦,并尽快从消息中返回。我们将在20章介绍一种更礼貌的方法,既能够执行耗时的操作,又不会让用户感到麻烦。
3. 总结
今天,我们一起学习了窗口和消息内容,包括窗口的创建过程、消息循环和处理,这是我们今后进一步学习Windows消息的基础,接下来我们讲学习“文本输出”。
Bye ^_^。