第2章 Windows窗口程序
本章首先带领大家认识一下Windows窗口程序,然后详细介绍开发一个标准Windows窗口程序的步骤与原理。很多内容都是Windows的规定。对于刚刚接触Windows程序设计的读者,第2章或许是全书最难的,但是只要明白了本章所涉及的原理,后面章节的学习就会顺理成章。
2.1 认识Windows窗口
图形用户界面(Graphical User Interface,GUI)是指采用图形方式显示程序界面。与早期计算机使用的命令行界面相比,图形界面对于用户来说在视觉上更易于接受。启动一个应用程序,桌面上就会显示一块矩形区域,这个矩形区域称为窗口。用户可以在窗口中操作应用程序,进行数据的管理和编辑。
第1章的HelloWorld程序仅仅是创建了一个最简单的窗口,称为对话框窗口,可以显示标题、提示文本以及几个系统内置的按钮和图标。让我们以记事本程序为例,了解一个典型窗口程序的组成,即了解组成程序窗口的各个元素。
记事本程序的窗口中包含以下元素。
(1)标题栏和窗口标题,标题栏是位于窗口顶部的一块矩形区域,其中含有窗口标题,双击标题栏可以将程序窗口最大化或从最大化的状态恢复,按下鼠标左键拖动标题栏可以进行窗口的移动。
(2)系统菜单按钮,位于窗口标题栏的最左侧,单击以后可以弹出系统菜单,有最小化、最大化、关闭等菜单项。
(3)菜单栏,位于标题栏的下方,会列出许多菜单项,用于提供各种不同的操作命令。
(4)最小化、最大化和关闭按钮,在窗口标题栏的最右侧有3个按钮,单击中间的最大化按钮,可以把窗口放大到最大(占据整个桌面);当窗口已经最大化时,最大化按钮就会变成还原按钮,单击可以将窗口还原为原来的大小;单击左边的最小化按钮,程序窗口就会从桌面隐藏,只剩下任务栏中的一个程序图标(含窗口标题),单击任务栏中的程序图标可以恢复显示到桌面;如果需要关闭该窗口,可以单击最右边的关闭按钮。
(5)滚动条,当窗口的大小不足以显示整个文件(档)的内容时,可以使用位于窗口底部或右边的滚动条(向右或向下移动),以查看该文件(档)中的剩余部分。
(6)窗口边框,窗口周边的网条边称为窗口边框,用鼠标移动一条边框的位置可以改变窗口的大小,也可以用鼠标移动窗口的一个角来同时改变窗口两个边框的位置以改变窗口大小。
(7)客户区,窗口内部的矩形区域称为客户区,客户区是指除标题栏、菜单栏、工具栏、状态栏、滚动条和边框以外的区域,程序在这里显示文本、图形、子窗口控件或其他信息。以后会遇到非客户区的概念,非客户区就是指除客户区以外的区域,就是标题栏、菜单栏、工具栏、状态栏、滚动条和边框等。
(8)状态栏,状态栏通常位于窗口的最下方,用来显示一些状态信息。
有的程序在菜单栏的下面还有工具栏,工具栏用图标的形式列出了常用的菜单项,相当于菜单项的快捷方式。并不是所有的程序都有上述这些元素,有的程序没有菜单栏,有的程序没有状态栏,等等。
一个窗口并不一定就是一个程序,它可能只是一个程序的一部分,一个程序可以创建多个顶级窗口,因此一个窗口并不是程序的代表。另外,Windows的窗口采用层次结构,在一个窗口中可以创建多个子窗口,例如记事本程序是一个窗口,窗口内部的客户区、状态栏等都是一个子窗口。一个程序的主窗口都是顶级窗口,顶级窗口(Top-Level,也叫顶层窗口)通常是指其父窗口是桌面的程序窗口。
相反,一个程序并不一定必须要有一个窗口,比如悄悄在后台运行的木马程序就不会显示一个窗口让用户发现它在做什么。打开任务管理器可以看到进程的数量比屏幕上的窗口多得多,这说明很多正在运行的程序并没有显示窗口。如果一个程序不想和用户进行交互,它可以选择不创建窗口。
2.2 第一个Windows窗口程序
看一下Windows程序设计的范本,其实很简单,只有70多行。理解这些,我相信读者对于Windows程序设计就已经入门了,并且掌握了这门编程语言的20%。这么说并不夸张,因为这是一大跨越!然后学习关于Windows各方面的几百个API函数,了解消息机制,学会查询MSDN,Windows程序设计就变得非常简单了。
创建一个Win32空项目HelloWindows,HelloWindows.cpp源文件的内容如下所示:
#include <Windows.h>
#include <tchar.h> // _tcslen函数需要该头文件
#pragma comment(lib, "Winmm.lib") // 播放声音的PlaySound函数需要Winmm导入库
// 函数声明,窗口过程
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int
nCmdShow)
{
WNDCLASSEX wndclass; // RegisterClassEx函数用的WNDCLASSEX结构
TCHAR szClassName[] = TEXT("MyWindow"); // RegisterClassEx函数注册的窗口类的名称
TCHAR szAppName[] = TEXT("HelloWindows"); // 窗口标题
HWND hwnd; // CreateWindowEx函数创建的窗口的句柄
MSG msg; // 消息循环所用的消息结构体
wndclass.cbSize = sizeof(WNDCLASSEX);
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WindowProc;
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 = szClassName;
wndclass.hIconSm = NULL;
RegisterClassEx(&wndclass);
hwnd = CreateWindowEx(0, szClassName, szAppName, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 300, 180, NULL, NULL, hInstance, NULL);
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
while (GetMessage(&msg, NULL, 0, 0) != 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
TCHAR szStr[] = TEXT("你好,Windows程序设计");
switch (uMsg)
{
case WM_CREATE:
PlaySound(TEXT("成都(两会版).wav"), NULL, SND_FILENAME | SND_ASYNC);
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
TextOut(hdc, 10, 10, szStr, _tcslen(szStr));
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
按Ctrl + F5组合键编译运行程序,计算机响起"春风吹过的时候 我矗立在街头......",并在客户区左上角显示"你好,Windows程序设计"。
程序有标题栏,系统菜单,最小化、最大化、关闭按钮,拖拉窗口边框可以改变窗口大小,这个窗口包含了一个典型窗口的大部分特征。
刚刚接触Windows程序设计的读者可能会觉得这个程序太过复杂,但是这个程序是大部分窗口程序的模板,以后要写一个新的程序,只要把它复制过来再向其中添砖加瓦即可。复杂的可能是WNDCLASSEX结构各个字段的填充。在VS中输入WNDCLASSEX,右键单击转到定义,就可以看到该结构的定义,然后按照定义依次填充每个字段即可(每个字段的含义稍后会详细介绍)。
在屏幕上显示一个窗口的过程一般包括以下步骤,也就是入口函数WinMain的执行流程。
(1)注册窗口类(RegisterClassEx),在注册之前,要先填写RegisterClassEx函数的参数WNDCLASSEX结构的各个字段。
(2)创建窗口(CreateWindowEx)。
(3)显示窗口(ShowWindow)、刷新窗口客户区(UpdateWindow)。
(4)进入无限的消息获取、分发的循环:获取消息(GetMessage),转换消息(TranslateMessage),将消息分发到回调函数WindowProc进行处理(DispatchMessage)。
下面分别介绍每一个步骤。
2.2.1 注册窗口类(RegisterClassEx)
RegisterClassEx,见名知义,注册窗口类,最后的Ex是扩展的意思,它是Win16中RegisterClass函数的扩展。该函数用于注册一个窗口类,在下一步调用CreateWindowEx函数创建窗口时使用:
ATOM WINAPI RegisterClassEx(_In_ const WNDCLASSEX *lpwcx);
函数返回值类型ATOM是原子的意思,就是说RegisterClassEx函数会返回一个独一无二的值,这个值就唯一代表我们注册的窗口类。我们通常不关心这个值,而是使用WNDCLASSEX结构中指定的窗口类名(WNDCLASSEX.lpszClassName字段)。ATOM数据类型在minwindef.h头文件中定义如下:
typedef WORD ATOM;
如果函数执行成功,则返回值是标识已注册窗口类的类原子值;如果函数执行失败,则返回值为0,可以通过调用GetLastError函数获取错误代码。
参数lpwcx是一个指向WNDCLASSEX结构的指针,调用RegisterClassEx函数之前必须初始化该结构:
typedef struct tagWNDCLASSEX {
UINT cbSize; // 该结构的大小,以字节为单位,设置为sizeof(WNDCLASSEX)
UINT style; // 用这个窗口类创建的窗口具有的样式
WNDPROC lpfnWndProc; // 指定窗口过程,所有基于这个窗口类创建的窗口都使用这个窗口过程
int cbClsExtra; // 紧跟在WNDCLASSEX结构后面的附加数据字节数,用来存放自定义数据
int cbWndExtra; // 紧跟在窗口实例后面的附加数据字节数,用来存放自定义数据
HINSTANCE hInstance; // 窗口类的窗口过程所属的实例句柄(模块句柄)
HICON hIcon; // 用这个窗口类创建的窗口所用的图标资源句柄
HCURSOR hCursor; // 用这个窗口类创建的窗口所用的光标资源句柄
HBRUSH hbrBackground;// 用这个窗口类创建的窗口所用的背景画刷句柄
LPCTSTR lpszMenuName; // 指定窗口类的菜单资源名称,菜单资源通常在资源文件中定义
LPCTSTR lpszClassName;// 指定窗口类的名称,调用CreateWindowEx函数创建窗口时需要使用窗口类名
HICON hIconSm; // 用这个窗口类创建的窗口所用的小图标的句柄
} WNDCLASSEX, *PWNDCLASSEX;
-
第1个字段cbSize指定该结构的大小,以字节为单位,设置为sizeof (WNDCLASSEX)或sizeof(wndclass)都可以。很多Windows API函数参数中使用的结构体中都有一个cbSize字段,它主要用来区分结构的版本。系统升级以后结构体可能会新增一些字段,结构体就相应增大。如果函数调用的时候发现cbSize还是旧的长度,就表示运行的是基于旧结构的程序,这样可以防止使用无效的字段。请注意,如果一个结构有cbSize结构大小的字段,必须把它初始化为sizeof(结构名称或结构变量名称)。
-
第2个字段style指定窗口类样式,也就是用这个窗口类创建的窗口具有的样式,常见的窗口类样式如表2.1所示。
表2.1
| 常量 | 含义 |
|---|---|
| CS_HREDRAW | 当窗口宽度发生变化时,重新绘制整个窗口 |
| CS_VREDRAW | 当窗口高度发生变化时,重新绘制整个窗口 |
| CS_NOCLOSE | 禁用关闭按钮,系统菜单的关闭菜单项也会消失 |
| CS_DBLCLKS | 当用户双击鼠标时,向窗口过程发送双击消息WM_LBUTTONDBLCLK |
| CS_GLOBALCLASS | 表示窗口类是应用程序全局类 |
| CS_OWNDC | 为窗口类的每个窗口分配唯一的设备环境 |
| CS_CLASSDC | 分配一个设备环境供基于窗口类的所有窗口共享 |
| CS_PARENTDC | 将子窗口的裁剪矩形设置为父窗口的裁剪矩形,以便子窗口可以在父窗口上进行绘制 |
指定为CS_HREDRAW | CS_VREDRAW表示如果窗口的水平尺寸或垂直尺寸改变了,那么所有基于该窗口类创建的窗口都将会被重新绘制。
CS_NOCLOSE表示禁用关闭按钮,系统菜单的关闭菜单项也会消失。大家把设置窗口类样式一行改为wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_NOCLOSE;看看会发生什么现象,为了关闭程序是不是只能打开任务管理器结束进程呢?
请大家不要急于完全理解或者自己写出本程序,随着本书内容的不断深入,一切都会慢慢明朗起来的。运行程序,双击客户区,没有任何反应,大家把设置窗口类样式一行改为wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;。
把窗口过程函数WindowProc的switch(uMsg)加上对WM_LBUTTONDBLCLK消息的处理:
case WM_LBUTTONDBLCLK:
MessageBox(hwnd, TEXT("客户区被双击"), TEXT("提示"), MB_OK);
return 0;
case WM_DESTROY:
重新编译运行,双击客户区会弹出一个消息框。
-
第3个字段lpfnWndProc指定窗口过程,所有基于这个窗口类创建的窗口都使用这个窗口过程。窗口过程的概念稍后讲解,现在只需要知道程序运行以后会发生很多事件,比如窗口创建、窗口重绘、窗口尺寸改变、鼠标双击、程序关闭等事件,操作系统会把这些事件通知应用程序,应用程序在lpfnWndProc字段指定的窗口过程(就是函数)中处理这些事件,WNDPROC是窗口过程指针类型。
-
第4个字段cbClsExtra指定紧跟在WNDCLASSEX结构后面的窗口类附加数据字节数,用来存放自定义数据,可以通过调用GetClassLong或GetClassLongPtr函数来获取这些数据。窗口类附加数据字节数不能超过40字节。
-
第5个字段cbWndExtra指定紧跟在窗口实例后面的的窗口附加数据字节数,用来存放自定义数据,可以通过调用GetWindowLong或GetWindowLongPtr函数来获取这些数据。窗口附加数据字节数不能超过40字节。
以上两个字段,以后用到的时候再去理解其含义,没有特别需求,这两个字段设置为0即可。
-
第6个字段hInstance指定窗口类的窗口过程所属的实例句柄,也就是所属的模块。
-
第7个字段hIcon指定图标资源句柄,这个图标用于生成可执行文件图标。Windows已经预定义了一些图标,程序也可以使用在资源文件中自定义的图标。这些图标的句柄可以通过调用LoadIcon或LoadImage函数获取(LoadImage函数的用法在以后学习资源文件的时候再讲解)。 资源文件在编译的时候会被打包到可执行文件中,LoadIcon函数用于从应用程序实例(模块)中加载指定的图标资源:
HICON WINAPI LoadIcon(
In_opt HINSTANCE hInstance, // 程序实例句柄(模块句柄)
In LPCTSTR lpIconName); // 要加载的图标资源的名称
HICON是Windows定义的图标句柄数据类型,Windows为不同的对象定义了不同名称的句柄类型,但是句柄只不过是一个数值,不需要深究。
如果函数执行成功,则返回值是所加载图标的句柄;如果函数执行失败,则返回值为NULL。
也可以使用一个系统预定义的图标,将hInstance参数设置为NULL,将lpIconName参数设置为表2.2所列的值。
表2.2
| 值 | 含义 | 形状 |
|---|---|---|
| IDI_APPLICATION或IDI_WINLOGO | 默认应用程序图标 | |
| IDI_ASTERISK或IDI_INFORMATION | 信息提示样式图标 | |
| IDI_ERROR或IDI_HAND | 错误提示样式图标 | |
| IDI_QUESTION | 问号图标 | |
| IDI_EXCLAMATION或IDI_WARNING | 感叹号图标 | |
| IDI_SHIELD | 安全防护盾牌图标 |
- 第8个字段hCursor指定用这个窗口类创建的窗口所用的光标资源句柄,就是当鼠标在客户区中时的光标形状。Windows预定义了一些光标,程序也可以使用在资源文件中自定义的光标。这些光标的句柄可以通过调用LoadCursor或LoadImage函数获取。
LoadCursor函数用于从应用程序实例(模块)中加载指定的光标资源:
HCURSOR WINAPI LoadCursor(
_In_opt_ HINSTANCE hInstance, // 程序实例句柄(模块句柄)
_In_ LPCTSTR lpCursorName); // 要加载的光标资源的名称
HCURSOR是Windows定义的光标句柄数据类型,在windef.h头文件中HCURSOR定义如下:
typedef HICON HCURSOR;
如果函数执行成功,则返回值是所加载光标的句柄;如果函数执行失败,则返回值为NULL。
也可以使用一个系统预定义的光标,将hInstance参数设置为NULL,将lpCursorName参数设置为表2.3所列的值。
表2.3
| 值 | 含义 | 形状 |
|---|---|---|
| IDC_APPSTARTING | 标准箭头和等待(忙碌) | |
| IDC_ARROW | 标准箭头 | |
| IDC_CROSS | 十字线 | |
| IDC_HAND | 手形 | |
| IDC_HELP | 箭头和问号 | |
| IDC_IBEAM | 工字 | |
| IDC_NO | 斜线圆 | |
| IDC_SIZEALL | 指向北、南、东和西的四角箭头 | |
| IDC_SIZENESW | 指向东北和西南的双尖箭头 | |
| IDC_SIZENS | 指向南北的双尖箭头 | |
| IDC_SIZENWSE | 指向西北和东南的双尖箭头 | |
| IDC_SIZEWE | 指向西和东的双尖箭头 | |
| IDC_UPARROW | 垂直箭头 | |
| IDC_WAIT | 等待(忙碌) |
- 第9个字段hbrBackground指定用这个窗口类创建的窗口所用的背景画刷句柄,也可以使用标准系统颜色。系统用指定的背景画刷或颜色填充客户区背景。HBRUSH是Windows定义的画刷句柄类型。
GetStockObject函数用于获取备用(或者说库存,实际上就是系统预定义的)画笔、画刷、字体等的句柄:
HGDIOBJ GetStockObject(_In_ int fnObject);
fnObject参数指定备用对象的类型,对于画刷来说,可以是表2.4所列的值。
表2.4
| 值 | 含义 |
|---|---|
| BLACK_BRUSH | 黑色画刷 |
| WHITE_BRUSH | 白色画刷 |
| DKGRAY_BRUSH | 深灰色画刷 |
| GRAY_BRUSH | 灰色画刷 |
| LTGRAY_BRUSH | 浅灰色画刷 |
| DC_BRUSH | DC画刷,默认颜色为白色,可以使用SetDCBrushColor函数更改颜色 |
| HOLLOW_BRUSH或NULL_BRUSH | 空画刷,表示什么也不画 |
如果函数执行成功,则返回指定备用对象的HGDIOBJ类型句柄;如果函数执行失败,则返回值为NULL。
也可以使用标准系统颜色,表2.5所列的值是可用的部分标准系统颜色。
表2.5
| 常量 | 颜色效果 |
|---|---|
| COLOR_ACTIVEBORDER | |
| COLOR_ACTIVECAPTION | |
| COLOR_APPWORKSPACE | |
| COLOR_BACKGROUND | |
| COLOR_BTNFACE | |
| COLOR_BTNSHADOW | |
| COLOR_BTNTEXT | |
| COLOR_CAPTIONTEXT | |
| COLOR_GRAYTEXT | |
| COLOR_HIGHLIGHT | |
| COLOR_HIGHLIGHTTEXT | |
| COLOR_INACTIVEBORDER | |
| COLOR_INACTIVECAPTION | |
| COLOR_MENU | |
| COLOR_MENUTEXT | |
| COLOR_SCROLLBAR | |
| COLOR_WINDOW | |
| COLOR_WINDOWFRAME | |
| COLOR_WINDOWTEXT |
关于每个常量值具体对应什么颜色,书本上可能看不出来,我已经保存了图片,参见Chapter2\ SystemColor.png。使用标准系统颜色的时候,需要加1,例如wndclass.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1);。
-
第10个字段lpszMenuName指定窗口类的菜单资源名称。菜单通常在资源文件中定义,也可以在创建窗口函数CreateWindowEx的参数中指定。如果在这两个地方都没有指定,那么程序就没有菜单。本程序没有使用菜单,所以设置为NULL。
-
第11个字段lpszClassName指定窗口类的名称,最大字符个数为256。调用CreateWindowEx函数创建窗口时需要使用这个窗口类名。如果需要获取指定窗口的窗口类名,可以调用GetClassName函数。
-
第12个字段hIconSm指定用这个窗口类创建的窗口所用的小图标句柄,小图标是显示在窗口标题栏左侧和任务栏的图标。如果该字段设置为NULL,则系统会搜索可执行文件的图标资源以查找合适大小的图标作为小图标。
应用程序在创建窗口时,必须首先注册窗口类。窗口类包含了一个窗口的重要信息,例如窗口样式、窗口过程、显示和绘制窗口所需要的信息等,每一个窗口都是一个窗口类的实例。一个程序可以基于同一个窗口类创建多个窗口实例,同一窗口类的窗口使用同一个窗口过程WindowProc。请读者在UpdateWindow(hwnd);语句后面加上如下语句,基于同一个窗口类再创建一个窗口实例看一看效果:
HWND hwnd2 = CreateWindowEx(0, szClassName, szAppName, WS_OVERLAPPEDWINDOW,
300, 200, 300, 180, NULL, NULL, hInstance, NULL);
ShowWindow(hwnd2, nCmdShow);
UpdateWindow(hwnd2);
2.2.2 创建窗口(CreateWindowEx)
注册窗口类后,就可以在窗口类的基础上通过调用CreateWindowEx函数添加其他的属性来创建窗口了。CreateWindowEx是CreateWindow函数的扩展版本,可以创建具有扩展窗口样式的重叠窗口、弹出窗口或子窗口:
HWND WINAPI CreateWindowEx(
_In_ DWORD dwExStyle, // 窗口的扩展窗口样式
_In_opt_ LPCTSTR lpClassName, // RegisterClassEx函数注册的窗口类名
_In_opt_ LPCTSTR lpWindowName, // 窗口标题
_In_ DWORD dwStyle, // 窗口的窗口样式
_In_ int x, // 窗口的初始水平位置,像素单位
_In_ int y, // 窗口的初始垂直位置,像素单位
_In_ int nWidth, // 窗口的初始宽度,像素单位
_In_ int nHeight, // 窗口的初始高度,像素单位
_In_opt_ HWND hWndParent, // 窗口的父窗口
_In_opt_ HMENU hMenu, // 菜单句柄
_In_opt_ HINSTANCE hInstance, // 与窗口关联的实例句柄(模块句柄)
_In_opt_ LPVOID lpParam); // 额外参数
- 第1个参数dwExStyle指定窗口的扩展样式。dwExStyle是Win32中扩展的参数,扩展窗口样式是一些以WSEX开头的预定义值,WS是Windows Style的缩写。表2.6所列是部分扩展窗口样式。
表2.6
| 常量 | 含义 |
|---|---|
| WS_EX_WINDOWEDGE | 窗口的边框带有凸起的边缘 |
| WS_EX_CLIENTEDGE | 窗口的边框带有凹陷的边缘 |
| WS_EX_OVERLAPPEDWINDOW | 等于WS_EX_WINDOWEDGE |
| WS_EX_ACCEPTFILES | 窗口接受拖放文件 |
| WS_EX_TOPMOST | 窗口始终保持在最顶层 |
| WS_EX_MDICHILD | 窗口是MDI子窗口 |
| WS_EX_CONTEXTHELP | 窗口的标题栏包含一个问号按钮,如果用户单击问号,则光标将变为带有指针的问号;如果用户随后单击子窗口,则子窗口将收到一条WM_HELP消息,子窗口应将消息传递给父窗口过程,父窗口过程使用HELP_WM_HELP命令调用WinHelp函数,应用程序显示一个弹出窗口,通常包含子窗口的帮助。WS_EX_CONTEXTHELP不能与WS_MAXIMIZEBOX或WS_MINIMIZEBOX样式一起使用 |
| WS_EX_LAYERED | 分层或透明窗口,该样式可以实现一些混合特效 |
| WS_EX_TOOLWINDOW | 创建一个工具窗口,通常用作浮动工具条 |
如果没有特别需求,第1个参数可以设置为0。
-
第2个参数lpClassName指定为RegisterClassEx函数注册的窗口类名。
-
第3个参数lpWindowName指定窗口标题,通常窗口类名和窗口标题可以使用相同的名称。
-
第4个参数dwStyle指定窗口样式,窗口样式是一些以WS_开头的预定义值。表2.7所列是部分窗口样式。
表2.7
| 常量 | 含义 |
|---|---|
| WS_BORDER | 窗口有细线边框 |
| WS_CAPTION | 窗口有一个标题栏(包括WS_BORDER) |
| WS_DLGFRAME | 创建一个带对话框边框的窗口,这种样式的窗口没有标题栏 |
| WS_HSCROLL | 窗口有一个水平滚动条 |
| WS_VSCROLL | 窗口有一个垂直滚动条 |
| WS_MAXIMIZEBOX | 窗口有一个最大化按钮,不能与WS_EX_CONTEXTHELP样式组合,还必须指定WS_SYSMENU样式 |
| WS_MINIMIZEBOX | 窗口有一个最小化按钮,不能与WS_EX_CONTEXTHELP样式组合,还必须指定WS_SYSMENU样式 |
| WS_ICONIC或WS_MINIMIZE | 窗口最初被最小化 |
| WS_MAXIMIZE | 窗口最初被最大化 |
| WS_SIZEBOX或WS_THICKFRAME | 可以通过边框调整窗口大小 |
| WS_SYSMENU | 窗口的标题栏上有一个系统菜单,还必须同时指定WS_CAPTION样式 |
| WS_OVERLAPPED或WS_TILED | 窗口是重叠窗口,重叠窗口具有标题栏和边框 |
| WS_POPUP | 窗口是一个弹出窗口,该样式不能与WS_CHILD样式一起使用 |
| WS_DISABLED | 窗口最初被禁用,禁用的窗口无法接收用户的输入 |
| WS_VISIBLE | 窗口最初可见,如果不设置这个样式,窗口将不可见 |
| WS_CHILD或WS_CHILDWINDOW | 窗口是子窗口,具有该样式的窗口不能有菜单栏。该样式不能与WS_POPUP样式一起使用 |
| WS_CLIPCHILDREN | 父窗口不对子窗口区域进行绘制 |
| WS_CLIPSIBLINGS | 不对兄弟窗口(属于同一个父窗口的多个子窗口)进行绘制,以后可能会用到 |
| WS_POPUPWINDOW | 相当于`WS_POPUP |
| WS_OVERLAPPEDWINDOW或WS_TILEDWINDOW | 相当于`WS_OVERLAPPED |
CreateWindowEx函数可以创建具有扩展窗口样式的重叠窗口、弹出窗口或子窗口。如果需要创建一个常见的重叠窗口,则使用WS_OVERLAPPEDWINDOW样式即可,具有标题栏、系统菜单、可调边框、最小化/最大化/关闭按钮等;如果需要创建一个弹出窗口,则使用WS_POPUPWINDOW | WS_CAPTION样式即可,具有标题栏、系统菜单、关闭按钮等;如果需要创建一个子窗口,则使用WS_CHILD样式。
-
第5个参数x和第6个参数y分别指定窗口左上角相对于屏幕左上角的初始水平和垂直位置,以像素为单位,设置为CW_USEDEFAULT(表示由Windows设置为默认值)。
-
第7个参数nWidth和第8个参数nHeight分别指定窗口的初始宽度和高度,以像素为单位,也可以设置为CW_USEDEFAULT(表示由Windows设置为默认值)。
-
第9个参数hWndParent指定窗口的父窗口。如果是创建子窗口,则需要指定父窗口句柄,以便父子窗口之间进行通信。本程序是顶级重叠窗口,设置为NULL即可。 重叠窗口是指具有标题栏、边框和客户区的顶级窗口,另外还可以有菜单、最小化和最大化按钮以及滚动条等,作为应用程序的主窗口;弹出窗口是一种特殊类型的重叠窗口,通常用于显示对话框、消息框和其他临时窗口。弹出窗口和重叠窗口的主要区别在于弹出窗口的标题栏是可选的,而重叠窗口必须具有标题栏(9.4节将创建一个没有标题栏的弹出窗口)。顶级窗口是指没有WS_CHILD属性的窗口,顶级窗口的父窗口为桌面窗口。重叠窗口和弹出窗口都可以是顶级窗口,它们的坐标定位相对于屏幕左上角,顶级窗口作为一个程序的主窗口。 子窗口必须具有父窗口。父窗口可以是重叠窗口、弹出窗口,甚至可以是其他子窗口。子窗口从父窗口的客户区左上角定位,而不是从屏幕左上角定位。可以为子窗口设置标题栏、最小化和最大化按钮、边框和滚动条等,但不能设置菜单。
-
第10个参数hMenu指定菜单句柄。本程序没有菜单,设置为NULL。如果创建的是子窗口,则该参数设置为子窗口的ID。
-
第11个参数hInstance指定与窗口关联的实例句柄,也就是指定窗口所属的模块。
-
第12个参数lpParam可以指定为指向某些数据或数据结构的指针。如果没有特别需求,则设置为NULL即可。后面会再次讲到这个参数。
窗口类只是定义了窗口的一般特征,基于同一窗口类可以创建许多不同的窗口。在调用CreateWindowEx函数创建窗口时,还可以指定许多与窗口有关的细节信息,例如WNDCLASSEX结构中没有定义的外观、窗口标题、位置、大小等属性。刚刚接触Windows程序设计的读者可能会疑惑窗口的特征为什么不能一次性指定完。基于同一窗口类可以创建许多不同的窗口实例,这些窗口可以样式、大小各异。窗口类指定的是这些窗口的通用特征,而调用CreateWindowEx函数创建每个窗口的时候可以再指定一些个性化的特征。
如果函数执行成功,则返回新创建窗口的窗口句柄;如果函数执行失败,则返回值为NULL。在Windows系统中,每一个窗口都有一个句柄,在程序中可以使用句柄对窗口进行引用。许多Windows API都以窗口句柄作为参数,通过窗口句柄Windows就可以知道该函数要对哪个窗口进行操作。如果一个程序创建了多个窗口,那么每个窗口都具有不同的窗口句柄。
2.2.3 显示窗口(ShowWindow)和刷新窗口客户区(UpdateWindow)
光标定位到ShowWindow(hwnd, nCmdShow);一行,按F9键设断点,按F5键开始调试。这时候程序窗口还没有显示出来,但如果读者打开任务管理器,则会发现HelloWindows.exe进程已经存在,窗口已经在Windows内部被创建了。ShowWindow函数用于设置指定窗口的显示状态:
BOOL WINAPI ShowWindow(
_In_ HWND hWnd, // 窗口句柄
_In_ int nCmdShow); // 窗口的显示方式
第一次调用ShowWindow时,nCmdShow参数可以指定为WinMain函数nCmdShow的值(通常为SW_SHOWDEFAULT),表示激活并显示窗口。
ShowWindow函数的作用是设置指定窗口的显示状态,如果程序以后需要设置窗口的显示状态,nCmdShow参数可以指定为表2.8所列的值。
表2.8
| 常量 | 含义 |
|---|---|
| SW_SHOW | 激活窗口并以当前大小和位置显示窗口 |
| SW_SHOWNA | 以当前大小和位置显示窗口。该值类似于SW_SHOW,不同之处在于未激活窗口 |
| SW_HIDE | 隐藏窗口 |
| SW_MINIMIZE | 最小化窗口 |
| SW_SHOWMAXIMIZED或 | |
| SW_MAXIMIZE | 激活窗口并将其显示为最大化窗口 |
| SW_RESTORE | 激活并恢复显示窗口。如果窗口最小化或最大化,则系统会将其还原到原始大小和位置 |
| SW_SHOWNORMAL | 激活并显示一个窗口,如果窗口最小化或最大化,则系统会将其还原到原始大小和位置。首次显示窗口时,通常使用该标志 |
| SW_SHOWNOACTIVATE | 显示一个窗口,如果窗口最小化或最大化,则系统会将其还原到原始大小和位置。该值类似于SW_SHOWNORMAL,不同之处在于未激活窗口 |
| SW_SHOWMINIMIZED | 激活窗口并将其显示为最小化窗口 |
| SW_SHOWMINNOACTIVE | 显示为最小化窗口。该值类似于SW_SHOWMINIMIZED,不同之处在于未激活窗口 |
| SW_SHOWDEFAULT | 根据传递给CreateProcess函数的STARTUPINFO结构中指定的SW_值设置显示状态(后面会学习CreateProcess函数) |
| SW_FORCEMINIMIZE | 最小化窗口(即使拥有该窗口的线程没有响应),仅当最小化其他线程中的窗口时才使用该标志 |
如果窗口以前可见,则返回值为非零;如果窗口以前隐藏,则返回值为0。
在ShowWindow(hwnd, nCmdShow);一行,按F9键设置断点,按F5键开始调试。按F10键单步执行完这一句以后,可以看到程序窗口已经显示出来了;继续按F10键单步执行完UpdateWindow (hwnd);,发现客户区已经显示出"你好,Windows程序设计",但是音乐还没有响起。
接下来谈一下UpdateWindow函数。UpdateWindow函数通过向窗口发送WM_PAINT消息来更新指定窗口的客户区。该函数将WM_PAINT消息直接发送到窗口的窗口过程,绕过应用程序的消息队列,即UpdateWindow函数导致窗口过程WindowProc执行case WM_PAINT逻辑,调用TextOut函数在窗口客户区中输出文字。消息机制将在下一节详细介绍:
BOOL UpdateWindow(_In_ HWND hWnd); // 更新哪个窗口
如果函数执行成功,则返回TRUE;如果函数执行失败,则返回FALSE。
2.2.4 消息循环
前面说过,程序运行以后会发生很多事件,比如窗口创建、窗口重绘、窗口尺寸改变、鼠标双击、程序关闭等事件,Windows会把这些事件通知应用程序。那么Windows是如何把这些事件通知应用程序的呢?Windows为每个应用程序维护消息队列,事件发生以后,Windows会自动将其转换为消息,并放置在应用程序的消息队列中;应用程序通过调用GetMessage函数从消息队列中获取消息;调用TranslateMessage函数转换消息;调用DispatchMessage函数分发消息到窗口过程,实际上DispatchMessage函数的处理机制是把消息传递给Windows,然后Windows去调用窗口过程;窗口过程处理完一个消息以后,将控制权返回给Windows,然后DispatchMessage函数返回。这一轮操作完成以后,又会进行下一轮的消息获取、转换和分发。
GetMessage函数用于从调用线程的消息队列中获取消息:
BOOL WINAPI GetMessage(
_Out_ LPMSG lpMsg, // MSG结构用于存放消息的具体信息
_In_opt_ HWND hWnd, // 要获取哪个窗口的消息
_In_ UINT wMsgFilterMin, // 要获取的消息的最小值
_In_ UINT wMsgFilterMax); // 要获取的消息的最大值
-
第1个参数lpMsg是一个指向MSG结构的指针,用于存放消息的具体信息,即函数在获取一个消息以后,会把这个消息的具体信息存放在这个结构中:
typedef struct tagMSG {
HWND hwnd; // 哪个窗口发生的消息
UINT message;// 消息类型,以WM_开头(Windows Message)
WPARAM wParam; // 消息参数,其含义取决于具体的消息类型
LPARAM lParam; // 消息参数,其含义取决于具体的消息类型
DWORD time; // 消息发生时的时间
POINT pt; // 消息发生时的光标位置,屏幕坐标
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;
pt字段是一个POINT结构,表示消息发生时的光标位置,该结构在windef.h头文件中定义如下:
typedef struct tagPOINT
{
LONG x;
LONG y;
} POINT, *PPOINT, NEAR *NPPOINT, FAR *LPPOINT;
关于WPARAM和LPARAM数据类型的定义如下所示:
typedef UINT_PTR WPARAM;
typedef LONG_PTR LPARAM;
#if defined(_WIN64)
typedef unsigned __int64 UINT_PTR, * PUINT_PTR;
#else
typedef _W64 unsigned int UINT_PTR, * PUINT_PTR;
#endif
#if defined(_WIN64)
typedef __int64 LONG_PTR, * PLONG_PTR;
#else
typedef _W64 long LONG_PTR, * PLONG_PTR;
#endif
如果程序编译为32位,则WPARAM和LPARAM都是一个32位的数值;如果程序编译为64位,则WPARAM和LPARAM都是一个64位的数值。
-
第2个参数hWnd指定要获取哪个窗口的消息。如果hWnd设置为NULL,则函数将获取属于当前线程的所有窗口的消息。
-
第3个参数wMsgFilterMin和第4个参数wMsgFilterMax指定要获取的消息的最小值和最大值。例如WM_PAINT消息实际上就是一个数值,设置要获取的消息的最小值和最大值是为了过滤消息,只对感兴趣的消息进行处理。如果wMsgFilterMin和wMsgFilterMax都设置为0,则函数将获取所有的可用消息,不执行范围筛选。不过,不管如何设置范围,WM_QUIT(程序退出)消息都是可以获取到的。如果获取到的消息不是WM_QUIT(程序退出),则函数返回值为非零;如果获取到的消息是WM_QUIT,则返回值为0;如果函数执行失败,则返回−1。
TranslateMessage函数将按键消息转换为字符消息,然后将字符消息发送到调用线程的消息队列,下次线程调用GetMessage函数时即可获取这个字符消息:
BOOL WINAPI TranslateMessage(_In_ const MSG *lpMsg); // 从GetMessage函数获取到的MSG结构
如果消息被转换,即一个字符消息被发送到线程的消息队列,则返回值为非零;如果消息没有被转换,即没有字符消息被发送到线程的消息队列,则返回值为0。
窗口显示以后,程序需要处理来自用户的键盘输入和鼠标输入等。用户按下一个按键时会产生WM_KEYDOWN消息,松开按键的时候会产生WM_KEYUP消息。如果用户按下的是字符按键,TranslateMessage函数可以把WM_KEYDOWN消息转换为WM_CHAR消息,这样一来我们就可以在窗口过程中处理WM_CHAR消息来判断用户是按下了哪个字符按键,即有了TranslateMessage函数的帮助,对于字符按键我们不需要处理WM_KEYDOWN和WM_KEYUP消息,只需处理WM_CHAR消息即可。在窗口过程中添加以下代码:
case WM_CHAR:
{
TCHAR szChar[16] = { 0 };
wsprintf(szChar, TEXT("用户按下了字符:%c"), wParam);
MessageBox(hwnd, szChar, TEXT("提示"), MB_OK);
return 0;
}
编译运行程序,输入法切换为英文状态。每当用户按下一个可显示字符,就会弹出一个消息框。如果按下的是Ctrl、Shift、Alt一类的按键,则不会产生WM_CHAR消息。如果把TranslateMessage(&msg);注释掉,则不会产生WM_CHAR消息。关于键盘与鼠标的输入情况,以后还会详细介绍。
DispatchMessage函数用于把GetMessage函数获取到的消息分发送到窗口过程:
LRESULT WINAPI DispatchMessage(_In_ const MSG *lpmsg); // 从GetMessage函数
// 获取到的MSG结构
函数返回为窗口过程处理完消息以后的返回值,在窗口过程中处理完一条消息以后通常都是返回0。
2.2.5 窗口过程
消息既可以是(消息)队列消息,也可以是非(消息)队列消息。队列消息是指那些由Windows放入程序的消息队列中的消息。在程序的消息循环中,队列消息被获取,并投递到窗口过程中。非队列消息则是由Windows对窗口过程的直接调用而产生的。队列消息被投递(Post)到消息队列中,而非队列消息则是被发送(Send)到窗口过程。无论在哪种情况下,窗口过程都会为程序处理所有消息(无论是队列消息还是非队列消息)。窗口过程是程序的消息处理中心。
-
队列消息主要由用户的输入产生,主要为按键消息(例如WM_KEYDOWN和WM_KEYUP消息)、由按键产生的字符消息(WM_CHAR)、鼠标移动(WM_MOUSEMOVE)、鼠标单击(WM_LBUTT0ND0WN)等。此外,队列消息还包括计时器消息(WM_TIMER)、重绘消息(WM_PAINT)和退出消息(WM_QUIT)等。
-
非队列消息则包括除队列消息以外的其他所有消息,通常由调用特定的Windows函数引起。例如,当WinMain调用CreateWindowEx 函数时,Windows就会创建窗口,并在创建过程中向窗口过程发送一条WM_CREATE消息;当WinMain调用ShowWindow函数时,Windows又会将WM_SIZE消息和WM_SHOWWINDOW消息发送给窗口过程;接下来,WinMain又对UpdateWindow函数进行调用,这便促使Windows向窗口过程发送一条WM_PAINT消息。另外,一些表示键盘或鼠标输入的队列消息也能够产生非队列消息。例如,当用键盘或鼠标选择某个菜单项时,键盘或鼠标消息会进入消息队列,而最终表明有某菜单项被选择的WM_COMMAND消息却是一个非队列消息。 对于各种类型的消息,大家先大致有一个印象即可,后面都会详细介绍。
窗口过程是Windows回调函数(Windows进行调用),它是通过注册窗口类时使用WNDCLASSEX结构的lpfnWndProc字段指定的。窗口过程的定义形式如下:
LRESULT CALLBACK WindowProc(
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam);
窗口过程的名称可以任意命名,只要不与其他函数名称冲突即可。WNDPROC是窗口过程指针类型:
typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);
一个Windows程序可以包含多个窗口过程,但是一个窗口过程总是与一个通过调用RegisterClassEx函数注册的窗口类相关联。窗口过程的4个参数与MSG结构的前4个字段是一一对应的。
窗口过程返回值类型为LRESULT,LRESULT定义如下:
typedef LONG_PTR LRESULT;
#if defined(_WIN64)
typedef __int64 LONG_PTR, * PLONG_PTR;
typedef unsigned __int64 ULONG_PTR, * PULONG_PTR;
#define __int3264 __int64
#else
typedef _W64 long LONG_PTR, * PLONG_PTR;
typedef _W64 unsigned long ULONG_PTR, * PULONG_PTR;
#define __int3264 __int32
#endif
如果编译为32位程序,LONG_PTR就是long类型;如果编译为64位程序,LONG_PTR就是__int64类型。通过使用*_PTR类型,一个程序既可以编译为32位程序,也可以编译为64位程序。类似的还有DWORD_PTR、INT_PTR、LONG_PTR、UINT_PTR、ULONG_PTR等。其他的例如指针、句柄类型等。编译为32位就是32位指针、句柄,编译为64位就是64位指针、句柄。VS 2019非常智能,如果在编写64位程序的时候使用了不合理的数据类型,则编译的时候会给出警告提示。
-
第1个参数hwnd表示接收消息的窗口的句柄。如果程序基于同一个窗口类创建了多个窗口 (这些窗口的窗口过程相同),hwnd参数将标识这个消息属于哪一个窗口。
-
第2个参数uMsg表示具体的消息类型,例如WM_CREATE、WM_PAINT等。
-
在Win32程序中,最后两个参数wParam和lParam都是32位的消息参数,用于提供关于该消息的更丰富的信息。wParam和lParam参数中所包含的内容依赖于具体的消息类型,例如对于字符消息WM_CHAR,wParam和lParam参数包含按下了哪个字符以及按下次数等信息。对于鼠标移动消息WM_MOUSEMOVE,wParam和lParam参数包含鼠标光标坐标等信息。
1.WM_CREATE消息
当WinMain调用CreateWindowEx 函数时,Windows就会创建窗口,并在创建过程中向窗口过程发送一条WM_CREATE消息。WM_CREATE消息是窗口过程较早收到的消息之一,程序通常会在这里做一些初始化的工作。本程序为了增加趣味性,在WM_CREATE消息中调用PlaySound函数播放一首音乐:
BOOL PlaySound(
LPCSTR pszSound, // 要播放的声音文件名称
HMODULE hmod, // 如果是从资源文件中加载音乐,该参数指定可执行文件模块句柄,否则可为NULL
DWORD fdwSound); // 播放声音的方式
fdwSound参数如果指定了SND_FILENAME,则表示pszSound参数指定的是声音文件名;如果指定了SND_ASYNC,则表示以异步方式播放声音,PlaySound函数在开始播放后立即返回;如果指定了SND_SYNC,则表示以同步方式播放声音,在播放完音乐以后PlaySound函数才返回。
WM_CREATE消息是窗口过程较早收到的消息之一,应该先播放音乐,再执行WM_PAINT消息显示字符串,但是为什么在2.2.3节调试运行的时候顺序却恰好相反呢?这是因为PlaySound函数的fdwSound参数指定了SND_ASYNC,表示以异步方式播放声音,更改为:
PlaySound(TEXT("成都(两会版).wav"), NULL, SND_FILENAME | SND_SYNC);
然后按F9键设置几个断点。
按F5键调试运行,程序首先中断在CreateWindowEx函数调用;继续按F5键运行,程序中断在PlaySound函数调用;按F10键单步执行,优雅的音乐响起来,在声音播放完以前,程序是不会继续往下执行的,让我们花几分钟时间休息一下吧;声音播放完毕,程序中断在下一行;继续按F5键运行,程序中断在ShowWindow函数调用;按F5键继续运行,程序界面出现。
程序处理完WM_CREATE消息以后,应该返回0,表示继续创建窗口;如果返回−1,则窗口将被销毁,程序退出。实际上,大部分消息在处理完以后返回0,有的消息在处理完以后可能需要返回其他值,例如TRUE,以后遇到需要返回其他值的消息,我会特别说明。
2.窗口关闭过程
用户单击程序窗口右上角的关闭按钮以后,窗口过程会收到WM_CLOSE消息,DefWindowProc函数会对WM_CLOSE消息进行处理,即调用DestroyWindow函数;DestroyWindow函数完成程序窗口的一些清理工作,然后向窗口过程发送WM_DESTROY消息,DefWindowProc函数不会处理WM_DESTROY消息,因此这个消息需要我们自己处理;我们在WM_DESTROY消息中调用PostQuitMessage函数,PostQuitMessage函数会发送WM_QUIT消息给程序的消息队列;GetMessage函数获取WM_QUIT消息后返回0,从而结束消息循环,程序退出。
DefWindowProc函数不会处理WM_DESTROY消息。删除程序的case WM_DESTROY逻辑,重新编译运行,然后关闭程序,读者会发现音乐还在继续,打开任务管理器可以看到HelloWindows.exe进程依然存在,也就是说窗口已经销毁,但是进程并没有结束,消息循环还在继续。
DefWindowProc函数会对WM_CLOSE消息进行处理,我们也可以自己处理WM_CLOSE消息,例如可以添加如下代码:
case WM_CLOSE:
{
int nClose = MessageBox(hwnd, TEXT("你真的要关闭程序吗?"), TEXT("程序关闭"), MB_YESNO);
// 只有用户单击的是消息框的"确定"按钮时才调用DestroyWindow函数
if (nClose == IDYES)
DestroyWindow(hwnd);
return 0;
}
PostQuitMessage函数通常用于处理WM_DESTROY消息,函数原型如下:
VOID WINAPI PostQuitMessage(_In_ int nExitCode); // 应用程序退出代码
nExitCode参数指定退出代码,这个参数会用作WM_QUIT消息的wParam参数。
PostQuitMessage函数将WM_QUIT消息发送到程序的消息队列并立即返回。当GetMessage函数从消息队列中获取到WM_QUIT消息时,程序退出消息循环并调用return语句将控制权返回给系统。return语句所用返回值是WM_QUIT消息的wparam参数,所以WinMain函数最后调用的是return msg.wParam;。
3.其他消息处理
Windows是基于消息驱动的系统,在Windows中发生的一切都可以用消息来表示,消息告诉操作系统发生了什么事件。消息类型数目巨大,一个程序在运行过程中会发生数不尽的消息,实际上在窗口过程WindowProc中我们仅仅是处理了极少量感兴趣的消息而已。
DefWindowProc函数用于调用Windows提供的默认窗口过程,默认窗口过程为应用程序不处理的消息提供默认处理。调用DefWindowProc函数可以确保程序的每个消息都得到处理,比如按住标题栏拖动窗口、单击最小化/最大化按钮、改变窗口大小等,都是默认窗口过程进行处理的。调用DefWindowProc函数时使用窗口过程接收到的相同参数即可:
LRESULT WINAPI DefWindowProc(
_In_ HWND hWnd,
_In_ UINT Msg,
_In_ WPARAM wParam,
_In_ LPARAM lParam);
函数返回值是默认窗口过程DefWindowProc对于消息处理的结果,取决于具体的消息。我们只需要简单地返回DefWindowProc函数的返回值即可。
大家可以删除return DefWindowProc(hwnd,uMsg,wParam,lParam);,不调用Windows提供的默认窗口过程看看什么现象。
4.WM_PAINT重绘消息
我们知道,WinMain调用UpdateWindow函数,会促使Windows向窗口过程发送WM_PAINT消息。
WM_PAINT是Windows编程中非常重要的一条消息,当窗口客户区的部分或全部变为"无效"且必须"更新"时,窗口过程会收到这个消息,这时候窗口客户区必须重绘。什么时候窗口客户区会变为无效呢?
-
当程序窗口被首次创建时,整个窗口客户区都是无效的,因为此时应用程序尚未在窗口客户区上绘制任何东西。在WinMain中调用UpdateWindow函数时会发送第一条WM_PAINT消息,指示窗口过程在窗口客户区中进行绘制。
-
在调整程序窗口的大小时,窗口客户区也会变为无效。我们把WNDCLASSEX结构的style字段设置为CS_HREDRAW | CS_VREDRAW,表示当窗口大小发生变化时,整个窗口都应宣布无效,窗口过程会收到一条WM_PAINT消息。
-
如果先最小化程序窗口,再将窗口恢复到原先的尺寸,Windows并不会保存原先窗口客户区的内容,因为假设由系统负责保存窗口客户区的内容,但是程序一直在运行,在系统恢复窗口客户区的内容以前它可能已经发生了变化,在图形用户界面下需要保存的这种数据太多了。对此,Windows采取的策略是宣布窗口无效,窗口过程接收到WM_PAINT消息后需要自行恢复窗口客户区的内容。
-
在屏幕中拖动程序窗口的全部或一部分到屏幕以外,然后再拖回屏幕中的时候,窗口被标记为无效,窗口过程会收到一条WM_PAINT消息,并对窗口的客户区进行重绘。
收到WM_PAINT消息以后,对于该消息的处理通常是调用BeginPaint函数来获取窗口客户区的显示设备的设备环境(Device Context,简称DC,也称设备上下文)句柄,然后调用GDI绘图函数(例如TextOut)来执行更新窗口客户区所需的绘图操作。完成绘图操作以后,应该调用EndPaint函数来释放显示设备DC。在绘制之前,必须首先获取显示设备DC句柄,显示设备DC定义一组图形对象及其关联属性,以及影响输出的图形模式等,系统提供与程序窗口相关联的显示设备DC,应用程序使用显示设备DC将其输出定向到指定的窗口。随着以后学习的深入,读者会慢慢理解设备环境(DC)的概念,调用GDI绘图函数来显示文本和图形时通常都需要使用DC句柄。
Beginpaint函数会将窗口的更新区域设置为空,就是说使"无效区域"变为有效。如果应用程序处理WM_PAINT消息但不调用Beginpaint或以其他方式清除无效区域,只要无效区域不为空,应用程序将会继续接收到WM_PAINT消息。
如果一个窗口过程不对WM_PAINT消息进行处理,那么应该交给DefWindowProc函数执行默认处理。DefWindowProc函数依次调用BeginPaint和EndPaint函数,以使客户区的无效区域变为有效。
BeginPaint函数为指定窗口进行绘图工作的准备,并把和绘图有关的信息填充到一个PAINTSTRUCT结构中:
HDC BeginPaint(
_In_ HWND hwnd, // 要重绘的窗口句柄
_Out_ LPPAINTSTRUCT lpPaint); // 指向接收绘制信息的结构PAINTSTRUCT的指针
应用程序除了响应WM_PAINT消息以外,不应该调用BeginPaint函数。如果函数执行成功,则返回指定窗口的显示设备DC句柄;如果函数执行失败,则返回NULL。
lpPaint参数是一个指向PAINTSTRUCT结构的指针,该结构包含绘制窗口客户区所需的信息:
typedef struct tagPAINTSTRUCT {
HDC hdc; // 设备环境句柄,和BeginPaint函数的返回值相同
BOOL fErase; // 是否已擦除背景
RECT rcPaint; // 指定请求重绘的矩形的左上角和右下角
BOOL fRestore; // 系统保留字段
BOOL fIncUpdate; // 系统保留字段
BYTE rgbReserved[32]; // 系统保留字段
} PAINTSTRUCT, *PPAINTSTRUCT;
窗口背景是绘图操作开始前窗口客户区填充的颜色或图案。如果在注册窗口类的WNDCLASSEX结构中的hbrBackground字段中指定了画刷,则在BeginPaint函数调用之前系统会发送WM_ERASEBKGND消息到窗口过程,默认窗口过程DefWindowProc会处理这个消息,用hbrBackground字段指定的画刷擦除背景,在我们的程序中所使用的画刷是一个备用的白色画刷,即Windows会将窗口的背景填充为白色;如果hbrBackground字段为空,那么窗口过程应该处理WM_ERASEBKGND消息擦除背景,处理完WM_ERASEBKGND消息以后应返回TRUE。如果默认窗口过程或我们的窗口过程已经处理了WM_ERASEBKGND消息,那么fErase字段会被设置为0。
擦除背景是为了防止应用程序的新输出与不相关的旧信息混合,读者可以不设置WNDCLASSEX结构的背景画刷并看一下现象,把背景画刷改为wndclass.hbrBackground = NULL;,然后重新编译运行,程序窗口尺寸为300 180,此时最大化窗口就会发现窗口客户区出现了混乱,因为没有擦除背景。
窗口过程收到WM_PAINT消息,并不代表整个窗口客户区都需要被重绘,可能需要重绘的区域只有一小块,这个区域就叫作"无效区域",程序只需要更新该区域即可。Windows为每个窗口维护一个绘图信息结构PAINTSTRUCT,无效区域的坐标就在其中,用的正是rcPaint字段。使用由BeginPaint函数返回的DC句柄是无法在无效区域以外的区域进行绘制的。rcPaint字段是一个RECT结构,在windef.h头文件中定义如下:
typedef struct tagRECT
{
LONG left; // 矩形左上角的X坐标
LONG top; // 矩形左上角的Y坐标
LONG right; // 矩形右下角的X坐标
LONG bottom; // 矩形右下角的Y坐标
} RECT, *PRECT, NEAR *NPRECT, FAR *LPRECT;
rcPaint字段定义了无效区域的边界,这4个字段的值是以像素为单位的,相对于客户区的左上角,客户区左上角为(0, 0)。rcPaint字段表示的无效区域就是程序需要进行重新绘制的区域。
每次调用BeginPaint函数完成相关绘制操作以后,必须调用EndPaint函数释放相关资源:
BOOL EndPaint(
_In_ HWND hWnd, // 重绘的窗口句柄
_In_ const PAINTSTRUCT *lpPaint); // 指向包含BeginPaint获取到的绘制信息结构的指针
函数返回值始终为非零,即不会失败。
TextOut函数用于在指定位置显示一个字符串,函数原型如下:
BOOL TextOut(
_In_ HDC hdc, // 设备环境句柄
_In_ int nXStart, // 字符串的开始位置X坐标,逻辑单位
_In_ int nYStart, // 字符串的开始位置Y坐标,逻辑单位
_In_ LPCTSTR lpString, // 指向要绘制的字符串,因为有cchString参数指定长度,
// 所以不要求以零结尾
_In_ int cchString);// lpString指向的字符串长度,以字符为单位
逻辑单位的概念后面再讲,可以暂时理解为逻辑单位就是像素单位。
2.3 Windows数据类型
Windows定义了许多数据类型,大部分是对C/C++基本数据类型的重定义。先回忆一下C/C++的基本数据类型,如表2.9所示。
表2.9
| 类型名称 | 字节数 | 别名 | 范围 |
|---|---|---|---|
| int | 4 | signed | −2 147 483 648~2 147 483 647 |
| unsigned int | 4 | unsigned | 0~4 294 967 295 |
| __int8 | 1 | char | −128~127 |
| unsigned __int8 | 1 | unsigned char | 0~255 |
| __int16 | 2 | short, short int, signed short int | −32 768~32 767 |
| unsigned __int16 | 2 | unsigned short, unsigned short int | 0~65 535 |
| __int32 | 4 | signed, signed int, int | −2 147 483 648~2 147 483 647 |
| unsigned __int32 | 4 | unsigned, unsigned int | 0~4 294 967 295 |
| __int64 | 8 | long long, signed long long | −9 223 372 036 854 775 808~9 223 372 036 854 775 807 |
| unsigned __int64 | 8 | unsigned long long | 0~18 446 744 073 709 551 615 |
| bool | 1 | 无 | false或true |
| char | 1 | 无 | −128~127(如果指定了/J编译开关,则是0~255) |
| signed char | 1 | 无 | −128~127 |
| unsigned char | 1 | 无 | 0~255 |
| short | 2 | short int, signed short int | −32 768~32 767 |
| unsigned short | 2 | unsigned short int | 0~65 535 |
| long | 4 | long int, signed long int | −2 147 483 648~2 147 483 647 |
| unsigned long | 4 | unsigned long int | 0~4 294 967 295 |
| long long | 8 | 无(等价于__int64) | −9 223 372 036 854 775 808~9 223 372 036 854 775 807 |
| unsigned long long | 8 | 无(等价于unsigned __int64) | 0~18 446 744 073 709 551 615 |
| enum | 变化 | 无 | |
| float | 4 | 无 | 3.4E +/−38(7位有效数字) |
| double | 8 | 无 | 1.7E +/−308(15位有效数字) |
| long double | 8 | 无 | 1.7E +/−308(15位有效数字) |
| wchar_t | 2 | __wchar_t | 0~65 535 |
Windows的数据类型特别多,读者先有所了解,以后遇到新的数据类型,我还会再解释。表2.10仅列出一些常用的数据类型。
表2.10
| 数据类型 | 描述 |
|---|---|
| BOOL | TRUE or FALSE,在WinDef.h定义如下:typedef int BOOL; |
| BYTE | 1字节(8位),在WinDef.h定义如下:typedef unsigned char BYTE; |
| CHAR | 一个8位ASCII字符,在WinNT.h定义如下:typedef char CHAR; |
| COLORREF | 一个32位的RGB颜色值,在WinDef.h定义如下:typedef DWORD COLORREF; |
| CONST | 在WinDef.h定义如下:#define CONST const |
| DWORD或DWORD32 | 一个32位的无符号int,在IntSafe.h定义如下:typedef unsigned long DWORD; |
| DWORDLONG或DWORD64 | 一个64位的无符号int,在IntSafe.h定义如下:typedef unsigned __int64 DWORDLONG; |
| DWORD_PTR | 在BaseTsd.h定义如下:typedef ULONG_PTR DWORD_PTR; |
| FLOAT | 在WinDef.h定义如下:typedef float FLOAT; |
| HANDLE | 对象句柄,在WinNT.h定义如下:typedef PVOID HANDLE; |
| HACCEL | 加速键句柄,在WinDef.h定义如下:typedef HANDLE HACCEL; |
| HBITMAP | 位图句柄,在WinDef.h定义如下:typedef HANDLE HBITMAP; |
| HBRUSH | 画刷句柄,在WinDef.h定义如下:typedef HANDLE HBRUSH; |
| HCURSOR | 光标句柄,在WinDef.h定义如下:typedef HICON HCURSOR; |
| HDC | DC(设备环境)句柄,在WinDef.h定义如下:typedef HANDLE HDC; |
| HFILE | OpenFile函数返回的文件句柄,在WinDef.h定义如下:typedef int HFILE; |
| HFONT | 字体句柄,在WinDef.h定义如下:typedef HANDLE HFONT; |
| HGDIOBJ | GDI对象句柄,在WinDef.h定义如下:typedef HANDLE HGDIOBJ; |
| HGLOBAL | 全局内存块句柄,在WinDef.h定义如下:typedef HANDLE HGLOBAL; |
| HLOCAL | 局部内存块句柄,在WinDef.h定义如下:typedef HANDLE HLOCAL; |
| HHOOK | 钩子句柄,在WinDef.h定义如下:typedef HANDLE HHOOK; |
| HICON | 图标句柄,在WinDef.h定义如下:typedef HANDLE HICON; |
| HINSTANCE或HMODULE | 实例或模块句柄,在WinDef.h定义如下:typedef HANDLE HINSTANCE; |
| HKEY | 注册表项句柄,在WinDef.h定义如下:typedef HANDLE HKEY; |
| HMENU | 菜单句柄,在WinDef.h定义如下:typedef HANDLE HMENU; |
| HMETAFILE | 图元文件句柄,在WinDef.h定义如下:typedef HANDLE HMETAFILE; |
| HPEN | 画笔句柄,在WinDef.h定义如下:typedef HANDLE HPEN; |
| HRESULT | COM接口使用的返回代码,在WinNT.h定义如下:typedef LONG HRESULT; |
| HRGN | 区域句柄,在WinDef.h定义如下:typedef HANDLE HRGN; |
| HWND | 窗口句柄,在WinDef.h定义如下:typedef HANDLE HWND; |
| INT | 在WinDef.h定义如下:typedef int INT; |
| INT_PTR | 在BaseTsd.h定义如下:#if defined(_WIN64) typedef __int64 INT_PTR; #else typedef int INT_PTR; #endif |
| INT8 | 在BaseTsd.h定义如下:typedef signed char INT8; |
| INT16 | 在BaseTsd.h定义如下:typedef signed short INT16; |
| INT32 | 在BaseTsd.h定义如下:typedef signed int INT32; |
| INT64 | 在BaseTsd.h定义如下:typedef signed __int64 INT64; |
| LCID | 区域ID,在WinNT.h定义如下:typedef DWORD LCID; |
| LONG | 在WinNT.h定义如下:typedef long LONG; |
| LONGLONG | 在WinNT.h定义如下:#if !defined(_M_IX86) // _M_IX86指32位处理器 typedef __int64 LONGLONG; #else typedef double LONGLONG; #endif |
| LONG32 | 在BaseTsd.h定义如下:typedef signed int LONG32, *PLONG32; |
| LONG64 | 在BaseTsd.h定义如下:typedef __int64 LONG64, *PLONG64; |
| LONG_PTR | 在BaseTsd.h定义如下: #if defined(_WIN64) typedef __int64 LONG_PTR; #else typedef long LONG_PTR; #endif |
| LPARAM | 消息参数,在WinDef.h定义如下:typedef LONG_PTR LPARAM; |
| LPBOOL | BOOL类型指针,在WinDef.h定义如下:typedef BOOL far *LPBOOL; |
| LPBYTE | BYTE类型指针,在WinDef.h定义如下:typedef BYTE far *LPBYTE; |
| LPCOLORREF | COLORREF类型指针,在WinDef.h定义如下:typedef DWORD *LPCOLORREF; |
| LPCSTR或PCSTR | ANSI常字符串指针,在WinNT.h定义如下:typedef __nullterminated CONST CHAR *LPCSTR; |
| LPCWSTR或PCWSTR | Unicode常字符串指针,在WinNT.h定义如下:typedef CONST WCHAR *LPCWSTR; |
| LPCTSTR或PCTSTR | 在WinNT.h定义如下: #ifdef UNICODE typedef LPCWSTR LPCTSTR; #else typedef LPCSTR LPCTSTR; #endif |
| LPDWORD或PDWORD | 在WinDef.h定义如下:typedef DWORD *LPDWORD; |
| LPHANDLE | 在WinDef.h定义如下:typedef HANDLE *LPHANDLE; |
| LPINT | 在WinDef.h定义如下:typedef int *LPINT; |
| LPLONG | 在WinDef.h定义如下:typedef long *LPLONG; |
| LPSTR或PSTR | 在WinNT.h定义如下:typedef CHAR *LPSTR; |
| LPWSTR或PWSTR | 在WinNT.h定义如下:typedef WCHAR *LPWSTR; |
| LPTSTR或PTSTR | 在WinNT.h定义如下: #ifdef UNICODE typedef LPWSTR LPTSTR; #else typedef LPSTR LPTSTR; #endif |
| LPVOID或PVOID | 在WinDef.h定义如下:typedef void *LPVOID; |
| LRESULT | 消息处理返回值,在WinDef.h定义如下:typedef LONG_PTR LRESULT; |
| PBOOL | 在WinDef.h定义如下:typedef BOOL *PBOOL; |
| PBYTE | 在WinDef.h定义如下:typedef BYTE *PBYTE; |
| PCHAR | 在WinNT.h定义如下:typedef CHAR *PCHAR; |
| PDWORD_PTR | 在BaseTsd.h定义如下:typedef DWORD_PTR *PDWORD_PTR; |
| PDWORD32 | 在BaseTsd.h定义如下:typedef DWORD32 *PDWORD32; |
| PDWORD64 | 在BaseTsd.h定义如下:typedef DWORD64 *PDWORD64; |
| PFLOAT | 在WinDef.h定义如下:typedef FLOAT *PFLOAT; |
| PHANDLE | 在WinNT.h定义如下:typedef HANDLE *PHANDLE; |
| PHKEY | 在WinDef.h定义如下:typedef HKEY *PHKEY; |
| PINT | 在WinDef.h定义如下:typedef int *PINT; |
| PINT_PTR | 在BaseTsd.h定义如下:typedef INT_PTR *PINT_PTR; |
| QWORD | typedef unsigned __int64 QWORD; |
| SHORT | 在WinNT.h定义如下:typedef short SHORT; |
| SIZE_T | 在BaseTsd.h定义如下:typedef ULONG_PTR SIZE_T; |
| SSIZE_T | 在BaseTsd.h定义如下:typedef LONG_PTR SSIZE_T; |
| TCHAR | 在WinNT.h定义如下: #ifdef UNICODE typedef WCHAR TCHAR; #else typedef char TCHAR; #endif |
| UCHAR | 在WinDef.h定义如下:typedef unsigned char UCHAR; |
| UINT | 在WinDef.h定义如下:typedef unsigned int UINT; |
| UINT_PTR | 在BaseTsd.h定义如下: #if defined(_WIN64) typedef unsigned __int64 UINT_PTR; #else typedef unsigned int UINT_PTR; #endif |
| ULONG | 在WinDef.h定义如下:typedef unsigned long ULONG; |
| ULONGLONG | 在WinNT.h定义如下: #if !defined(_M_IX86) typedef unsigned __int64 ULONGLONG; #else typedef double ULONGLONG; #endif |
| ULONG_PTR | 在BaseTsd.h定义如下: #if defined(_WIN64) typedef unsigned __int64 ULONG_PTR; #else typedef unsigned long ULONG_PTR; #endif |
| VOID | 在WinNT.h定义如下:#define VOID void |
| WORD | 在WinDef.h定义如下:typedef unsigned short WORD; |
| WPARAM | 消息参数,在WinDef.h定义如下:typedef UINT_PTR WPARAM; |
2.4 函数名、变量名命名规则
驼峰命名法是指混合使用大小写字母来构成变量和函数的名字。当变量名或函数名是由多个单词组成时,第一个单词以小写字母开始,第二个单词及以后的每个单词的首字母都采用大写字母,例如myFirstName、myLastName,这样的变量名看上去就像骆驼峰一样此起彼伏,故得名。
1.小驼峰法
变量名一般用小驼峰法标识,就是除第一个单词以外,其他单词首字母都大写。例如:
int myStudentNum; // 我的学号
2.大驼峰法
函数名、类名一般用大驼峰法标识,就是每一个单词的首字母都大写。例如:
VOID PrintStudentScore(); // 函数声明,打印学生成绩
匈牙利命名法是一位叫查尔斯·西蒙尼(Charles Simonyi)的匈牙利程序员发明的,后来他在微软工作了几年,于是这种命名法就通过微软的各种产品和文档资料传播开了。该命名法的做法是把变量名按"属性 + 类型 + 对象描述"的顺序组合起来,这样的命名方法可以直观了解变量的属性、类型和用途,属性部分如表2.11所示,类型部分如表2.12所示。
表2.11
| 变量属性 | 表示 |
|---|---|
| 全局变量 | g_ |
| 常量 | c_ |
| 结构体字段、类成员变量 | m_(member) |
| 静态变量 | s_ |
表2.12
| 变量类型 | 表示 |
|---|---|
| 数组 | a或arr |
| 指针 | p或lp |
| 函数 | fn |
| 句柄 | h |
| 短整型 | n |
| 整型 | i或n |
| 长整型 | l |
| 布尔 | b或f(flag) |
| 浮点型(有时也指文件) | f |
| 双精度浮点 | d |
| 字 | w |
| 双字 | dw |
| 字符 | c或ch |
| 字符串 | sz(String Zero,指以零结尾的字符串) |
| 字节 | b或by |
| 实型 | r |
| 无符号 | u |
例如,定义一个全局变量,表示文件名缓冲区:TCHAR g_szFileName256;。
定义一个局部整型变量,表示我的学号:int nMyStudentNum;或int iMyStudentNum;。