跳转至

通用GUI编程技术——Win32 原生编程实战(八)——模态对话框实战指南

上一篇文章我们搞定了一堆标准控件,你的窗口现在应该能响应用户的各种操作了。但说实话,光有一个填满控件的主窗口还不够用。实际做项目的时候,你经常需要弹出一个对话框让用户确认操作、输入一些配置信息,或者显示一个"关于"对话框。今天我们要聊的就是这种最常见的对话框类型——模态对话框。


为什么一定要搞懂模态对话框

说实话,我在刚学 Win32 的时候对对话框是有点抗拒的。一方面是因为前面学窗口创建的时候已经折腾够多了,另一方面是对话框的 API 看起来和普通窗口不太一样,总觉得是一套需要单独记忆的东西。但当你真正开始做项目的时候,会发现对话框其实是你用得最多的 UI 组件之一。

首先,模态对话框最核心的特点就是它会在显示时"阻塞"父窗口。这是什么意思呢?简单来说,当模态对话框弹出后,用户必须先处理完对话框里的操作(点确定或取消),才能回到主窗口继续操作。这个行为模式非常适合需要用户确认或者集中输入某些信息的场景。

比如你想让用户确认是否删除一个重要文件,这时候用模态对话框就非常合适——用户不能假装没看见直接去点别的地方,必须明确做出选择。再比如设置页面,让用户在一个专门的窗口里集中配置各种选项,配置完成后再回到主程序。这些场景都是模态对话框的典型用例。

另一个现实原因是:现代框架像 Qt、WPF、WinUI 3 把对话框封装得太好了,你可能从来不需要关心 DialogBox 和 EndDialog 这些 API。但一旦你需要做一些底层优化,或者遇到了框架无法解决的 bug,或者只是想用纯 Win32 写一个小工具,这些知识就会成为你的救命稻草。

而且,理解了 Win32 的对话框机制之后,你会发现它实际上并不复杂。对话框本质上就是一种特殊的窗口,系统帮你做了很多默认处理。你只需要知道怎么创建它、怎么处理它的消息、怎么正确地关闭它,就能应付绝大多数场景。

这篇文章会带着你从零开始,把模态对话框彻底搞透。我们不只是学会怎么用,更重要的是理解"为什么要这么用"。


环境说明

在我们正式开始之前,先明确一下我们这次动手的环境:

  • 平台:Windows 10/11(理论上 Windows Vista+ 都行,但谁还用那些老古董)
  • 开发工具:Visual Studio 2019 或更高版本
  • 编程语言:C++(C++17 或更新)
  • 项目类型:桌面应用程序(Win32 项目)

代码假设你已经熟悉前面文章的内容——至少知道怎么创建一个基本的窗口、消息循环是怎么跑的、WM_COMMAND 消息怎么处理。如果这些概念对你来说还比较陌生,建议先去看看前面的笔记,不然接下来的内容可能会让你有点头晕。


第一步——理解对话框的本质

在 Win32 的世界里,对话框不是什么神秘的魔法,它们本质上就是窗口。准确地说,对话框是一种由系统管理的窗口,系统已经帮你写好了很多默认处理逻辑。比如 Tab 键在控件之间切换焦点、Enter 键触发默认按钮、Esc 键触发取消按钮,这些东西都是对话框管理器自动处理的,你不需要自己写代码。

但你可能会问:既然对话框就是窗口,为什么不直接用 CreateWindow 创建呢?这就是对话框和普通窗口的第一个区别——创建方式。对话框不是用 CreateWindow 创建的,而是用专门的 API 函数创建的,比如我们今天要讲的 DialogBox 和 DialogBoxParam。

这两个函数的区别其实不大,DialogBoxParam 只是多了一个参数可以让你在创建对话框时传递一些自定义数据。如果你需要给对话框传递初始化参数,比如要显示的文件名、要编辑的内容等等,用 DialogBoxParam 会更方便。如果不需要传任何数据,用 DialogBox 就够了。

另一个重要的区别是消息处理。对话框用的是"对话框过程"而不是"窗口过程",这两个函数的签名和返回值都不一样。窗口过程返回 LRESULT,而对话框过程返回 BOOL(TRUE 或 FALSE)。这个区别很容易踩坑,我们后面会详细讲。

⚠️ 注意

