第二课——窗口和消息
一、窗口与消息处理
(1)一个“窗口”就是:屏幕上的一个矩形区域,它接收用户的输入,并以文本或图形方式来显示内容。
(2)窗口还是用户操作的区域界面,在编程中除创建等操作外,还要处理用户输入、窗口本身事件所产生的“消息”。
二、程序框架代码——窗口创建和消息处理
#include <windows.h> LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); //窗口过程 int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { 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 = GetStockObject(WHITE_BRUSH); wndclass.lpszMenuName = NULL; wndclass.lpszClassName = "HelloWin"; //窗口类名 if (!RegisterClass(&wndclass)) { //注册窗口 MessageBox(NULL, "窗口注册失败!", "HelloWin", 0); return 0; } //创建窗口 hwnd = CreateWindow( "HelloWin", //窗口类名 "我的窗口", //窗口标题 WS_OVERLAPPEDWINDOW, //窗口样式 CW_USEDEFAULT, //窗口最初的x位置 CW_USEDEFAULT, //窗口最初的y位置 480, //窗口最初的x大小 320, //窗口最初的y大小 NULL, //父窗口句柄 NULL, //窗口菜单句柄 hInstance, //应用程序实例句柄 NULL); //创建窗口的参数 //显示窗口 ShowWindow(hwnd, nCmdShow); //更新窗口,包括窗口的客户区 UpdateWindow(hwnd); //进入消息循环:当从应用程序消息队列中检取的消息是WM_QUIT时,则退出循环 while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); //转换某些键盘消息 DispatchMessage(&msg); //将消息发送给窗口过程,这里是WndProc } return msg.wParam; } //窗口过程函数WndProc LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { HDC hdc; PAINTSTRUCT ps; RECT rc; switch (message) { case WM_CREATE: //窗口创建产生的消息 return 0; case WM_PAINT: hdc = BeginPaint(hwnd, &ps); GetClientRect(hwnd, &rc); //获取窗口客户区大小 DrawText(hdc, TEXT("Hello Windows!"), -1, &rc, DT_SINGLELINE | DT_CENTER | DT_VCENTER); EndPaint(hwnd, &ps); return 0; case WM_DESTROY: //当窗口关闭时产生的消息 PostQuitMessage(0); return 0; } //执行默认的消息处理 return DefWindowProc(hwnd, message, wParam, lParam); }
注:上面代码可以分解成两个基本函数的程序结构,一个就是前面所讨论的WinMain函数,另一个还是用户定义的窗口过程函数WndProc。窗口过程函数WndProc用来接收和处理各种不同的消息。
三、分析——注册窗口类
1. 注册窗口代码:if(!RegisterClass(&wndclass))
2. 在为程序创建窗口之前,必须首先调用创建RegisterClass注册应用程序的窗口类。该函数只要一个参数,即一个指向类型为WNDCLASS的结构指针。它(即WNDCLASS结构)包含了一个窗口的基本属性,如窗口边框、窗口标题栏文字、窗口大小和位置、鼠标、背景色、处理窗口消息函数的名称等。
事实上,注册的过程也就是将这些属性告诉系统,然后再调用CreateWindow函数创建出窗口。
3. 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;
可以看出,该结构有10个域(成员),其中第一个域style表示窗口类的风格,它往往是由一些基本的预定义风格通过位的“或”操作组合而成的。如,在HelloWin.c中,有:
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 = GetStockObject(WHITE_BRUSH); wndclass.lpszMenuName = NULL; wndclass.lpszClassName = "HelloWin"; //窗口类名
(1)可以看到,wndclass.style被设为CS_VREDRAW | CS_HREDRAW,表示只要窗口的高度或宽度发生变化,都会重画整个窗口。
(2)第二个域lpfnWndProc的值为WndProc,表明该窗口类的消息处理函数是WndProc函数。这里,可简单直接地输入消息处理(窗口过程)函数的函数名即可。
(3)对于hInstance成员,给它的值是由WinMain传来的应用程序的实例句柄,表明该窗口与该实例是相关联的。事实上,只要是注册窗口类,该成员的值始终是该程序的实例句柄。
(4)对于hIcon成员,是要给这个窗口指定一个图标,LoadIcon(NULL, IDI_APPLICATION)就是调用系统内部预先定义好的标识符为IDC_APPLICATION的图标作为该窗口的图标。同样,LoadCursor(NULL, IDC_ARROW)就是调用预定义的箭形鼠标指针。
(5)对于hbrBackground成员,它是用来定义窗口的背景画刷颜色,也就是该窗口的背景色。调用GetStockObject(WHITE_BRUSH)可以获得系统内部预先定义好的白色画刷作为窗口的背景色。即这里LoadIcon、LoadCursor、GetStockObject等都是Windows 的API函数,在程序中可直接调用。
(6)lpszMenuName域的值若为NULL,则表示该窗口将没有菜单。否则,需要指定表示菜单资源的字符串。
(7)最后一个域lpszClassName是要给这个窗口类起一个唯一的名称,因为Windows操作系统中有许许多多的窗口类,必须用一个独一无二的名称来代表它们。
四、分析——创建和显示窗口
1. 窗口类注册完毕之后,并不会有窗口显示出来,因为注册的过程仅仅是为创建窗口所做的准备工作。
实际创建一个窗口是通过调用CreateWindow函数完成的。窗口类中已经预先定义了窗口的一般属性,而CreateWindow中的参数可以进一步指定一个窗口的更具体的属性。
2. 在HelloWin.c程序中,是用下列调用CreateWindow函数的代码来创建窗口的:
//创建窗口 hwnd = CreateWindow( "HelloWin", //窗口类名 "我的窗口", //窗口标题 WS_OVERLAPPEDWINDOW, //窗口样式 CW_USEDEFAULT, //窗口最初的x位置 CW_USEDEFAULT, //窗口最初的y位置 480, //窗口最初的x大小 320, //窗口最初的y大小 NULL, //父窗口句柄 NULL, //窗口菜单句柄 hInstance, //应用程序实例句柄 NULL); //创建窗口的参数
(1)第一个参数:创建该窗口所使用的窗口类的名称,该名称与前面所注册的窗口类的名称一致。
(2)第三个参数:创建的窗口的风格,它们通常是一些预定义风格的“|”组合。其中,WS_OVERLAPPEDWINDOW表示创建一个层叠式窗口,有边框、标题栏、系统菜单、最大化和最小化按钮等。
(3)后面的参数中仍用到了该应用程序的实例句柄hInstance。如果窗口创建成功,返回值是新窗口的句柄,否则返回NULL。
3. 窗口创建后,并不会在屏幕上显示出来。显示窗口要调用ShowWindow函数。
ShowWindow函数的原型:BOOL ShowWindow(HWND hWnd, int nCmdShow);
其中,参数hWnd指定要显示的窗口的句柄,nCmdShow表示窗口的显示方式,这里指定为从WinMain函数的nCmdShow所传递过来的值。
由于ShowWindow函数的执行优先级不高,所以当系统正忙着执行其他的任务时,窗口不会立即显示出来,此时,调用UpdateWindow函数可以立即显示窗口。同时,它将会给窗口过程发出WM_PAINT消息。
五、分析——消息和消息处理
1. 消息循环:
//进入消息循环:当从应用程序消息队列中检取的消息是WM_QUIT时,则退出循环 while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); //转换某些键盘消息 DispatchMessage(&msg); //将消息发送给窗口过程,这里是WndProc }
(1)Windows应用程序可以接收以各种形式输入的信息,这包括键盘、鼠标动作、计时器产生的消息,也可以是其他应用程序发来的消息等。Windows系统自动监控所有的输入设备,并将其消息放入该应用程序的消息队列中。
(2)GetMessage函数就是用来从应用程序的消息队列中按照先进先出的原则将这些消息一个个地取出来,放进一个MSG结构中去。
(3)函数GetMessage的原型:
BOOL GetMessage( LPMSG lpMsg; //指向一个MSG结构的指针,用来保存消息 HWND hWnd; //指定哪个窗口的消息将被获取 UINT wMsgFilterMin; //指定获取的主消息值的最小值 UINT wMsgFilterMax; //指定获取的主消息值的最大值 );
(4)GetMessage函数用来将获取的消息复制到一个MSG结构中。如果队列中没有任何消息,该函数将一直空闲直到队列中又有消息时再返回。如果队列中已有消息,它将取出一个后返回。
(5)MSG结构包含Windows消息的完整信息,其定义如下:
typedef struct { HWND hwnd; //消息发向的窗口的句柄 UINT message; //主消息的标识值 WPARAM wParam; //附消息值,其具体含义依赖于主消息值 LPARAM lParam; //附消息值,其具体含义依赖于主消息值 DWORD time; //消息放入消息队列中的时间 POINT pt; //消息放入消息队列中的鼠标坐标 }MSG, *PMSG;
主消息:表明了消息的类型,例如,是键盘消息还是鼠标消息等。
附消息:其含义则依赖于主消息值,例如,如果主消息是键盘消息,那么附消息中则存储了键盘的那个具体键的信息。
(6)GetMessage函数还可以过滤消息,它的第二个参数是用来指定从哪个窗口的消息队列中获取消息,其他窗口的消息将被过滤掉。如果该参数为NULL,则GetMessage从该应用程序线程的所有窗口的消息队列中获取消息。第三个和第四个参数是用来过滤MSG结构中主消息值的,主消息值在wMsgFilterMin和wMsgFilterMax之外的消息将被过滤掉。如果这两个参数为0,则表示接收所有消息。
(7)特别地,当且仅当GetMessage函数在获取WM_QUIT消息后,将返回0值,于是程序退出消息循环。
(8)TranslateMessage函数的作用:把虚拟键消息转换到字符消息,以满足键盘输入的需要。
(9)DispatchMessage函数的作用:把当前的消息发送到对应的窗口过程中去。
2. 消息处理:
(1)窗口过程:用于消息处理的函数。
用于消息处理的函数又叫窗口过程,在这个函数中,不同的消息将用switch语句分配到不同的处理程序中去。Windows的消息处理函数都有一个确定的统一方式,即这种函数的参数个数和类型以及其返回值的类型都有明确的规定。
在HelloWin.c中,WinProc函数明确处理了3个消息,分别是WM_CREATE(创建窗口消息)、WM_PAINT(窗口重画消息)、WM_DESTROY(销毁窗口消息)。
事实上,应用程序发送到窗口的消息远远不止以上这几条,像WM_SIZE、WM_MINIMIZE、WM_MOVE等这样经常使用的消息就有好几十条。为了减轻编程的负担,Windows的API提供了DefWindowProc函数来处理这些最常用的消息,调用这个函数后,这些消息将按照系统默认的方式得到处理。因此,在switch语句中,只需明确处理那些有必要进行特别响应的消息,把其余的消息交给DefWindowProc函数来处理,即将消息的控制交由Windows进行默认处理。
3. 结束消息循环:
当用户关闭窗口时,系统就向应用程序发送一条WM_DESTROY的消息。在处理此消息时,调用了PostQuitMessage函数,该函数会向窗口的消息队列中发送一条WM_QUIT消息。在消息循环中,GetMessage函数一旦检索到这条消息,就会返回FALSE,从而结束消息循环,随后程序也结束。
六、分析——WM_PAINT函数
1. WM_PAINT是Win32的图形和文本编程中经常使用到的消息。
2. 重绘:当窗口客户区的一部分或全部变成“无效”时,必须“刷新”重绘,此时将向程序发出此消息(WM_PAINT)。
无效:在最初窗口创建时,整个客户区都是“无效”的,因为窗口上还没有绘制任何东西。所以,在创建窗口时,会发出第一个WM_PAINT消息。在HelloWin.c程序中,由于在注册窗口时,制定了wndclass.style的风格为CS_VREDRAW和CS_HREDRAW,这表明只要窗口的高度或宽度发生变化,就将使整个窗口“无效”,从而发出WM_PAINT消息,使得系统重画整个窗口。当窗口最小化再恢复为以前的大小时,Windows将令窗口“无效”,并发出WM_PAINT消息使系统重画整个窗口。当窗口移至与另一窗口有重叠被遮挡时,Windows也将窗口视为“无效”,发出WM_PAINT消息以便刷新窗口。
3. 在窗口过程函数WndProc中,WM_PAINT消息处理通常有下列代码:
case WM_PAINT: hdc = BeginPaint(hwnd, &ps); GetClientRect(hwnd, &rc); //获取窗口客户区大小 DrawText(hdc, TEXT("Hello Windows!"), -1, &rc, DT_SINGLELINE | DT_CENTER | DT_VCENTER); EndPaint(hwnd, &ps); return 0;
(1)它总是从BeginPaint函数开始,而从EndPaint函数结束。BeginPaint函数用来指定窗口句柄的设备描述表句柄,设备描述表用来将程序与计算机外部输出设备连接起来。
(2)hdc定义的是句柄HDC变量,DrawText等GDI函数都需要通过这样的HDC句柄来绘制图形和文本。
(3)EndPaint用来释放设备描述表句柄,并使先前无效区域变为有效,从而使Windows不再发送WM_PAINT消息。
(4)PAINTSTRUCT是“绘图信息结构”,BeginPaint和EndPaint函数都需要PAINTSTRUCT结构变量作为自己的参数。需要说明的是,BeginPaint和EndPaint函数必须成对出现,所有GDI函数的调用也应在这两个函数之间进行。
(5)DrawText函数用来在参考矩形内使用指定的格式来绘制文本,它的函数原型如下:
int DrawText( HDC hDC; //绘制设备的句柄 LPCTSTR lpString; //要绘制的文本 int nCount; //文本的字符个数 LPRECT lpRect; //参考矩形 UINT uFormat; //文本绘制格式 );
其中,当nCount为-1时,表示lpString指定的是以“\0”为结尾的字符串,并自动计算该字符串的字符个数。
lpRect是一个指向RECT类型的“矩形”结构指针,该“矩形”结构含有left、top、right和bottom4个LONG域。为了能在窗口客户区中间绘制文本,该函数的lpRect被填为RECT变量rc的指针,它通过调用GetClientRect函数,获取hwnd窗口的客户区大小。
同时,指定uFormat格式为DT_SINGLELINE(单行输出)、DT_CENTER(水平居中)、DT_VCENTER(垂直居中)。
七、Windows基本数据类型
在前面的示例中,有一些“奇怪”的数据类型,如HINSTANCE和LPSTR等,事实上,很多这样的数据类型只是一些基本数据类型的别名。
1. Windows编程中常用的基本数据类型
Windows所用的数据类型 | 对应的基本数据类型 | 说明 |
BOOL | bool | 布尔值 |
BSTR | unsigned short * | 32位字符指针 |
BYTE | unsigned char | 8位无符号整数 |
COLORREF | unsigned long | 用作颜色值的32位值 |
DWORD | unsigned long | 32位无符号整数,段地址和相关的偏移地址 |
LONG | long | 32位带符号整数 |
LPARAM | long | 作为参数传递给窗口过程或回调函数的32位值 |
LPCSTR | const char * | 指向字符串常量的32位指针 |
LPSTR | char * | 指向字符串的32位指针 |
LPVOID | void * | 指向未定义类型的32位指针 |
LRESULT | long | 来自窗口过程或回调函数的32位返回值 |
UINT | unsigned int | 32位无符号整数 |
WORD | unsigned short | 16位无符号整数 |
WPARAM | unsigned int | 作为参数传递给窗口过程或回调函数的32位值 |
2. Windows编程中常用的句柄类型
句柄类型 | 说明 |
HBITMAP | 保存位图信息的内存域的句柄 |
HBRUSH | 画刷句柄 |
HCURSOR | 鼠标光标句柄 |
HDC | 设备描述表句柄 |
HFONT | 字体句柄 |
HICON | 图标句柄 |
HINSTANCE | 应用程序的实例句柄 |
HMENU | 菜单句柄 |
HPALETTE | 颜色调色板句柄 |
HPEN | 在设备上画图时用于指明线型的笔的句柄 |
HWND | 窗口句柄 |
补充:
窗口过程:用于消息处理的函数,例如,本例中的窗口过程函数WndProc
回调函数
GDI函数
预定义风格:如CS_VREDRAW、CS_HREDRAW等,它们可以通过“|”组合
API函数:如LoadIcon、LoadCursor、GetStockObject等都是Windows 的API函数,在程序中可直接调用