acmilan作者
11年4个月前修改于 11年4个月前IP:四川
783415
2楼
(三) 窗口的创建与消息处理

原来那个程序是不是感觉有点简单?这里我们要制作一个复杂一点的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中窗口事件的处理是靠窗口消息来完成的,为了处理窗口消息,每一个窗口都至少对应一个窗口消息处理函数,窗口消息处理函数的定义如下:
LONG_PTR __stdcall WndProc(void* hWnd, unsigned int msg, UINT_PTR wp, LONG_PTR lp)
{
    return DefWindowProc(hWnd, msg, wp, lp);
}

程序在处理完消息之后,return 0;直接返回,否则应该调用DefWindowProc来处理不想处理的消息。
常用的消息如下:
WM_CREATE - 窗口创建时被调用
WM_DESTROY - 窗口销毁时被调用
WM_PAINT - 窗口绘制时被调用
WM_COMMAND - 控件或窗口有消息时被调用
WM_SIZE - 窗口大小改变时被调用
为了在同一个函数中处理不同消息,我们一般使用switch (msg) { case WM_XXX: ... }分支来编写WndProc函数:
// 窗口消息处理函数 - 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);
}

在这里,我们只需处理WM_DESTROY即可:
// 窗口消息处理函数 - 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);
}

二、全局变量

我们需要两个全局变量保存模块和主窗口的句柄:
void* hInst; // 模块句柄
void* hMainWnd; // 主窗口句柄

在wWinMain中,第一个参数提供了模块句柄,现在保存到全局变量:
// 入口点 - wWinMain
int __stdcall wWinMain(void* hInstance, void* reserved, wchar_t* cmdline, int nshow)
{
    hInst = hInstance; // 保存模块句柄
                       
    // ... 注册窗口类 建立窗口 显示和更新窗口 消息循环 ...
}

三、注册窗口类

在Windows中,窗口的部分特性需要与窗口类(WNDCLASS)进行自定义。WNDCLASS是一个C语言结构体,我们需要填写它们的内容。
WNDCLASS {
    样式 函数(样式=窗口类样式,常用的有CS_HREDRAW和CS_VREDRAW;函数=窗口处理函数)
    类增 窗增(高级参数,表示窗口类和每个窗口附加数据的字节数,一般填0)
    模块 图标 光标(模块=模块句柄;图标=左上角图标;光标=常说的鼠标指针)
    背景 菜单 类名(背景=背景画刷;菜单=菜单资源标识,字符串或小于65536的整数;类名=窗口类的名称,随便写)
}
在这里如下填写,这些内容一般情况下是比较固定的:
// 窗口类设定
// 样式、函数
// 类增、窗增            (高级参数)
// 模块、图标、光标     (光标=鼠标指针)
// 背景、菜单、类名
WNDCLASS wc = {
    CS_HREDRAW | CS_VREDRAW, // 样式:横向、纵向改变大小时重绘
    WndProc, // 窗口消息处理函数
    0, 0,
    hInst, // 模块句柄
    LoadIcon(NULL, IDI_APPLICATION), // 图标:默认图标
    LoadCursor(NULL, IDC_ARROW), // 光标:箭头
    GetStockObject(WHITE_BRUSH), // 背景画刷:白色
    NULL, // 菜单:无
    L"MyWindow", // 窗口类名称
};

填写完这个类之后,我们可以调用RegisterClass函数注册这个窗口类:
if (!RegisterClass(&wc)) // 注册窗口类
    return 1;

四、新建窗口

新建窗口需要使用CreateWindowEx函数,这个函数有12个参数,看似比较多,其实也不难记:
CreateWindowEx(
    扩展样式, 窗口类名, 窗口标题, 窗口样式, // 窗口样式常用WS_OVERLAPPEDWINDOW
    左, 上, 宽, 高, // 不想填写可以使用CW_USEDEFAULT
    父窗句柄, 菜单句柄, 模块句柄, 可选参数
)
不是所有参数都是必要的,部分参数可以填0或NULL。
在这里,我们这样建立一个标准的窗口:
// 创建主窗口 - 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); // 更新窗口

五、进入消息循环

窗口显示以后,我们的程序并没有就此停住并继续接收窗口消息,因此我们要在wWinMain中不停地等待消息,并让系统进行处理。
其中GetMessage等待并获取消息,TranslateMessage将虚拟键码转换为Unicode字符,DispatchMessage将消息分发到处理函数
代码如下:
MSG msg;
int rslt;
                       
// 消息循环 - 等待和处理各种消息
while (rslt = GetMessage(&msg, NULL, 0, 0)) // 获取消息
{
    if (rslt == -1) // 错误则退出
        return 1;
    TranslateMessage(&msg); // 将虚拟键码转换为Unicode字符
    DispatchMessage(&msg); // 分发消息到窗口消息处理函数
}

六、退出消息循环

接收到一般消息时,GetMessage会返回TRUE,这时消息循环继续运行。当接收到WM_QUIT消息时,GetMessage会返回FALSE,程序将退出。退出代码保存在msg.wParam中:
return (int)msg.wParam;

在窗口被销毁时,我们期望消息循环退出,因此要向消息循环发送WM_QUIT消息,方法是调用PostQuitMessage(n)函数,n为退出码。在WndProc中添加以下代码:
// 窗口消息处理函数 - 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);
}

七、运行效果


主窗口0.png

八、完整的程序
// 不严格区分句柄,均定义为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>
评论
文号 / 783415

千古风流
名片发私信
学术分 4
总主题 466 帖总回复 2942 楼拥有证书:进士 学者 笔友
注册于 2009/05/30 21:22:42最后登录 2019/01/31 17:16:38
主体类型:个人
所属领域:无
认证方式:邮箱
IP归属地:0

个人简介

暂未填写
笔记
{{note.content}}
{{n.user.username}}
{{fromNow(n.toc)}} {{n.status === noteStatus.disabled ? "已屏蔽" : ""}} {{n.status === noteStatus.unknown ? "正在审核" : ""}} {{n.status === noteStatus.deleted ? '已删除' : ''}}
  • 编辑
  • 删除
  • {{n.status === 'disabled' ? "解除屏蔽" : "屏蔽" }}
我也是有底线的