(三) 窗口的创建与消息处理
原来那个程序是不是感觉有点简单?这里我们要制作一个复杂一点的Windows程序 - 显示一个空白窗口。
出于稳定性考量,Visual C++编译器中int、long等基本类型基本上没有变化(16->32仅int变化了一次,64位没有变化),但是指针则16位、32位、64位一直在变化,为了更好地适应指针的变化,以及方便地与指针相互转换,Windows中定义了平台相关整数,它们随指针长度而变化:
INT_PTR 和
UINT_PTR - 16/32/64位平台相关整数,前者有符号,后者无符号
LONG_PTR 和
ULONG_PTR - 32/32/64位平台相关整数,前者有符号,后者无符号
这四个类型在以后的编程中要经常用到,务必明白它们的意思。
关于调用约定:调用约定表示调用函数时参数的传递方式。在Windows中绝大多数WinAPI都是
__stdcall,只有wsprintf等参数数量可变的函数是
__cdecl,自己定义的各种Win32函数也最好采用
__stdcall。关于调用约定的内部信息请参阅相关资料,这里不详述。
一、准备一个窗口消息处理函数
Windows中
窗口事件的处理是靠
窗口消息来完成的,为了处理窗口消息,每一个窗口都至少对应一个窗口消息处理函数,窗口消息处理函数的定义如下:
<code class="lang-cpp">LONG_PTR __stdcall WndProc(void* hWnd, unsigned int msg, UINT_PTR wp, LONG_PTR lp)
{
return DefWindowProc(hWnd, msg, wp, lp);
}</code>
程序在处理完消息之后,return 0;直接返回,否则应该调用
DefWindowProc来处理不想处理的消息。
常用的消息如下:
WM_CREATE - 窗口创建时被调用
WM_DESTROY - 窗口销毁时被调用
WM_PAINT - 窗口绘制时被调用
WM_COMMAND - 控件或窗口有消息时被调用
WM_SIZE - 窗口大小改变时被调用
为了在同一个函数中处理不同消息,我们一般使用switch (msg) { case WM_XXX: ... }分支来编写WndProc函数:
<code class="lang-cpp">// 窗口消息处理函数 - WndProc
LONG_PTR __stdcall WndProc(void* hWnd, unsigned int msg, UINT_PTR wp, LONG_PTR lp)
{
switch (msg) // 判断窗口消息的种类
{
case WM_CREATE: // 窗口创建
// ...
return 0;
case WM_DESTROY: // 窗口销毁
// ...
return 0;
// case ...
}
return DefWindowProc(hWnd, msg, wp, lp);
}</code>
在这里,我们只需处理WM_DESTROY即可:
<code class="lang-cpp">// 窗口消息处理函数 - WndProc
LONG_PTR __stdcall WndProc(void* hWnd, unsigned int msg, UINT_PTR wp, LONG_PTR lp)
{
switch (msg) // 判断窗口消息的种类
{
case WM_DESTROY: // 窗口销毁
// ...
return 0;
}
return DefWindowProc(hWnd, msg, wp, lp);
}</code>
二、全局变量
我们需要两个全局变量保存模块和主窗口的句柄:
<code class="lang-cpp">void* hInst; // 模块句柄
void* hMainWnd; // 主窗口句柄</code>
在wWinMain中,第一个参数提供了模块句柄,现在保存到全局变量:
<code class="lang-cpp">// 入口点 - wWinMain
int __stdcall wWinMain(void* hInstance, void* reserved, wchar_t* cmdline, int nshow)
{
hInst = hInstance; // 保存模块句柄
// ... 注册窗口类 建立窗口 显示和更新窗口 消息循环 ...
}</code>
三、注册窗口类
在Windows中,窗口的部分特性需要与窗口类(
WNDCLASS)进行自定义。WNDCLASS是一个C语言结构体,我们需要填写它们的内容。
WNDCLASS {
样式 函数(样式=窗口类样式,常用的有CS_HREDRAW和CS_VREDRAW;函数=窗口处理函数)
类增 窗增(高级参数,表示窗口类和每个窗口附加数据的字节数,一般填0)
模块 图标 光标(模块=模块句柄;图标=左上角图标;光标=常说的鼠标指针)
背景 菜单 类名(背景=背景画刷;菜单=菜单资源标识,字符串或小于65536的整数;类名=窗口类的名称,随便写)
}
在这里如下填写,这些内容一般情况下是比较固定的:
<code class="lang-cpp">// 窗口类设定
// 样式、函数
// 类增、窗增 (高级参数)
// 模块、图标、光标 (光标=鼠标指针)
// 背景、菜单、类名
WNDCLASS wc = {
CS_HREDRAW | CS_VREDRAW, // 样式:横向、纵向改变大小时重绘
WndProc, // 窗口消息处理函数
0, 0,
hInst, // 模块句柄
LoadIcon(NULL, IDI_APPLICATION), // 图标:默认图标
LoadCursor(NULL, IDC_ARROW), // 光标:箭头
GetStockObject(WHITE_BRUSH), // 背景画刷:白色
NULL, // 菜单:无
L"MyWindow", // 窗口类名称
};</code>
填写完这个类之后,我们可以调用
RegisterClass函数注册这个窗口类:
<code class="lang-cpp">if (!RegisterClass(&wc)) // 注册窗口类
return 1;</code>
四、新建窗口
新建窗口需要使用
CreateWindowEx函数,这个函数有12个参数,看似比较多,其实也不难记:
CreateWindowEx(
扩展样式, 窗口类名, 窗口标题, 窗口样式, // 窗口样式常用WS_OVERLAPPEDWINDOW
左, 上, 宽, 高, // 不想填写可以使用CW_USEDEFAULT
父窗句柄, 菜单句柄, 模块句柄, 可选参数
)
不是所有参数都是必要的,部分参数可以填0或NULL。
在这里,我们这样建立一个标准的窗口:
<code class="lang-cpp">// 创建主窗口 - CreateWindowEx
// 扩展样式、窗口类名、窗口标题、窗口样式(样式=覆盖式窗口)
// 左、上、宽、高
// 父窗句柄、菜单句柄、模块句柄、可选参数
hMainWnd = CreateWindowEx(
0, L"MyWindow", L"这是主窗口", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInst, NULL);</code>
建立窗口以后,我们一般需要显示并更新窗口,这一步比较简单:
<code class="lang-cpp">ShowWindow(hMainWnd, nshow); // 显示窗口
UpdateWindow(hMainWnd); // 更新窗口</code>
五、进入消息循环
窗口显示以后,我们的程序
并没有就此停住并继续接收窗口消息,因此我们要在wWinMain中不停地等待消息,并让系统进行处理。
其中
GetMessage等待并获取消息,
TranslateMessage将虚拟键码转换为Unicode字符,
DispatchMessage将消息分发到处理函数
代码如下:
<code class="lang-cpp">MSG msg;
int rslt;
// 消息循环 - 等待和处理各种消息
while (rslt = GetMessage(&msg, NULL, 0, 0)) // 获取消息
{
if (rslt == -1) // 错误则退出
return 1;
TranslateMessage(&msg); // 将虚拟键码转换为Unicode字符
DispatchMessage(&msg); // 分发消息到窗口消息处理函数
}</code>
六、退出消息循环
接收到一般消息时,
GetMessage会返回TRUE,这时消息循环继续运行。当接收到WM_QUIT消息时,GetMessage会返回FALSE,程序将退出。退出代码保存在msg.wParam中:
<code class="lang-cpp">return (int)msg.wParam;</code>
在窗口被销毁时,我们期望消息循环退出,因此要向消息循环发送WM_QUIT消息,方法是调用
PostQuitMessage(n)函数,n为退出码。在WndProc中添加以下代码:
<code class="lang-cpp">// 窗口消息处理函数 - WndProc
LONG_PTR __stdcall WndProc(void* hWnd, unsigned int msg, UINT_PTR wp, LONG_PTR lp)
{
switch (msg) // 判断窗口消息的种类
{
case WM_DESTROY: // 窗口销毁
PostQuitMessage(0); // 发送WM_QUIT,退出消息循环
return 0;
}
return DefWindowProc(hWnd, msg, wp, lp);
}</code>
七、运行效果
八、完整的程序
<code class="lang-cpp">// 不严格区分句柄,均定义为void*
#define NO_STRICT
// 包含WinAPI定义
#include <windows.h>
// 平台相关的整数类型:
// INT_PTR 16 -> 32 -> 64
// UINT_PTR 16 -> 32 -> 64
// LONG_PTR 32 -> 32 -> 64
// ULONG_PTR 32 -> 32 -> 64
void* hInst; // 模块句柄
void* hMainWnd; // 主窗口句柄
// 窗口消息处理函数 - WndProc
LONG_PTR __stdcall WndProc(void* hWnd, unsigned int msg, UINT_PTR wp, LONG_PTR lp)
{
switch (msg) // 判断窗口消息的种类
{
case WM_DESTROY: // 窗口被销毁时
PostQuitMessage(0); // 发送WM_QUIT,退出消息循环
return 0;
}
// 如果消息未经处理,则由DefWindowProc处理
return DefWindowProc(hWnd, msg, wp, lp);
}
// 入口点 - wWinMain
int __stdcall wWinMain(void* hInstance, void* reserved, wchar_t* cmdline, int nshow)
{
hInst = hInstance; // 保存模块句柄
// 窗口类设定
// 样式、函数
// 类增、窗增 (高级参数)
// 模块、图标、光标 (光标=鼠标指针)
// 背景、菜单、类名
WNDCLASS wc = {
CS_HREDRAW | CS_VREDRAW, // 样式:横向、纵向改变大小时重绘
WndProc, // 窗口消息处理函数
0, 0,
hInst, // 模块句柄
LoadIcon(NULL, IDI_APPLICATION), // 图标:默认图标
LoadCursor(NULL, IDC_ARROW), // 光标:箭头
GetStockObject(WHITE_BRUSH), // 背景画刷:白色
NULL, // 菜单:无
L"MyWindow", // 窗口类名称
};
if (!RegisterClass(&wc)) // 注册窗口类
return 1;
// 创建主窗口 - CreateWindowEx
// 扩展样式、窗口类名、窗口标题、窗口样式(样式=覆盖式窗口)
// 左、上、宽、高
// 父窗句柄、菜单句柄、模块句柄、可选参数
hMainWnd = CreateWindowEx(
0, L"MyWindow", L"这是主窗口", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInst, NULL);
ShowWindow(hMainWnd, nshow); // 显示窗口
UpdateWindow(hMainWnd); // 更新窗口
MSG msg;
int rslt;
// 消息循环 - 等待和处理各种消息
while (rslt = GetMessage(&msg, NULL, 0, 0)) // 获取消息
{
if (rslt == -1) // 错误则退出
return 1;
TranslateMessage(&msg); // 将虚拟键码转换为Unicode字符
DispatchMessage(&msg); // 分发消息到窗口消息处理函数
}
return (int)msg.wParam;
}</windows.h></code>