【Windows】《深入浅出Windows API程序设计:编程基础篇》笔记-Chapter9-对话框

第9章 对话框

在图形用户界面中,对话框是一种特殊的窗口,用来向用户显示信息(例如MessageBox消息框),或者在需要的时候获得用户的输入以作出响应(例如记事本Notepad的查找、替换对话框)。之所以称之为"对话框",是因为它们使计算机和用户之间构成了一个对话:通知用户一些信息,或者是请求用户的输入,又或者两者皆有。通常在可以打开对话框的菜单项后面加上"...",例如"文件"菜单下的"另存为..."表示会打开一个选择文件名的对话框。对话框中的按钮、文本框和图标等称为"子窗口控件"。

对话框分为模态(modal,也称有模式)对话框和非模态(modeless,也称无模式)对话框。模态对话框要求用户必须作出回应,否则用户不能继续进行本程序的其他操作,例如记事本程序"格式"菜单下的"字体"对话框,只有在用户单击确定或取消以后才可以回到记事本编辑界面继续自己的工作;非模态对话框是一种不强制用户回应的对话框,例如记事本程序"编辑"菜单下的"查找"和"替换"对话框,在不关闭的情况下用户仍然可以回到编辑界面继续做自己的工作。

Windows在资源脚本文件中定义对话框资源模板,然后在程序中利用这个模板创建对话框。模态对话框和非模态对话框的资源定义可以说是相同的,但是创建模态和非模态对话框所调用的函数不同,销毁模态和非模态对话框所调用的函数也不同。

9.1 模态对话框

先看一下创建模态对话框的宏DialogBox,它的作用是从一个对话框模板资源中创建模态对话框:

复制代码
INT_PTR DialogBox(
    HINSTANCE hInstance,      // 模块句柄,该模块包含对话框模板
    LPCTSTR   lpTemplate,     // 对话框模板,可以使用MAKEINTRESOURCE(对话框模板ID)宏
    HWND      hWndParent,     // 父窗口句柄
 DLGPROC   lpDialogFunc);  // 对话框窗口过程

DialogBox宏通过调用DialogBoxParam函数来实现,在WinUser.h头文件中有如下定义:

复制代码
#define DialogBoxA(hInstance, lpTemplate, hWndParent, lpDialogFunc) \
DialogBoxParamA(hInstance, lpTemplate, hWndParent, lpDialogFunc, 0L)
#define DialogBoxW(hInstance, lpTemplate, hWndParent, lpDialogFunc) \
DialogBoxParamW(hInstance, lpTemplate, hWndParent, lpDialogFunc, 0L)

DialogBoxParam函数原型如下:

复制代码
INT_PTR DialogBoxParam(
    HINSTANCE hInstance,      // 模块句柄,该模块包含对话框模板资源
    LPCTSTR   lpTemplateName, // 对话框模板,可以使用MAKEINTRESOURCE(对话框模板ID)宏
    HWND      hWndParent,     // 父窗口句柄
    DLGPROC   lpDialogFunc,   // 对话框窗口过程
    LPARAM    dwInitParam);   // 传递到对话框窗口过程中WM_INITDIALOG消息的lParam参数

通常情况下使用DialogBoxParam函数创建对话框,调用DialogBoxParam函数时,可以通过dwInitParam参数向对话框窗口过程中传递一些附加数据(通过对话框窗口过程的WM_INITDIALOG消息的lParam参数接收)。

对话框窗口过程的定义形式如下:

复制代码
INT_PTR CALLBACK DialogProc(
    HWND   hwndDlg,   // 对话框窗口句柄
    UINT   uMsg,      // 消息类型
    WPARAM wParam,    // 消息的wParam参数
    LPARAM lParam);   // 消息的lParam参数

只有在对话框窗口过程中调用EndDialog函数后,DialogBox / DialogBoxParam函数才返回,这两个函数的返回值是EndDialog函数第2个参数指定的值。

INT_PTR数据类型在BaseTsd.h头文件中定义:

复制代码
#if defined(_WIN64) 
typedef __int64 INT_PTR;
#else 
typedef int INT_PTR;
#endif

即如果编译为64位,则它是__int64类型;如果编译为32位,则它是int类型。

结束模态对话框的函数是EndDialog:

复制代码
BOOL EndDialog(
    HWND    hDlg,       // 对话框窗口句柄
    INT_PTR nResult);   // 返回给DialogBox/DialogBoxParam函数的值

创建非模态对话框用的是CreateDialog宏或CreateDialogParam函数,所需参数和DialogBox、DialogBoxParam函数相同;关闭非模态对话框需要调用DestroyWindow函数。