千万别把窗口过程和对话框过程混用,它们的返回值语义完全不同。如果你在对话框过程里返回 DefWindowProc 的结果,或者忘记返回 TRUE/FALSE,你的对话框大概率不会按预期工作。


第二步——最简单的模态对话框

我们先从最简单的例子开始,创建一个只显示一行文字的"关于"对话框。这个例子足够简单,让你能专注理解对话框的基本流程,而不是被各种控件的消息处理分散注意力。

用资源定义对话框

在现代的 Win32 开发中,对话框通常是在资源文件(.rc 文件)中定义的,而不是在代码里用一堆 CreateWindow 调用来硬编码。这种方式有很多好处:UI 布局和业务逻辑分离、更容易维护、可以用 Visual Studio 的资源编辑器可视化设计等等。

假设我们有一个简单的资源文件:

#include <windows.h>
#include "resource.h"

IDD_ABOUTBOX DIALOGEX 0, 0, 200, 80
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "关于本程序"
FONT 9, "MS Shell Dlg", 400, 0, 0x1
BEGIN
    LTEXT           "这是一个用纯 Win32 编写的示例程序",IDC_STATIC,10,20,180,20
    DEFPUSHBUTTON   "确定",IDOK,143,59,50,14
END

这个资源定义描述了一个 200×80 像素的对话框,包含一段静态文本和一个确定按钮。其中 IDD_ABOUTBOX 是这个对话框资源的 ID,我们在代码里需要用这个 ID 来引用这个对话框。resource.h 文件里会定义这些 ID 常量:

#define IDD_ABOUTBOX                100
#define IDC_STATIC                  -1

IDC_STATIC 是一个特殊值,表示这个控件不需要处理消息,只是用来显示内容。

创建和显示对话框

有了资源定义之后,在代码里创建对话框就非常简单了:

// 在菜单或者按钮的点击处理里
case IDM_HELP_ABOUT:
{
    DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hwnd, AboutDlgProc);
    break;
}

DialogBox 函数的第一个参数是程序实例句柄,第二个参数是对话框资源的 ID,第三个参数是父窗口句柄,第四个参数是对话框过程函数的指针。

这里有个值得注意的点:DialogBox 是一个"阻塞"函数。这意味着当你调用这个函数后,代码会停在这里,直到对话框被关闭才会继续执行后面的代码。这就是"模态"的由来——在对话框显示期间,父窗口的消息循环实际上被暂停了,系统运行的是对话框自己的内部消息循环。

对话框过程

对话框过程和窗口过程类似,也是用来处理消息的,但它的函数签名不太一样:

INT_PTR CALLBACK AboutDlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    UNREFERENCED_PARAMETER(lParam);
    switch (message)
    {
    case WM_INITDIALOG:
        return TRUE;

    case WM_COMMAND:
        if (LOWORD(wParam) == IDOK)
        {
            EndDialog(hDlg, IDOK);
            return TRUE;
        }
        break;
    }
    return FALSE;
}

这个函数的返回类型是 INT_PTR,而不是 LRESULTINT_PTR 本质上就是一个能容纳指针大小的整数类型,在 32 位系统上是 32 位,在 64 位系统上是 64 位。但这里的关键是返回值的意义:TRUE 表示你已经处理了这个消息,FALSE 表示你没有处理。

WM_INITDIALOG 是对话框收到的第一个消息,它在对话框显示之前发送,作用类似于普通窗口的 WM_CREATE。你可以在这里做初始化操作,比如设置控件的初始状态、从 lParam 参数获取传递过来的数据等等。

WM_INITDIALOG 的返回值有个特殊的约定:如果你返回 TRUE,系统会自动把键盘焦点设置到对话框中第一个具有 WS_TABSTOP 样式的控件上。如果你返回 FALSE,系统不会设置焦点,你需要自己用 SetFocus 来指定焦点控件。

大多数情况下,你只需要返回 TRUE 就行了,让系统自动处理焦点。但如果你确实需要在初始化时把焦点设置到某个特定的控件,可以这样做:

case WM_INITDIALOG:
{
    // 手动设置焦点到某个控件
    HWND hEdit = GetDlgItem(hDlg, IDC_EDIT_INPUT);
    SetFocus(hEdit);
    return FALSE;  // 注意这里返回 FALSE
}

