Windows编程系列——第三讲:创建窗口(下)
这里介绍的是典型的win32窗口应用程序结构,通常需要以下7个步骤:
- 程序入口点(WinMain函数)
- 注册窗口类(RegisterClass/EX)
- 创建窗口类(CreateWindow/ex)
- 显示主窗口(Show Windows)
- 更新主窗口(UpdateWindows)
- 进入消息循环(GetMessage->TranslateMessage->DispatchMessage->对应的消息处理)
- 程序出口点(WinMain返回)
我们依然使用第一讲创建的Windows桌面应用程序进行说明。其中一些代码我们跳过,因为不(wo)重(bu)要(hui)。
注册窗口类
程序入口点我们上一讲已经讲过,接下来看注册窗口类。任何窗口创建前需要先注册窗口。对应程序中的代码如下:
// 函数: MyRegisterClass()
//
// 目标: 注册窗口类。
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEXW wcex;//窗口类结构体,声明一个变量,其中包含了窗口的各种属性
wcex.cbSize = sizeof(WNDCLASSEX);//结构体的大小
wcex.style = CS_HREDRAW | CS_VREDRAW;//窗口的风格
wcex.lpfnWndProc = WndProc;//指定窗口函数为WndProc
wcex.cbClsExtra = 0;//窗口类占用的额外内存,可以存放窗口类的共有数据,默认为0,不重要
wcex.cbWndExtra = 0;//窗口占用的额外内存,可以选择其中存放的每个窗口所拥有的数据,默认为0,不重要
wcex.hInstance = hInstance;//应用程序实例句柄,已经由WinMain函数传递过来了
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WINDOWSPROJECT1));//图标句柄,这里通过LoadIcon获得系统默认的图标
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);//鼠标的光标句柄,依然使用系统默认的光标
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);//背景画刷句柄,即指窗口背景颜色
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_WINDOWSPROJECT1);//菜单的资源名称
wcex.lpszClassName = szWindowClass;//窗口类的名字,我们创建窗口时是依靠名字来制定窗口类的
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));//小图标的句柄,指当窗口最小化时,任务栏显示的程序图标
return RegisterClassExW(&wcex);//传递给RegisterClassExW以注册窗口
}
上面我们使用了结构体WNDCLASSEXW(或去掉W)。当然也可以使用WNDCLASS结构体,它和WNDCLASSEX的区别仅仅是EX是增强型窗口,比它多两个参数而已。多的参数一个是指定数据结构的cbsize,一个是支持“小图标”的hIconsm。以后我们接触的很多类和函数也常有EX结尾和无EX结尾之分,区别也都是增强型和非增强型而已。
cbSize代表wcex结构体变量的大小,我们就是通过这个参数区别wcex是增强版还是非增强版。
窗口类风格
style是类的风格,CS_HREDRAW | CS_VREDRAW意味着当窗口宽度或高度发生变化时,窗口将根据窗口大小重新绘制。其中前缀CS表示class type,HREDRAW代表当宽度(H代表水平方向Horizontal)发生改变时进行重绘(Redraw),VREDRAW代表着当高度(V代表垂直方向Vertical)方向发生变化时进行重绘。按位或|则意味着两个同时有效。为什么会这样呢?在WinUser.h中定义了全部的可选样式,如下:
#define CS_VREDRAW | 0x0001 |
---|---|
#define CS_HREDRAW | 0x0002 |
#define CS_DBLCLKS | 0x0008 |
#define CS_OWNDC | 0x0020 |
#define CS_CLASSDC | 0x0040 |
#define CS_PARENTDC | 0x0080 |
#define CS_NOCLOSE | 0x0200 |
#define CS_SAVEBITS | 0x0800 |
#define CS_BYTEALIGNCLIENT | 0x1000 |
#define CS_BYTEALIGNWINDOW | 0x2000 |
#define CS_GLOBALCLASS | 0x4000 |
在这里,预定义的符号常量实际上使用了不同的数据位,所以在进行或运算不会发生混淆。
创建窗口类
注册完毕后,调用CreateWindow(或CreateWindowW、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,//窗口的宽度
_In_ int nHeight,//窗口的高度
_In_opt_ HWND hWndParent,//父窗口的句柄
_In_opt_ HMENU hMenu,//菜单句柄
_In_opt_ HINSTANCE hInstance,//应用程序句柄
_In_opt_ LPVOID lpParam//通过WM_CREATE消息参数传递给窗口函数
);
创建窗口对应于我们程序中的这两行(在InitInstance函数里面):
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
其中,CW_USEDEFAULT是指默认窗口尺寸。CreateWindowW的返回值:如果函数成功, 则返回值是新窗口的句柄;如果函数失败, 返回值为NULL。
窗口风格
第三个参数,窗口的风格(或称窗口的类型),供用户指定窗口的绘制及其行为。其中有3种最重要的风格创建了3种最基本的窗口类型:重叠窗口、弹出窗口、子窗口。
- 重叠窗口(Overlapped Window):具有应用程序主窗口的全部特点。它的非客户区包括一个可伸缩的框架、菜单栏、标题栏和最大化、最小化、关闭按钮。
- 弹出窗口(Popup Window):具有消息框或对话框的全部特点。它的非客户区包括一个固定大小的框架和一个标题栏。
- 子窗口(Child Window):具有类似按钮控件的全部特点。没有非客户区,窗口的处理过程负责绘制窗口的每个部分。
如果我们想创建一个在游戏中常用的全屏窗口,应该如下调用CreateWindow:
HWND hWnd=CreateWindow(
szWindowClass,
szTitle,
WS_POPUP,//固定大小,选择弹出窗口
0,0,//窗口的位置在屏幕的左上角
GetSystemMetrics(SM_CXSCREEN),//与屏幕等宽
GetSystemMetrics(SM_CYSCREEN),//与屏幕等高
nullptr,
nullptr,
hInstance,
nullptr
);
可以用上述代码取代原程序中的代码,运行以下,看一下效果。
其中,GetSystemMetrics函数获得了屏幕的宽度和高度。除此之外,该函数还可以获得其他系统属性,包括菜单栏的高度、工具栏的高度、窗口边框的高度等等,不一一描述了。
原程序的窗口风格使用的是WS_OVERLAPPEDWINDOW,我们可以查看一下它的定义(按住ctrl,鼠标悬在它上面时会变成一个小手,就可以点击查看了)。定义如下:
#define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED | \
WS_CAPTION | \
WS_SYSMENU | \
WS_THICKFRAME | \
WS_MINIMIZEBOX | \
WS_MAXIMIZEBOX)
(上述代码中的\表示换行输入)然后我们就知道了,这种风格其实上包含了WS_OVERLAPPED(可重叠)、WS_CAPTION(有标题栏)、WS_SYSMENU (有系统菜单)、WS_THICKFRAME(粗边框)、WS_MINIMIZEBOX(最小化按钮)、WS_MAXIMIZEBOX(最大化按钮)。
如果现在我们想在该风格的基础上把最大化按钮去掉,其他保留,应该怎么做呢?根据与或运算,应该这样写:WS_OVERLAPPEDWINDOW &~ WS_MAXIMZEBOX
(不理解的话可以举几个数运算一下)
显示窗口
ShowWindow(hWnd, nCmdShow);
这句话的功能是把我们创建好的窗口显示出来。
窗口句柄hWnd作为实参传递,告诉系统要显示哪个窗口。nCmdShow则是说明窗口显示的方式,是系统传递给WinMain的参数。nCmdShow可以取不同的值,例如SW_HIDE将隐藏创建的窗口,SW_MINIMIZE将最小化创建窗口,等等。
更新窗口
UpdateWindow(hWnd);
这句话用来更新窗口。
消息循环
当显示完窗口,应用程序已经准备好从用户接收键盘和鼠标的输入了,必须加入消息循环,不断对各种消息进行响应,否则程序会陷入无响应的状态。程序中消息循环的语句是:
MSG msg;
// 主消息循环:
while (GetMessage(&msg, nullptr, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
MSG是一个结构体,其原型如下:
typedef struct tagMSG {
HWND hwnd;//窗口句柄,nullptr意味着当前窗口
UINT message;//消息类型
WPARAM wParam;//消息参数
LPARAM lParam;//消息参数
DWORD time;//获取消息的时间
POINT pt;//获取消息时光标的位置
} MSG, *PMSG, *LPMSG;
我们对它的原型不需要过多了解,只知道利用GetMessage函数可以中从消息队列取出一个消息放在MSG类型中。GetMessage的原型如下:
BOOL WINAPI GetMessage(
_Out_ LPMSG lpMsg,//结构体变量的指针
_In_opt_ HWND hWnd,//窗口句柄,nullptr意味着当前窗口
_In_ UINT wMsgFilterMin,//这两个参数用于消息过滤,通常为0
_In_ UINT wMsgFilterMax
);
如果消息队列中没有任何消息,GetMessage 会一直等下去,直到有消息进入消息队列。如果GetMessage从消息队列中取得的消息是WM_QUIT(退出),则返回0;否则返回非零值。我们利用返回值0来结束消息循环。而消息循环的结束,最终导致WinMain函数的结束,意味着程序结束。
TranslateMessage(&msg)用于把键盘输入转换成字符消息,例如把WM_KEYDOWN和WM_KEYUP转换成WM_CHAR。
DispatchMessage(&msg)用于把消息分发到对应窗口的窗口函数中。
窗口函数
上一讲我们提到了窗口函数,下面详细讲解。我们把函数原型再搬过来:
LRESULT CALLBACK WndProc(
HWND hWnd,//窗口句柄
UINT uMsg,//消息类型
WPARAM wParam,//消息参数
LPARAM lParam //消息参数)
窗口函数接收到的所有消息都被标志为一个符号常量,这就是该函数第二个参数uMsg。这些符号常量在WinUser.h中定义,常用的Windows消息可以参考下面的博客:
https://blog.csdn.net/zhangguofu2/article/details/19236081
下面我们看看WindowProc函数里面都有什么东西:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND://处理WM_COMMAND消息
{
int wmId = LOWORD(wParam);
// 分析菜单选择:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_PAINT://处理WM_PAINT消息
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: 在此处添加使用 hdc 的任何绘图代码...
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY://处理WM_DESTROY消息
PostQuitMessage(0);
break;
default://对于不需要处理的消息,则调用默认的信息处理函数
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
应用程序的退出
当用户关闭窗口时,窗口函数会收到一个WM_DESTROY消息。正如上面的代码所示,窗口函数应该调用PostQuitMessage(0)向消息队列中插入一个WM_QUIT消息。
一定要注意,WM_DESTROY是窗口函数必须要处理的消息,DefWindowProc并没有我们所需要的功能。如果把PostQuitMessage(0);这句话注释掉,运行程序,然后关闭窗口,你会发现窗口已然消失了,似乎一切正常。但你会发现调试菜单表明程序依然在运行;打开任务管理器,也会发现程序在运行。这能说明两个问题:
- DefWindowProc没有实现PostQuitMessage(0)的功能。
- 在Windows中,窗口和进程是有联系的两个对象。窗口关闭了,不能说明进程也关闭了。
其实,从单击窗口右上角的“关闭”按钮,到程序退出,是一个复杂的过程,描述如下:
- 单击关闭按钮,系统向消息队列中插入WM_CLOSE消息
- 窗口函数调用DefWindowsProc处理WM_CLOSE消息:调用DestroyWindow函数
- 窗口关闭,并向消息队列中插入WM_DESTROY消息
- 窗口函数处理WM_DESTROY消息:调用PostQuitMessage函数,向消息队列中插入WM_QUIT消息
- 主函数的消息循环中的GetMessage获取WM_QUIT消息,返回0,导致消息循环结束,进而WinMain函数结束,再进而整个进程结束。
现在思考一下,能不能在结束应用程序之前弹出一个对话框来确认我们的操作呢(有误操作的可能)?我们看到,窗口函数中没有对WM_CLOSE消息进行处理,而是调用DefWindowProc进行默认处理。所以,要想在退出前弹出对话框,就要用自己的代码处理WM_CLOSE消息。修改如下:
switch(msg)
{
case WM_CLOSE:
nsel=MessageBox(hWnd,
L"你真的要退出吗?",
szWindowTitle,
MB_YESNO|MB_ICONQUESTION);
if(nsel==IDYES)DestroyWindow(hwnd);
return 0;
case WM_DESTROY:
POSTQUITMESSAGE(0);
RETURN 0;
default:
return DefWindowProc(hWnd,msg,wParam,lParam);
到此为止,我们将程序中的代码讲解得差不多了。可以进行下一个部分了