普通重叠窗口在创建之前需要调用RegisterClassEx函数注册一个窗口类,然后调用CreateWindowEx函数创建窗口。创建窗口所需的参数(例如窗口样式、位置、大小和窗口过程地址等参数)由窗口类及CreateWindowEx函数中的参数共同提供。实际上对话框和普通重叠窗口类似,创建对话框的函数在内部还是通过调用CreateWindowEx函数创建,使用的窗口样式、位置、大小等参数取自资源脚本文件中定义的对话框模板。使用的窗口类是系统内置类名"#32770","#32770"类的窗口过程叫作"对话框管理器",Windows在这里处理对话框的大部分消息,例如维护客户区的刷新、键盘接口(按Tab键在不同的子窗口之间切换、按Enter键相当于单击了默认按钮等)。"对话框管理器"在初始化对话框时会根据对话框模板资源中定义的子窗口控件来创建对话框中的所有子窗口,它类似于普通重叠窗口的默认窗口过程。程序中我们定义的对话框窗口过程由"对话框管理器"调用。在处理一个消息前,"对话框管理器"会先调用程序指定的对话框窗口过程,然后根据对话框窗口过程的返回值决定是否处理该消息(这一点参见9.1.2节对话框窗口过程的讲解)。

Windows对模态对话框和非模态对话框的处理有些不同。在DialogBoxParam函数创建模态对话框后使拥有窗口(父窗口)失效,Windows暂时控制整个应用程序的消息队列,Windows在内部为对话框启动消息循环来获取和分发消息,在这个消息循环中把消息发送给"对话框管理器","对话框管理器"会调用用户定义的对话框窗口过程。在对话框窗口过程调用EndDialog函数后,DialogBoxParam函数结束对话框并终止消息循环,系统使拥有窗口(父窗口)有效,且返回EndDialog函数调用中的nResult参数,即模态对话框创建以后系统会屏蔽本程序其他窗口的消息,应用程序只会响应该对话框的消息,直到调用EndDialog函数收到结束模态对话框的消息后,才会把控制权交还给应用程序。

而对于非模态对话框,CreateDialogParam函数在创建非模态对话框后立即返回。非模态对话框的消息是通过用户程序(父窗口)中的消息循环分发的。

创建模态对话框或非模态对话框时,WM_INITDIALOG消息最先被发送到对话框窗口过程,这类似于普通重叠窗口的WM_CREATE消息,程序可以在该消息中做一些初始化的工作。

9.1.1 模态对话框示例程序

我们通过一个示例来说明模态对话框的用法。大部分程序的"帮助"菜单下会有一个子菜单项"关于本程序",单击"关于本程序"菜单项通常可以弹出一个模态对话框(调用DialogBoxParam函数),对话框中可以是一些程序版本、版权信息等,如原书的图9.1所示。

可以在WM_COMMAND消息中处理"关于本程序"菜单消息,调用DialogBoxParam函数创建模态对话框:

复制代码
case ID_HELP_ABOUT:
    // 创建模态对话框
    DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_DIALOG), hwnd, DialogProc, NULL);
    break;

完整代码参见Chapter9\ModalDialogBox项目。

ID为IDD_DIALOG的资源是对话框模板资源,其ID为资源ID。要创建对话框,就需要先新建一个对话框资源,在VS中打开资源视图,用鼠标右键单击项目名称,然后选择添加→资源,打开添加资源对话框,选择类型Dialog,单击"新建"按钮,系统会自动创建一个对话框模板资源。默认情况下,对话框标题为Dialog,标题栏中有一个关闭按钮,对话框中有确定、取消按钮。对话框的大小可以通过拖拉调整,对话框的样式可以通过VS右侧的属性面板来修改。先选中对话框,然后通过属性面板来设置各种属性,选择一个属性后,属性面板底部会显示其含义。常用的属性如表9.1所示。

表9.1

属性 含义
Border 边框样式,可以设置为:调整大小(Resizing,WS_THICKFRAME)、对话框外框(Dialog Frame,DS_MODALFRAME)、无(None)
Caption 设置对话框标题
ID 设置对话框资源的ID
Menu 为对话框指定菜单资源ID
Maximize Box 对话框标题栏中包含最大化按钮
Minimize Box 对话框标题栏中包含最小化按钮
System Modal 对话框具有Topmost样式
X Pos和Y Pos 指定对话框左上角的X、Y坐标(对话框单位),本例中是相对于父窗口客户区左上角。在9.3节中,我们会学习创建一个模态对话框作为程序主窗口,即顶级窗口,这时X、Y坐标相对于屏幕左上角

设置对话框的属性后,可以向其中添加子窗口控件。打开VS右侧的工具箱面板,可以看到里面几乎包含了前面我们学习过的各种子窗口控件,选择一个子窗口控件,在对话框中单击鼠标左键就可以将其插入对话框中,或者可以从工具箱中拖曳一个子窗口控件到对话框中。添加子窗口控件以后,可以选中一个控件,通过属性面板设置其属性与样式。示例程序中我添加了2个Static Text静态控件,分别设置其Caption属性为"程序版本:ModalDialogBox V1.0"和"本程序由老王设计开发\n微信号:WindowsSuper\n版权所有 老王"。

对话框模板用于定义对话框的宽度、高度、样式以及包含的子窗口控件等。打开资源脚本文件ModalDialogBox.rc看一下VS生成的对话框模板:

复制代码
IDD_DIALOG DIALOGEX 16, 16, 160, 80
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "关于ModalDialogBox"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
    DEFPUSHBUTTON   "确定", IDOK, 45, 57, 50, 14
    PUSHBUTTON      "取消", IDCANCEL, 102, 57, 50, 14
    LTEXT           "程序版本:ModalDialogBox V1.0", IDC_STATIC, 7, 7, 115, 8
    LTEXT           "本程序由老王设计开发\n微信号:WindowsSuper\n版权所有 老王", IDC_STATIC,   
                         7, 24, 145, 29
END

对话框资源的定义形式如下:

复制代码
对话框ID DIALOGEX X坐标, Y坐标, 宽度, 高度
[可选属性]
BEGIN
    子窗口控件定义
    ......
END

对话框中的子窗口控件定义语句在BEGIN/END(当然也可以用{})之中。在这之前,可以定义对话框的一些可选属性,每种属性用一行定义,常用的可选属性如表9.2所示。

表9.2

定义语法 含义
CAPTION "文字" 对话框的标题
STYLE 样式组合 定义对话框的窗口样式,同CreateWindowEx中的dwStyle参数
EXSTYLE 样式组合 定义对话框的扩展窗口样式,同CreateWindowEx中的dwExStyle参数
FONT 字体大小, "字体名称", 字体粗细, 是否斜体, 字符集 对话框所用字体
MENU 菜单名称或ID 对话框中使用的菜单
CLASS "类名" 对话框使用的窗口类,如果不定义,则使用Windows内置的类#32770

STYLE样式组合还可以包含对话框专用样式,常用的对话框样式如表9.3所示。

表9.3

对话框样式 含义
DS_FIXEDSYS 使用SYSTEM_FIXED_FONT字体,而不是默认的SYSTEM_FONT
DS_MODALFRAME 模态对话框边框
DS_SETFONT 对话框模板中包含字体数据,通过FONT属性设置一种字体
DS_SHELLFONT 表示对话框应使用系统字体,FONT属性必须设置为MS Shell Dlg字体(系统映射字体),否则该样式无效
DS_ABSALIGN 对话框的坐标相对于屏幕坐标,而不是默认的相对于父窗口客户区坐标
DS_CENTER 对话框显示在屏幕中央
DS_CENTERMOUSE 对话框以鼠标当前位置为中心显示

无论是否指定WS_VISIBLE样式,系统始终都会显示模态对话框。

两种定义子窗口控件的语法如下,\[\]里面的表示可选项。

1.子窗口控件定义的通用语法如下:

复制代码
CONTROL "窗口文本", ID, "类名", 样式组合, x, y, 宽度, 高度 [, 扩展样式组合]

前面都已经介绍过窗口样式、控件专用样式和扩展窗口样式。类名可以是我们前面学习过的各种子窗口控件类名。"对话框管理器"在初始化的时候把每一条控件定义语句转换成类似下面的CreateWindowEx函数调用:

复制代码
CreateWindowEx(扩展样式组合, 窗口文本, 类名, 样式组合, 
    x, y, 宽度, 高度, 对话框窗口句柄, ID, hInstance, NULL);

Windows在创建子窗口时会自动包含WS_CHILD和WS_VISIBLE样式,因此子窗口控件定义的通用语法中不需要额外指定这两个样式。所有可以用CreateWindowEx函数创建的子窗口都可以在资源脚本文件中定义,只要知道需要使用的类名和样式即可。

2.子窗口控件定义的通用语法看上去不直观,资源编辑器允许使用另一种语法来定义子窗口控件:

复制代码
控件名称 [窗口文本,] ID, x, y, 宽度, 高度 [, 样式] [, 扩展样式]

控件名称并不是类名。基于同一个类名指定不同的样式可以创建不同风格的控件,例如基于Button类指定BS_PUSHBUTTON、BS_DEFPUSHBUTTON、BS_AUTORADIOBUTTON和BS_AUTOCHECKBOX样式可以分别创建普通按钮、默认按钮、自动单选按钮和自动复选框。相应的,控件名称可以分别指定为PUSHBUTTON、DEFPUSHBUTTON、AUTORADIOBUTTON和AUTOCHECKBOX。控件名称是资源编译器使用的缩写名称,资源编译器把控件名称解释为相应的类名与样式,常见的控件名称如表9.4所示。

表9.4

控件名称 控件 基于的类 默认样式(还包括WS_CHILD和WS_VISIBLE)
PUSHBUTTON 按钮 Button BS_PUSHBUTTON、WS_TABSTOP
DEFPUSHBUTTON 默认按钮 Button BS_DEFPUSHBUTTON、WS_TABSTOP
CHECKBOX 复选框 Button BS_CHECKBOX、WS_TABSTOP
AUTOCHECKBOX 自动复选框 Button BS_AUTOCHECKBOX、WS_TABSTOP
STATE3 三态复选框 Button BS_3STATE、WS_TABSTOP
AUTO3STATE 自动三态复选框 Button BS_AUTO3STATE、WS_TABSTOP
RADIOBUTTON 单选按钮 Button BS_RADIOBUTTON、WS_TABSTOP
AUTORADIOBUTTON 自动单选按钮 Button BS_AUTORADIOBUTTON、WS_TABSTOP
GROUPBOX 分组框 Button BS_GROUPBOX
SCROLLBAR 滚动条 ScrollBar SBS_HORZ
CTEXT 居中文本 Static SS_CENTER、WS_GROUP
LTEXT 左对齐文本 Static SS_LEFT、WS_GROUP
RTEXT 右对齐文本 Static SS_RIGHT、WS_GROUP
ICON 图标框 Static SS_ICON
EDITTEXT 编辑控件 Edit ES_LEFT、WS_BORDER、WS_TABSTOP
COMBOBOX 组合框 ComboBox CBS_SIMPLE、WS_TABSTOP
LISTBOX 列表框 ListBox LBS_NOTIFY、WS_BORDER