EndDialog 函数用于关闭对话框。这里的"关闭"不是说立即销毁窗口,而是告诉系统"这个对话框可以结束了"。系统会做一些清理工作,然后 DialogBox 函数才会返回。EndDialog 的第二个参数会作为 DialogBox 的返回值,这样调用者就能知道用户是怎么关闭对话框的。


第三步——从对话框获取数据

上面的"关于"对话框太简单了,它只是显示一些信息,不需要从用户那里获取任何输入。但在实际使用中,对话框的主要目的是让用户输入一些数据,然后把这些数据返回给主程序。我们来看一个更实用的例子:一个让用户输入姓名和年龄的对话框。

资源定义

首先定义对话框资源:

IDD_INPUTDLG DIALOGEX 0, 0, 220, 100
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "输入信息"
FONT 9, "MS Shell Dlg", 400, 0, 0x1
BEGIN
    LTEXT           "姓名:",IDC_STATIC,10,15,40,10
    EDITTEXT        IDC_EDIT_NAME,55,13,150,14,ES_AUTOHSCROLL
    LTEXT           "年龄:",IDC_STATIC,10,40,40,10
    EDITTEXT        IDC_EDIT_AGE,55,38,150,14,ES_AUTOHSCROLL | ES_NUMBER
    DEFPUSHBUTTON   "确定",IDOK,110,75,50,14
    PUSHBUTTON      "取消",IDCANCEL,165,75,50,14
END

这里我们用了 EDITTEXT 来定义编辑框控件,IDC_EDIT_NAMEIDC_EDIT_AGE 是我们需要在代码里引用这两个编辑框的 ID。ES_NUMBER 样式让年龄编辑框只能输入数字。

对话框过程

struct InputData
{
    wchar_t name[64];
    int age;
};

INT_PTR CALLBACK InputDlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    InputData* pData = nullptr;

    switch (message)
    {
    case WM_INITDIALOG:
    {
        // lParam 传递过来的数据指针保存在窗口的长指针里
        SetWindowLongPtr(hDlg, DWLP_USER, lParam);

        // 如果有初始数据,设置到编辑框里
        pData = (InputData*)lParam;
        if (pData != nullptr)
        {
            SetDlgItemText(hDlg, IDC_EDIT_NAME, pData->name);
            if (pData->age > 0)
            {
                wchar_t ageStr[16];
                _itow_s(pData->age, ageStr, 10);
                SetDlgItemText(hDlg, IDC_EDIT_AGE, ageStr);
            }
        }
        return TRUE;
    }

    case WM_COMMAND:
    {
        if (LOWORD(wParam) == IDOK)
        {
            // 获取保存的数据指针
            pData = (InputData*)GetWindowLongPtr(hDlg, DWLP_USER);
            if (pData != nullptr)
            {
                // 从编辑框获取数据
                GetDlgItemText(hDlg, IDC_EDIT_NAME, pData->name, 64);

                wchar_t ageStr[16];
                GetDlgItemText(hDlg, IDC_EDIT_AGE, ageStr, 16);
                pData->age = _wtoi(ageStr);
            }
            EndDialog(hDlg, IDOK);
            return TRUE;
        }

        if (LOWORD(wParam) == IDCANCEL)
        {
            EndDialog(hDlg, IDCANCEL);
            return TRUE;
        }
        break;
    }
    }
    return FALSE;
}

这里有个值得讲一下的技巧:我们用 SetWindowLongPtrlParam 传递过来的数据指针保存在对话框的 DWLP_USER 位置。这样在后续的消息处理里,我们就能通过 GetWindowLongPtr 把这个指针取出来,而不需要用全局变量或者其他蹩脚的方式。

SetDlgItemTextGetDlgItemText 是两个非常方便的辅助函数,它们可以根据控件 ID 直接设置或获取控件的文本内容,不需要先获取控件句柄。如果你需要频繁操作一个控件,可以先获取句柄缓存起来,用 SendMessage 直接操作会更高效一些。但对于对话框这种偶尔才操作一次的场景,用这两个函数就足够了。

调用对话框