看一下示例。语句

复制代码
GROUPBOX    "分组框", -1, 5, 5, 100, 100
PUSHBUTTON  "退出", IDCANCEL, 50, 85, 50, 14

和语句

复制代码
CONTROL "分组框", -1, "Button", BS_GROUPBOX, 5, 5, 100, 100
CONTROL "退出", IDCANCEL, "Button", BS_PUSHBUTTON | WS_TABSTOP, 50, 85, 50, 14

编译后产生的二进制资源文件是一样的

当用到的控件没有缩写语法时,就必须用CONTROL语句定义了。下面定义了一条横线和一个图片框:

复制代码
CONTROL "", IDC_STATIC, "Static", SS_ETCHEDHORZ, 5, 50, 100, 1
CONTROL IDB_BITMAP, IDC_STATIC, "Static", SS_BITMAP, 5, 55, 50, 50

上面讲的这些资源脚本文件中定义子窗口控件的语法,都可以通过工具箱直观地添加。关于通过工具箱添加控件并设置其属性的方法,请读者自行测试。通过工具箱添加控件后,可以查看资源脚本文件如何定义,并了解哪一个属性对应哪一个窗口样式、控件样式或扩展窗口样式。

在子窗口控件的ID定义中有两个特殊的ID值------ IDOK和IDCANCEL,在WinUser.h头文件中,它们的值被定义为1和2。IDOK是默认的"确定"按钮ID,IDCANCEL是默认的"取消"按钮ID。如果一个按钮的ID是IDOK,那么当键盘焦点不在其他控件上时,按下回车键就相当于按下了这个按钮;按下Esc键相当于按下ID为IDCANCEL的按钮;单击标题栏的关闭按钮也相当于按下了ID为IDCANCEL的按钮。如果对话框中没有ID为IDOK的默认按钮,那么当用户按下Enter键时,系统也会向对话框窗口过程发送一条LOWORD(wParam)等于IDOK(如果有其他默认按钮,则是其ID)的WM_COMMAND消息。

在对话框中,可以定义多个子窗口控件。有的子窗口控件可以拥有输入焦点(如按钮、文本框与组合框等),有的则不能(如静态文本与图标等),当对话框中有多个允许包含输入焦点的子窗口控件时(设置了WS_TABSTOP样式),用户可以按Tab键将输入焦点切换到下一个具有WS_TABSTOP样式的子窗口控件上,也可以按Shift + Tab组合键切换到上一个,Tab键切换的顺序就叫作Tab键顺序。Tab键顺序并不是根据子窗口控件的坐标位置自动排列的,而是按照子窗口控件在资源脚本文件中定义的先后顺序来排列的。在定义时最好根据子窗口控件的位置适当排列定义语句的顺序,以免按下Tab键切换的时候焦点会上下左右无规则地跳来跳去。使用资源编辑器的工具箱插入子窗口控件后,选中对话框,打开菜单格式→Tab键顺序,可以看到每个控件旁边显示了序号,可以依次单击每个控件以重新排列,资源编辑器会根据设置重新调整资源脚本文件中定义语句的顺序。

对话框的位置、大小以及所有子窗口控件的度量单位根据当前所选择字体的大小来决定,横向(x坐标和宽度)每单位为字符平均宽度的1/4,纵向(y坐标和高度)每单位为字符平均高度的1/8,由于字体的字符高度大致为宽度的2倍,因此虽然这种计算方法有些令人费解,但是横向和纵向的数值还是基本相同的,即对话框水平单位是当前所选择字体字符平均宽度的1/4,垂直单位是当前所选择字体字符平均高度的1/8。在上面的对话框模板定义中,对话框的左上角距离父窗口的左边有4个字符宽,距离父窗口的上边有2个字符高,对话框本身有45个字符宽,10个字符高。通过这种对话框单位,使用同样的坐标和大小值可以使对话框在使用各种显示器和字体的情况下,保待同样的尺寸和外观。同时也因为对话框单位的这种特性,确定字体后,通常不可以再改变,否则容易使对话框界面混乱。

在不指定DS_SETFONT | DS_FIXEDSYS对话框样式,以及不设置FONT属性的情况下,对话框使用SYSTEM_FONT字体。GetDialogBaseUnits函数可以获取系统字体字符的平均宽度和高度,例如:

复制代码
xChar = LOWORD(GetDialogBaseUnits());
yChar = HIWORD(GetDialogBaseUnits());

9.1.2 对话框窗口过程

模态和非模态对话框的窗口过程定义形式一样,对话框窗口过程的处理逻辑通常是下面的样子:

复制代码
// 对话框窗口过程
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_INITDIALOG:
        // 初始化工作
        return TRUE;

    case WM_COMMAND:
        // 子窗口控件
        switch (LOWORD(wParam))
        {
        case IDOK:
        case IDCANCEL:
            EndDialog(hwndDlg, 0);
            break;
        }
        return TRUE;
    }

    return FALSE;
}

对话框窗口过程中的很多消息通常不需要处理,例如WM_PAINT和WM_DESTROY消息等,因为对话框的大部分消息由"对话框管理器"处理,例如维护客户区的刷新、键盘接口(按Tab键在不同的子窗口之间切换、按Enter键相当于单击了默认按钮等)。对话框窗口过程和普通的窗口过程在使用上有以下区别。

  • 普通窗口过程对于不同的消息处理有各种不同含义的返回值。前面说过,程序中我们定义的对话框窗口过程是由"对话框管理器"调用的。在处理消息前,"对话框管理器"会先调用程序指定的对话框窗口过程,然后根据对话框窗口过程的返回值决定是否处理该消息。对话框窗口过程应该返回BOOL类型的值,返回TRUE表示告诉"对话框管理器"已经处理了某条消息;返回FALSE表示告诉"对话框管理器"没有处理该消息,在这种情况下"对话框管理器"可以执行默认处理。

  • 创建模态对话框或非模态对话框时,WM_INITDIALOG消息最先被发送到对话框窗口过程,这类似于普通重叠窗口的WM_CREATE消息,程序可以在该消息中做一些初始化的工作。WM_INITDIALOG消息的返回值有一些特殊,如果程序想自行设置输入焦点到某个子窗口控件,那么可以调用SetFocus函数把输入焦点设置到需要的子窗口控件上,然后返回FALSE;如果返回TRUE的话,那么系统会自动将输入焦点设置到第一个具有WS_TABSTOP样式的子窗口控件上。

  • 默认情况下对话框窗口的标题栏上没有图标。如果需要像普通窗口一样显示一个图标,则可以在WM_INITDIALOG消息中通过发送WM_SETICON消息来设置。

9.1.3 模态对话框示例程序2

本节引用9.1.1节的ModalDialogBox项目,但是模态对话框的设计和功能不同。用户单击颜色组的颜色单选按钮选择一个颜色,然后单击形状组的单选按钮选择一个形状后,会在对话框右下角的静态控件中绘制出对应颜色和形状的图形。用户单击对话框的"确定"按钮后,父窗口客户区中会显示模态对话框中所选择颜色和形状的图形。ModalDialogBox2项目的对话框设计如原书的图9.2所示(右图显示了Tab键顺序)。

对话框模板定义如下,我通过工具箱添加并设置每个控件的属性:

复制代码
IDD_DIALOG DIALOGEX 16, 16, 159, 145
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "关于ModalDialogBox"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
  GROUPBOX        "颜色",IDC_STATIC,8,7,64,109
  CONTROL         "White",IDC_RADIO_WHITE,"Button",BS_AUTORADIOBUTTON | WS_GROUP |   
                       WS_TABSTOP,13,18,35,10
  CONTROL         "Red",IDC_RADIO_RED,"Button",BS_AUTORADIOBUTTON,13,30,29,10
  CONTROL         "Green",IDC_RADIO_GREEN,"Button",BS_AUTORADIOBUTTON,13,42,35,10
  CONTROL         "Blue",IDC_RADIO_BLUE,"Button",BS_AUTORADIOBUTTON,13,54,29,10
  CONTROL         "Cyan",IDC_RADIO_CYAN,"Button",BS_AUTORADIOBUTTON,13,66,33,10
  CONTROL         "Magenta",IDC_RADIO_MAGENTA,"Button",BS_AUTORADIOBUTTON,13,78,44,10
  CONTROL         "Yellow",IDC_RADIO_YELLOW,"Button",BS_AUTORADIOBUTTON,13,90,36,10
  CONTROL         "Black",IDC_RADIO_BLACK,"Button",BS_AUTORADIOBUTTON,13,102,32,10
  GROUPBOX        "形状",IDC_STATIC,83,7,64,39,WS_GROUP
  CONTROL         "Rectangle",IDC_RADIO_RECT,"Button",BS_AUTORADIOBUTTON | WS_GROUP |   
                       WS_TABSTOP,87,18,48,10
  CONTROL         "Ellipse",IDC_RADIO_ELLIPSE,"Button",BS_AUTORADIOBUTTON,87,30,35,10
  LTEXT           "",IDC_STATIC_DRAW,83,54,60,55         // 控件名称LTEXT隐含WS_GROUP
  DEFPUSHBUTTON   "确定",IDOK,35,122,50,14,WS_GROUP      // 控件名称DEFPUSHBUTTON隐含
                                                         // WS_TABSTOP
  PUSHBUTTON      "取消",IDCANCEL,95,122,50,14,WS_GROUP  // 控件名称PUSHBUTTON隐含
                                                         // WS_TABSTOP
END

表9.5再次列出WS_GROUP和WS_TABSTOP样式的含义。

表9.5