void ShowInputDialog(HWND hwndParent)
{
    InputData data = {};
    wcscpy_s(data.name, L"张三");
    data.age = 25;

    // DialogBoxParam 可以传递自定义数据
    INT_PTR result = DialogBoxParam(
        hInst,
        MAKEINTRESOURCE(IDD_INPUTDLG),
        hwndParent,
        InputDlgProc,
        (LPARAM)&data
    );

    if (result == IDOK)
    {
        // 用户点击了确定,data 里保存了输入的内容
        wchar_t msg[256];
        swprintf_s(msg, 256, L"姓名:%s\n年龄:%d", data.name, data.age);
        MessageBox(hwndParent, msg, L"输入结果", MB_OK);
    }
    else
    {
        // 用户点击了取消或者关闭了对话框
        MessageBox(hwndParent, L"用户取消了输入", L"提示", MB_OK);
    }
}

DialogBoxParam 的最后一个参数 lParam 会作为 WM_INITDIALOG 消息的 lParam 传递给对话框过程。这就是我们传递初始化数据的途径。

⚠️ 注意

lParam 传递的是一个指针,所以你需要确保这个指针指向的数据在对话框显示期间一直有效。如果你传递了一个局部变量的指针,然后对话框还在显示时这个变量已经被销毁了,就会出现悬空指针的问题。最安全的做法是使用静态变量、堆内存,或者像上面例子一样在调用函数的栈帧内。


第四步——处理输入验证

对话框的一个重要职责是验证用户输入。你肯定不希望用户在年龄编辑框里输入"abc",或者在姓名编辑框里什么都不填就点确定。这些验证逻辑应该放在哪里呢?

最直接的做法是在 IDOK 的处理里做验证:

if (LOWORD(wParam) == IDOK)
{
    // 获取输入内容
    wchar_t name[64];
    GetDlgItemText(hDlg, IDC_EDIT_NAME, name, 64);

    wchar_t ageStr[16];
    GetDlgItemText(hDlg, IDC_EDIT_AGE, ageStr, 16);

    // 验证输入
    if (wcslen(name) == 0)
    {
        MessageBox(hDlg, L"请输入姓名", L"输入错误", MB_OK | MB_ICONWARNING);
        SetFocus(GetDlgItem(hDlg, IDC_EDIT_NAME));
        return TRUE;  // 不关闭对话框
    }

    int age = _wtoi(ageStr);
    if (age <= 0 || age > 150)
    {
        MessageBox(hDlg, L"请输入有效的年龄(1-150)", L"输入错误", MB_OK | MB_ICONWARNING);
        SetFocus(GetDlgItem(hDlg, IDC_EDIT_AGE));
        // 选中编辑框里的文字,方便用户直接修改
        SendDlgItemMessage(hDlg, IDC_EDIT_AGE, EM_SETSEL, 0, -1);
        return TRUE;  // 不关闭对话框
    }

    // 验证通过,保存数据
    pData = (InputData*)GetWindowLongPtr(hDlg, DWLP_USER);
    if (pData != nullptr)
    {
        wcscpy_s(pData->name, name);
        pData->age = age;
    }

    EndDialog(hDlg, IDOK);
    return TRUE;
}

这里的关键点是:如果验证失败,不要调用 EndDialog,而是返回 TRUE 表示你已经处理了这个消息。对话框会继续显示,用户可以修正输入后再试一次。我们还用 SetFocus 把焦点设置到有问题的控件上,用 EM_SETSEL 选中编辑框里的文字,让用户能直接输入新的内容,这些都是提升用户体验的小细节。

更好的做法是使用实时验证,在用户输入的时候就给出反馈,而不是等到点确定才报错。这可以通过处理编辑框的 EN_CHANGE 通知来实现:

case WM_COMMAND:
{
    switch (LOWORD(wParam))
    {
    case IDC_EDIT_AGE:
        if (HIWORD(wParam) == EN_CHANGE)
        {
            // 实时验证年龄输入
            wchar_t ageStr[16];
            GetDlgItemText(hDlg, IDC_EDIT_AGE, ageStr, 16);
            int age = _wtoi(ageStr);

            HWND hHint = GetDlgItem(hDlg, IDC_STATIC_HINT);
            if (age < 0 || age > 150)
            {
                SetWindowText(hHint, L"年龄必须在 1-150 之间");
            }
            else
            {
                SetWindowText(hHint, L"");
            }
        }
        break;

    case IDOK:
        // ... 确定按钮处理
        break;
    }
    break;
}