样式 含义
WS_GROUP 该窗口是一组控件中的第一个控件。该组由第一个具有WS_GROUP样式的控件和在其后定义的所有控件组成,直到下一个具有WS_GROUP样式的控件(不包括该控件)出现。如果是在对话框程序中,则用户可以使用方向键将键盘焦点从组中的一个控件移动到组中的下一个控件上。另外,每个组中的第一个控件通常具有WS_TABSTOP样式,如果是在对话框程序中,则用户可以使用Tab键将键盘焦点从一个组移动到另一个组
WS_TABSTOP 该窗口是一个控件,当用户按下Tab键时,该控件可以接收键盘焦点,如果是在对话框程序中,则按下Tab键可以将键盘焦点移动到下一个具有WS_TABSTOP样式的控件上

在上面的对话框模板定义中,子窗口控件分为6组:颜色组的8个单选按钮是一组;形状分组框是一组;形状组的2个单选按钮是一组;控件名称LTEXT隐含WS_GROUP,因此静态控件也是一组;确定按钮是一组;取消按钮是一组。分组框和静态控件不会接收键盘焦点,因此按下Tab键的时候,只会在颜色组、形状组、确定按钮和取消按钮之间切换。当输入焦点在一组中的一个控件上时,可以通过按下方向键在该组的所有控件之间来回切换。确定和取消按钮的定义用的是控件名称方式,默认具有WS_TABSTOP样式。如果不为这两个按钮设置WS_GROUP样式,那么确定和取消按钮就属于一个组。假设当前确定按钮具有输入焦点,按下方向键可以切换到取消按钮,而这两个按钮分别设置WS_GROUP样式以后就各自成为一组,只能通过按下Tab键进行切换。

默认情况下添加的静态控件ID为IDC_STATIC(−1),本例中控件名称为LTEXT,是静态控件,因为需要在上面绘图,会用到ID,因此设置其ID为IDC_STATIC_DRAW(1011)。

参见Chapter9\ModalDialogBox2项目理解本程序。用户单击"关于ModalDialogBox"菜单项后,弹出模态对话框。如果用户单击模态对话框的"确定"按钮,对话框窗口过程中EndDialog函数的nResult参数设置为IDOK,单击"取消"按钮则设置为IDCANCEL。如果单击"确定"按钮,就刷新父窗口客户区以绘制模态对话框中所选择颜色和形状的图形。

颜色和形状被定义为一个COLORSHAPE结构体,并在WindowProc窗口过程中初始化为红色和椭圆,然后传递给DialogBoxParam函数的dwInitParam参数。可以通过DialogProc对话框窗口过程的WM_INITDIALOG消息中的lParam参数获取该结构指针,在对话框窗口过程DialogProc中有如下两个变量:

复制代码
static COLORSHAPE cs, *pCS;
static HWND hwndStatic;