这样用户在输入的时候就能立即看到提示,而不是等到点确定才发现有问题。


第五步——完整示例

为了让你能够完整运行这个例子,我把完整的代码写出来。假设你的程序入口点(WinMain)和前面文章里的一样,只是注册窗口类名换成了 MYWNDCLASS

#include <windows.h>
#include "resource.h"

// 资源 ID 定义
#define IDD_ABOUTBOX    100
#define IDD_INPUTDLG    101
#define IDC_EDIT_NAME   200
#define IDC_EDIT_AGE    201
#define IDC_STATIC_HINT 202
#define IDM_HELP_ABOUT  300
#define IDM_INPUT       301
#define IDM_EXIT        302

// 输入数据结构
struct InputData
{
    wchar_t name[64];
    int age;
};

// 前向声明
INT_PTR CALLBACK AboutDlgProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK InputDlgProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

// 对话框过程:关于对话框
INT_PTR CALLBACK AboutDlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    UNREFERENCED_PARAMETER(lParam);
    switch (message)
    {
    case WM_INITDIALOG:
        return TRUE;

    case WM_COMMAND:
        if (LOWORD(wParam) == IDOK)
        {
            EndDialog(hDlg, IDOK);
            return TRUE;
        }
        break;
    }
    return FALSE;
}

// 对话框过程:输入对话框
INT_PTR CALLBACK InputDlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    InputData* pData = nullptr;

    switch (message)
    {
    case WM_INITDIALOG:
    {
        SetWindowLongPtr(hDlg, DWLP_USER, lParam);

        pData = (InputData*)lParam;
        if (pData != nullptr)
        {
            SetDlgItemText(hDlg, IDC_EDIT_NAME, pData->name);
            if (pData->age > 0)
            {
                wchar_t ageStr[16];
                _itow_s(pData->age, ageStr, 10);
                SetDlgItemText(hDlg, IDC_EDIT_AGE, ageStr);
            }
        }
        return TRUE;
    }

    case WM_COMMAND:
    {
        switch (LOWORD(wParam))
        {
        case IDC_EDIT_AGE:
            if (HIWORD(wParam) == EN_CHANGE)
            {
                wchar_t ageStr[16];
                GetDlgItemText(hDlg, IDC_EDIT_AGE, ageStr, 16);
                int age = _wtoi(ageStr);

                HWND hHint = GetDlgItem(hDlg, IDC_STATIC_HINT);
                if (age < 0 || age > 150)
                {
                    SetWindowText(hHint, L"年龄必须在 1-150 之间");
                }
                else
                {
                    SetWindowText(hHint, L"");
                }
            }
            break;

        case IDOK:
        {
            wchar_t name[64];
            GetDlgItemText(hDlg, IDC_EDIT_NAME, name, 64);

            if (wcslen(name) == 0)
            {
                MessageBox(hDlg, L"请输入姓名", L"输入错误", MB_OK | MB_ICONWARNING);
                SetFocus(GetDlgItem(hDlg, IDC_EDIT_NAME));
                return TRUE;
            }

            wchar_t ageStr[16];
            GetDlgItemText(hDlg, IDC_EDIT_AGE, ageStr, 16);
            int age = _wtoi(ageStr);

            if (age <= 0 || age > 150)
            {
                MessageBox(hDlg, L"请输入有效的年龄(1-150)", L"输入错误", MB_OK | MB_ICONWARNING);
                SetFocus(GetDlgItem(hDlg, IDC_EDIT_AGE));
                SendDlgItemMessage(hDlg, IDC_EDIT_AGE, EM_SETSEL, 0, -1);
                return TRUE;
            }

            pData = (InputData*)GetWindowLongPtr(hDlg, DWLP_USER);
            if (pData != nullptr)
            {
                wcscpy_s(pData->name, name);
                pData->age = age;
            }

            EndDialog(hDlg, IDOK);
            return TRUE;
        }

        case IDCANCEL:
            EndDialog(hDlg, IDCANCEL);
            return TRUE;
        }
        break;
    }
    }
    return FALSE;
}

// 主窗口过程
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_CREATE:
    {
        // 创建菜单
        HMENU hMenu = CreateMenu();
        HMENU hFileMenu = CreatePopupMenu();
        AppendMenuA(hFileMenu, MF_STRING, IDM_INPUT, "输入信息...");
        AppendMenuA(hFileMenu, MF_SEPARATOR, 0, NULL);
        AppendMenuA(hFileMenu, MF_STRING, IDM_EXIT, "退出");
        HMENU hHelpMenu = CreatePopupMenu();
        AppendMenuA(hHelpMenu, MF_STRING, IDM_HELP_ABOUT, "关于...");
        InsertMenuA(hMenu, 0, MF_POPUP, (UINT_PTR)hFileMenu, "文件");
        InsertMenuA(hMenu, 1, MF_POPUP, (UINT_PTR)hHelpMenu, "帮助");
        SetMenu(hwnd, hMenu);
        return 0;
    }

    case WM_COMMAND:
    {
        switch (LOWORD(wParam))
        {
        case IDM_INPUT:
        {
            InputData data = {};
            wcscopy_s(data.name, L"张三");
            data.age = 25;

            INT_PTR result = DialogBoxParam(
                GetModuleHandle(NULL),
                MAKEINTRESOURCE(IDD_INPUTDLG),
                hwnd,
                InputDlgProc,
                (LPARAM)&data
            );

            if (result == IDOK)
            {
                wchar_t msg[256];
                swprintf_s(msg, 256, L"姓名:%s\n年龄:%d", data.name, data.age);
                MessageBox(hwnd, msg, L"输入结果", MB_OK);
            }
            break;
        }

        case IDM_HELP_ABOUT:
            DialogBox(GetModuleHandle(NULL), MAKEINTRESOURCE(IDD_ABOUTBOX), hwnd, AboutDlgProc);
            break;

        case IDM_EXIT:
            DestroyWindow(hwnd);
            break;
        }
        return 0;
    }

    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(hwnd, message, wParam, lParam);
}

// 程序入口点
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    // 注册窗口类
    WNDCLASSEX wcex = {};
    wcex.cbSize = sizeof(WNDCLASSEX);
    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WndProc;
    wcex.hInstance = hInstance;
    wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wcex.lpszClassName = L"MYWNDCLASS";

    if (!RegisterClassEx(&wcex))
    {
        MessageBox(NULL, L"窗口类注册失败!", L"错误", MB_OK | MB_ICONERROR);
        return 1;
    }

    // 创建主窗口
    HWND hwnd = CreateWindow(
        L"MYWNDCLASS",
        L"模态对话框示例",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT,
        500, 400,
        NULL, NULL, hInstance, NULL
    );

    if (!hwnd)
    {
        MessageBox(NULL, L"窗口创建失败!", L"错误", MB_OK | MB_ICONERROR);
        return 1;
    }

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    // 消息循环
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return (int)msg.wParam;
}

编译运行这个程序,你应该能看到一个带有菜单栏的窗口。点击"文件→输入信息"会弹出输入对话框,输入姓名和年龄后点确定,主窗口会显示你输入的内容。点击"帮助→关于"会显示关于对话框。


后续可以做什么

到这里,Win32 模态对话框的基础知识就讲完了。你现在应该能够创建和运行模态对话框,理解对话框过程和窗口过程的区别,知道怎么传递数据、怎么验证输入。但模态对话框的世界远不止这些,还有很多高级主题等着我们去探索。

下一篇文章,我们会聊一聊 非模态对话框——那种不会阻塞父窗口、可以一直显示在屏幕上的对话框。非模态对话框在很多场景下比模态对话框更实用,比如工具面板、查找替换对话框等等。而且,非模态对话框的创建方式、消息循环集成方式都和模态对话框有所不同,需要特别注意。

在此之前,建议你先把今天的内容消化一下。试着做一些小练习,巩固一下知识:

  1. 创建一个设置对话框,包含多种类型的控件(复选框、单选按钮、组合框等)
  2. 实现输入验证,在用户输入错误的时候给出清晰的提示
  3. 使用 DialogBoxParam 传递初始化数据,让对话框打开时显示上次输入的内容
  4. 创建一个多页面的属性表(Property Sheet),用多个对话框组合成一个设置向导

这些练习看似简单,但能帮你把今天学到的知识真正变成自己的东西。好了,今天的文章就到这里,我们下一篇再见!


相关资源