switch (uMsg)
{
case WM_INITDIALOG:
 // lParam 参数就是DialogBoxParam函数的dwInitParam参数传递过来的自定义数据
 pCS = (PCOLORSHAPE)lParam;  // 该参数的值在用户单击确定按钮以后被设置
 cs = *pCS;                  // 该参数在本窗口过程中作为静态局部变量

pCS已经指向WindowProc函数中定义的COLORSHAPE结构,通过cs = *pCS;语句复制出一个变量cs,在对话框窗口过程DialogProc中作为静态局部变量使用。如果用户单击了模态对话框的"确定"按钮,则通过pCS指针设置WindowProc函数中定义的COLORSHAPE结构,在WindowProc函数的WM_PAINT消息中会使用COLORSHAPE结构的新的颜色和形状值;之所以复制出一个cs变量,是因为如果用户单击的是"取消"按钮,则不改变WindowProc函数中定义的COLORSHAPE结构的值。实际上,把颜色和形状值定义为全局变量更简单,这种通过DialogBoxParam函数的dwInitParam参数传递值的方式,是为了在程序设计中避免使用全局变量。但在以后的学习中,遇到类似情况我可能会使用定义全局变量的方式。

前面说过,对话框窗口过程通常不需要处理WM_PAINT和WM_DESTROY消息。但是为了随着用户选择不同的颜色或形状而即时刷新静态控件IDC_STATIC_DRAW的客户区,必须处理WM_PAINT消息。处理完该消息后一定要返回FALSE,这相当于告诉"对话框管理器"还需要执行对WM_PAINT消息的默认处理,否则模态对话框的客户区将得不到正确的刷新。

9.2 非模态对话框

如果需要在一段时间内一直显示或使用某个对话框,可以创建非模态对话框,例如文字处理程序使用的搜索对话框,对话框会一直保留在屏幕上,用户可以在显示对话框的同时返回编辑界面继续自己的工作,再次搜索相同的单词或在对话框中输入新的单词以搜索。创建非模态对话框用的是CreateDialog宏或CreateDialogParam函数,函数根据对话框模板资源创建一个非模态对话框:

复制代码
HWND CreateDialogParam(
    HINSTANCE hInstance,      // 模块句柄,该模块包含对话框模板
    LPCTSTR   lpTemplateName, // 对话框模板,可以使用MAKEINTRESOURCE(对话框模板ID)宏创建此值
    HWND      hWndParent,     // 父窗口句柄
    DLGPROC   lpDialogFunc,   // 对话框窗口过程
    LPARAM    dwInitParam);   // 传递到对话框窗口过程中WM_INITDIALOG消息的lParam参数的值

创建非模态对话框后,系统会将其设置为活动窗口。在非模态对话框保持显示的情况下,允许用户操作本程序的其他窗口,但是即使非模态对话框变为非活动状态,它也会在Z序中始终处于父窗口的上方。

模态和非模态对话框在使用中有以下几个不同点。

  • CreateDialogParam函数在创建对话框后,会根据对话框模板是否指定了WS_VISIBLE样式来决定是否显示对话框窗口。如果指定了,则显示;如果没有指定,则程序需要在以后自行调用ShowWindow函数来显示非模态对话框。而通过调用DialogBoxParam函数创建模态对话框的时候,不管是否指定了WS_VISIBLE样式都会显示模态对话框。

  • CreateDialogParam函数在创建对话框窗口后立即返回,返回值是非模态对话框窗口的句柄;而DialogBoxParam函数要在对话框窗口关闭以后才返回,返回值是EndDialog函数中的nResult参数。非模态对话框不能像模态对话框那样向应用程序返回一个值,但是非模态对话框过程可以通过调用SendMessage函数向父窗口发送消息。

  • CreateDialogParam函数调用返回后,通常需要在父窗口的消息循环中获取对话框消息;而DialogBoxParam函数创建的模态对话框是使用Windows循环它内建的消息。

  • 关闭非模态对话框使用DestroyWindow函数,而不能使用关闭模态对话框的EndDialog函数。DestroyWindow函数调用使对话框的窗口句柄无效,如果在其他函数调用中使用该句柄则会失败,有的程序使用一个对话框句柄的全局变量(例如g_hwndDlgModeless),当销毁非模态对话框时,应同时将对话框句柄全局变量设置为NULL,在其他需要该句柄的函数调用以前检查句柄是否为NULL。例如下面的非模态对话框窗口过程对WM_CLOSE消息的处理:

    复制代码
    case WM_CLOSE:
      DestroyWindow(hwndDlg);
      g_hwndDlgModeless = NULL;  // g_hwndDlgModeless是非模态对话框句柄全局变量
      return TRUE;

非模态对话框模板资源中通常应该指定WS_VISIBLE样式,否则程序需要在以后自行调用ShowWindow函数来显示。调用CreateDialogParam函数以后会返回一个非模态对话框句柄,可以把这个句柄保存在一个全局变量中,例如g_hwndDlgModeless,通常需要在父窗口的消息循环中获取对话框消息(会用到g_hwndDlgModeless)。父窗口消息循环的代码如下所示:

复制代码
while (GetMessage(&msg, NULL, 0, 0) != 0)
{
    if (g_hwndDlgModeless == NULL || !IsDialogMessage(g_hwndDlgModeless, &msg))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}

如果父窗口消息循环中的某条消息是针对非模态对话框的,IsDialogMessage函数就会将该消息发送到对话框窗口过程并返回TRUE,否则返回FALSE。只有当g_hwndDlgModeless为NULL或者消息循环中的消息不是发送给对话框的时候才应该调用TranslateMessage和DispatchMessage函数(分发消息给父窗口)。

如果父窗口程序使用了键盘加速键,那么消息循环应该按如下方式编写:

复制代码
HACCEL hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACC));
while (GetMessage(&msg, NULL, 0, 0) != 0)
{
    if (g_hwndDlgModeless == NULL || !IsDialogMessage(g_hwndDlgModeless, &msg))
    {
        if (!TranslateAccelerator(hwnd, hAccel, &msg))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
}

全局变量g_hwndDlgModeless初始值为0,g_hwndDlgModeless将保持为0直到非模态对话框被创建。

关于非模态对话框的简单示例程序参见Chapter9\ ModelessDialogBox项目。

9.3 对话框程序的书写

鉴于模态对话框的特征,使用它来做小程序的主窗口非常方便,因为用一句DialogBoxParam函数调用就可以搞定,既不用注册窗口类,也不用写消息循环,这对看到创建窗口的几十行代码就感到头疼的读者来说是个福音,我也很喜欢用模态对话框作为程序的主窗口。这种方法的缺点就是无法使用依赖消息循环来完成功能,例如无法使用加速键。对话框程序的常用格式如下所示:

复制代码
#include <windows.h>
#include "resource.h"

#pragma comment(linker,"\"/manifestdependency:type='win32' \
    name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
    processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")

// 全局变量
HINSTANCE g_hInstance;

// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int   
nCmdShow)
{
    g_hInstance = hInstance;

    // 创建模态对话框
    DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
    return 0;
}

INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_INITDIALOG:
        // 设置标题栏左侧和任务栏中的程序小图标
        SendMessage(hwndDlg, WM_SETICON,
            ICON_SMALL, (LPARAM)LoadIcon(g_hInstance, MAKEINTRESOURCE(IDI_ICON_PANDA)));
        return TRUE;

    case WM_COMMAND:
        switch (LOWORD(wParam))
        {
        case IDC_BTN_BROWSE:    // 处理按下浏览按钮
            break;

        case IDC_BTN_OPEN:      // 处理按下打开按钮
            break;
        }
        return TRUE;

    case WM_CLOSE:
        EndDialog(hwndDlg, 0);
        return TRUE;
    }

    return FALSE;
}

具体代码参见Chapter9\DialogBoxProgram项目。如何设计对话框的客户区内容,以及要实现什么样的功能,是用户的工作。另外,前面介绍的普通重叠窗口中子窗口控件的用法基本上都适用于对话框中的控件,在此不再一一列举。简单起见,以后的示例程序基本上全部采用对话框程序。

表9.6再次列出WM_COMMAND消息的wParam和lParam参数的含义。

表9.6

从哪发送过来的消息 HIWORD(wParam) LOWORD(wParam) lParam
菜单命令项 0 菜单项ID 0
加速键 1 菜单项ID 0
子窗口控件 通知码 控件ID 控件句柄

前面说过,如果一个按钮的ID是IDOK,当键盘焦点不在其他控件上时,按下回车键就相当于按下了这个按钮,按下Esc键时相当于按下了ID为IDCANCEL的按钮,单击标题栏的关闭按钮也相当于按下了ID为IDCANCEL的按钮。要想避免在用户按下回车或Esc键时结束对话框,可以删除默认的"确定"和"取消"按钮,并删除WM_COMMAND消息中对IDOK和IDCANCEL的处理,要关闭对话框可以处理WM_CLOSE消息。

9.4 通过Photoshop切片和自绘技术实现一个优雅的程序界面

如果对Windows默认的程序界面不满意,熟悉Photoshop的读者,可以通过自己制作程序界面图片,并对需要的组件(例如按钮)进行切片,实现一个优雅的程序界面。例如ProgramPicture程序的运行效果如原书的图9.3所示,该程序所用图片素材是2002版本的金山毒霸软件。

有了素材,实现这样一个程序界面是非常简单的,本程序用到了.bmp图片素材(见原书的图9.4)。

程序界面背景可以通过处理WM_CTLCOLORDLG消息来实现;静态控件和复选框等的背景颜色可以通过处理WM_CTLCOLORSTATIC消息来实现;Home按钮、关闭按钮、浏览按钮和扫描按钮可以通过处理WM_DRAWITEM消息来进行按钮自绘,按钮指定为BS_OWNERDRAW样式后不应该再指定其他样式。需要注意的是,开始扫描和停止扫描用同一个按钮,因此需要一个BOOL全局变量g_bStartScanning以标记是否已经开始扫描,然后根据不同的状态显示不同的图片。

创建一个对话框程序,通过可视化的资源编辑器,可以很方便地插入各个子窗口控件。创建对话框资源后,需要设置对话框的Border属性为None,这样一来就去掉了对话框程序的标题栏。为了实现窗口拖动,可以处理WM_LBUTTONDOWN消息,没有标题栏就没有窗口标题,因此在WM_INITDIALOG消息中应该调用SetWindowText函数设置一个窗口标题(否则在任务栏中不显示窗口标题)。

还有一个问题,那就是各个子窗口控件的摆放位置和大小怎么确定。资源编辑器左下角有一个原型图像功能,可以选择Chapter9\ProgramPicture\ProgramPicture\ Res\JinShan.png作为原型图像,这样一来就可以将各个子窗口控件摆放到正确的位置并设置合理的大小,如原书的图9.5所示。

具体代码参见Chapter9\ProgramPicture项目。

最后补充一下,还可以调用DialogBoxIndirect宏或DialogBoxIndirectParam函数,通过内存中的对话框模板来创建模态对话框;调用CreateDialogIndirect宏或CreateDialogIndirectParam函数,通过内存中的对话框模板来创建非模态对话框。在内存中创建对话框模板的方法和在内存中创建菜单的方法类似,这些函数的用法很简单,如果需要,请读者自行参考MSDN。

相关推荐
David猪大卫1 小时前
【C++修炼】异常
开发语言·c++·经验分享·笔记·学习·考研·面试
youm20031 小时前
【学习笔记】reids的数据类型
redis·笔记·学习
问简2 小时前
Windows11自定义分区大小安装系统
windows
hanlin033 小时前
刷题笔记:力扣第84题-柱状图中最大的矩形
笔记·算法·leetcode
sukalot5 小时前
windows 驱动实例分析系列: HidHide驱动分析-HidHideClient 篇(中)
windows·驱动开发
一米阳光866111 小时前
软考高级【信息系统项目管理师】高项重要考点(3)高级项目管理-下 助你顺利上岸!
笔记·职场发展·软考·信息系统项目管理师·高级职称
dianziyao_11 小时前
第 4.1 篇:让 Codex 理解你的双链——AI 辅助发现笔记间的关联
人工智能·笔记
xian_wwq14 小时前
【学习笔记】OWASP Agentic 应用安全(二)
笔记·学习·安全·owasp
我是Superman丶16 小时前
Arduino IDE 安装教程:Windows/macOS/Linux 开发环境搭建
ide·windows·macos