【Windows】《深入浅出Windows API程序设计:编程基础篇》笔记-Chapter8-子窗口控件

第8章 子窗口控件

调用CreateWindow / CreateWindowEx函数创建窗口的时候,将窗口样式指定为WS_CHILD或WS_CHILDWINDOW就可以创建一个子窗口。子窗口的位置可以在父窗口客户区的任何地方,这样的子窗口需要程序注册窗口类并提供窗口过程。程序也可以通过系统预定义的窗口类和窗口过程来创建标准子窗口控件,程序中常见的标准子窗口控件有按钮、单选按钮、复选按钮、编辑框、组合框、列表框、滚动条控件等,像资源管理器窗口就包含多个子窗口控件,例如工具栏、树视图控件、滚动条控件等。标准子窗口控件也称为通用控件(Common Control)。

程序可以把窗口类名指定为系统预定义的相关子窗口控件类名来调用CreateWindow/CreateWindowEx函数创建一个子窗口控件,子窗口控件和父窗口之间可以互相发送消息来进行通信。子窗口控件在对话框程序中用得更普遍,不需要调用CreateWindow / CreateWindowEx函数,直接在资源脚本文件中定义子窗口控件即可,也可以通过资源编辑器从工具箱中拖曳子窗口控件到对话框程序界面的合适位置。

ComCtl32.dll提供对通用控件的支持。Windows Vista及以后的系统支持通用控件6系列版本,程序可以通过调用DllGetVersion函数来确定系统中ComCtl32.dll的版本号,DllGetVersion函数由动态链接库Shell32.dll提供,如果需要调用该函数,则只能通过LoadLibrary和GetProcAddress函数动态加载。

常见的子窗口控件的系统预定义窗口类名如表8.1所示。

表8.1

系统预定义的窗口类名字符串 含义
Button 按钮(普通按钮、单选按钮、复选按钮、分组框)
Edit 编辑框
ListBox 列表框
ComboBox 组合框
ScrollBar 滚动条
Static 静态控件
MDIClient MDI客户窗口

另外,有一些类名是系统内部使用的,例如ComboLBox(多一个L)表示组合框内的列表框的类名,#32770表示对话框类名等。

8.1 按钮类

按钮(Button)类有许多样式属性,基于Button类指定不同的样式可以创建普通按钮、单选按钮、复选按钮和分组框等。Button类可以指定的按钮样式如表8.2所示,BS_前缀表示Button Style。

表8.2

常量 含义
BS_PUSHBUTTON 普通按钮
BS_DEFPUSHBUTTON 默认按钮,和普通按钮一样,但是有一个较深的轮廓。如果是在对话框程序中,当其他按钮没有键盘焦点时,用户可以通过按下Enter键选择该按钮
BS_RADIOBUTTON 单选按钮,可以选中、取消选中
BS_AUTORADIOBUTTON 自动单选按钮
BS_CHECKBOX 复选框,可以选中、取消选中
BS_AUTOCHECKBOX 自动复选框
BS_3STATE 三态复选框,可以选中、取消选中或显示为灰色,灰色状态表示未确定复选框的状态
BS_AUTO3STATE 自动三态复选框
BS_LEFTTEXT或BS_RIGHTBUTTON 对于单选按钮、复选框或三态复选框,文本默认显示在按钮右侧,该样式表示显示在按钮左侧
BS_TEXT 按钮矩形内显示文本
BS_MULTILINE 如果文本字符串太长而无法显示在一行,则将按钮文本显示为多行
BS_LEFT 左对齐按钮矩形内的文本
BS_RIGHT 右对齐按钮矩形内的文本
BS_CENTER 文本在按钮矩形内水平居中
BS_TOP 文本在按钮矩形的顶部
BS_BOTTOM 文本在按钮矩形的底部
BS_VCENTER 文本在按钮矩形内垂直居中
BS_FLAT 按钮默认具有三维样式,该样式表示按钮是二维样式
BS_PUSHLIKE 使单选按钮、复选框或三态复选框看起来像按钮一样
BS_OWNERDRAW 自绘按钮,当按钮需要重绘时父窗口会收到WM_DRAWITEM消息,不能将该样式与任何其他按钮样式一起使用
BS_ICON 图标按钮
BS_BITMAP 位图按钮
BS_GROUPBOX 分组框,用于对其他控件进行分组,由一个矩形和显示在矩形左上角的文本组成
BS_NOTIFY 系统可以发送包含BN_KILLFOCUS和BN_SETFOCUS通知码的WM_COMMAND消息到其父窗口
BS_SPLITBUTTON 拆分按钮,拆分按钮分为两部分,左侧是主要部分,类似于普通或默认按钮;右侧有一个向下的小箭头,单击箭头时可以弹出一个菜单。拆分按钮是通用控件版本6中引入的一种按钮,如原书图所示:
BS_DEFSPLITBUTTON 默认拆分按钮,有一个较深的轮廓。如果是在对话框程序中,当其他按钮没有键盘焦点时,用户可以通过按下Enter键选择该按钮
BS_COMMANDLINK 命令链接按钮,左侧有一个蓝色箭头指向按钮文本(Windows 7系统中为绿色箭头)。命令链接按钮是通用控件版本6中引入的一种按钮,如原书图所示:
BS_DEFCOMMANDLINK 默认命令链接按钮,有一个较深的轮廓。如果是在对话框程序中,当其他按钮没有键盘焦点时,用户可以通过按下Enter键选择该按钮

下面介绍自动单选按钮、自动复选框和自动三态复选框。以三态复选框与自动三态复选框为例,用户单击三态复选框以后不会自动选中或变灰,程序需要响应单击事件设置选中或变灰状态;而如果是自动三态复选框,则单击一次就会自动选中,再单击一次则复选框显示为灰色,第三次单击则会取消选中,如此循环,程序在需要的时候只需要获取其状态即可。

普通按钮、默认按钮、单选按钮和复选框都可以同时指定BS_ICON或BS_BITMAP样式,例如BS_PUSHBUTTON | BS_BITMAP,或BS_AUTORADIOBUTTON | BS_BITMAP,表示在普通按钮上显示一副位图,或单选按钮旁边显示一副位图。BS_ICON或BS_BITMAP样式也可以单独使用,表示在普通按钮上显示一个图标或一副位图。指定BS_ICON或BS_BITMAP样式以后,需要发送BM_SETIMAGE消息为其设置图标或位图,后面再详细解释这个消息。

BS_前缀的样式是按钮专用样式。除了这些按钮专用样式,因为子窗口控件也是一个窗口,所以大部分用于普通窗口的API函数和窗口样式适用于子窗口控件。例如表8.3的窗口样式可以用于按钮。

表8.3

窗口样式 含义
WS_CHILD或
WS_CHILDWINDOW 窗口是子窗口
WS_VISIBLE 窗口最初可见,可以通过调用ShowWindow或SetWindowPos函数显示和隐藏窗口
WS_GROUP 该窗口是一组控件的第一个控件,该组由第一个具有WS_GROUP样式的控件和在其后定义的所有控件组成,直到下一个具有WS_GROUP样式的控件(不包括该控件)出现。如果是在对话框程序中,用户可以使用方向键将键盘焦点从组中的一个控件移动到下一个控件。另外,每个组中的第一个控件通常具有WS_TABSTOP样式,如果是在对话框程序中,按下Tab键可以将键盘焦点移动到下一个具有WS_TABSTOP样式的控件
WS_TABSTOP 该窗口是一个控件,当用户按下Tab键时,该控件可以接收键盘焦点,如果是在对话框程序中,按下Tab键可以将键盘焦点移动到下一个具有WS_TABSTOP样式的控件上
WS_BORDER 窗口有一个细线边框
WS_DLGFRAME 窗口具有对话框样式的边框
WS_SIZEBOX或
WS_THICKFRAME 窗口具有大小调整边框
WS_DISABLED 窗口最初被禁用,禁用的窗口无法接收用户的输入,要想启用可以调用EnableWindow函数

其中,WS_CHILD和WS_VISIBLE是必须指定的,WS_CHILD表示该窗口是一个子窗口。如果不指定WS_VISIBLE样式,则子窗口控件不会显示。

另外,一些扩展窗口样式也可以用于子窗口控件,例如WS_EX_ACCEPTFILES表示该子窗口控件可以接受拖放文件。

8.1.1 按钮与父窗口通信

按钮可以向其父窗口发送消息,父窗口也可以向按钮发送消息。父窗口可以通过调用SendMessage或SendDlgItemMessage函数发送消息到子窗口控件:

复制代码
LRESULT SendMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
LRESULT SendDlgItemMessage(HWND hDlg, int nIDDlgItem, UINT Msg, WPARAM wParam, LPARAM lParam);

SendDlgItemMessage函数的Dlg指的是Dialog对话框,该函数通常用于在对话框程序中向子窗口控件发送消息,但是也可以在普通重叠窗口中向子窗口控件发送消息。hDlg参数指定父窗口句柄,nIDDlgItem参数指定子窗口控件ID,每个子窗口控件都有一个ID。调用CreateWindow/CreateWindowEx函数创建子窗口控件的时候,第10个参数hMenu不再指定为菜单句柄,而是指定为子窗口控件ID。

(1)对于BS_ICON或BS_BITMAP样式的按钮,BM_SETIMAGE消息用于为按钮设置图标或位图,wParam参数指定图像类型,可以是IMAGE_ICON(图标)或IMAGE_BITMAP(位图),lParam参数可以指定为图像的句柄(HICON或HBITMAP),返回值是先前与按钮关联的图像的句柄(如果有的话),否则返回值是NULL;BM_GETIMAGE消息用于获取与按钮关联的图标或位图的句柄,wParam参数指定图像类型,可以是IMAGE_ICON(图标)或IMAGE_BITMAP(位图),lParam参数没有用到,返回值是与按钮关联的图像的句柄(如果有的话),否则返回值是NULL。

(2)BM_SETSTYLE消息用于设置按钮的样式,wParam参数可以指定为按钮新样式的组合;lParam参数可以指定为TRUE(重绘按钮)或FALSE(不重绘按钮)。当然,通过调用SetWindowLongPtr函数也可以达到同样的目的。

(3)BM_SETCHECK消息用于设置单选按钮、复选框或三态复选框的选中状态,wParam参数可以指定为BST_CHECKED(选中)、BST_UNCHECKED(取消选中)或BST_INDETERMINATE(灰色,表示不确定状态,用于三态复选框),lParam参数没有用到,指定为0即可;BM_GETCHECK消息用于获取单选按钮、复选框或三态复选框的选中状态,wParam和lParam参数没有用到,都指定为0即可,返回值可以是BST_CHECKED(已选中)、BST_UNCHECKED(未选中)或BST_INDETERMINATE(灰色,表示不确定状态,用于三态复选框)。

(4)BM_SETSTATE消息用于设置普通按钮是否按下的状态,wParam参数可以指定为TRUE(突出显示,按下状态)或FALSE(取消突出显示,非按下状态),lParam参数没有用到;BM_GETSTATE消息用于获取普通按钮、单选按钮、复选框或三态复选框的当前状态(选中状态,是否按下和是否具有键盘焦点等),wParam和lParam参数没有用到,都指定为0即可,返回值可以是BST_CHECKED(已选中)、BST_UNCHECKED(未选中)、BST_INDETERMINATE(灰色,表示不确定状态,用于三态复选框)、BST_PUSHED(普通按钮处于按下状态)、BST_FOCUS(按钮具有键盘焦点)、BST_HOT(鼠标悬停在按钮上)、BST_DROPDOWNPUSHED(按钮处于下拉状态,并且仅当按钮具有BTNS_DROPDOWN样式时用于工具栏按钮)。

(5)BM_CLICK消息可以模拟用户单击按钮,该消息的wParam和lParam参数都没有用到。该消息会导致系统向按钮的父窗口发送包含BN_CLICKED通知码的WM_COMMAND消息。

调用CheckDlgButton或CheckRadioButton函数等同于发送BM_SETCHECK消息;调用IsDlgButtonChecked函数等同于发送BM_GETCHECK消息:

复制代码
BOOL CheckDlgButton(
    _In_ HWND hDlg,             // 父窗口句柄
    _In_ int  nIDButton,        // 子窗口控件ID
    _In_ UINT uCheck);          // 设置选中状态,BST_CHECKED、BST_UNCHECKED或BST_INDETERMINATE
BOOL CheckRadioButton(
    _In_ HWND hDlg,             // 父窗口句柄
    _In_ int  nIDFirstButton,   // 组中第一个单选按钮的ID
    _In_ int  nIDLastButton,    // 组中最后一个单选按钮的ID
_In_ int  nIDCheckButton);      // 要设置选中的单选按钮的ID

UINT IsDlgButtonChecked(
    _In_ HWND hDlg,             // 父窗口句柄
    _In_ int nIDButton);        // 子窗口控件ID,函数返回值可以是BST_CHECKED、BST_UNCHECKED
                                // 或BST_INDETERMINATE

一组复选按钮中通常可以同时选中多个,而一组单选按钮中通常只能选中一个。CheckDlgButton函数通常用于设置复选框的选中状态。

CheckDlgButton函数也可以用于设置单选按钮,但是如果在一组单选按钮中选中一个,还需要多次调用CheckDlgButton取消选中其他的,所以设置单选按钮的状态通常使用CheckRadioButton函数。指定一组单选按钮的第一个和最后一个以及需要选中哪一个,函数会自动完成任务。

建议使用自动单选按钮,同组的单选按钮会随着用户选中一个而自动取消选中其他单选按钮。在程序中只需要在初始化的时候设置选中哪一个,并在需要的时候调用IsDlgButtonChecked函数检查选中状态即可。同样,复选按钮也不会因为用户的单击而自动变为选中或取消选中等,因此建议使用自动复选按钮,在初始化的时候设置选中哪些,并在需要的时候调用IsDlgButtonChecked函数检查每一个的状态即可。

当用户单击按钮时,它会接收键盘焦点,系统会向按钮的父窗口发送包含BN_CLICKED通知码的WM_COMMAND消息。不过对于自动单选按钮、自动复选框或自动三态复选框通常不需要处理该消息,因为这些按钮可以自动设置其状态。如果是普通按钮,则可能需要处理该消息。表8.4再次列出WM_COMMAND消息的wParam和lParam参数的含义。

表8.4

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

除了BN_CLICKED通知码,其他常见的与按钮有关的包含在WM_COMMAND消息中的通知码如表8.5所示。

表8.5

通知码 含义
BN_SETFOCUS 按钮获得了键盘焦点
BN_KILLFOCUS 按钮失去了键盘焦点
BN_DBLCLK或BN_DOUBLECLICKED 双击按钮

获得键盘焦点的按钮周围会显示一圈虚线,此时按下空格键就相当于单击了按钮。只有具有BS_NOTIFY样式的按钮才会发送BN_SETFOCUS、BN_KILLFOCUS和BN_DBLCLK通知码,但是无论如何设置按钮样式,单击按钮都会发送BN_CLICKED通知码。

BS_GROUPBOX样式表示分组框,用于对其他控件进行分组,由一个矩形和显示在矩形左上角的文本组成。分组框不会获得键盘焦点,既不处理鼠标或键盘输入,也不会导致系统发送WM_COMMAND消息到父窗口。

对于自绘按钮、组合框和列表框等,程序只需要在必要的时候负责绘制它们的外观即可,系统会正常处理用户和这些控件的交互,例如用户单击控件,系统会向父窗口发送WM_COMMAND消息。在介绍自绘菜单的时候说过,WM_DRAWITEM消息既用于菜单项,也用于其他一些子窗口控件的自绘,当菜单项或子窗口控件需要自绘的时候,窗口过程会收到WM_DRAWITEM消息。如果WM_DRAWITEM消息是由子窗口控件发送的,则wParam参数是子窗口控件的ID;如果是由菜单项发送的,则wParam参数为0。lParam参数是一个指向DRAWITEMSTRUCT结构的指针,关于该结构的定义参见6.1.5节。

接下来实现一个例子,Buttons程序在窗口客户区中使用系统预定义的Button类调用CreateWindowEx函数分别创建了普通按钮、图标按钮、位图按钮、自绘按钮、一组自动单选按钮、一组自动复选按钮、一组自动三态复选按钮、默认按钮。Buttons.cpp源文件的内容如下所示:

复制代码
#include <Windows.h>
#include <tchar.h>
#include <strsafe.h>
#include "resource.h"

// 函数声明
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
// 按下默认按钮
VOID OnDefPushButton(HWND hwnd);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int   
                       nCmdShow)
{
    WNDCLASSEX wndclass;
    TCHAR szAppName[] = TEXT("Buttons");    // 程序标题、窗口类名
    HWND hwnd;
    MSG msg;

    wndclass.cbSize = sizeof(WNDCLASSEX);
    wndclass.style = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc = WindowProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = hInstance;
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = szAppName;
    wndclass.hIconSm = NULL;
    RegisterClassEx(&wndclass);

    hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 300, 600, NULL, NULL, hInstance, NULL);

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

    while (GetMessage(&msg, NULL, 0, 0) != 0)
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return msg.wParam;
}

// 全局变量
struct
{
    int     m_nStyle;
    PTSTR   m_pText;
}Buttons[] = {
    BS_PUSHBUTTON | BS_NOTIFY | WS_TABSTOP,             TEXT("普通按钮"), // CtrlID 1000
    BS_ICON | BS_NOTIFY | WS_TABSTOP,                   TEXT("图标按钮"),
    BS_BITMAP | BS_NOTIFY | WS_TABSTOP,                 TEXT("位图按钮"),
    BS_OWNERDRAW,                                       TEXT("自绘按钮"),

    BS_GROUPBOX,                                        TEXT("政治面貌"), // CtrlID 1004
    BS_AUTORADIOBUTTON | BS_NOTIFY | WS_GROUP | WS_TABSTOP, TEXT("中共党员"),
    BS_AUTORADIOBUTTON | BS_NOTIFY,                     TEXT("共青团员"),
    BS_AUTORADIOBUTTON | BS_NOTIFY,                     TEXT("无党派人士"),

    BS_GROUPBOX,                                        TEXT("个人爱好"), // CtrlID 1008
    BS_AUTOCHECKBOX | BS_NOTIFY | WS_GROUP | WS_TABSTOP,    TEXT("看书"),
    BS_AUTOCHECKBOX | BS_NOTIFY,                        TEXT("唱歌"),
    BS_AUTOCHECKBOX | BS_NOTIFY,                        TEXT("听音乐"),

    BS_GROUPBOX,                                        TEXT("荣誉称号"), // CtrlID 1012
    BS_AUTO3STATE | BS_NOTIFY | WS_GROUP | WS_TABSTOP,  TEXT("团队核心"),
    BS_AUTO3STATE | BS_NOTIFY,                          TEXT("技术能手"),
    BS_AUTO3STATE | BS_NOTIFY,                          TEXT("先进个人"),

    BS_DEFPUSHBUTTON | BS_NOTIFY | WS_TABSTOP,          TEXT("默认按钮"), // CtrlID 1016
};

#define NUM (sizeof(Buttons) / sizeof(Buttons[0]))

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    static HWND hwndButton[NUM];                // 子窗口控件句柄数组
    int arrPos[NUM] = {10, 40, 70, 100,         // 每个子窗口控件的起始Y坐标
        130, 150, 180, 210, 
        250, 270, 300, 330, 
        370, 390, 420, 450, 
        490 };
    LPDRAWITEMSTRUCT lpDIS;

    switch (uMsg)
    {
    case WM_CREATE:
        // 创建17个子窗口控件
        for (int i = 0; i < NUM; i++)
        {
            hwndButton[i] = CreateWindowEx(0, TEXT("Button"), Buttons[i].m_pText,
                WS_CHILD | WS_VISIBLE | Buttons[i].m_nStyle, 20, arrPos[i],
                150, 25, hwnd, (HMENU)(1000 + i), 
                ((LPCREATESTRUCT)lParam)->hInstance, NULL);
        }

        // 移动3个分组框的位置
        MoveWindow(hwndButton[4], 10, arrPos[4], 170, 115, TRUE);
        MoveWindow(hwndButton[8], 10, arrPos[8], 170, 115, TRUE);
        MoveWindow(hwndButton[12], 10, arrPos[12], 170, 115, TRUE);

        // 为图标按钮、位图按钮设置图标、位图
        SendDlgItemMessage(hwnd, IDC_ICONBUTTON, BM_SETIMAGE, IMAGE_ICON,
            (LPARAM)LoadImage(((LPCREATESTRUCT)lParam)->hInstance, MAKEINTRESOURCE(IDI_SMILE),
                IMAGE_ICON, 20, 20, LR_DEFAULTCOLOR));
        SendDlgItemMessage(hwnd, IDC_BITMAPBUTTON, BM_SETIMAGE, IMAGE_BITMAP,
            (LPARAM)LoadBitmap(((LPCREATESTRUCT)lParam)->hInstance, MAKEINTRESOURCE(IDB                                   _KONGLONG)));

        // 设置默认按钮的文本
        SetDlgItemText(hwnd, IDC_DEFPUSHBUTTON, TEXT("获取单选复选状态"));

        // 单选按钮组、复选按钮组、三态复选按钮组,默认情况下分别选中一项
        CheckRadioButton(hwnd, IDC_AUTORADIOBUTTON1, IDC_AUTORADIOBUTTON3, IDC_AUTORADI-  
                             OBUTTON2);
        CheckDlgButton(hwnd, IDC_AUTOCHECKBOX3, BST_CHECKED);
        CheckDlgButton(hwnd, IDC_AUTO3STATE2, BST_INDETERMINATE);
        return 0;

    case WM_COMMAND:
        if (HIWORD(wParam) == BN_CLICKED)   // 可以省略该判断
        {
            switch (LOWORD(wParam))
            {
            // 子窗口控件ID常量定义参见resource.h,可以根据需要在此处理每个控件的单击事件
            case IDC_PUSHBUTTON:      break;
            case IDC_ICONBUTTON:      break;
            case IDC_BITMAPBUTTON:    break;
            case IDC_OWNERDRAWBUTTON: break;
            case IDC_AUTORADIOBUTTON1:break;
            case IDC_AUTORADIOBUTTON2:break;
            case IDC_AUTORADIOBUTTON3:break;
            case IDC_AUTOCHECKBOX1:   break;
            case IDC_AUTOCHECKBOX2:   break;
            case IDC_AUTOCHECKBOX3:   break;
            case IDC_AUTO3STATE1:     break;
            case IDC_AUTO3STATE2:     break;
            case IDC_AUTO3STATE3:     break;
            case IDC_DEFPUSHBUTTON:   OnDefPushButton(hwnd); break;
            }
        }
        return 0;

    case WM_DRAWITEM:
        lpDIS = (LPDRAWITEMSTRUCT)lParam;
        // 先把按钮矩形填充为和窗口背景一致的白色,然后画一个黑色圆角矩形
        SelectObject(lpDIS->hDC, GetStockObject(NULL_PEN));
        SelectObject(lpDIS->hDC, GetStockObject(WHITE_BRUSH));
        Rectangle(lpDIS->hDC, 0, 0, lpDIS->rcItem.right + 1, lpDIS->rcItem.bottom + 1);
        SelectObject(lpDIS->hDC, GetStockObject(BLACK_BRUSH));
        RoundRect(lpDIS->hDC, 0, 0, lpDIS->rcItem.right + 1, lpDIS->rcItem.bottom + 1, 20, 20);

        // 当用户单击按钮的时候,绘制COLOR_HIGHLIGHT颜色的圆角矩形
        if (lpDIS->itemState & ODS_SELECTED)
        {
          SelectObject(lpDIS->hDC, GetSysColorBrush(COLOR_HIGHLIGHT));
          RoundRect(lpDIS->hDC, 0, 0, lpDIS->rcItem.right + 1, lpDIS->rcItem.bottom + 1, 20, 20);
        }
        // 当按钮获得焦点的时候,可以绘制一个焦点矩形
        if (lpDIS->itemState & ODS_FOCUS)
        {
          InflateRect(&lpDIS->rcItem, -2, -2);
          DrawFocusRect(lpDIS->hDC, &lpDIS->rcItem);
        }

        // 自绘按钮的文本,透明背景的白色文字
        SetBkMode(lpDIS->hDC, TRANSPARENT);
        SetTextColor(lpDIS->hDC, RGB(255, 255, 255));
        DrawText(lpDIS->hDC, TEXT("自绘按钮"), _tcslen(TEXT("自绘按钮")),
            &lpDIS->rcItem, DT_CENTER | DT_VCENTER | DT_SINGLELINE);

        // 恢复设备环境
        SelectObject(lpDIS->hDC, GetStockObject(BLACK_PEN));
        SelectObject(lpDIS->hDC, GetStockObject(WHITE_BRUSH));
        return TRUE;

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

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

VOID OnDefPushButton(HWND hwnd)
{
    TCHAR szBuf[128] = { 0 };

    if (IsDlgButtonChecked(hwnd, IDC_AUTORADIOBUTTON1) & BST_CHECKED)
        StringCchCopy(szBuf, _countof(szBuf), TEXT("政治面貌:中共党员\n"));
    if (IsDlgButtonChecked(hwnd, IDC_AUTORADIOBUTTON2) & BST_CHECKED)
        StringCchCopy(szBuf, _countof(szBuf), TEXT("政治面貌:共青团员\n"));
    if (IsDlgButtonChecked(hwnd, IDC_AUTORADIOBUTTON3) & BST_CHECKED)
        StringCchCopy(szBuf, _countof(szBuf), TEXT("政治面貌:无党派人士\n"));

    StringCchCat(szBuf, _countof(szBuf), TEXT("个人爱好:"));
    if (IsDlgButtonChecked(hwnd, IDC_AUTOCHECKBOX1) & BST_CHECKED)
        StringCchCat(szBuf, _countof(szBuf), TEXT("看书 "));
    if (IsDlgButtonChecked(hwnd, IDC_AUTOCHECKBOX2) & BST_CHECKED)
        StringCchCat(szBuf, _countof(szBuf), TEXT("唱歌 "));
    if (IsDlgButtonChecked(hwnd, IDC_AUTOCHECKBOX3) & BST_CHECKED)
        StringCchCat(szBuf, _countof(szBuf), TEXT("听音乐"));
    StringCchCat(szBuf, _countof(szBuf), TEXT("\n"));

    StringCchCat(szBuf, _countof(szBuf), TEXT("荣誉称号:"));
    if (IsDlgButtonChecked(hwnd, IDC_AUTO3STATE1) & BST_CHECKED)
        StringCchCat(szBuf, _countof(szBuf), TEXT("团队核心 "));
    if (IsDlgButtonChecked(hwnd, IDC_AUTO3STATE2) & BST_CHECKED)
        StringCchCat(szBuf, _countof(szBuf), TEXT("技术能手 "));
    if (IsDlgButtonChecked(hwnd, IDC_AUTO3STATE3) & BST_CHECKED)
        StringCchCat(szBuf, _countof(szBuf), TEXT("先进个人"));
    StringCchCat(szBuf, _countof(szBuf), TEXT("\n"));

    MessageBox(hwnd, szBuf, TEXT("个人简介汇总"), MB_OK);
}

具体代码参见Chapter8\Buttons项目。在WM_CREATE消息中,程序使用系统预定义的Button类调用CreateWindowEx函数分别创建了17个子窗口控件,lpWindowName参数对于普通重叠窗口或弹出窗口来说是窗口标题,对于按钮来说是显示在按钮上的文本;窗口位置参数x和y指定子窗口左上角的位置,相对于父窗口客户区左上角;宽度和高度参数nWidth和nHeight指定每个子窗口的宽度和高度;因为创建的是子窗口,所以父窗口参数hWndParent指定为hwnd;对于普通重叠窗口或弹出窗口来说,hMenu参数指定菜单句柄,对于子窗口来说,则用于指定其ID,每个子窗口的ID应该是唯一的,在WM_COMMAND消息中通过子窗口ID来确定是从哪个子窗口发送过来的消息。

然后调用MoveWindow函数改变3个分组框的位置和大小,让每个分组框可以包围相应的组。分组框没有什么实际意义,仅仅提供一种视觉上的分组效果。一个组的界定依靠的是WS_GROUP样式,一个组由第一个具有WS_GROUP样式的控件和在其后定义的所有控件组成,直到下一个具有WS_GROUP样式的控件出现。如果是在对话框程序中,用户可以使用方向键将键盘焦点从组中的一个控件移动到下一个控件。每个组中的第一个控件通常具有WS_TABSTOP样式,如果是在对话框程序中,按下Tab键可以将键盘焦点移动到下一个具有WS_TABSTOP样式的控件上。但是,默认情况下,普通重叠窗口中的子窗口控件无法使用方向键或Tab键达到上述目的,这是一个问题。

然后程序调用SendDlgItemMessage函数向图标按钮和位图按钮分别发送BM_SETIMAGE消息设置其图像。资源脚本文件中的图标大小是32 32,而按钮大小为150 25,因此加载图标用的是LoadImage函数,该函数可以指定图标的宽度和高度。资源脚本文件中的恐龙位图大小正好是150 25,所以使用LoadBitmap函数直接加载即可。最后一个子窗口控件是默认按钮,程序调用SetDlgItemText函数改变按钮文本为"获取单选复选状态"。

SetWindowText函数可以设置指定程序窗口的窗口标题,也可以设置子窗口控件的文本:

复制代码
BOOL WINAPI SetWindowText(
    _In_     HWND    hWnd,      // 程序窗口或控件的句柄,其文本将被更改
    _In_opt_ LPCTSTR lpString); // 新窗口标题或控件文本

也可以通过发送WM_SETTEXT消息达到同样的目的。但如果是设置其他程序中的窗口标题或控件文本,则需要发送WM_SETTEXT消息,而不能调用SetWindowText。

GetWindowText函数用于获取指定程序窗口的窗口标题或子窗口控件的文本:

复制代码
int WINAPI GetWindowText(
    _In_  HWND   hWnd,         // 程序窗口或控件的句柄
    _Out_ LPTSTR lpString,     // 接收文本的缓冲区
    _In_  int    nMaxCount);   // 缓冲区的大小,以字符为单位

如果函数执行成功,则返回值是复制到缓冲区中的字符串长度,以字符为单位,不包括终止空字符;如果窗口没有标题或控件没有文本,或指定的窗口、控件句柄无效,则返回值为0。也可以通过发送WM_GETTEXT消息达到同样的目的。但如果是获取其他程序中的窗口标题或控件文本,则需要发送WM_GETTEXT消息,而不能调用GetWindowText。

在获取指定程序窗口的窗口标题或子窗口控件的文本前,可以先调用GetWindowTextLength函数获取其文本的长度:

复制代码
int WINAPI GetWindowTextLength(_In_ HWND hWnd);

如果函数执行成功,则返回值是窗口标题或子窗口控件文本的长度,以字符为单位,不包括终止的空字符,程序可以根据返回值分配合适大小的缓冲区以进一步获取具体的文本;如果窗口没有标题或控件没有文本,或指定的窗口、控件句柄无效,则返回值为0。也可以通过发送WM_GETTEXTLENGTH消息达到同样的目的。但如果是获取其他程序中的窗口标题或控件文本长度,则需要发送WM_GETTEXTLENGTH消息,而不能调用GetWindowTextLength。

对于子窗口控件,SetWindowText和GetWindowText函数需要提供子窗口控件句柄,这可能需要先调用GetDlgItem函数获取子窗口控件句柄:

复制代码
HWND WINAPI GetDlgItem(
    _In_opt_ HWND hDlg,         // 父窗口句柄
    _In_     int  nIDDlgItem);  // 子窗口控件ID

如果已经知道了子窗口控件句柄,可以通过调用GetDlgCtrlID函数获取其ID:

复制代码
int WINAPI GetDlgCtrlID(_In_ HWND hwndCtl);

当然,要获取子窗口控件ID,还可以通过指定GWLP_ID索引调用GetWindowLongPtr函数获取。

要获取子窗口控件文本,还有一个更简单的函数:

复制代码
UINT WINAPI GetDlgItemText(
    _In_  HWND   hDlg,         // 父窗口句柄
    _In_  int    nIDDlgItem,   // 子窗口控件ID
    _Out_ LPTSTR lpString,     // 接收文本的缓冲区
    _In_  int    nMaxCount);   // 缓冲区的长度,以字符为单位

如果函数执行成功,则返回值是复制到缓冲区的字符数,不包括终止空字符;如果函数执行失败,则返回值为0。

要设置子窗口控件文本可以使用SetDlgItemText函数:

复制代码
BOOL WINAPI SetDlgItemText(
    _In_ HWND    hDlg,         // 父窗口句柄
    _In_ int     nIDDlgItem,   // 子窗口控件ID
    _In_ LPCTSTR lpString);    // 字符串指针

上面这两个函数实际上也是通过发送WM_GETTEXT和WM_SETTEXT消息实现的。

另外需要介绍的只有WM_DRAWITEM消息的处理了。如果不处理该消息,那么自绘按钮只显示一个灰色背景,因为程序窗口客户区是白色背景,所以先把自绘按钮矩形填充为和窗口背景一致的白色,然后画一个黑色圆角矩形,这是自绘按钮的正常状态。当用户单击按钮的时候,绘制COLOR_HIGHLIGHT颜色的圆角矩形,当按钮获得焦点的时候,绘制一个焦点矩形,最后绘制自绘按钮的文本,其背景是透明的,文字颜色为白色。

再介绍两个函数。要接收鼠标和键盘输入,子窗口必须是可见(显示)并且启用的。如果一个子窗口是可见的,但是没有启用,那么子窗口中的文本是灰色的。如果在创建子窗口的时候没有指定WS_VISIBLE样式,则子窗口将不会显示,程序可以通过调用ShowWindow函数来显示:

复制代码
ShowWindow(hwndChild, SW_SHOW);

程序可以通过调用以下语句来隐藏一个子窗口:

复制代码
ShowWindow(hwndChild, SW_HIDE);

可以通过调用IsWindowVisible函数来判断一个窗口是否可见:

复制代码
IsWindowVisible(hwnd);

可以通过调用EnableWindow函数启用或禁用指定的窗口或子窗口控件。窗口禁用以后无法接收鼠标和键盘输入:

复制代码
BOOL WINAPI EnableWindow(
    _In_ HWND hWnd,     // 窗口或子窗口控件句柄
    _In_ BOOL bEnable); // TRUE表示启用,FALSE表示禁用

可以通过调用IsWindowEnabled函数来判断一个窗口或子窗口控件是否处于启用状态:

复制代码
BOOL WINAPI IsWindowEnabled(_In_ HWND hWnd);

最后,看一下Buttons程序存在的几个问题。

(1)可以看到按钮都有一个灰色背景,而程序窗口客户区的背景是白色背景,两者颜色不一致,看上去不太美观。要想将客户区的背景改变为灰色背景很简单,只需要引入wndclass.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);语句即可(如果想更改子窗口控件的背景,则稍微麻烦一些)。

(2)子窗口控件的文本字体有点难看,这个问题很好解决,只需要调用SendMessage或SendDlgItemMessage函数给每个子窗口控件发送一个WM_SETFONT消息即可,wParam参数可以指定为新字体句柄,lParam参数可以指定为TRUE或FALSE,表示是否立即重绘控件。例如下面的代码:

复制代码
hFont = CreateFont(12, 0, 0, 0, 0, 0, 0, 0, GB2312_CHARSET, 0, 0, 0, 0, TEXT("宋体"));
for (int i = 0; i < NUM; i++)
    SendMessage(hwndButton[i], WM_SETFONT, (WPARAM)hFont, FALSE);

(3)要修改这些子窗口控件的视觉样式,可以用鼠标右键单击一个文件,然后选择"属性",打开属性对话框,看一看Windows 10系统中诸如"确定""取消"等按钮的视觉样式,以及当鼠标经过时按钮的色彩变化。

(4)虽然设置了WS_GROUP、WS_TABSTOP样式,但还是无法使用方向键或Tab键来达到移动焦点到组中另一个控件或移动焦点到下一个具有WS_TABSTOP样式的控件上的目的。

8.1.2 系统颜色

Windows提供了几十种系统预定义的颜色,用于显示窗口、菜单栏、工具栏、滚动条、按钮等不同的部分。程序可以通过调用GetSysColor函数获取这些颜色值,也可以通过调用SetSysColors函数改变系统预定义的颜色值,但是这会影响其他程序。表8.6列出了部分系统预定义的颜色。(颜色效果请查看Chapter8\SysColors.png。)

表8.6

常量 含义 颜色
COLOR_3DFACE、COLOR_BTNFACE 三维显示元素和对话框背景的颜色,文本颜色为COLOR_BTNTEXT
COLOR_BTNTEXT 按钮上的文字,背景色为COLOR_BTNFACE
COLOR_3DHIGHLIGH、COLOR_3DHILIGHT、COLOR_BTNHILIGHT、COLOR_BTNHIGHLIGHT 三维显示元素的高亮颜色
COLOR_3DLIGHT 三维显示元素的浅色
COLOR_3DSHADOW、COLOR_BTNSHADOW 三维显示元素的阴影颜色
COLOR_3DDKSHADOW 三维显示元素的暗阴影颜色
COLOR_ACTIVEBORDER 活动窗口边框颜色
COLOR_ACTIVECAPTION 活动窗口标题栏背景色,文本颜色为COLOR_CAPTIONTEXT
COLOR_CAPTIONTEXT 活动窗口标题栏文本颜色,背景色为COLOR_ACTIVECAPTION
COLOR_INACTIVEBORDER 非活动窗口边框颜色
COLOR_INACTIVECAPTION 非活动窗口标题栏背景色,文本颜色为COLOR_INACTIVECAPT- IONTEXT
COLOR_INACTIVECAPTIONTEXT 非活动窗口标题栏文本颜色,背景色为COLOR_INACTIVECAPTION
COLOR_APPWORKSPACE 多文档界面(MDI)程序的背景色
COLOR_GRAYTEXT 灰色(禁用)文本颜色
COLOR_HIGHLIGHT 选择的控件背景色,文本颜色为COLOR_HIGHLIGHTTEXT
COLOR_HIGHLIGHTTEXT 选择的控件文本颜色,背景色为COLOR_HIGHLIGHT
COLOR_HOTLIGHT 超链接的文本颜色,背景色为COLOR_WINDOW
COLOR_INFOBK 工具提示控件的背景色,文本颜色为COLOR_INFOTEXT
COLOR_INFOTEXT 工具提示控件的文本颜色,背景色为COLOR_INFOBK
COLOR_MENUBAR 菜单栏的背景色
COLOR_MENU 菜单背景色,文本颜色为COLOR_MENUTEXT
COLOR_MENUTEXT 菜单中的文本颜色,背景色为COLOR_MENU
COLOR_MENUHILIGHT 突出显示菜单项的颜色
COLOR_SCROLLBAR 滚动条背景色
COLOR_WINDOWFRAME 窗口边框颜色
COLOR_WINDOW 窗口背景,文本颜色为COLOR_WINDOWTEXT和COLOR_HOTLITE
COLOR_WINDOWTEXT 窗口中的文本颜色,背景色为COLOR_WINDOW

例如,COLOR_BTNFACE用于三维显示元素和对话框背景的颜色,也是普通按钮的表面颜色和其他按钮的背景颜色,它们对应的文本颜色为COLOR_BTNTEXT,普通窗口的窗口背景和文本颜色分别是COLOR_WINDOW和COLOR_WINDOWTEXT。在不同的系统中系统颜色值的定义有所不同,而且有的颜色值可能与系统实际使用的有所不同,例如COLOR_MENUBAR表示菜单栏背景色,是浅灰色,实际上在Windows 10系统默认情况下是白色背景。

8.1.3 视觉样式

视觉样式定义了通用控件的外观。Windows Vista及以后的系统支持通用控件库ComCtl32.dll版本6,只有版本6及更高版本才支持视觉样式,程序创建的子窗口控件默认使用版本5定义的控件,因此要使用视觉样式,必须添加应用程序清单或预编译指令,以指定程序应该使用通用控件版本6定义的控件。

程序清单以XML格式编写,程序清单文件的名称是可执行文件的名称,后跟扩展名.manifest,例如要为Buttons项目添加程序清单,程序清单文件名为Buttons.exe.manifest。以Buttons项目为例,为其添加程序清单文件,首先打开解决方案资源管理器视图,用鼠标右键单击Buttons项目下的源文件,然后选择添加→新建项,打开添加新项对话框,输入文件名称Buttons.exe.manifest,单击"添加",然后输入以下内容:

复制代码
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>

<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" >
  <assemblyIdentity 
    version="1.0.0.0"                                 // 程序清单的版本
    processorArchitecture="*"                         // 处理器类型
    name="CompanyName.ProductName.YourApplication"    // 公司名.产品名.程序名称
    type="win32"                                      // 应用程序的类型
  />

  <description>Your application description here.</description>    // 程序的描述,随意写

  <dependency>
    <dependentAssembly>
      <assemblyIdentity 
        type="win32"                                // 控件的类型
        name="Microsoft.Windows.Common-Controls"    // 控件的名称
        version="6.0.0.0"                           // 控件的版本
        processorArchitecture="*"                   // 处理器类型
        publicKeyToken="6595b64144ccf1df"           // 密钥令牌
        language="*"                                // 控件的语言
      />
    </dependentAssembly>
  </dependency>
</assembly>

如果程序仅面向32位Windows平台,则可以将processorArchitecture项设置为"x86";如果程序仅面向64位Windows平台,则可以将processorArchitecture项设置为"amd64";如果面向所有平台,则可以指定为"*"。

如果使用的是VS 2005或以上版本的集成开发工具,则可以在源文件中使用预编译指令代替程序清单文件,效果是一样的:

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

8.1.4 控件的子类化与超类化

普通重叠窗口中的子窗口控件无法使用方向键或Tab键将焦点移动到组中另一个控件或下一个具有WS_TABSTOP样式的控件上。在这里,以Tab键为例说明处理方法,程序可以处理字符消息。如果用户按下了Tab键,就把键盘焦点设置到下一个具有WS_TABSTOP样式的子窗口控件上;如果用户按下了Shift + Tab组合键,就把键盘焦点设置到上一个具有WS_TABSTOP样式的子窗口控件上。同时,程序处理Enter键,用户按下Enter键相当于单击了默认按钮,发送一个BM_CLICK消息。

在Buttons程序中,具有WS_TABSTOP样式的子窗口控件一共有7个。自绘按钮虽然没有显式设置WS_TABSTOP样式,但我们还是为之响应Tab键。添加以下全局变量:

复制代码
// 响应Tab键的8个子窗口控件
int idFocus[] = { IDC_PUSHBUTTON, IDC_ICONBUTTON, IDC_BITMAPBUTTON, IDC_OWNERDRAWBUTTON,
            IDC_AUTORADIOBUTTON1, IDC_AUTOCHECKBOX1, IDC_AUTO3STATE1, IDC_DEFPUSHBUTTON };
// 当前具有键盘焦点的按钮索引
int idFocusIndex;

添加对WM_SETFOCUS和WM_CHAR消息的处理。在WM_SETFOCUS消息中把键盘焦点设置到当前具有键盘焦点的按钮上:

复制代码
case WM_SETFOCUS:
    SetFocus(GetDlgItem(hwnd, idFocus[idFocusIndex]));
    return 0;

case WM_CHAR:
    if (wParam == VK_TAB)
    {
        idFocusIndex += GetKeyState(VK_SHIFT) < 0 ? 7 : 1;
        idFocusIndex %= 8;
        SetFocus(GetDlgItem(hwnd, idFocus[idFocusIndex]));
    }
    if (wParam == VK_RETURN)
    {
        SendDlgItemMessage(hwnd, IDC_DEFPUSHBUTTON, BM_CLICK, 0, 0);
    }
    return 0;

完整代码参见Chapter8\Buttons2。编译运行程序,第一个按钮获得了键盘焦点,按Tab键测试一下,没有任何反应!这是因为子窗口控件获得键盘焦点以后,父窗口就失去了键盘焦点,导致父窗口无法接收键盘输入。子窗口控件获得键盘焦点以后,该子窗口控件会接收所有键盘输入,但实际上它只会响应空格键,此时的空格键具有和单击鼠标相同的效果。现在的情况是,父窗口已经失去键盘焦点,无法接收键盘输入,而子窗口控件获得键盘焦点以后,系统预定义的窗口过程只会响应空格键,Tab键和Enter键得不到响应。

我们希望子窗口控件窗口过程响应Tab键和Enter键,可以为子窗口控件设置一个新的窗口过程ButtonProc来处理Tab键和Enter键,其他情况则转交给控件的默认窗口过程去处理。具体做法是,在WM_CREATE消息中,为每一个子窗口控件调用ButtonProcOldi = SetWindowLongPtr(hwndButtoni, GWLP_WNDPROC, (LONG_PTR)ButtonProc)函数,设置其新窗口过程为ButtonProc。SetWindowLongPtr函数返回的是原窗口过程地址,这个地址需要保存下来。

未通过新窗口过程处理的任何消息都应该调用CallWindowProc函数将其传递给原窗口过程。新窗口过程ButtonProc代码如下,控件和普通重叠窗口的窗口过程函数定义是一样的:

复制代码
LRESULT CALLBACK ButtonProc(HWND hwndButton, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    int id = GetWindowLongPtr(hwndButton, GWLP_ID);

    switch (uMsg)
    {
    case WM_CHAR:
        if (wParam == VK_TAB)
        {
            idFocusIndex += GetKeyState(VK_SHIFT) < 0 ? 7 : 1;
            idFocusIndex %= 8;
            SetFocus(GetDlgItem(GetParent(hwndButton), idFocus[idFocusIndex]));
        }
        if (wParam == VK_RETURN)
        {
            SendDlgItemMessage(GetParent(hwndButton), IDC_DEFPUSHBUTTON, BM_CLICK, 0, 0);
        }
        break;
    }

    return CallWindowProc(ButtonProcOld[id - 1000], hwndButton, uMsg, wParam, lParam);
}

完整代码参见Chapter8\Buttons3。实际上对于处理方向键和Tab键,上面的代码是远远不够的。这个问题点到为止,子窗口控件通常用于对话框程序,对话框的默认窗口过程内置了键盘处理。对于对话框程序,直接在资源脚本文件中定义子窗口控件即可,也可以通过资源编辑器从工具箱中拖曳子窗口控件到对话框程序的合适位置,控件的大小可以直接拖拉调整,样式可以通过属性对话框直接设置,系统会根据我们设置的相关参数自动调用CreateWindow / CreateWindowEx函数创建子窗口控件,所以说在对话框程序中使用子窗口控件非常简单,但是它隐匿了太多的细节。为了学习到原理性的内容,我们先苦后甜,为以后合理使用子窗口控件奠定基础。

GetParent函数用于获取指定窗口的父窗口句柄:

复制代码
HWND WINAPI GetParent(_In_ HWND hWnd);

与之对应的,还有一个SetParent函数用于设置一个窗口的父窗口:

复制代码
HWND WINAPI SetParent(
    _In_     HWND hWndChild,        // 子窗口句柄
    _In_opt_ HWND hWndNewParent);   // 新父窗口句柄

如果函数执行成功,则返回值是前一个父窗口的句柄;如果函数执行失败,则返回值为NULL。

CallWindowProc函数用于把新窗口过程未处理的消息传递给原窗口过程:

复制代码
LRESULT WINAPI CallWindowProc(
    _In_ WNDPROC lpPrevWndFunc, // 原窗口过程
    _In_ HWND    hWnd,
    _In_ UINT    Msg,
    _In_ WPARAM  wParam,
    _In_ LPARAM  lParam);

该函数仅起到将后面4个参数入栈和调用指定地址(lpPrevWndFunc)的作用。

重叠窗口中子窗口控件的默认窗口过程无法满足我们的要求,为此调用SetWindowLongPtr函数为子窗口控件设置一个新的窗口过程。拦截控件的部分消息进行处理,这就是控件的子类化,也是通用控件版本6之前的子类化方法。

旧的子类化方法存在一些缺点,版本6的子类化方法仅支持Unicode,涉及4个函数:SetWindowSubclass、DefSubclassProc、GetWindowSubclass和RemoveWindowSubclass,前两个函数是必须使用的,后两个函数是可选使用的。

SetWindowSubclass函数为需要子类化的窗口设置新的窗口过程:

复制代码
BOOL SetWindowSubclass(
    _In_ HWND         hWnd,         // 要子类化的窗口句柄
    _In_ SUBCLASSPROC pfnSubclass,  // 指向新子类窗口过程的指针
    _In_ UINT_PTR     uIdSubclass,  // 子类ID,该参数与pfnSubclass参数一起唯一标识一个子类
    _In_ DWORD_PTR    dwRefData);   // 用户自定义数据,传递给新子类窗口过程的dwRefData参数

SetWindowSubclass函数需要引入头文件CommCtrl.h,还需要使用导入库Comctl32.lib。在引入相关头文件以后,添加以下预编译指令:

复制代码
#pragma comment(lib, "Comctl32.lib")

新版本的子类窗口过程的函数声明略有不同,它有两个额外的子类ID和自定义数据参数:

复制代码
LRESULT CALLBACK MyWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, 
    UINT_PTR uIdSubclass, DWORD_PTR dwRefData);

SUBCLASSPROC是子类窗口过程指针类型,在CommCtrl.h头文件中定义:

复制代码
typedef LRESULT (CALLBACK *SUBCLASSPROC)(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
 UINT_PTR uIdSubclass, DWORD_PTR dwRefData);

可以在WM_CREATE消息中调用SetWindowSubclass函数为子窗口控件设置新窗口过程:

复制代码
for (int i = 0; i < NUM; i++)
    SetWindowSubclass(hwndButton[i], ButtonProc, i, 0);

子窗口控件新窗口过程如下:

复制代码
LRESULT CALLBACK ButtonProc(HWND hwndButton, UINT uMsg, WPARAM wParam, LPARAM lParam,
    UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
{
    int id = GetWindowLongPtr(hwndButton, GWLP_ID);

    switch (uMsg)
    {
    case WM_KEYDOWN:
        if (wParam == VK_TAB)
        {
            idFocusIndex += GetKeyState(VK_SHIFT) < 0 ? 7 : 1;
            idFocusIndex %= 8;
            SetFocus(GetDlgItem(GetParent(hwndButton), idFocus[idFocusIndex]));
        }
        if (wParam == VK_RETURN)
        {
            SendDlgItemMessage(GetParent(hwndButton), IDC_DEFPUSHBUTTON, BM_CLICK, 0, 0);
        }

        break;
}

    // 其他消息通过调用DefSubclassProc函数传递给默认窗口过程
    return DefSubclassProc(hwndButton, uMsg, wParam, lParam);
}

完整代码参见Chapter8\Buttons4。可以看到,新版本的子类化方法功能更强大,使用更简单。

要获取子类窗口的自定义数据,可以调用GetWindowSubclass函数:

复制代码
BOOL GetWindowSubclass(
    _In_  HWND         hWnd,        // 子类窗口句柄
    _In_  SUBCLASSPROC pfnSubclass, // 子类窗口过程
    _In_  UINT_PTR     uIdSubclass, // 子类ID
    _Out_ DWORD_PTR    *pdwRefData);// 在这里返回自定义数据的指针

RemoveWindowSubclass函数用于删除一个子类:

复制代码
BOOL RemoveWindowSubclass(
    _In_ HWND         hWnd,         // 子类窗口
    _In_ SUBCLASSPROC pfnSubclass,  // 子类窗口过程
    _In_ UINT_PTR     uIdSubclass); // 子类ID

对子窗口控件进行子类化,影响的仅是被子类化的窗口,要对多个控件窗口进行子类化就必须对每个窗口都进行子类化操作。还有一个超类化的概念,在C++中,可以通过继承一个基类来形成一个派生的类,子窗口控件超类化可以完成的功能与之类似。注册一个窗口类并提供窗口过程,就可以调用CreateWindow / CreateWindowEx函数创建一个窗口。我们可以获取一个系统预定义控件类的WNDCLASSEX结构,然后根据需要来修改部分字段,例如可以将结构中的窗口过程地址指定为自定义的窗口过程地址。最后,使用这个修改后的结构注册一个类(以自定义的名称),一个新的窗口类就派生出来了。

GetClassInfoEx函数用于获取一个窗口类的信息:

复制代码
BOOL WINAPI GetClassInfoEx(
    _In_opt_ HINSTANCE    hinst,    // lpszClass所属的模块句柄,要获取系统预定义类信息,则设置为NULL
    _In_     LPCTSTR      lpszClass,// 要获取的窗口类的名称
    _Out_    LPWNDCLASSEX lpwcx);   // 在这个WNDCLASSEX结构中返回窗口类信息

在调用GetClassInfoEx函数前,WNDCLASSEX结构的cbSize字段必须设置为WNDCLASSEX结构的长度。在获取WNDCLASSEX结构的信息后,可以根据需要修改结构的内容,其中有两个字段是必须要修改的,hInstance字段必须设置为应用程序的模块句柄,lpszClassName字段必须设置为一个自定义的名称,还可以将窗口过程地址指定为自定义的窗口过程地址。当然,原窗口过程地址应该保存下来,不感兴趣的消息应该转发到原窗口过程中。完成这些修改以后,可以使用经过修改的WNDCLASSEX结构调用RegisterClassEx函数注册窗口类,然后可以把窗口类名指定为刚刚注册的窗口类名,调用CreateWindow / CreateWindowEx函数创建窗口。关于超类化的示例参见Chapter8\Buttons5项目。

8.1.5 命令链接按钮与拆分按钮

命令链接按钮是通用控件版本6中引入的一种按钮。指定BS_COMMANDLINK样式可以创建一个命令链接按钮,左侧有一个蓝色箭头指向按钮文本,按钮文本下面可以显示一些说明文字作为按钮文本的补充。要设置按钮文本,可以通过调用SetWindowText/SetDlgItemText函数或发送WM_SETTEXT消息来进行;要设置说明文字,可以通过发送BCM_SETNOTE消息来进行,该消息的wParam参数没有用到,将lParam参数指定为字符串指针。

拆分按钮也是通用控件版本6中引入的一种按钮。指定BS_SPLITBUTTON样式可以创建一个拆分按钮。拆分按钮分为两部分,左侧是主要部分,类似于普通或默认按钮;右侧有一个下拉箭头,单击箭头通常会弹出一个菜单。单击下拉箭头,系统会发送包含BCN_DROPDOWN通知码的WM_NOTIFY消息。和其他按钮一样,单击命令链接按钮和拆分按钮的左侧会发送包含BN_CLICKED通知码的WM_COMMAND消息。

WM_NOTIFY消息的wParam参数是控件的ID,不过通常不使用这个参数,而是使用lParam参数。lParam参数通常是一个指向NMHDR结构的指针,该结构包含通知码和一些附加信息。对于其他通知码,lParam参数可能是指向一个更大结构的指针,但是这些结构的第一个字段总是NMHDR结构,所以把lParam参数强制转换为指向NMHDR结构的指针总是正确的。NMHDR结构在WinUser.h头文件中定义如下:

复制代码
typedef struct tagNMHDR
{
    HWND      hwndFrom; // 控件的窗口句柄
    UINT_PTR  idFrom;   // 控件的ID
    UINT      code;     // 通知码
}   NMHDR;

处理WM_NOTIFY消息的代码通常如下,先把lParam参数转换为指向NMHDR结构的指针,确定通知码的类型,然后才可以进一步确定lParam指向的是什么结构:

复制代码
case WM_NOTIFY:
    switch (((LPNMHDR)lParam)->code)
    {
    case BCN_DROPDOWN:
        // BCN_DROPDOWN通知码的lParam参数是一个指向NMBCDROPDOWN结构的指针
        // 把lParam参数转换为指向NMBCDROPDOWN结构的指针,然后使用结构中的字段
        break;

case 其他通知码:
        // 对于其他通知码,lParam参数可能是指向其他数据结构的指针
        break;
}

    return 0;

如果程序需要发送WM_NOTIFY消息,SendMessage函数的窗口句柄参数需指定为父窗口句柄,例如:

复制代码
NMHDR nmh;
nmh.hwndFrom = hwndCtrl;                // 控件窗口句柄
nmh.idFrom = GetDlgCtrlID(hwndCtrl);    // 控件ID
nmh.code = 通知码;                       // 通知码
SendMessage(GetParent(hwndCtrl), WM_NOTIFY, nmh.idFrom, (LPARAM)&nmh);

BCN_DROPDOWN通知码的lParam参数是一个指向NMBCDROPDOWN结构的指针,该结构在CommCtrl.h头文件中定义如下:

复制代码
typedef struct tagNMBCDROPDOWN
{
    NMHDR   hdr;
    RECT    rcButton;   // 按钮的矩形区域,相对于自己的客户区左上角
} NMBCDROPDOWN, *LPNMBCDROPDOWN;

举一个例子,单击拆分按钮的下拉箭头,系统会发送一个包含BCN_DROPDOWN通知码的WM_NOTIFY消息。程序处理该消息弹出一个菜单:

复制代码
case WM_NOTIFY:
    switch (((LPNMHDR)lParam)->code)
    {
    case BCN_DROPDOWN:
        pDropDown = (NMBCDROPDOWN*)lParam;
        if (pDropDown->hdr.hwndFrom = GetDlgItem(hwnd, IDC_SPLITBUTTON))
        {
            POINT pt;
            HMENU hMenu;
            pt.x = pDropDown->rcButton.left;
            pt.y = pDropDown->rcButton.bottom;
            // 拆分按钮的矩形客户区坐标转换为屏幕坐标
            ClientToScreen(pDropDown->hdr.hwndFrom, &pt);
            hMenu = LoadMenu(hInstance, MAKEINTRESOURCE(IDR_MENU));
            TrackPopupMenu(GetSubMenu(hMenu, 0), TPM_LEFTALIGN | TPM_TOPALIGN,
                pt.x, pt.y, 0, hwnd, NULL);
        }
        break;
    }
    return 0;

完整代码参见Chapter8\CommandLinkAndSplitButton项目。

另外,发送BCM_SETSPLITINFO / BCM_GETSPLITINFO消息可以设置/获取拆分按钮控件的信息,这两个消息的wParam参数没有用到。lParam参数是一个指向BUTTON_SPLITINFO结构的指针,该结构包含有关拆分按钮的信息。

8.2 编辑控件

编辑控件通常叫作编辑框或文本框,是一个矩形窗口,可以用于输入和编辑文本,编辑控件的应用比较广泛。编辑控件的常用样式如表8.7所示。

表8.7

样式 含义
ES_MULTILINE 编辑控件默认为单行编辑控件,指定ES_MULTILINE样式表示创建一个多行编辑控件
ES_AUTOHSCROLL 对于单行编辑控件,当用户输入文本时,如果文本字数填满了编辑控件,则无法继续输入。指定该样式以后,在必要时编辑控件会自动水平滚动,这样一来用户输入的文本字数就不受编辑控件的长度影响。对于多行编辑控件,如果没有指定ES_AUTOHSCROLL样式,当用户输入的文本多于可在单行上显示的字数时,文本将自动换行显示到下一行;如果指定了ES_AUTOHSCROLL样式,则当用户输入的文本多于可在单行上显示的字数时,控件将自动水平滚动,文字不会换行
ES_AUTOVSCROLL 仅适用于多行编辑控件。如果没有指定该样式,当用户输入的文本行数高于编辑控件的高度时,无法继续输入。在指定该样式后,在必要时编辑控件会自动垂直滚动,这样一来用户输入的文本行数就不受编辑控件的高度影响
ES_LEFT 文本在编辑控件中左对齐
ES_RIGHT 文本在编辑控件中右对齐
ES_CENTER 文本在编辑控件中居中对齐。右对齐和居中对齐的多行编辑控件不能具有ES_AUTOHSCROLL样式,即不能自动水平滚动,超过一行限制以后会自动换行显示
ES_LOWERCASE 输入的所有大写字母都转换为小写
ES_UPPERCASE 输入的所有小写字母都转换为大写
ES_NUMBER 只能在编辑控件中输入数字
ES_READONLY 将编辑控件设置为只读状态,不允许编辑其中的文本
ES_PASSWORD 将单行编辑控件中的所有字符显示为星号,版本6中显示为黑圆圈(如果需要显示为其他字符,可以通过发送EM_SETPASSWORDCHAR消息进行设置),该样式通常用于密码一类的敏感信息
ES_NOHIDESEL 默认情况下,当编辑控件失去输入焦点时,所选中的文本会失去突出显示。在指定该样式后,即使编辑控件失去输入焦点,所选中的文本也会突出显示
ES_WANTRETURN 对于对话框程序中的多行编辑控件,如果没有指定ES_WANTRETURN样式,当用户按下Enter键时,不会换行,不过可以按Ctrl + Enter组合键进行换行。在指定该样式后,按下Enter键就可以换行

默认情况下,编辑控件是没有边框的,可以指定WS_BORDER窗口样式为其添加一个边框。当然,WS_CHILD | WS_VISIBLE窗口样式是子窗口控件必不可少的。如果需要为编辑控件添加水平或垂直滚动条,可以指定WS_HSCROLL或WS_VSCROLL窗口样式。

系统在创建编辑控件时,会自动创建文本缓冲区,并设置其初始大小,默认情况下最大缓冲区大小约为32KB个字符,有时候可能需要限制用户输入。比如有一个用户名文本框,可能想限制用户最多可以输入20个字符,有时候可能需要比32KB更大的缓冲区,可以通过向编辑控件发送EM_SETLIMITTEXT(和EM_LIMITTEXT相同)消息来设置缓冲区大小,将wParam参数指定为最大字符数,没有用到lParam参数。对于单行编辑控件,可以设置的最大字符数为0x7FFFFFFE(约2G);对于多行编辑控件,可以设置为系统支持的最大大小,如果wParam参数为0,则表示使用可用的最大大小。

对于每个编辑控件,系统维护一个只读标志,指示控件的文本是可读可写(默认)或只读,可以通过向控件发送EM_SETREADONLY消息来设置文本的可读可写或只读标志,wParam参数为TRUE表示只读,FALSE表示可读可写,没有用到lParam参数;要确定编辑控件是否为只读,而没有名为EM_GETREADONLY的消息,可以使用GWL_STYLE常量调用GetWindowLongPtr函数获取控件样式(ES_READONLY)。

可以通过调用SetWindowText / SetDlgItemText函数或发送WM_SETTEXT消息来设置编辑控件的文本,这几个设置窗口文本的方法对于所有窗口几乎都适用。当然,还有GetWindowText / GetDlgItemText和WM_GETTEXT消息。

有时候需要把一个数值型数据显示到编辑控件中,或者从编辑控件中获取一个字符串作为数值型使用,将文本转换为数值或将数值转换为文本需要额外的函数调用。为了简化操作,Windows提供了两个函数来处理这个问题。

SetDlgItemInt函数可以把一个数值型数据显示到编辑控件中:

复制代码
BOOL WINAPI SetDlgItemInt(
    _In_ HWND hDlg,         // 父窗口句柄
    _In_ int  nIDDlgItem,   // 编辑控件ID
    _In_ UINT uValue,       // 数值型数据
    _In_ BOOL bSigned);     // 指示uValue参数是有符号还是无符号数

参数bSigned指示uValue参数是有符号数还是无符号数。如果该参数为TRUE且uValue小于0,则在编辑控件中的第一个数字之前会添加一个减号;如果该参数为FALSE,则把uValue视为无符号数。

GetDlgItemInt函数可以从编辑控件中获取一个字符串并返回数值型:

复制代码
UINT WINAPI GetDlgItemInt(
    _In_      HWND hDlg,            // 父窗口句柄
    _In_      int  nIDDlgItem,      // 编辑控件ID
    _Out_opt_ BOOL *lpTranslated,   // 函数执行成功还是失败,返回TRUE表示成功,FALSE表示失败
 _In_      BOOL bSigned);        // 是否检查编辑控件中的字符串开头有没有减号
  • 参数bSigned表示是否检查编辑控件中的字符串开头有没有减号。如果该参数为TRUE并且在字符串开头发现了减号,则返回有符号整数值,在这种情况下需要把返回值强制转换为int类型;否则返回无符号整数值。

  • 因为函数返回值是从编辑控件中获取到的十进制数值,所以通过参数lpTranslated表示函数执行结果,返回TRUE表示成功,FALSE表示失败。如果不需要检查函数执行成功还是失败,可以将该参数设置为NULL。

除了上面介绍的消息,表所示的消息也可以用于编辑控件,不过这些消息通常用于多行编辑控件。多行编辑控件可以用于实现一个简单的文本编辑器。

表8.8

消息类型 含义
EM_UNDO 撤销最近一次的编辑操作,即删除刚刚插入的文本或恢复刚刚已删除的文本,例如:SendMessage(hwndEdit, EM_UNDO, 0, 0);
EM_CANUNDO 编辑控件的撤销队列是否不为空,即能不能撤销上次的编辑操作,如果可以,返回TRUE,例如:bResult = SendMessage(hwndEdit, EM_CANUNDO, 0, 0);
EM_GETLINECOUNT 获取多行编辑控件中的总行数,例如:nCount = SendMessage(hwndEdit, EM_GETLINECOUNT, 0, 0);如果编辑控件中没有文本,则返回值为1
EM_LINELENGTH 对于单行编辑控件,wParam和lParam参数都没有用到,返回单行编辑控件中的字符个数,不包含终止空字符;对于多行编辑控件,wParam参数指定为一个字符的字符索引(第1行第1个字符索引为0),没有用到lParam参数,返回指定字符所在行的字符个数,例如下面的多行编辑控件:nLength = SendMessage(hwndEdit, EM_LINELENGTH, 4, 0);
EM_LINELENGTH 返回2。上面编辑控件中的文本在内存中的形式为(回车和换行符也包括在内):3100 0D00 0A00 3200 3300 0D00 0A00 3400 3500 3600 0D00 0A00;索引为4的字符就是第2行的3,第2行的字符个数是2
EM_GETLINE 将单行编辑控件中的文本复制到指定的缓冲区并返回复制的字符数,不包含终止空字符;对于多行编辑控件,则是复制指定行的文本并返回复制的字符数,不包含终止空字符。对于单行编辑控件,没有用到wParam参数,lParam参数指定为缓冲区指针;对于多行编辑控件,wParam参数指定为从0开始的行号,lParam参数指定为缓冲区指针。需要注意的是,因为返回的文本不包括终止空字符,所以缓冲区应该清零。另外,在发送消息前,缓冲区的第一个字符必须设置为缓冲区的长度。例如:TCHAR szBuf128 = { 0 }; szBuf0 = 128; // 单行编辑控件 nCount = SendMessage(hwndEdit, EM_GETLINE, 0, (LPARAM)szBuf); // 多行编辑控件 nCount = SendMessage(hwndEdit, EM_GETLINE, 2, (LPARAM)szBuf);
EM_LINEINDEX 获取多行编辑控件中指定行的第1个字符的字符索引,该消息与EM_LINEFROMCHAR消息相反,例如:nIndex = SendMessage(hwndEdit, EM_LINEINDEX, 2, 0);wParam参数指定为从0开始的行号,指定为−1表示当前行号(光标所在的行),没有用到lParam参数。上面的函数调用返回10,因为回车和换行符也包括在内,上面编辑控件中的文本在内存中的形式为:3100 3200 3300 0D00 0A00 3400 3500 3600 0D00 0A00 3700 3800 3900 0D00 0A00
EM_LINEFROMCHAR 用于多行编辑控件,返回指定字符索引的字符所在行的行索引。该消息与EM_LINEINDEX消息相反
EM_GETMODIFY 编辑控件的内容是否已被修改,如果已被修改返回TRUE,否则返回FALSE,例如:bResult = SendMessage(hwndEdit, EM_GETMODIFY, 0, 0);
EM_SETSEL 通过指定一段字符的开始和结束位置(字符索引),在编辑控件中选中一段文字,例如:SendMessage(hwndEdit, EM_SETSEL, 3, 8); 函数执行后显示如下:
EM_SETSEL 即包括开始位置,但不包括结束位置。如果开始位置为0且结束位置为−1,则选中编辑控件中的所有文本;如果开始位置为−1,则取消当前选中。选中文本以后,控件会在结束位置显示闪烁的光标。如果开始和结束位置为相同的值,则会移动光标到此处,这是设置光标位置的一种方法
EM_GETSEL 返回编辑控件中当前所选中文本的开始和结束位置,例如:DWORD dwResult; DWORD dwStart, dwEnd;dwResult = SendMessage(hwndEdit, EM_GETSEL, (WPARAM)&dwStart,(LPARAM)&dwEnd); 在dwStart和dwEnd中返回当前选中文本的开始和结束位置(字符索引),结束位置是选中的最后一个字符的索引加1。该消息的返回值是DWORD类型,LOWORD(dwResult)等于开始位置,HIWORD(dwResult)等于结束位置。如果没有选中文本,则开始和结束位置都是光标的位置
EM_REPLACESEL 将当前选中的文本替换为指定的的文本,例如:SendMessage(hwndEdit, EM_REPLACESEL, TRUE, (LPARAM)szStr);wParam参数可以指定为TRUE或FALSE,表示是否可以撤销本次替换操作。如果指定为TRUE;则表示可以撤销操作;如果指定为FALSE,则表示无法撤销操作。lParam参数指定为要替换的字符串。如果没有选中的文本,则将lParam参数指定的字符串插入光标位置
EM_SCROLLCARET 在编辑控件中将光标滚动到可见视图中,在设置选中区域(或改变了光标位置)后,这个区域可能落在客户区的外面,用户看不到它。如果希望控件能够滚动以将新位置的内容落在客户区中,可以发送EM_SCROLLCARET消息,例如:SendMessage(hwndEdit, EM_SCROLLCARET, 0, 0);
WM_COPY 复制当前选中的内容到剪贴板(如果样式为ES_PASSWORD,则不支持该消息),例如:SendMessage(hwndEdit, WM_COPY, 0, 0);
WM_CUT 删除编辑控件中当前选中的内容,并把当前选中的内容以CF_TEXT格式复制到剪贴板,例如:SendMessage(hwndEdit, WM_CUT, 0, 0);
WM_PASTE 把剪贴板中CF_TEXT格式的内容插入编辑控件的光标位置,例如:SendMessage(hwndEdit, WM_PASTE, 0, 0);
WM_CLEAR 删除编辑控件中当前选中的内容,如果当前没有选中文本,则删除光标右侧的字符,例如:SendMessage(hwndEdit, WM_CLEAR, 0, 0);
WM_UNDO 撤销最近一次的编辑操作,即删除刚刚插入的任何文本或恢复刚刚已删除的文本,例如:SendMessage(hwndEdit, WM_UNDO, 0, 0);

编辑控件的通知码以WM_COMMAND消息的形式发送给父窗口,常见的通知码如表8.9所示。

表8.9

通知码 含义
EN_SETFOCUS 编辑控件获得了输入焦点
EN_KILLFOCUS 编辑控件失去了输入焦点
EN_UPDATE 编辑控件的内容将变化
EN_CHANGE 编辑控件的内容已变化。如果是多行编辑控件,并且是通过程序代码(例如SetWindowText、SetDlgItemText、SetDlgItemInt等)改变了多行编辑控件的内容,则不会收到EN_CHANGE和EN_UPDATE这两个通知码
EN_ERRSPACE 编辑控件的缓冲区已满
EN_MAXTEXT 编辑控件已经没有空间完成文本插入
EN_HSCROLL 编辑控件的水平滚动条被单击
EN_VSCROLL 编辑控件的垂直滚动条被单击

系统自带的记事本程序实际上用的就是Edit多行编辑控件。通过本节所学,实现记事本的文本编辑功能很容易。如果需要实现一个文本编辑器,那么建议学习功能更加强大的RichEdit富文本控件。如果说Edit编辑控件可以实现一个记事本程序,那么RichEdit富文本控件可以实现一个写字板程序(也是系统自带的)。本节的知识点几乎能适用于RichEdit控件,RichEdit控件提供了更多的功能。

接下来,实现一个单行编辑控件的例子,EditDemo程序运行效果如原书的图8.1所示。

具体代码不在此处列出,参见Chapter8\EditDemo项目。

"会员注册""用户名""密码"和"年龄"这些文本用的都是Static类静态控件。静态控件后面再讲。因为用不到这些静态控件的ID,所以静态控件的ID我都设置为−1。至于静态控件、编辑控件和按钮控件摆放的位置,读者可以自己设计,这不是重点。在WM_CREATE消息中,创建所有控件以后,发送WM_SETFONT消息设置所有控件的字体;发送EM_SETLIMITTEXT消息设置用户名,且分别限制密码、年龄3个编辑控件输入20、12、3个字符;调用AdjustWindowRectEx函数设置程序窗口大小。

在WM_COMMAND消息中,要获取用户名和密码字符串,首先发送EM_LINELENGTH消息确定字符串的长度,然后分配缓冲区,发送EM_GETLINE消息获取字符串,要获取年龄则直接调用GetDlgItemInt函数得到无符号数值型,然后通过wsprintf函数格式化。当然,使用GetWindowText / GetDlgItemText或WM_GETTEXT消息获取编辑控件的文本字符串也完全可以。

另外需要介绍的是WM_CTLCOLORSTATIC消息。默认情况下静态控件有一个灰色背景,而程序客户区的背景是白色,因此应该将静态控件的背景颜色设置为白色。在绘制静态控件以前,系统会发送WM_CTLCOLORSTATIC消息到静态控件的父窗口。该消息的wParam参数是静态控件窗口对应的DC句柄,lParam参数是静态控件的窗口句柄。程序可以调用SetTextColor函数设置文本颜色,调用SetBkColor函数设置文本的背景颜色等,最后返回一个画刷句柄,静态控件会使用这个画刷来擦除背景。本例对WM_CTLCOLORSTATIC消息的处理,仅仅是通过调用GetSysColorBrush函数返回一个白色画刷。

常用的类似消息如表8.10所示。

表8.10

消息类型 针对的控件类型
WM_CTLCOLORSTATIC 静态控件和只读或禁用的编辑控件(正常状态的编辑控件不会收到该消息,而是收到WM_CTLCOLOREDIT消息)
WM_CTLCOLOREDIT 非只读或禁用的编辑控件
WM_CTLCOLORBTN 自绘按钮(但是对于自绘按钮,通常处理的是WM_DRAWITEM消息)
WM_CTLCOLORLISTBOX 列表框
WM_CTLCOLORSCROLLBAR 滚动条控件
WM_CTLCOLORDLG 对话框

后面5个消息的wParam和lParam参数的含义和WM_CTLCOLORSTATIC消息完全相同,wParam参数是控件对应的DC句柄,lParam参数是控件的窗口句柄。例如下面的代码:

复制代码
case WM_CTLCOLOR***:
    SetTextColor((HDC)wParam, RGB(255, 255, 255));
    SetBkColor((HDC)wParam, RGB(0, 0, 0));
    hbrBkgnd = CreateSolidBrush(RGB(0, 0, 0));
    return (LRESULT)hbrBkgnd;

禁用的编辑控件的文本颜色始终为COLOR_GRAYTEXT,在WM_CTLCOLOR***消息中调用SetTextColor函数设置禁用编辑控件的文本颜色是无效的。

8.3 列表框

列表框也是经常使用的子窗口控件,包含可供用户选择的项目列表,用户可以从中选择一个或多个项目。如果列表框不够大,显示不了所有列表项,则列表框可以显示一个滚动条。另外,每一个列表项都可以设置一个与之关联的32位数据,称为项目数据,可以设置为指向某些自定义数据的指针。普通的列表框效果如原书的图8.2所示。

列表框控件可用的样式如表8.11所示。

表8.11

样式 含义
LBS_HASSTRINGS 默认样式(除自绘列表框)。列表框中的列表项是字符串,程序可以通过发送LB_GETTEXT消息来获取指定列表项的文本
LBS_MULTIPLESEL 多选列表框。选中一个列表项以后,可以继续单击其他列表项以选中
LBS_EXTENDEDSEL 多选列表框。选中一个列表项以后,如果继续单击其他列表项,则会自动取消选中前一个;要想多选,可以在选中一个以后按住Ctrl键继续选中其他列表项,或者按住Shift键同时选中一个范围的多个列表项,个人感觉该样式比LBS_MULTIPLESEL样式的用户的体验更友好
LBS_NOTIFY 当用户单击(LBN_SELCHANGE)、双击(LBN_DBLCLK)或取消选中(LBN_SELCANCEL)列表项时,会向父窗口发送包含上述通知码的WM_COMMAND消息,通常都会指定该样式
LBS_SORT 按字母顺序对列表框中的列表项进行排序
LBS_STANDARD WS_BORDER
LBS_NOREDRAW 在默认情况下,如果增加或删除了列表项,列表框会自动重绘。指定该样式可以阻止这种情况发生。有时候如果需要大量添加或删除列表项,则可以暂时关闭自动重绘。也可以通过发送WM_SETREDRAW消息来开启或关闭自动重绘,wParam参数可以指定为TRUE表示开启自动重绘,或FALSE表示关闭自动重绘,没有用到lParam参数
LBS_DISABLENOSCROLL 该样式与WS_VSCROLL样式一起使用,在指定该样式后,如果列表框中的列表项比较少,则不需要使用滚动条,但还是会显示禁用的垂直滚动条。如果未指定该样式,则不需要使用滚动条时会隐藏滚动条
LBS_NOSEL 列表项只能查看,不能选择
LBS_OWNERDRAWFIXED 自绘列表框,并且列表框中的列表项高度相同,创建列表框时父窗口会收到WM_MEASUREITEM消息,列表框每次需要重绘时会收到WM_DRAWITEM消息
LBS_OWNERDRAWVARIABLE 自绘列表框,并且列表框中的列表项高度可以不同,创建列表框时系统会为每一个列表项发送WM_MEASUREITEM消息,当列表框需要重绘时会收到WM_DRAWITEM消息。该样式会导致启用LBS_NOINTEGRALHEIGHT样式。如果指定了LBS_MULTICOLUMN样式,则忽略该样式。Variable表示可变。讲解菜单项自绘的时候说过,除了用于自绘菜单项,WM_MEASUREITEM消息也用于列表框、组合框等,该消息的lParam参数是一个指向MEASUREITEMSTRUCT结构的指针,对WM_MEASUREITEM消息的处理通常是设置itemWidth和itemHeight字段,也就是设置这些控件的宽度和高度,窗口过程处理完WM_MEASUREITEM消息以后应返回TRUE。LBS_OWNERDRAWFIXED和LBS_OWNERDRAWVARIABLE样式的不同之处在于,指定为后者,系统会为每一个列表项都发送WM_MEASUREITEM消息,如何区分每一个列表项呢?MEASUREITEMSTRUCT结构的itemID字段表示菜单项ID,或列表框、组合框等的列表项位置索引(从0开始),因此可以为列表框中的列表项设置不同的宽度和高度
LBS_NOINTEGRALHEIGHT 指定列表框的大小与程序在创建列表框时指定的大小完全相同,默认情况下系统可能会自动调整列表框的大小。对于具有LBS_OWNERDRAWVARIABLE样式的列表框,始终强制使用LBS_NOINTEGRALHEIGHT样式
LBS_MULTICOLUMN 多列列表框,通常不使用该样式。如果列表框的高度显示不了所有列表项,则可以在一行中显示多个列表项,列表框会自动计算列的宽度,程序也可以发送LB_SETCOLUMNWIDTH消息设置宽度,该样式的列表框不能垂直滚动,因为它忽略收到的任何WM_VSCROLL消息。如果列表框具有LBS_OWNERDRAWFIXED样式,可以在列表框发送WM_MEASUREITEM消息时设置宽度;不能同时指定LBS_MULTICOLUMN和LBS_OWNERDRAWVARIABLE样式,如果同时指定了两者,则忽略LBS_OWNERDRAWVARIABLE样式
LBS_USETABSTOPS 允许识别并展开列表项字符串中的\t制表符,程序可以通过发送LB_SETTABSTOPS消息指定每个制表符的位置,wParam参数指定为制表符的个数,lParam参数指定为制表符位置的数组,类似于TabbedTextOut函数指定制表符的最后两个参数

当列表框中发生事件时,系统会以WM_COMMAND消息的形式向父窗口发送通知码,常见的通知码如表8.12所示。

表8.12

通知码 含义
LBN_SETFOCUS 列表框获取了键盘焦点
LBN_KILLFOCUS 列表框失去了键盘焦点
LBN_SELCHANGE 列表框中的选择已更改,通常是在用户单击一个列表项的时候发生,用户按下方向键改变选择或键盘焦点的时候也会发送该通知码。如果是通过发送LB_SETSEL、LB_SETCURSEL、LB_SELECTSTRING、LB_SELITEMRANGE或LB_SELITEMRANGEEX消息更改了选择,则不会发送该通知码,即如果是程序代码更改了选中项,就不会收到LBN_SELCHANGE通知码
LBN_DBLCLK 双击列表框中的某个列表项
LBN_SELCANCEL 取消选中列表框中的某个列表项
LBN_ERRSPACE 列表框无法分配足够的内存来完成请求

列表项的添加、删除、查询等都是通过发送消息来实现的。列表项的位置索引是从0开始的,列表框中第1个列表项的索引为0,第2个列表项的索引为1,以此类推。表8.13列出常见的列表框消息。

表8.13

消息类型 含义
LB_ADDSTRING 添加一个字符串类型的列表项,如果没有指定LBS_SORT样式,则该项会被添加到列表的末尾,否则添加以后会自动排序。该消息的wParam参数没有用到,lParam参数指定为字符串指针,该消息返回该新添加列表项的索引,如果发生错误,则返回LB_ERR(−1)。如果没有足够的内存空间来完成插入,则返回LB_ERRSPACE(−2)。例如:nIndex = SendMessage(hwndListBox, LB_ADDSTRING, 0, (LPARAM)TEXT("汇编语言"));
LB_DELETESTRING 删除一个列表项,wParam参数指定为列表项索引,lParam参数没有用到,返回值是列表框中剩余列表项的个数,如果发生错误,则返回LB_ERR(−1)。例如:nCount = SendMessage(hwndListBox, LB_DELETESTRING, 2, 0);
LB_INSERTSTRING 与LB_ADDSTRING消息不同的是,LB_INSERTSTRING消息的wParam参数可以指定为插入位置索引。即使列表框具有LBS_SORT样式,也可以插入正确的位置。如果wParam参数指定为−1,则插入列表的末尾。该消息返回该新添加列表项的索引,如果发生错误,则返回LB_ERR(−1)。如果没有足够的内存空间来完成插入,则返回LB_ERRSPACE(−2)。例如:nIndex = SendMessage(hwndListBox, LB_INSERTSTRING, 1, (LPARAM)TEXT("汇编语言"));
LB_RESETCONTENT 删除列表框中的所有列表项,例如:SendMessage(hwndListBox, LB_RESETCONTENT, 0, 0);
LB_FINDSTRING 查找列表框中以指定字符串开头的第一个列表项,wParam参数指定从哪个位置开始搜索。如果搜索到列表框的末尾还是没有找到匹配项,则继续从头搜索到wParam参数指定的位置,如果wParam参数指定为−1,则表示从头开始搜索;lParam参数指定为要搜索的字符串,不区分大小写。返回值是第一个匹配项的索引,如果没有找到,则返回LB_ERR(−1)。例如:nIndex = SendMessage(hwndListBox, LB_FINDSTRING, -1, (LPARAM)TEXT("C++"));
LB_FINDSTRINGEXACT 和LB_FINDSTRING消息不同的是,该消息查找列表框中与指定字符串完全匹配的第一个列表项,不区分大小写。除此之外,和LB_FINDSTRING消息完全相同
LB_SELECTSTRING 用于单选列表框,该消息与LB_FINDSTRING消息的不同之处在于,找到匹配项后会选中该项,除此之外完全相同
LB_GETTEXT 获取指定列表项的字符串文本,wParam参数指定为列表项的索引,lParam参数指定为字符串缓冲区,返回值是字符个数,不包括终止空字符,如果发生错误,则返回LB_ERR(−1)。可以在发送LB_GETTEXT消息前发送LB_GETTEXTLEN消息,以获取字符串的字符个数(不包括终止空字符)。例如:nCount = SendMessage(hwndListBox, LB_GETTEXT, 2, (LPARAM)szBuf);
LB_GETTEXTLEN 获取列表框中指定列表项的文本字符个数,wParam参数指定为列表项的索引,lParam参数没有用到,返回值是字符个数,不包括终止空字符,如果发生错误,则返回LB_ERR(−1)。例如:nCount = SendMessage(hwndListBox, LB_GETTEXTLEN, 2, 0);
LB_SETCURSEL 用于单选列表框,选中一个列表项,wParam参数指定为列表项的索引,如果指定为−1,则表示取消所有选择,在这种情况下即使没有发生错误,返回值也为LB_ERR(−1),lParam参数没有用到,如果发生错误,则返回LB_ERR(−1)。例如:nResult = SendMessage(hwndListBox, LB_SETCURSEL, 2, 0);
LB_GETCURSEL 用于单选列表框,获取当前选中项的索引,wParam和lParam参数都没有用到,如果当前没有选中项,则返回值为LB_ERR(−1)。例如:nIndex = SendMessage(hwndListBox, LB_GETCURSEL, 0, 0);
LB_SETSEL 用于多选列表框,选中或取消选中一个列表项,wParam参数指定为TRUE表示选中一个列表项,指定为FALSE表示取消选中一个列表项。lParam参数指定为列表项索引,如果指定为−1,表示选中或取消选中所有列表项;如果发生错误,返回值为LB_ERR (−1)。例如:nResult = SendMessage(hwndListBox, LB_SETSEL, TRUE, 1);
LB_GETSEL 可用于单选列表框和多选列表框,获取一个列表项的选中状态,wParam参数指定为列表项索引,lParam参数没有用到。如果该列表项是选中状态,则返回值大于0,否则返回值为0;如果发生错误,则返回值为LB_ERR(−1)。例如:nResult = SendMessage(hwndListBox, LB_GETSEL, 1, 0);
LB_GETSELCOUNT 用于多选列表框,获取多选列表框中选中列表项的数量,wParam和lParam参数都没有用到,返回值是多选列表框中选中列表项的数量,如果发生错误,则返回值为LB_ERR(−1)。例如:nCount = SendMessage(hwndListBox, LB_GETSELCOUNT, 0, 0);
LB_GETSELITEMS 用于多选列表框,发送LB_GETSELCOUNT消息获得了多选列表框中选中列表项的数量以后,可以发送LB_GETSELITEMS消息获取这些选中项的索引,LB_GETSELITEMS消息的wParam参数指定为需要获取的列表项个数,lParam参数指定为一个整数数组,存放选中项的索引。返回值是返回到数组中的选中项索引个数,如果发生错误,则返回值为LB_ERR(−1)。例如:nCount = SendMessage(hwndListBox, LB_GETSELCOUNT, 0, 0); LPINT pInt = new INTnCount; nCount = SendMessage(hwndListBox, LB_GETSELITEMS, nCount, (LPARAM)pInt);
LB_SELITEMRANGE 用于多选列表框,选中或取消选中一个或多个连续的列表项,wParam参数指定为TRUE表示选中,或FALSE表示取消选中,LOWORD(lParam)指定为连续列表项的第一项的索引,HIWORD(lParam)指定为连续列表项的最后一项的索引,可以使用MAKELPARAM宏构造lParam参数。如果发生错误,则返回值为LB_ERR(−1)。例如:nResult = SendMessage(hwndListBox, LB_SELITEMRANGE, TRUE, MAKELPARAM(1, 3));还有一个LB_SELITEMRANGEEX消息与之类似
LB_SETTOPINDEX 滚动列表框使指定的列表项位于列表框的顶部,如果到了最大滚动范围,则无法将其滚动到顶部,仅仅是让指定的列表项可见,wParam参数指定为列表项的索引,lParam参数没有用到。如果发生错误,则返回值为LB_ERR(−1)。例如:SendMessage(hwndListBox, LB_SETTOPINDEX, 3, 0);
LB_GETTOPINDEX 返回列表框可见列表项中最顶部的索引,最初,索引为0的列表项位于列表框的顶部,但是如果列表框发生了滚动,则位于顶部的可能是其他列表项,该消息的wParam和lParam参数都没有用到。如果发生错误,则返回LB_ERR(−1)。例如:nIndex = SendMessage(hwndListBox, LB_GETTOPINDEX, 0, 0);
LB_GETCOUNT 获取列表框中的列表项总数,wParam和lParam参数都没有用到。如果发生错误,则返回LB_ERR(−1)
LB_SETITEMDATA 为一个列表项设置项目数据,wParam参数指定为列表项索引,如果设置为−1,表示为列表框中的所有列表项设置相同的项目数据。lParam参数指定为项目数据。如果发生错误,则返回值为LB_ERR(−1)。例如:nResult = SendMessage(hwndListBox, LB_SETITEMDATA, 2, (LPARAM)lpData);
LB_GETITEMDATA 获取与一个列表项关联的项目数据,wParam参数指定为列表项索引,lParam参数没有用到,返回值是与指定列表项关联的项目数据。如果发生错误,则返回LB_ERR(−1)。例如:lpData = (LPVOID)SendMessage(hwndListBox, LB_GETITEMDATA, 2, 0);
LB_DIR 这个消息后面地单独讲解
LB_ADDFILE 这个消息后面会单独讲解

总结一下,调用CreateWindow / CreateWindowEx函数创建列表框控件后,列表框中还没有列表项,需要发送LB_ADDSTRING / LB_INSERTSTRING消息添加列表项。要删除一个列表项可以发送LB_DELETESTRING消息,要清空列表框可以发送LB_RESETCONTENT消息,要获取一个列表项的文本可以发送LB_GETTEXTLEN、LB_GETTEXT消息,可以发送LB_SETITEMDATA消息为每个列表项设置一个关联的项目数据,要获取与列表项关联的项目数据可以发送LB_GETITEMDATA消息,要获取一个列表项的选中状态可以发送LB_GETSEL消息,根据字符串查找一个列表项可以发送LB_FINDSTRING / LB_FINDSTRINGEXACT消息,获取列表框中的列表项总数可以发送LB_GETCOUNT消息。

对于单选列表框:可以发送LB_SETCURSEL或LB_GETCURSEL消息来设置或获取当前选中项。另外,LB_SELECTSTRING消息也是针对单选列表框的。

对于多选列表框:要选中或取消选中一个列表项可以发送LB_SETSEL消息,要选中或取消选中一片连续的列表项可以发送LB_SELITEMRANGE消息,要获取所有选中项的索引可以发送LB_GETSELCOUNT、LB_GETSELITEMS消息。

对自绘列表框感兴趣的读者需要注意几点,对于没有指定LBS_HASSTRINGS样式的自绘列表框:发送LB_ADDSTRING/LB_INSERTSTRING消息添加列表项时,lParam参数指定的字符串会存储为项目数据而不是列表项字符串;发送LB_DELETESTRING消息会导致系统向列表框的父窗口发送WM_DELETEITEM消息,以删除与该列表项关联的项目数据;发送LB_RESETCONTENT消息会导致系统中的每个列表项向列表框的父窗口发送WM_DELETEITEM消息,以删除与每个列表项关联的项目数据;发送LB_GETTEXTLEN消息返回的始终是DWORD的大小(4),发送LB_GETTEXT消息获取的是与列表项关联的项目数据;可以发送LB_SETITEMDATA或LB_GETITEMDATA消息以设置、修改或获取项目数据;发送LB_FINDSTRING消息时,wParam参数指定从哪个位置开始搜索,lParam参数指定为要搜索的字符串,LB_FINDSTRING采取的操作取决于是否指定了LBS_SORT样式,如果指定了LBS_SORT样式,系统会将WM_COMPAREITEM消息发送给列表框父窗口,以确定哪个列表项与指定的字符串匹配,否则将查找与lParam参数匹配的项目数据。

再看一下拖动列表框。拖动列表框允许用户将列表项从一个位置拖动到另一个位置,以改变排列顺序。拖动列表框仅用于单选列表框。如果想让一个列表框可以拖动,在创建列表框控件以后,则需要调用MakeDragList(hwndListBox);函数将其更改为拖动列表框。

调用MakeDragList函数时系统会定义拖动列表消息。当发生拖动事件时,系统会向拖动列表框的父窗口发送拖动列表消息,父窗口必须处理拖动列表消息。拖动列表消息的ID值是多少呢?拖动列表消息的具体值还没有确定,程序可以通过注册消息的方式获取这个值,调用RegisterWindowMessage (DRAGLISTMSGSTRING);函数会返回一个0xC000~0xFFFF范围内的消息ID,该消息ID在整个系统中是唯一的。DRAGLISTMSGSTRING是一个常量,在CommCtrl.h头文件中定义如下:

复制代码
#define DRAGLISTMSGSTRING  TEXT("commctrl_DragListMsg")

有了拖动列表消息ID,就可以在窗口过程中处理该消息。例如下面的代码:

复制代码
static UINT WM_DRAGLIST;
......
MakeDragList(hwndListBox);
WM_DRAGLIST = RegisterWindowMessage(DRAGLISTMSGSTRING);

拖动列表消息的wParam参数是拖动列表框的控件ID,lParam参数是一个指向DRAGLISTINFO结构的指针,其中包含拖动事件的通知码和其他信息,拖动列表消息的返回值取决于具体的通知码。DRAGLISTINFO结构在CommCtrl.h头文件中定义如下:

复制代码
typedef struct tagDRAGLISTINFO {
    UINT uNotification;    // 通知码,指示拖动事件的类型
    HWND hWnd;             // 拖动列表框的窗口句柄
    POINT ptCursor;        // 拖动事件发生时鼠标光标的X和Y坐标
} DRAGLISTINFO, *LPDRAGLISTINFO;

uNotification字段表示通知码,指示拖动事件的类型,该字段可以是表8.14所示的值之一。

表8.14

常量 含义
DL_BEGINDRAG 用户在列表项上单击了鼠标左键,开始拖动
DL_DRAGGING 用户正在拖动列表项,在开始拖动后,只要移动鼠标就会发送DL_DRAGGING通知码
DL_CANCELDRAG 用户通过单击鼠标右键或按Esc键取消了拖动操作
DL_DROPPED 用户已释放鼠标左键,拖动操作完成

当用户单击一个列表项时,将发送包含DL_BEGINDRAG通知码的拖动列表消息,可以通过调用LBItemFromPt函数来确定光标下的列表项索引,保存该列表项索引,在拖动操作完成后需要移动该列表项,然后返回TRUE表示开始拖动操作,或返回FALSE表示禁止拖动。LBItemFromPt函数用于获取指定坐标处的列表项索引:

复制代码
int LBItemFromPt(
    HWND  hLB,          // 列表框控件句柄
    POINT pt,           // 点坐标,使用((LPDRAGLISTINFO)lParam)->ptCursor字段
    BOOL  bAutoScroll); // 是否自动滚动

参数bAutoScroll指定是否自动滚动。如果设置为TRUE,则表示在拖动过程中当鼠标光标移到列表框的上方或下方时,列表框自动滚动一行,在这种情况下因为鼠标光标不在列表项上,所以该函数会返回LB_ERR(−1);设置为FALSE,则不会自动滚动。在处理DL_BEGINDRAG通知码的时候,因为还没有开始拖动,所以该参数设置为TRUE或FALSE都可以。

开始拖动以后,只要移动鼠标就会发送DL_DRAGGING通知码,可以通过调用LBItemFromPt函数来确定光标下的列表项索引,然后可以调用DrawInsert函数在光标下列表项的左上方绘制插入图标。处理完该通知码以后的返回值指定了鼠标光标的类型,返回值可以是DL_STOPCURSOR、DL_COPYCURSOR或DL_MOVECURSOR。DrawInsert函数用于在拖动列表框的父窗口中绘制插入图标:

复制代码
void DrawInsert(
    HWND handParent,// 拖动列表框的父窗口句柄
    HWND hLB,       // 拖动列表框的句柄
    int  nItem);    // 在哪个位置显示插入图标,在DL_DRAGGING通知码中可以设置为光标下的列表项索引

如果用户通过单击鼠标右键或按Esc键取消了拖动操作,则会发送DL_CANCELDRAG通知码,程序通常不需要处理该通知码。如果用户通过释放鼠标左键完成了拖动操作,则会发送DL_DROPPED通知码,可以通过调用LBItemFromPt函数来确定光标下的列表项索引,然后把被拖动的列表项移动到光标下列表项的前面。系统会忽略DL_CANCELDRAG和DL_DROPPED通知码的返回值,通常可以返回0。

接下来实现一个例子,ListBoxDemo程序运行效果如原书的图8.3所示。

在WinMain函数中调用CreateWindowEx的时候,没有指定WS_THICKFRAME和WS_MAXIMIZEBOX样式,因此程序窗口不可调整大小,而且最大化按钮失效;程序的列表框是一个拖动列表框,用户可以随意拖动列表项的位置。

在WM_CREATE消息中,创建了列表框控件、两个静态控件、两个编辑框控件和3个按钮,项目数据编辑框使用了ES_NUMBER样式,用户只能输入数字。初始情况下,3个按钮是禁用的。

在WM_COMMAND消息中,程序处理了BN_CLICKED、LBN_SELCHANGE和EN_UPDATE通知码,分别对应着用户单击3个按钮、列表框中的选择已更改和编辑控件的内容已变化。

switch (uMsg)中包含case消息ID。因为case后面需要的是一个常量,所以对于WM_DRAGLIST消息的处理单独用了一个if判断,然后在if语句中分别处理DL_BEGINDRAG、DL_DRAGGING和DL_DROPPED通知码。

对于各控件ID的定义,请查看resource.h头文件,由于篇幅关系,具体代码不在此处列出,参见Chapter8\ListBoxDemo项目。

最后,研究一下LB_DIR和LB_ADDFILE消息。要了解这两个消息,不妨先看一下DlgDirList函数。DlgDirList函数把指定目录中指定属性的子目录和文件的名称显示到列表框中:

复制代码
int DlgDirList(
    _In_    HWND   hDlg,            // 列表框的父窗口句柄
    _Inout_ LPTSTR lpPathSpec,      // 目录名称和文件名的组合,可以是绝对路径或相对路径
    _In_    int    nIDListBox,      // 列表框控件ID
    _In_    int    nIDStaticPath,   // 静态控件ID,用于显示当前当前驱动器和目录,可以设置为0
 _In_    UINT   uFileType);      // 指定文件或目录的属性
  • 参数lpPathSpec是包含目录名称和文件名的字符串缓冲区指针,可以是绝对路径或相对路径,例如"C:\""C:\Windows\*.*"或"*.*"。该函数将字符串拆分为目录和文件名,然后在目录中搜索与文件名匹配的文件名称。如果字符串未指定目录,则在当前目录中搜索。如果字符串包含文件名,则文件名必须至少包含一个通配符(?或*);如果字符串不包含文件名,则把文件名指定为通配符*。将指定目录中与文件名匹配并且具有uFileType参数指定的属性的所有文件名称都添加到列表框中。

  • 参数uFileType指定文件或目录的属性,lpPathSpec目录中具有该属性的文件或目录的名称会被添加到列表框中。该参数可以是表8.15所示的一个或多个值。

表8.15

常量 含义
DDL_READWRITE 可读可写文件,这是默认设置
DDL_ARCHIVE 存档文件
DDL_HIDDEN 隐藏文件
DDL_READONLY 只读文件
DDL_SYSTEM 系统文件
DDL_DIRECTORY 包括子目录,指定目录中的子目录名称都会显示到列表框中,子目录名称显示为子目录名称
DDL_DRIVES 所有映射的驱动器都显示到列表框中,显示为-x-,其中x是驱动器号
DDL_EXCLUSIVE 仅包含具有指定属性的文件。默认情况下,即使未指定DDL_READWRITE,也会列出可读可写文件
DDL_POSTMSGS 如果设置了该标志,则DlgDirList函数使用PostMessage函数将消息发送到列表框;如果未设置,则使用SendMessage函数

如果函数执行成功,则返回值为非零值;如果函数执行失败,则返回值为0。调用该函数实际上就是向列表框发送LB_RESETCONTENT和LB_DIR消息,因此LB_DIR消息的用法和该函数完全一样。例如下面的代码:

复制代码
TCHAR szPath[] = TEXT("*.*");
......
case WM_CREATE:
    hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
    hwndStatic = CreateWindowEx(0, TEXT("Static"), TEXT(""),
        WS_CHILD | WS_VISIBLE, 10, 0, 350, 20, hwnd, (HMENU)(1000), hInstance, NULL);
    hwndListBox = CreateWindowEx(0, TEXT("ListBox"), NULL,
        WS_CHILD | WS_VISIBLE | WS_BORDER | WS_VSCROLL | LBS_NOTIFY,
        10, 25, 200, 240, hwnd, (HMENU)1001, hInstance, NULL);
    DlgDirList(hwnd, szPath, 1001, 1000,
        DDL_ARCHIVE | DDL_READONLY | DDL_SYSTEM | DDL_DIRECTORY | DDL_DRIVES);
    return 0;

程序执行效果如原书的图8.4所示。

DlgDirList函数的nIDStaticPath参数指定的静态控件的作用是显示当前的完整目录名称;如果uFileType参数包含DDL_DIRECTORY标志并且当前目录存在上一级目录,则会在列表框中显示一个...表示上一级目录。DlgDirList函数调用中的lpPathSpec参数不可以直接指定为TEXT("*.*")常字符串:

复制代码
DlgDirList(hwnd, TEXT("*.*"), IDC_LISTBOX, IDC_STATICPATH, 
    DDL_ARCHIVE | DDL_READONLY | DDL_SYSTEM | DDL_DIRECTORY | DDL_DRIVES);

因为该函数会将lpPathSpec参数指定的字符串拆分为目录和文件名。

DlgDirSelectEx函数获取由DlgDirList函数填充的列表框中当前选定的列表项的文本内容:

复制代码
BOOL DlgDirSelectEx(
    _In_  HWND   hDlg,          // 列表框的父窗口句柄
    _Out_ LPTSTR lpString,      // 接收所选列表项文本内容的缓冲区指针
    _In_  int    nCount,        // 缓冲区的长度,以字符为单位
    _In_  int    nIDListBox);   // 列表框控件ID

如果当前选择的是目录名称,则返回值为TRUE;如果当前选择的不是目录名称,则返回值为FALSE。如果当前选择的是目录名称或驱动器号,函数将删除封闭的方括号\[\](目录名称前后的\[\])和连字符-(驱动器号前后的-)。该函数实际上就是向列表框发送LB_GETCURSEL和LB_GETTEXT消息。

LB_ADDFILE消息向由DlgDirList函数填充的列表框中添加指定的文件名,wParam参数没有用到,lParam参数是指向文件名缓冲区的指针,返回值是新添加列表项的索引。如果发生错误,则返回LB_ERR(−1)。例如:

复制代码
SendMessage(hwndListBox, LB_ADDFILE, 0, 
    (LPARAM)TEXT("F:\\Source\\Windows\\Chapter8\\DirList\\DirList.sln"));

关于目录列表的一个简单例子,参见Chapter8\DirList项目。

8.4 组合框

组合框是编辑控件和列表框组合起来的一种子窗口控件,结合了编辑控件和单选列表框的大部分功能,其用法和编辑控件、列表框类似。编辑控件部分用于显示当前选择的列表项,列表框部分列出了用户可以选择的列表项。有3种样式的组合框,如原书的图8.5所示。

在组合框的列表框中没有选中项的情况下,组合框的编辑控件中不会显示任何内容,但是可以在编辑控件中设置一个提示文本,以提示用户进行选择,如原书的图8.6所示。

先看一下组合框的样式,表8.16所示是常用的部分组合框样式。

表8.16

样式 含义
CBS_SIMPLE 始终显示列表框(前提是组合框必须具有一定高度,高度至少可以容纳编辑控件和一行列表项),列表框中的当前选中项会显示到编辑控件中,用户也可以在编辑控件中自行输入内容
CBS_DROPDOWN 列表框部分平时是收起的,用户可以通过单击右侧的下拉箭头来展开列表框以选择列表项。选中的列表项会显示到编辑控件中,在编辑控件中用户也可以自行输入内容
CBS_DROPDOWNLIST 与CBS_DROPDOWN类似,不同之处在于编辑控件部分被静态文本项替换,列表框部分平时也是收起的,用户可以通过单击组合框来展开列表框以选择列表项。选中的列表项会显示到静态文本项中,用户无法在静态文本项中输入内容,只能选择列表框中的列表项
CBS_AUTOHSCROLL 当用户输入文本时,如果文本字数填满了编辑控件,则无法继续输入。在指定该样式后,在必要时编辑控件会自动水平滚动,这样一来用户输入的文本字数就不受编辑控件的长度影响
CBS_DISABLENOSCROLL 该样式通常与WS_VSCROLL样式一起使用。在指定该样式后,如果列表框中的列表项比较少,则不需要使用滚动条,但还是会显示禁用的垂直滚动条;如果未指定该样式,则不需要使用滚动条时会隐藏滚动条
CBS_SORT 按字母顺序对列表框中的列表项进行排序
CBS_HASSTRINGS 默认样式(除自绘组合框)
CBS_OWNERDRAWFIXED 自绘列表框,列表框中的列表项具有相同的高度
CBS_OWNERDRAWVARIABLE 自绘列表框,列表框中的列表项高度可变

除了上面这些组合框样式,通常还需要指定窗口样式WS_CHILD | WS_VISIBLE | WS_VSCROLL。

组合框的列表框中列表项的添加、删除、查询等都是通过发送消息来实现的。表8.17列出了常见的组合框消息,表中前半部分消息的用法和编辑控件、列表框类似。

表8.17

消息类型 含义
CB_ADDSTRING 添加一个字符串类型的列表项。如果没有指定CBS_SORT样式,则该项会添加到列表的末尾,否则添加以后会自动排序。wParam参数没有用到,lParam参数指定为字符串指针,返回值是新添加项的索引。如果发生错误,则返回值为CB_ERR(−1);如果没有足够的内存空间来完成操作,则返回值为CB_ERRSPACE(−2)
CB_DELETESTRING 删除列表框中的一个列表项。wParam参数指定为要删除列表项的索引,lParam参数没有用到,返回值是列表框中剩余列表项的数目。如果发生错误,则返回值为CB_ERR(−1)
CB_INSERTSTRING 与CB_ADDSTRING消息不同的是,CB_INSERTSTRING消息的wParam参数可以指定为插入位置索引,即使组合框具有CBS_SORT样式,也可以插入正确的位置。如果wParam参数指定为−1,则插入列表的末尾。该消息返回新添加列表项的索引,如果发生错误,则返回CB_ERR(−1);如果没有足够的内存空间来完成插入,则返回CB_ERRSPACE(−2)
CB_RESETCONTENT 删除列表框中的所有列表项,编辑控件中的内容也会消失。该消息的wParam和lParam参数都没有用到
CB_FINDSTRING 查找列表框中以指定字符串开头的第一个列表项,不区分大小写。wParam参数指定从哪个位置开始搜索。如果搜索到列表框的末尾还是没有找到匹配项,则继续从头搜索到wParam参数指定的位置,如果wParam参数指定为−1,则从头开始搜索。lParam参数指定为要搜索的字符串,返回值是第一个匹配项的索引;如果没有找到,则返回CB_ERR(−1)
CB_FINDSTRINGEXACT 和CB_FINDSTRING消息不同的是,该消息查找列表框中与指定字符串完全匹配的第一个列表项,不区分大小写。除此之外,和CB_FINDSTRING消息完全相同
CB_SELECTSTRING 该消息与CB_FINDSTRING消息的不同之处在于,找到匹配项以后会选中该项,并把选中项的文本显示到编辑控件中,除此之外完全相同
CB_SETCURSEL 从列表框中选中一个列表项,并把列表项的文本显示到编辑控件中。wParam参数指定为要选中列表项的索引,如果指定为−1,则取消所有选中并清除编辑控件的内容(这种情况下即使没有发生错误返回值也为CB_ERR(−1)),lParam参数没有用到,返回值是所选列表项的索引。如果发生错误,则返回CB_ERR(−1)
CB_GETCURSEL 获取列表框中当前选中项的位置索引。wParam和lParam参数都没有用到,如果当前没有选中项,则返回值为CB_ERR(−1)
CB_GETLBTEXT 获取列表框中一个列表项的文本。wParam参数指定为列表项的索引。lParam参数指定为字符串缓冲区,返回值是字符个数,不包括终止空字符。如果发生错误,则返回CB_ERR(−1)。可以在发送CB_GETTEXT消息之前发送CB_GETTEXTLEN消息,以获取字符串的字符个数(不包括终止空字符)
CB_GETLBTEXTLEN 获取列表框中一个列表项的文本长度,以字符为单位。wParam参数指定为列表项的索引,lParam参数没有用到,返回值是字符个数,不包括终止空字符。如果发生错误,则返回CB_ERR(−1)
CB_LIMITTEXT 限制用户可以在组合框的编辑控件中输入的文本长度,以字符为单位。wParam参数指定为最大字符数,可以设置的最大字符数为0x7FFFFFFE(约2G),如果设置为0,则表示限制为最大支持长度。lParam参数没有用到,如果组合框没有CBS_AUTOHSCROLL样式,则将字符个数设置为大于编辑控件的长度所能容纳的大小是无效的,默认情况下最大缓冲区大小约为32KB个字符,返回值始终为TRUE
CB_GETCOUNT 获取列表框中的列表项总数。wParam和lParam参数都没有用到。如果发生错误,则返回CB_ERR(−1)
CB_SETTOPINDEX 滚动列表框以使指定的列表项位于列表框的顶部,如果到了最大滚动范围,则无法将其滚动到顶部,仅仅是让指定的列表项可见。wParam参数指定为列表项的索引,lParam参数没有用到,执行成功返回值为0。如果发生错误,则返回CB_ERR(−1)
CB_GETTOPINDEX 返回列表框可见列表项中最顶部的索引,最初,索引为0的列表项位于列表框的顶部,但是如果列表框内容发生了滚动,则位于顶部的可能是其他列表项。wParam和lParam参数都没有用到,返回值是列表项中最顶部的索引。如果发生错误,则返回CB_ERR(−1)
CB_SETEDITSEL 选择组合框编辑控件中的部分字符。wParam参数没有用到,LOWORD(lParam)指定起始位置,如果指定为−1,则表示删除所有选择;HIWORD(lParam)指定结束位置,如果指定为−1,则表示从起始位置选择到末尾,该消息仅支持CBS_SIMPLE和CBS_DROPDOWN样式的组合框,执行成功返回值为TRUE,会选中起始位置到结束位置前一个字符之间的所有字符。如果发生错误,则返回CB_ERR(−1)
CB_GETEDITSEL 获取组合框编辑控件中当前选择的字符的开始和结束位置。wParam参数可以设置为一个指向DWORD值的指针,该值接收选择的起始位置,也可以设置为NULL。lParam参数可以设置为一个指向DWORD值的指针,该值接收选择的结束位置,也可以设置为NULL,返回值是一个DWORD值,LOWORD(返回值)表示选择的起始位置,HIWORD(返回值)表示选择的结束位置
CB_SETITEMDATA 设置列表框中指定列表项的项目数据。wParam参数指定为列表项的索引,lParam参数指定为与列表项关联的32位数据。如果发生错误,则返回CB_ERR(−1)
CB_GETITEMDATA 获取列表框中指定列表项的项目数据。wParam参数指定为列表项的索引,lParam参数没有用到,返回值是与列表项关联的项目数据。如果发生错误,则返回CB_ERR(−1)
CB_SETCUEBANNER 在组合框的列表框中没有选中项的情况下,为组合框的编辑控件设置提示文本。wParam参数没有用到,lParam参数指定为提示文本字符串指针,执行成功返回值为1。如果发生错误,则返回CB_ERR(−1)
CB_GETCUEBANNER 获取组合框的编辑控件中显示的提示文本。wParam参数指定为字符串缓冲区指针。lParam参数指定为缓冲区的长度,以字符为单位,执行成功返回值为1;如果编辑控件中没有提示文本,则返回0。如果发生错误,则返回CB_ERR(−1)
CB_SHOWDROPDOWN 显示或隐藏具有CBS_DROPDOWN或CBS_DROPDOWNLIST样式的组合框的列表框。wParam参数可以指定为TRUE表示显示下拉列表框,或FALSE表示隐藏下拉列表框,lParam参数没有用到,返回值始终为TRUE
CB_GETDROPPEDSTATE 检查下拉列表框是显示的还是隐藏的,wParam和lParam参数都没有用到。如果下拉列表框是显示的,则返回TRUE;否则返回FALSE
CB_GETCOMBOBOXINFO 获取组合框的相关信息。wParam参数没有用到,lParam参数指定为一个指向COMBOBOXINFO结构的指针,执行成功返回值为TRUE,执行失败返回值为FALSE。该消息和调用GetComboBoxInfo函数的效果相同。COMBOBOXINFO结构在WinUser.h头文件中定义如下:typedef struct tagCOMBOBOXINFO { DWORD cbSize; // 该结构的大小,以字节为单位 RECT rcItem; // 组合框中编辑控件的矩形范围 RECT rcButton; // 下拉箭头按钮的矩形范围 DWORD stateButton;// 下拉箭头的按钮状态,可以是 // STATE_SYSTEM_INVISIBLE、 // STATE_SYSTEM_PRESSED或0,分别表示 // 没有按钮、按下了按钮或未按下按钮 HWND hwndCombo; // 组合框的窗口句柄 HWND hwndItem; // 编辑控件的窗口句柄 HWND hwndList; // 列表框的窗口句柄 } COMBOBOXINFO, PCOMBOBOXINFO, LPCOMBOBOXINFO;
CB_GETDROPPEDCONTROLRECT 获取整个组合框的矩形区域,包括编辑控件和展开以后的下拉列表框,wParam参数没有用到,lParam参数是一个指向RECT结构的指针,在这个RECT结构中返回整个组合框的矩形范围,执行成功返回值为TRUE,执行失败返回FALSE
CB_SETDROPPEDWIDTH 设置具有CBS_DROPDOWN或CBS_DROPDOWNLIST样式的组合框的列表框的最小允许宽度,以像素为单位。wParam参数设置为最小允许宽度,lParam参数没有用到,执行成功,则返回值为列表框的新最小宽度;如果发生错误,则返回CB_ERR (−1)。默认情况下,下拉列表框的最小允许宽度为0或组合框宽度,列表框的实际宽度是最小允许宽度或组合框宽度,以较大者为准,即默认情况下列表框的宽度和组合框的宽度相同,因为列表框平时都处于隐藏状态,为了完整显示一些较长的列表项,有必要设置一个比组合框宽度更大的最小宽度
CB_GETDROPPEDWIDTH 获取具有CBS_DROPDOWN或CBS_DROPDOWNLIST样式的组合框的列表框的最小允许宽度,以像素为单位。wParam和lParam参数都没有用到,执行成功,则返回值为最小宽度;如果发生错误,则返回CB_ERR(−1)。默认情况下,下拉列表框的最小允许宽度为0或组合框宽度,列表框的实际宽度是最小允许宽度或组合框宽度,以较大者为准
CB_DIR 调用DlgDirListComboBox函数实际上就是向组合框发送CB_RESETCONTENT和CB_DIR消息。DlgDirListComboBox、DlgDirSelectComboBoxEx函数的用法和DlgDirList、DlgDirSelectEx完全相同,在此不再赘述

对于CBS_SIMPLE和CBS_DROPDOWN样式的组合框,用户如果在编辑控件中自行输入了内容,那么列表框中的选中项就会自动取消选中,这时获取编辑控件中的文本可以使用GetWindowText/ GetDlgItemText函数;如果没有自行输入内容,既可以使用GetWindowText/GetDlgItemText函数,也可以发送CB_GETLBTEXT消息。因此对于CBS_SIMPLE和CBS_DROPDOWN样式的组合框,最好还是使用GetWindowText/GetDlgItemText函数获取编辑控件中的文本。

对于CBS_DROPDOWNLIST样式的组合框,不存在用户自行输入内容,因此要获取编辑控件中的文本,既可以使用GetWindowText / GetDlgItemText函数,也可以发送CB_GETLBTEXT消息。

通常可以发送CB_SETCUEBANNER消息为组合框设置一个提示文本,提示文本不属于编辑控件的真正内容,即调用GetWindowText / GetDlgItemText函数无法获取显示在编辑控件中的提示文本内容,函数返回的字符个数始终为0。如果组合框的列表框中存在比较长的列表项,可以通过发送CB_SETDROPPEDWIDTH消息为具有CBS_DROPDOWN或CBS_DROPDOWNLIST样式的组合框的列表框设置一个最小宽度。如果需要,可以通过发送CB_SETITEMDATA消息为组合框中列表框的每个列表项设置一个项目数据。

接下来看一下组合框的通知码。当组合框中发生事件时,系统会以WM_COMMAND消息的形式向父窗口发送通知码。常见的通知码如表8.18所示。

表8.18

通知码 含义
CBN_SETFOCUS 组合框获得了键盘焦点
CBN_KILLFOCUS 组合框失去了键盘焦点
CBN_SELCHANGE 当更改了组合框的列表框中的当前选择时,用户可以通过单击列表项或使用方向键来更改选择。和列表框的LBN_SELCHANGE通知码类似,如果是程序代码更改了选中项,则不会收到CBN_SELCHANGE通知码
CBN_SELENDOK 用户完成了列表项的选择,例如选择了一个列表项并且列表项收起时,如果用户使用方向键更改了选中项并且列表框收起可能收不到该通知码,则建议处理CBN_SELCHANGE通知码(如果是用户在编辑控件中自行输入了内容,则不会发送这两个通知码)。如果是程序代码更改了选中项,则不会收到CBN_SELENDOK通知码
CBN_DBLCLK 当用户双击组合框的列表框中的列表项时,该通知码通常用于CBS_SIMPLE样式的组合框,对于CBS_DROPDOWN或CBS_DROPDOWNLIST样式的组合框,单击会导致隐藏列表框,因此无法双击列表项
CBN_DROPDOWN 当组合框的下拉列表框即将可见时
CBN_CLOSEUP 当组合框的下拉列表框收起时
CBN_EDITUPDATE 当组合框的编辑控件即将显示用户更改的文本时发送,用于CBS_SIMPLE和CBS_DROPDOWN样式的组合框。如果是因为列表项选择的更改而改变了编辑控件中的文本,则不会收到该通知码
CBN_EDITCHANGE 用户已更改组合框的编辑控件中的文本,CBN_EDITUPDATE通知码是在屏幕显示更改的文本前发送的,而CBN_EDITCHANGE通知码是在屏幕显示更改的文本后发送的,用于CBS_SIMPLE和CBS_DROPDOWN样式的组合框,如果是因为列表项选择的更改而改变了编辑控件中的文本,则不会收到该通知码
CBN_ERRSPACE 组合框无法分配足够的内存来完成请求

还有一个扩展组合框类,类名是ComboBoxEx,ComboBoxEx的列表项前面可以设置一个图像。这和列表视图控件有点类似,但是列表视图控件的功能更强大,因此关于ComboBoxEx的用法不再讲解。Chapter8\ControlSpy提供了一个ControlSpy V6小工具,该工具对于了解常用子窗口控件的样式、消息和通知码等有一定的帮助。有需要的读者可以依赖使用文档研究一下。

再介绍一个Up-Down控件,类名是msctls_updown32。Up-Down控件就是一对箭头()。该控件通常与编辑控件组合在一起使用,单击箭头可以增加或减小编辑控件中的值。编辑控件称为其伙伴窗口,这需要先创建一个编辑控件,然后在编辑控件旁边创建一个Up-Down控件()。感兴趣的读者请自行参考MSDN。

8.5 滚动条控件

前面我们学过在调用CreateWindowEx函数创建重叠窗口或弹出窗口时,可以指定WS_HSCROLL或WS_VSCROLL窗口样式创建一个标准水平或垂直滚动条ScrollBar。标准滚动条显示在客户区的底部或右侧,当程序窗口的输出内容比较多导致无法在一个客户区范围内完全显示时,可以滚动标准滚动条以显示超出客户区范围的内容。标准滚动条在非客户区,属于程序窗口的一部分,因此没有自己的窗口句柄。

除了标准滚动条,还可以通过指定窗口类名ScrollBar调用CreateWindowEx函数创建一个水平或垂直滚动条控件。滚动条控件可以以任何大小显示在客户区的任何地方,这取决于CreateWindowEx函数的x、y、nWidth和nHeight参数的设置。滚动条控件的用法和标准滚动条类似,同样需要处理WM_HSCROLL或WM_VSCROLL消息,这两个消息的LOWORD(wParam)表示用户的滚动请求,如果LOWORD(wParam)是SB_THUMBPOSITION或SB_THUMBTRACK,那么HIWORD(wParam)表示滑块的当前位置,在其他情况下HIWORD(wParam)无意义。如果消息是由滚动条控件发送的,则lParam参数是滚动条控件的句柄;如果消息是由标准滚动条发送的,则lParam参数为NULL。通过lParam参数可以区分消息是标准滚动条还是滚动条控件发送的。滚动条控件内置键盘接口,不需要像标准滚动条那样处理WM_KEYDOWN消息。当用户按上下左右方向键、PgUp键、PgDn键、Home键、End键时,系统会向滚动条控件发送包含相应滚动请求的WM_HSCROLL或WM_VSCROLL消息。

滚动条控件的样式很简单,通常就是指定SBS_HORZ或SBS_VERT样式,分别表示创建一个水平或垂直滚动条控件。例如下面的代码:

复制代码
case WM_CREATE:
    hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
    GetClientRect(hwnd, &rect);
    // 位于客户区顶部的水平滚动条控件
    hwndSBHorzTop = CreateWindowEx(0, TEXT("ScrollBar"), 
        NULL, WS_CHILD | WS_VISIBLE | SBS_HORZ, 
        0, 0, rect.right, GetSystemMetrics(SM_CYHSCROLL), 
        hwnd, (HMENU)(1001), hInstance, NULL);
    // 位于客户区底部的水平滚动条控件
    hwndSBHorzBottom = CreateWindowEx(0, TEXT("ScrollBar"), 
        NULL, WS_CHILD | WS_VISIBLE | SBS_HORZ, 
        0, rect.bottom - GetSystemMetrics(SM_CYHSCROLL), rect.right, GetSystemMetrics             (SM_CYHSCROLL), 
        hwnd, (HMENU)(1002), hInstance, NULL);
    // 位于客户区左侧的垂直滚动条控件
    hwndSBVertLeft = CreateWindowEx(0, TEXT("ScrollBar"), 
        NULL, WS_CHILD | WS_VISIBLE | SBS_VERT, 
        0, 0, GetSystemMetrics(SM_CXVSCROLL), rect.bottom, 
        hwnd, (HMENU)(1003), hInstance, NULL);
    // 位于客户区右侧的垂直滚动条控件
    hwndSBVertRight = CreateWindowEx(0, TEXT("ScrollBar"), 
        NULL, WS_CHILD | WS_VISIBLE | SBS_VERT, 
        rect.right - GetSystemMetrics(SM_CXVSCROLL), 0, GetSystemMetrics(SM_CXVSCROLL),  
            rect.bottom, 
        hwnd, (HMENU)(1004), hInstance, NULL);
    return 0;

标准滚动条的默认范围是0~100,滚动条控件的默认范围为空。可以通过调用SetScrollRange函数把范围改成对程序有意义的值,通过调用SetScrollPos函数设置滑块在滚动条中的位置。也可以通过调用SetScrollInfo函数设置滚动条的最小和最大滚动范围、页面大小以及滑块位置。

接下来实现一个屏幕取色、调色程序。Color程序运行效果如原书的图8.7所示。

程序有两个功能:调色和取色。拖动水平滚动条可以分别设置红、绿、蓝颜色值(0~255)。红、绿、蓝颜色的值会实时显示在滚动条下面的颜色值静态控件中,颜色结果会实时显示在两个编辑控件和下方的小矩形框中。单击"开始"按钮可以获取鼠标光标处的COLORREF颜色值,通过GetRValue、GetGValue和GetBValue宏可以提取其红、绿、蓝颜色值,然后更新水平滚动条、颜色值静态控件、编辑控件和颜色结果矩形的显示。鼠标光标附近的40像素 40像素图像放大3倍以后显示在程序客户区左上角(0, 0, 120, 120)的地方,程序可以随时按下空格键停止取色。

具体代码参见Chapter8\Color项目,请大家参考程序界面和程序源代码阅读下面的说明。

在WM_CREATE消息中,程序创建了表8.19所示的控件,并做了一些设置与初始化工作。

表8.19

控件 类名 样式
开始、停止按钮 Button WS_CHILD
3个水平滚动条 ScrollBar WS_CHILD
RGB颜色值 Static WS_CHILD
网页颜色和RGB颜色 Static WS_CHILD
网页颜色和RGB颜色编辑框 Edit WS_CHILD
单击开始按钮开始取色 Static WS_CHILD

关于WM_CTLCOLORSCROLLBAR和WM_CTLCOLORSTATIC消息的处理,在讲解编辑控件的时候已经说过。在WM_CTLCOLORSCROLLBAR消息中分别为3个水平滚动条返回红、绿、蓝画刷以填充其背景;在WM_CTLCOLORSTATIC消息中调用SetTextColor分别设置红、绿、蓝颜色值静态控件的文本颜色。文本的背景色默认就是白色,因此不需要调用SetBkColor或SetBkMode函数设置。静态控件默认情况下是灰色背景,因此返回一个GetSysColorBrush(COLOR_WINDOW)白色画刷。如果不明确静态控件背景、文本背景具体指的是哪个范围,请调用以上函数自行测试。

WM_HSCROLL、WM_COMMAND和WM_SYSCOMMAND消息的处理很简单,用户单击"开始"按钮,调用SetWindowPos函数置顶显示本程序。启动2个计时器,一个是100 ms触发一次,用于获取鼠标光标处的COLORREF颜色值,然后更新水平滚动条、颜色值静态控件、两个编辑框、颜色结果矩形的显示,并把鼠标光标附近的40像素 40像素图像放大3倍以后显示在程序客户区左上角(0, 0, 120, 120)。另一个是1s触发一次,用于让本程序的"停止"按钮实时具有键盘焦点,以响应用户按下空格键停止取色,因为SetActiveWindow、BringWindowToTop、SwitchToThisWindow和SetForegroundWindow等激活窗口函数的效果不尽如人意,所以本程序采用模拟鼠标单击客户区的方法以激活程序窗口。程序窗口激活以后会收到WM_SETFOCUS消息,程序处理该消息,把输入焦点设置为开始或停止按钮。

另外,还有一个Trackbar跟踪条控件,如原书的图8.8所示,类名是msctls_trackbar32,和滚动条控件用法类似,感兴趣的读者请自行参考MSDN。

8.6 静态控件

静态控件可以用于显示简单图形、文本或图像,也可以通过自绘定制其外观。静态控件必须指定SS_NOTIFY样式才能接收鼠键输入并在用户单击或双击时通知其父窗口(WM_COMMAND消息)。

要想在静态控件中显示简单图形,可以指定表8.20所示的样式。

表8.20

样式 含义
SS_BLACKFRAME 创建一个黑色边框的无填充矩形框
SS_GRAYFRAME 创建一个灰色边框的无填充矩形框
SS_WHITEFRAME 创建一个白色边框的无填充矩形框
SS_ETCHEDFRAME 创建一个具有三维外观边框的无填充矩形框
SS_ETCHEDHORZ 创建一条具有三维外观的横线
SS_ETCHEDVERT 创建一条具有三维外观的竖线
SS_BLACKRECT 创建一个黑色填充的无边框矩形
SS_GRAYRECT 创建一个灰色填充的无边框矩形
SS_WHITERECT 创建一个白色填充的无边框矩形

以上9种样式不能组合使用。调用CreateWindowEx函数创建上述9种简单图形时,会忽略窗口标题参数lpWindowName,9种简单图形的显示效果如原书的图8.9所示(在COLOR_BTNFACE窗口背景上绘制的)。简单图形样式的静态控件可以用于标记或分隔其他子窗口控件。

要想在静态控件中显示文本,可以指定表8.21所示的样式。

表8.21

样式 含义
SS_LEFT 在指定的矩形范围内左对齐显示文本。如果一行显示不了,则会自动换行到下一行;如果一个单词的长度超过控件的宽度,则会被裁剪
SS_CENTER 在指定的矩形范围内居中对齐显示文本。如果一行显示不了,则会自动换行到下一行;如果一个单词的长度超过控件的宽度,则会被裁剪
SS_RIGHT 在指定的矩形范围内右对齐显示文本。如果一行显示不了,则会自动换行到下一行;如果一个单词的长度超过控件的宽度,则会被裁剪
SS_SIMPLE 在指定的矩形范围内显示一行左对齐文本。即使控件指定了较高的高度也不会自动换行显示,如果禁用该控件,则控件不会使其文本变灰
SS_LEFTNOWORDWRAP 与SS_SIMPLE类似,也不会自动换行,但是控件的高度会完全显示

在原书的图8.10中,分别指定上面的样式创建了宽高为80 60的5个静态控件(在白色窗口背景上绘制)。

SS_LEFT、SS_CENTER和SS_RIGHT样式的文本静态控件都可以自动换行,但前提是控件具有足够的高度,否则超出控件大小范围的部分不会显示。如果需要根据字符串的实际长度和高度来指定静态控件的宽度和高度参数,可以使用相关字符、字符串计算函数,例如GetCharWidth32、GetTextExtentPoint32等。

要想在静态控件中显示图像,可以指定表8.22所示的样式。

表8.22

样式 含义
SS_ICON 图标。CreateWindowEx函数的窗口标题参数lpWindowName指定为图标ID值,该样式会忽略CreateWindowEx函数的宽高参数nWidth和nHeight,系统自动调整静态控件的大小以适应图标,图标的大小默认情况下使用GetSystemMetrics (SM_CXICON)和GetSystemMetrics(SM_CYICON)返回的值,通常是32 32,可以同时指定SS_REALSIZEIMAGE样式以使用图标的实际大小。即在默认情况下,不管宽高参数nWidth和nHeight指定为多少,也不管图标的实际大小是多少,系统总认为图标大小为32 32,并调整静态控件大小为32 32,如果指定了SS_REALSIZEIMAGE样式,则会使用图标的实际大小,并把静态控件调整为图标的实际大小
SS_BITMAP 位图。CreateWindowEx函数的窗口标题参数lpWindowName指定为位图ID值,该样式会忽略CreateWindowEx函数的宽高参数nWidth和nHeight,系统会根据位图的实际大小自动调整静态控件的大小。如果想使用nWidth和nHeight指定的大小,则可以同时指定SS_REALSIZECONTROL样式,系统会自动调整位图(放大或缩小)以适应静态控件的大小
SS_ENHMETAFILE 图元文件

例如下面的代码:

复制代码
hwndIcon = CreateWindowEx(0, TEXT("Static"), TEXT("#101"),
           WS_CHILD | WS_VISIBLE | SS_ICON | SS_NOTIFY,
           10, 10, 100, 100, hwnd, (HMENU)(1001), hInstance, NULL);
hwndBmp = CreateWindowEx(0, TEXT("Static"), TEXT("#102"),
          WS_CHILD | WS_VISIBLE | SS_BITMAP | SS_NOTIFY,
          10, 50, 100, 100, hwnd, (HMENU)(1002), hInstance, NULL);

ID为101的资源是一个64 64大小的图标,ID为102的资源是一个77 73大小的位图,资源脚本文件的部分内容如下所示:

复制代码
IDI_PANDA               ICON                    "Panda.ico"
IDB_SMILE               BITMAP                  "SmileFace.bmp"

资源头文件的部分内容如下所示:

复制代码
#define IDI_PANDA                       101
#define IDB_SMILE                       102

CreateWindowEx函数的lpWindowName参数只能设置为TEXT("#101")的形式。TEXT("#IDI_PANDA")是不可以的,使用MAKEINTRESOURCE宏也不可以。运行效果如原书的图8.11所示。

在图8.11中,对于图标,因为没有指定SS_REALSIZEIMAGE样式,所以系统会认为图标大小为32 32,系统自动调整64 64大小的图标为32 32,并调整静态控件大小为32 32;对于位图,因为没有指定SS_REALSIZECONTROL样式,所以系统会使用位图的实际大小,也就是77 73,并根据位图的实际大小自动调整静态控件的大小为77 73。

对于图像静态控件,还可以同时指定SS_CENTERIMAGE样式,该样式可以让图像在CreateWindowEx函数的(x, y, nWidth, nHeight)定义的矩形范围内水平和垂直居中显示。

在讲解资源的时候说过,菜单、图标、光标等的ID也可以使用字符串,例如:

复制代码
Panda                   ICON                    "Panda.ico"
SmileFace               BITMAP                  "SmileFace.bmp"

此时,CreateWindowEx函数的lpWindowName参数可以指定为TEXT("Panda")、TEXT("SmileFace")。

可以通过发送STM_SETIMAGE消息为图像静态控件设置一个新图像。wParam参数指定图像类型,可以是IMAGE_ICON(图标,用于SS_ICON样式)、IMAGE_BITMAP(位图,用于SS_BITMAP样式)、IMAGE_CURSOR(光标,用于SS_ICON样式)或IMAGE_ENHMETAFILE(增强图元文件,用于SS_ENHMETAFILE样式)。lParam参数指定为图像的句柄(HICON、HBITMAP、HCURSOR等)。返回值是先前与静态控件关联的图像的句柄(如果有的话);否则返回值是NULL。

STM_GETIMAGE消息用于获取与图像静态控件关联的图像的句柄,wParam参数指定图像类型,lParam参数没有用到。返回值是与静态控件关联的图像的句柄(如果有的话);否则返回值是NULL。

对于文本静态控件,如果想在一个字符的底部显示下划线,则可以在该字符的前面使用一个&符号,如果本意是想显示一个&符号,则需要指定SS_NOPREFIX样式;SS_SUNKEN样式表示在静态控件周围绘制一个半凹陷的边框,对于边框,还可以指定普通窗口样式中那些与窗口边框有关的样式;SS_OWNERDRAW样式表示自绘静态控件,每当需要绘制控件时,父窗口就会收到WM_DRAWITEM消息。

静态控件的通知码有STN_CLICKED单击时、STN_DBLCLK双击时、STN_DISABLE禁用静态控件时和STN_ENABLE启用静态控件时,这些通知码通过WM_COMMAND消息的形式发送,但前提是必须指定SS_NOTIFY样式,默认情况下静态控件不会发送通知码。

最后,需要处理的消息可能还有WM_CTLCOLORSTATIC,该消息针对静态控件和只读或禁用的编辑控件。

8.7 SysLink控件

SysLink控件是Comctl32.dll版本6以后引入的一个子窗口控件,可以用于显示普通文本和超链接。超链接的文本颜色默认情况下是蓝色,带下划线,超链接支持Href、ID属性。学过HTML的读者对于创建超链接的方法应该很熟悉。Href支持任何协议,例如http、https、ftp、mailto等。一个SysLink控件中可以有多个超链接。ID为可选属性,它在一个SysLink控件中必须是唯一的。当用户单击超链接时,系统会发送包含NM_CLICK通知码的WM_NOTIFY消息。要区分是哪一个超链接,可以使用其位置索引,索引从0开始。例如下面的代码创建了一个SysLink控件(只有一个超链接):

复制代码
hwndSysLink = CreateWindowEx(0, TEXT("SysLink"), 
    TEXT("我喜欢<a href=\"http://www.WindowsChs.com/\">Windows程序设计</a>"),
    WS_CHILD | WS_VISIBLE, 10, 10, 200, 20, hwnd, (HMENU)(1001), hInstance, NULL);

运行效果如下所示:

普通文本"我喜欢"的颜色是黑色,超链接文本的颜色是蓝色,SysLink控件的背景是灰色。

超链接的状态和属性可以通过发送LM_SETITEM消息来进行设置。wParam参数没有用到。lParam参数是一个指向LITEM结构的指针,该结构包含超链接所需的新状态和属性,也用于LM_GETITEM消息中获取超链接的状态和属性。LITEM结构在CommCtrl.h头文件中定义如下:

复制代码
typedef struct tagLITEM
{
    UINT        mask;                   // 标志,要设置或获取哪些项目
    int         iLink;                  // 超链接的索引
    UINT        state;                  // 超链接的状态,和stateMask设置为相同的值
    UINT        stateMask;              // 超链接的状态掩码
    WCHAR       szID[MAX_LINKID_TEXT];  // ID,最大字符数MAX_LINKID_TEXT(48)
    WCHAR       szUrl[L_MAX_URL_LENGTH];// URL,最大字符数L_MAX_URL_LENGTH (2048 +
                                        // 32 + sizeof("://"))
} LITEM, *PLITEM;
  • mask字段指定要设置或获取哪些项目,可以是表8.23所示的一个或多个标志的组合。

表8.23

常量 含义
LIF_ITEMINDEX 超链接的索引,因为通常都是通过索引来确定SysLink控件中的一个超链接,所以不管是设置还是获取,都需要指定该标志,并为iLink字段设置一个值
LIF_ITEMID 超链接的ID,对应szID字段
LIF_URL 超链接的URL,对应szUrl字段
LIF_STATE 超链接的状态,对应stateMask字段
  • state和stateMask字段使用相同的值,可用的值如表8.24所示。

表8.24

常量 含义
LIS_ENABLED 默认值,该链接可以响应用户输入,除非创建控件的时候指定了WS_DISABLED样式
LIS_FOCUSED 该链接具有键盘焦点,此时按Enter键会发送包含NM_CLICK通知码的WM_NOTIFY消息
LIS_VISITED 该链接已被用户访问过
LIS_HOTTRACK 当鼠标悬停在控件上时,将以不同的颜色(COLOR_HIGHLIGHT为蓝色)突出显示

SysLink控件可用的部分样式如表8.25所示。

表8.25

常量 含义
LWS_TRANSPARENT SysLink控件背景透明
LWS_NOPREFIX 如果文本包含&符号,则将其视为文字字符&,而不是快捷键的前缀

当用户单击一个超链接时,系统会发送包含NM_CLICK通知码的WM_NOTIFY消息。当超链接具有输入焦点时,按下Enter键会发送包含NM_RETURN通知码的WM_NOTIFY消息。程序通常用同样的方法处理这两个消息,要区分是哪一个超链接,可以使用其位置索引,索引从0开始。这两个通知码的lParam参数是一个指向NMLINK结构的指针,该结构在CommCtrl.h头文件中定义如下:

复制代码
typedef struct tagNMLINK
{
    NMHDR   hdr;    // NMHDR结构
    LITEM   item;   // LITEM结构,包含超链接的状态和属性信息
} NMLINK, *PNMLINK;

例如下面的示例:

复制代码
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    HINSTANCE hInstance;
    static HFONT hFont;
    static HWND hwndSysLink;

    LITEM li = { 0 };
    PNMLINK pnmLink;

    switch (uMsg)
    {
    case WM_CREATE:
        hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
        hwndSysLink = CreateWindowEx(0, TEXT("SysLink"),
          TEXT("我喜欢<a href=\"http://www.WindowsChs.com/\" ID= \"Windows\" >Windows程序  
                  设计</a>\n")
          TEXT("我喜欢<a href=\"http://www.taobao.com/\" ID=\"taobao\" >淘宝购物</a>\n")
          TEXT("我喜欢<a href=\"http://www.jd.com/\" ID=\"jd\" >京东商城</a>"),
          WS_CHILD | WS_VISIBLE | WS_TABSTOP | LWS_TRANSPARENT,
          10, 10, 200, 60, hwnd, (HMENU)(1001), hInstance, NULL);
        hFont = CreateFont(18, 0, 0, 0, 0, 0, 0, 0, DEFAULT_CHARSET, 0, 0, 0, 0, TEXT                               ("微软雅黑"));
        SendMessage(hwndSysLink, WM_SETFONT, (WPARAM)hFont, FALSE);
        return 0;

    case WM_LBUTTONDBLCLK:
        li.mask = LIF_ITEMINDEX | LIF_URL;
        li.iLink = 0;
        StringCchCopy(li.szUrl, L_MAX_URL_LENGTH, TEXT("https://msdn.microsoft.com/"));
        SendMessage(hwndSysLink, LM_SETITEM, 0, (LPARAM)&li);
        return 0;

    case WM_NOTIFY:
        switch (((LPNMHDR)lParam)->code)
        {
        case NM_CLICK:
        case NM_RETURN:
            pnmLink = (PNMLINK)lParam;
            if (pnmLink->hdr.hwndFrom == hwndSysLink)
            {
                if (pnmLink->item.iLink == 0)
                    ShellExecute(NULL, TEXT("open"), pnmLink->item.szUrl, NULL, NULL,   
SW_SHOW);
                else if (pnmLink->item.iLink == 1)
                    ShellExecute(NULL, TEXT("open"), pnmLink->item.szUrl, NULL, NULL,   
SW_SHOW);
                else if (pnmLink->item.iLink == 2)
                    ShellExecute(NULL, TEXT("open"), pnmLink->item.szUrl, NULL, NULL,   
SW_SHOW);
            }
            break;
        }
        return 0;

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

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

完整代码参见Chapter8\SysLinkDemo。在创建SysLink控件时,指定LWS_TRANSPARENT透明样式;在客户区中双击时,程序发送一个LM_SETITEM消息,设置第1个超链接的URL为https://msdn.microsoft.com/;在WM_NOTIFY消息中处理NM_CLICK和NM_RETURN通知码,根据超链接的索引分别进行处理,ShellExecute函数用于打开一个文件或URL,后面会学习这个函数。程序运行效果如原书的图8.12所示。

实际上我更倾向于使用静态控件Static来替代SysLink控件,因为SysLink控件的外观不太容易控制,例如改变超链接的文本颜色、去除超链接的下划线等。

8.8 全局热键与特定线程热键

热键,也叫快捷键。当用户在热键控件中输入用于热键的组合键时,组合键的名称会显示在热键控件中,如,组合键包括修饰键(Ctrl、Alt、Shift)和伴随键(数字字母键、方向键、功能键F1~F12)。用户输入组合键以后,程序可以获取热键控件中的组合键,以设置全局热键或特定于线程的热键。

发送HKM_GETHOTKEY消息可以获取热键控件中的修饰键和伴随键,消息的wParam和lParam参数都没有用到。该消息返回一个包含修饰键标志和伴随键的虚拟键码的DWORD值,LOBYTE(LOWORD(返回值))表示热键的伴随键的虚拟键码,HIBYTE(LOWORD(返回值))表示热键的修饰键标志。修饰键标志可以是表8.26所示的值的组合。

表8.26

常量 含义
HOTKEYF_SHIFT 1 Shift键
HOTKEYF_CONTROL 2 Ctrl键
HOTKEYF_ALT 4 Alt键

发送 HKM_SETHOTKEY 消息可以设置热键控件中的修饰键和伴随键,LOBYTE(LOWORD (wParam))表示热键的伴随键的虚拟键码,HIBYTE(LOWORD (wParam))表示热键的修饰键标志,lParam参数没有用到,该消息始终返回0。

全局热键与指定的窗口相关联,不管该窗口是否处于活动状态,按下全局热键以后,系统都会通知该窗口。可以通过发送WM_SETHOTKEY消息设置全局热键。每当用户按下全局热键时,如果发送WM_SETHOTKEY消息设置全局热键的窗口处于活动状态,那么将收到WM_SYSCOMMAND消息(wParam等于SC_HOTKEY,lParam等于窗口的句柄);如果该窗口没有处于活动状态,那么系统会激活该窗口到前台显示,在调用WM_SETHOTKEY消息设置全局热键的应用程序退出之前,热键一直有效。

WM_SETHOTKEY消息的LOWORD(wParam)指定热键的伴随键的虚拟键码,HIWORD(wParam)指定热键的修饰键标志,因此LOWORD(HKM_GETHOTKEY)消息的返回值可以用作WM_SETHOTKEY消息的wParam参数,wParam参数设置为NULL表示删除与窗口关联的全局热键;lParam参数没有用到。具有WS_CHILD窗口样式的窗口不能设置全局热键。该消息的返回值包括表8.27所示的几种情况。

表8.27

返回值 含义
−1 热键无效(例如VK_ESCAPE、VK_SPACE和VK_TAB等都是无效的热键)
0 窗口无效
1 成功,没有其他窗口具有相同的热键
2 成功,但另一个窗口已具有相同的热键

一个窗口只能关联一个全局热键,如果窗口已经有一个与之关联的全局热键,则新设置的全局热键将替换旧的全局热键;如果多个窗口具有相同的全局热键,则由全局热键激活的窗口是随机的。

发送WM_SETHOTKEY消息可以设置一个与指定窗口相关联的全局热键。按下全局热键以后,如果该窗口没有处于活动状态,则系统会激活该窗口;如果该窗口处于活动状态,则会收到WM_SYSCOMMAND消息(wParam等于SC_HOTKEY,lParam等于窗口的句柄),所以该消息主要用于将程序窗口调到前台。全局热键也称为窗口激活热键。

如果需要在用户按下热键以后执行某种操作,例如QQ程序不管是处于最小化还是活动状态,按下Ctrl + Alt + A组合键都可以打开QQ截图程序,这可以通过调用RegisterHotKey函数设置特定于线程的系统范围的热键来实现。在用户按下RegisterHotKey函数指定的热键以后,系统会发送WM_HOTKEY消息到线程的消息队列,该热键不会把程序窗口调到前台。线程的概念后面再讲,程序执行以后会创建一个主线程。如果需要,程序可以通过调用CreateThread函数创建其他线程:

复制代码
BOOL WINAPI RegisterHotKey(
    _In_opt_ HWND hWnd,         // 窗口句柄,将接收由热键生成的WM_HOTKEY消息
    _In_     int  id,           // 热键的ID
    _In_     UINT fsModifiers,  // 修饰键标志
 _In_     UINT vk);          // 伴随键的虚拟键码
  • hWnd参数指定窗口句柄,该窗口将接收由热键生成的WM_HOTKEY消息,热键与hWnd指定的窗口相关联。如果设置为NULL,则WM_HOTKEY消息将发送到调用该函数的线程的消息队列中,即在这种情况下热键与调用该函数的线程相关联。程序可以在消息循环中处理该消息以决定发送给哪个窗口,如同计时器的其他方式的消息循环中的处理代码。

  • id参数指定热键的ID,因为一个程序可以通过调用RegisterHotKey函数设置多个热键,在WM_HOTKEY消息中可以通过id来确定是哪个热键。程序可以指定0x0000~0xBFFF范围内的id值,如果是动态链接库,则必须指定0xC000~0xFFFF范围内的id值(一个程序可以同时加载多个动态链接库,为避免与其他动态链接库定义的热键id冲突,动态链接库应使用GlobalAddAtom函数来分配一个热键id)。

  • fsModifiers参数指定修饰键标志,可以是表8.28所示的值的组合,这些标志的值和HOTKEYF_开头的那些标志的值并不对应。

表8.28

常量 含义
MOD_ALT 1 Alt
MOD_CONTROL 2 Ctrl
MOD_SHIFT 4 Shift
MOD_WIN 8 Windows键,但是Windows键通常是保留给操作系统使用

如果函数执行成功,则返回值为非零值;如果函数执行失败,则返回值为0。要获取错误信息,请调用GetLastError。

特定于线程的系统范围的热键表示在系统中是唯一的。如果系统中其他程序已经注册过相同的热键组合键,则RegisterHotKey函数调用会失败。如果使用相同的Hwnd和id又创建了一个或多个组合键不同的特定线程热键,则旧热键与新热键都可以工作。

如果需要取消注册热键,可以调用UnregisterHotKey函数:

复制代码
BOOL WINAPI UnregisterHotKey(_In_opt_ HWND hWnd, _In_ int id);

WM_HOTKEY消息的wParam参数是生成消息的热键的id,LOWORD(lParam)是以MOD_开头的修饰键标志,HIWORD(lParam)是伴随键的虚拟键码。

关于全局热键和特定线程热键的示例参见Chapter8\HotKeyDemo项目。部分代码如下:

复制代码
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    HINSTANCE hInstance;
    static HWND hwndHotKeyHwnd, hwndBtnSetHwnd;     // 与窗口激活热键相关的热键控件和按钮句柄
    static HWND hwndHotKeyThread, hwndBtnSetThread; // 与特定线程热键相关的热键控件和按钮句柄
    static HFONT hFont;
    DWORD dwHotKey;         // 发送HKM_GETHOTKEY消息获取热键控件的修饰键和伴随键的返回值
    DWORD dwRet;            // 发送WM_SETHOTKEY消息设置窗口激活热键的返回值
    UINT fsModifiers = 0;   // RegisterHotKey函数的fsModifiers参数,修饰键标志

    switch (uMsg)
    {
    case WM_CREATE:
        hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
        // 2个热键控件
        hwndHotKeyHwnd = CreateWindowEx(0, TEXT("msctls_hotkey32"), NULL,
            WS_CHILD | WS_VISIBLE,
            10, 10, 120, 22, hwnd, (HMENU)IDC_HOTKEYHWND, hInstance, NULL);
        hwndHotKeyThread = CreateWindowEx(0, TEXT("msctls_hotkey32"), NULL,
            WS_CHILD | WS_VISIBLE,
            140, 10, 120, 22, hwnd, (HMENU)IDC_HOTKEYTHREAD, hInstance, NULL);

        // 2个按钮
        hwndBtnSetHwnd = CreateWindowEx(0, TEXT("Button"), TEXT("设置窗口激活热键"),
            WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
            10, 40, 120, 25, hwnd, (HMENU)IDC_BTNSETHWND, hInstance, NULL);
        hwndBtnSetThread = CreateWindowEx(0, TEXT("Button"), TEXT("注册特定线程热键"),
            WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
            140, 40, 120, 25, hwnd, (HMENU)IDC_BTNSETTHREAD, hInstance, NULL);

        // 设置字体
        hFont = CreateFont(18, 0, 0, 0, 0, 0, 0, 0, GB2312_CHARSET, 0, 0, 0, 0, TEXT                               ("微软雅黑"));
        SendMessage(hwndHotKeyHwnd, WM_SETFONT, (WPARAM)hFont, FALSE);
        SendMessage(hwndHotKeyThread, WM_SETFONT, (WPARAM)hFont, FALSE);
        SendMessage(hwndBtnSetHwnd, WM_SETFONT, (WPARAM)hFont, FALSE);
        SendMessage(hwndBtnSetThread, WM_SETFONT, (WPARAM)hFont, FALSE);
        return 0;

    case WM_SETFOCUS:
        SetFocus(hwndHotKeyHwnd);
        return 0;

    case WM_COMMAND:
        switch (LOWORD(wParam))
        {
        case IDC_BTNSETHWND:
            dwHotKey = SendMessage(hwndHotKeyHwnd, HKM_GETHOTKEY, 0, 0);
            if (!HIBYTE(LOWORD(dwHotKey)) || !LOBYTE(LOWORD(dwHotKey)))
            {
                MessageBox(hwnd, TEXT("设置窗口激活热键需要修饰键和伴随键"), TEXT("错误"),    
                 MB_OK);
                return 0;
            }
            // 设置窗口激活热键
            dwRet = SendMessage(hwnd, WM_SETHOTKEY, LOWORD(dwHotKey), 0);
            if (dwRet <= 0)
                MessageBox(hwnd, TEXT("窗口激活热键设置失败"), TEXT("错误"), MB_OK);
            else if (dwRet == 1)
                MessageBox(hwnd, TEXT("成功,没有其他窗口具有"), TEXT("成功"), MB_OK);
            else if (dwRet == 2)
                MessageBox(hwnd, TEXT("成功,但另一个窗口已具有"), TEXT("警告"), MB_OK);
            break;

        case  IDC_BTNSETTHREAD:
            dwHotKey = SendMessage(hwndHotKeyThread, HKM_GETHOTKEY, 0, 0);
            if (HIBYTE(LOWORD(dwHotKey)) & HOTKEYF_SHIFT)
                fsModifiers |= MOD_SHIFT;
            if (HIBYTE(LOWORD(dwHotKey)) & HOTKEYF_CONTROL)
                fsModifiers |= MOD_CONTROL;
            if (HIBYTE(LOWORD(dwHotKey)) & HOTKEYF_ALT)
                fsModifiers |= MOD_ALT;
            if (!fsModifiers || !LOBYTE(LOWORD(dwHotKey)))
            {
                MessageBox(hwnd, TEXT("注册特定线程热键需要修饰键和伴随键"), TEXT("错误"),    
                               MB_OK);
                return 0;
            }
            // 注册特定线程热键
            if (RegisterHotKey(hwnd, 1, fsModifiers, LOBYTE(LOWORD(dwHotKey))))
                MessageBox(hwnd, TEXT("注册特定线程热键成功"), TEXT("注册成功"), MB_OK);
            else
                MessageBox(hwnd, TEXT("注册特定线程热键失败"), TEXT("注册失败"), MB_OK);
            break;
        }
        return 0;

    case WM_SYSCOMMAND:
        // 如果该窗口处于活动状态,则会收到WM_SYSCOMMAND消息
        // 实际编程中很少处理SC_HOTKEY的WM_SYSCOMMAND消息
        if ((wParam & 0xFFF0) == SC_HOTKEY)
        {
            MessageBox(hwnd, TEXT("窗口激活热键消息"), TEXT("消息"), MB_OK);
            return 0;
        }
        return DefWindowProc(hwnd, uMsg, wParam, lParam);

    case WM_HOTKEY:
        // 处理特定线程热键消息
        if (wParam == 1)
            MessageBox(hwnd, TEXT("特定线程热键消息"), TEXT("消息"), MB_OK);
        return 0;

    case WM_DESTROY:
        UnregisterHotKey(hwnd, 1);
        DeleteObject(hFont);
        PostQuitMessage(0);
        return 0;
    }

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

当用户更改了热键控件中的内容时,系统会发送包含EN_CHANGE通知码的WM_COMMAND消息。

8.9 IP地址控件

目前应用最广泛的IP地址是基于IPv4的,一个IP地址的长度为32位,即4字节(DWORD)数据。IP地址中的每字节使用一个十进制数字来表示,每字节的数值范围是0~255,数字之间使用小数点分隔。IPv4的IP地址格式为XXX.XXX.XXX.XXX,这种IP地址表示法称为点分十进制表示法。IP地址控件允许用户以点分十进制表示法输入IP地址,如原书的图8.13所示。

4个数字是4个域,实际上每个域都是一个编辑控件。当IP地址控件获得、失去键盘焦点时会收到EN_SETFOCUS、EN_KILLFOCUS通知码。当IP地址控件中的任何域更改时,都会收到EN_CHANGE通知码(通过WM_COMMAND消息的形式),这些通知码通常都不需要处理。

IP地址控件常用的消息如表8.29所示。

表8.29

消息类型 含义
IPM_SETADDRESS 设置IP地址控件中的IP地址。wParam参数没有用到。lParam参数指定为新地址的32位DWORD值,但是我们习惯上使用点分十进制书写IP地址,使用MAKEIPADDRESS宏可以解决这个问题,例如MAKEIPADDRESS(192, 168, 0, 1)
IPM_GETADDRESS 获取IP地址控件中的IP地址。wParam参数没有用到。lParam参数指定为接收地址的32位DWORD值的指针。IP地址的4个数字是4个域,可以使用FIRST_IPADDRESS、SECOND_IPADDRESS、THIRD_IPADDRESS和FOURTH_IPADDRESS宏分别提取每个域的值,如果某个域为空,则值为0。IPM_GETADDRESS消息的返回值为非空域的个数
IPM_CLEARADDRESS 清空IP地址控件中的内容,wParam和lParam参数都没有用到
IPM_ISBLANK IP地址控件中的所有域是否都为空。wParam和lParam参数都没有用到。如果所有域都为空,返回非零值;否则返回0
IPM_SETRANGE 默认情况下每个域字节的数值范围是0~255,可以通过发送该消息设置IP地址控件中指定域的数值范围,wParam参数指定为域的索引,LOBYTE(LOWORD(lParam))设置为范围的最小值,HIBYTE (LOWORD(lParam))设置为范围的最大值,可以使用MAKEIPRANGE宏,执行成功,则返回非零值;否则返回0

当用户更改了IP地址控件中的域,或鼠标光标从一个域移动到另一个域时,会发送包含IPN_FIELDCHANGED通知码的WM_NOTIFY消息(发送IPM_SETADDRESS消息不会生成该通知码)。对于WM_NOTIFY消息,要先把lParam参数转换为指向NMHDR结构的指针,确定通知码的类型,然后才可以确定lParam参数是不是一个指向更大结构的指针。IPN_FIELDCHANGED通知码的lParam参数是一个指向NMIPADDRESS结构的指针,该结构包含当前域的索引和值,在CommCtrl.h头文件中定义如下:

复制代码
typedef struct tagNMIPADDRESS
{
    NMHDR hdr;
    int iField; // 域索引
    int iValue; // 该域的值
} NMIPADDRESS, *LPNMIPADDRESS;

程序可以获取((LPNMIPADDRESS)lParam)->iValue值,也可以修改该值。如果某个域为空,则值为−1。

8.10 图像列表ImageList_Create函数

图像列表是具有相同大小的多个图像的集合,每个图像都可以通过其索引来引用,图像列表用于有效管理大量图标或位图。图像列表并不是一个窗口,因此没有窗口类名。可以通过调用ImageList_Create函数创建一个图像列表,函数返回HIMAGELIST图像列表句柄:

复制代码
HIMAGELIST ImageList_Create(
    int  cx,        // 每个图像的宽度,以像素为单位
    int  cy,        // 每个图像的高度,以像素为单位
    UINT flags,     // 要创建的图像列表类型标志,设置为0表示默认
    int  cInitial,  // 图像列表最初包含的图像个数
    int  cGrow);    // 当图像列表中的图像个数超过cInitial时,可以动态增长的图像个数,可以设置为0

flags参数指定要创建的图像列表的类型,可以是表8.30所示的值的组合。

表8.30

常量 含义
ILC_COLOR 默认情况,使用ILC_COLOR4(4位色DIB)
ILC_COLOR24 24位色DIB
ILC_COLOR32 32位色DIB
ILC_COLORDDB 使用设备相关位图DDB
ILC_MASK 使用掩码,包含两个位图,其中一个是用作掩码的单色位图。如果没有指定该标志,则仅包含一个位图。图标或光标通常需要指定该类型,因为图标或光标内含掩码数据
ILC_ORIGINALSIZE 使用所添加的图像的原始实际大小

当不再需要图像列表的时候,可以通过调用ImageList_Destroy函数将其销毁:

复制代码
BOOL ImageList_Destroy(_In_opt_ HIMAGELIST himl);

创建图像列表以后,可以添加、删除、替换、合并、绘制和拖动图像。本书主要讲解添加、删除和替换图像。可以通过调用ImageList_Add函数把位图添加到图像列表中,通过调用ImageList_AddIcon宏把图标或光标添加到图像列表中:

复制代码
int ImageList_Add(
    _In_     HIMAGELIST himl,       // 图像列表句柄
    _In_     HBITMAP    hbmImage,   // 要添加的位图句柄
_In_opt_ HBITMAP    hbmMask);       // 掩码位图的句柄,如果不需要,可以设置为NULL

ImageList_Add函数可以一次将一个或多个位图添加到图像列表中,hbmImage参数指定要添加到图像列表的位图句柄。假设图像列表的宽高为32像素 32像素,如果需要添加10个32 32大小的位图,可以把这10个位图制作到一个宽高为320像素 32像素的大位图中,函数会根据图像列表和位图的宽度自动计算位图个数。

复制代码
int ImageList_AddIcon(
    HIMAGELIST himl,    // 图像列表句柄
    HICON      hicon);  // 要添加的图标或光标句柄,图标或光标文件本身包含掩码数据

在ImageList_Add和ImageList_AddIcon将每个图像添加到图像列表时系统会为其分配索引。索引从0开始,列表中的第一个图像的索引为0,下一个图像的索引为1,以此类推。添加单个图像时,函数将返回图像的索引;当一次添加多个图像时,函数返回第一个图像的索引;函数执行失败,则返回−1。

调用ImageList_Replace函数可以用一个新位图替换图像列表中的旧位图,调用ImageList_ReplaceIcon函数可以用一个新图标或光标替换图像列表中的旧图标或光标:

复制代码
BOOL ImageList_Replace(
    HIMAGELIST himl,        // 图像列表句柄
    int        i,           // 要替换的位图的索引
    HBITMAP    hbmImage,    // 新位图句柄
    HBITMAP    hbmMask);    // 新掩码位图句柄
int ImageList_ReplaceIcon(
    _In_ HIMAGELIST himl,   // 图像列表句柄
    _In_ int        i,      // 要替换的图标或光标的索引 
    _In_ HICON      hicon); // 新图标或光标句柄

对于ImageList_ReplaceIcon函数,执行成功,返回图像的索引;否则返回−1。

如果ImageList_ReplaceIcon函数的参数i设置为−1,则表示添加到图像列表的末尾,ImageList_AddIcon宏实际上就是调用的ImageList_ReplaceIcon函数:

复制代码
#define ImageList_AddIcon(himl, hicon) ImageList_ReplaceIcon(himl, -1, hicon)

要从图像列表中删除图像可以调用ImageList_Remove函数:

复制代码
BOOL ImageList_Remove(
    HIMAGELIST himl,    // 图像列表句柄
    int        i);      // 图像索引

如果图像索引参数i设置为−1,则从图像列表中删除所有图像。

8.11 工具提示控件

工具提示控件是一个小窗口。当鼠标光标悬停在一个控件或特定区域上时,该窗口会自动弹出,通常用于显示一些提示或帮助信息。在资源管理器中,当用户把鼠标悬停在某一文件上时,会弹出一个工具提示控件,显示该文件的大小、类型和修改日期。

通常,可以在WM_CREATE消息中按如下方式创建一个工具提示控件:

复制代码
hwndTip = CreateWindowEx(WS_EX_TOPMOST, TEXT("tooltips_class32"), NULL,
    WS_POPUP | TTS_ALWAYSTIP,
    CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 
    hwnd, NULL, hInstance, NULL);

先看一下工具提示控件所用的窗口样式。工具提示控件是一个没有标题栏的弹出窗口,因此使用WS_POPUP,而不是WS_CHILD。实际上不指定WS_POPUP样式也可以,因为当创建工具提示控件时,系统总是默认添加WS_POPUP和WS_EX_TOOLWINDOW窗口样式;没有指定WS_VISIBLE样式,是因为当鼠标光标悬停在指定控件或特定区域上时工具提示控件才显示;TTS_ALWAYSTIP是工具提示控件专用样式,该样式表示即使工具提示控件的父窗口处于非活动状态,当鼠标光标悬停在指定控件或特定区域上时也会弹出工具提示控件。

不管程序窗口是否位于Z序的顶部,例如被其他程序窗口遮挡了一半的情况下,我们都希望工具提示控件在需要显示的时候显示在顶层,因此指定了WS_EX_TOPMOST扩展窗口样式。

工具提示控件的窗口位置和大小不需要指定,系统会自动决定如何显示,因此窗口位置和大小参数指定为CW_USEDEFAULT即可。如果需要设置工具提示控件的最大宽度,则可以向其发送TTM_SETMAXTIPWIDTH消息。当文本在一行显示不完全时,会自动换行显示。

当鼠标光标悬停在一个控件或特定区域上时,工具提示控件会自动弹出,工具提示控件的"工具"指的就是这些控件或特定区域。一个工具提示控件可以为多个"工具"提供服务,在创建工具提示控件后,可以通过发送TTM_ADDTOOL消息为工具提示控件添加"工具"。这里以Chapter8\EditDemo为例,为3个编辑控件("工具")设置提示文本,在WM_CREATE消息中添加如下代码:

复制代码
TOOLINFO ti;
ZeroMemory(&ti, sizeof(TOOLINFO));
ti.cbSize = sizeof(TOOLINFO);
ti.uFlags = TTF_IDISHWND | TTF_SUBCLASS;

ti.uId = (UINT_PTR)hwndUserName;
ti.lpszText = TEXT("请输入用户名,最少3个字符");
SendMessage(hwndTip, TTM_ADDTOOL, 0, (LPARAM)&ti);

ti.uId = (UINT_PTR)hwndPassword;
ti.lpszText = TEXT("请输入密码,最少3个字符");
SendMessage(hwndTip, TTM_ADDTOOL, 0, (LPARAM)&ti);

ti.uId = (UINT_PTR)hwndAge;
ti.lpszText = TEXT("请输入0~120岁的年龄");
SendMessage(hwndTip, TTM_ADDTOOL, 0, (LPARAM)&ti);

现在,当鼠标光标悬停在用户名、密码或年龄编辑控件("工具")上时,会弹出相应的提示文本;在用户单击鼠标按钮、鼠标光标离开"工具"或等待几秒后,工具提示控件会消失。

发送TTM_ADDTOOL消息可以为工具提示控件添加"工具",也可以说是向工具提示控件注册该"工具"。wParam参数没有用到。lParam参数是一个指向TOOLINFO结构的指针,该结构包含工具提示控件显示"工具"所需的相关信息。TOOLINFO结构在CommCtrl.h头文件中定义如下:

复制代码
typedef struct {
    UINT      cbSize;       // 该结构的大小
    UINT      uFlags;       // 控制工具提示控件显示的标志
    HWND      hwnd;         // rect 字段指定的边界矩形所属的窗口句柄
    UINT_PTR  uId;          //"工具"的窗口句柄或ID
    RECT      rect;         //"工具"的边界矩形坐标,如果uFlags字段包含TTF_IDISHWND标志,
                            // 则忽略该字段
    HINSTANCE hinst;        // 包含字符串资源的模块句柄(如果lpszText字段指定为字符串资源ID)
    LPTSTR    lpszText;     //"工具"的提示文本字符串指针,或指定为字符串资源ID
    LPARAM    lParam;       // 与"工具"关联的32位自定义数据
    void      *lpReserved;  // 保留,必须设置为NULL
} TOOLINFO, *PTOOLINFO, *LPTOOLINFO;
  • uFlags字段是控制工具提示控件显示的标志,常用的标志如表8.31所示。

表8.31

标志 含义
TTF_IDISHWND uId字段是"工具"的窗口句柄。如果没有设置该标志,则uId字段是"工具"的ID
TTF_PARSELINKS 应解析工具提示文本中的链接,例如:ti.lpszText = TEXT("请输入用户名,最少3个字符,<a href=\"https: //www.baidu.com/\">百度一下</a>"); // 如果未指定该标志 , 则不会解析a标签
TTF_SUBCLASS 默认情况下,系统会将与鼠标相关的消息发送给"工具"。"工具"可以是一个控件或子窗口,也可以是一个矩形区域,即鼠标消息会被发送到控件或子窗口的窗口过程,或矩形区域所属窗口的窗口过程,而不是工具提示控件,因此与鼠标相关的消息还需要通过发送TTM_RELAYEVENT消息手动转发到工具提示控件,这样工具提示控件才可以在适当的时间和位置显示"工具"提示文本。在指定该标志后,系统会子类化"工具"以拦截消息,例如WM_MOUSEMOVE,然后鼠标消息将自动转发到工具提示控件以得到处理
TTF_TRACK 使工具提示控件窗口显示在与其对应的"工具"旁边。在指定该标志后需要通过发送TTM_TRACKACTIVATE消息激活工具提示控件,并根据TTM_TRACKPOSITION消息提供的坐标移动工具提示控件
TTF_ABSOLUTE 使工具提示控件窗口显示在TTM_TRACKPOSITION消息提供的坐标处(屏幕坐标),该标志必须与TTF_TRACK标志一起使用
TTF_TRANSPARENT 把工具提示控件的鼠标消息转发到父窗口
  • hwnd字段是rect字段指定的边界矩形所属的窗口句柄。如果lpszText字段指定为LPSTR_TEXTCALLBACK,则hwnd是接收TTN_GETDISPINFO通知码(WM_NOTIFY)的窗口句柄。

  • uId字段表示"工具"的窗口句柄或ID,取决于uFlags字段是否指定了TTF_IDISHWND标志。

  • rect字段指定"工具"的边界矩形坐标,相对于hwnd字段指定的窗口的客户区。如果uFlags包含TTF_IDISHWND标志,则忽略该字段。

  • hinst字段指定包含字符串资源的模块句柄(如果lpszText字段指定为字符串资源ID)。

  • lpszText字段表示"工具"的提示文本字符串指针,或指定为字符串资源ID。如果该字段设置为LPSTR_TEXTCALLBACK,则工具提示控件会将TTN_GETDISPINFO通知码(WM_NOTIFY)发送到hwnd字段指定的窗口,程序可以处理该通知码以设置提示文本。

两种用法:"工具"可以是一个控件或子窗口,也可以是一个矩形区域。如果uFlags字段指定了TTF_IDISHWND标志,那么uId字段指定为"工具"的窗口句柄,此时"工具"是一个控件或子窗口,当鼠标光标悬停在这个控件或子窗口上时,会弹出工具提示控件。

如果uFlags字段没有指定TTF_IDISHWND标志,可以把rect字段指定为一个矩形区域以作为"工具",hwnd字段指定为rect字段指定的"工具"所属的窗口句柄,uId字段指定为"工具"的ID。例如,把程序窗口的客户区右下角50像素 50像素的矩形区域定义为"工具":

复制代码
RECT rcClient;
GetClientRect(hwnd, &rcClient);
SetRect(&rcClient, rcClient.right - 50, rcClient.bottom - 50, rcClient.right, rcClient.bottom);
ZeroMemory(&ti, sizeof(ti));
ti.cbSize = sizeof(TOOLINFO);
ti.uFlags = TTF_SUBCLASS;
ti.rect = rcClient;
ti.hwnd = hwnd;
ti.uId = 1001;
ti.lpszText = TEXT("把程序窗口的客户区右下角50像素*50像素的矩形区域定义为"工具"");
SendMessage(hwndTip, TTM_ADDTOOL, 0, (LPARAM)&ti);

上述代码执行效果如原书的图8.14所示。

对于矩形区域的"工具",如果想改变矩形区域的大小,可以通过发送TTM_NEWTOOLRECT消息为"工具"设置新的边界矩形。wParam参数没有用到,lParam参数是一个指向TOOLINFO结构的指针。

添加"工具"以后,还可以通过发送TTM_UPDATETIPTEXT消息来更改"工具"的提示文本等信息。wParam参数没有用到,lParam参数是一个指向TOOLINFO结构的指针。

"工具"的提示文本可以通过发送TTM_GETTEXT消息获取。wParam参数指定为要复制到TOOLINFO结构的lpszText字段指向的缓冲区中的字符个数,包括终止空字符。lParam参数同样是一个指向TOOLINFO结构的指针。

工具提示控件的常用样式如表8.32所示。

表8.32

样式 含义
TTS_ALWAYSTIP 即使工具提示控件的父窗口处于非活动状态,当鼠标光标悬停在指定控件或特定区域上时也会弹出工具提示控件
TTS_BALLOON 工具提示控件具有卡通"气球"的外观,控件窗口是圆角矩形,并有一个指向"工具"的箭头
TTS_CLOSE 在工具提示控件上显示"关闭"按钮,仅当工具提示控件具有TTS_BALLOON样式和标题时才有效。可以通过发送TTM_SETTITLE消息为工具提示控件设置标题和图标。例如:SendMessage(hwndTip, TTM_SETTITLE, (WPARAM)LoadIcon(NULL, IDI_INFORMATION), (LPARAM)TEXT("提示信息"));
TTS_NOPREFIX 程序可以使用相同的字符串资源作为菜单项和工具提示控件中的文本。默认情况下系统会自动删除&字符,并在第一个制表符(\t)处终止字符串,在指定该样式后可以防止从字符串中删除&字符或在制表符处终止字符串
复制代码
SendMessage(hwndTip, TTM_SETTITLE,      (WPARAM)LoadIcon(NULL, IDI_INFORMATION), (LPARAM)TEXT("提示信息"));

8.11.1 超时时间

鼠标光标必须悬停在"工具"上一段时间后工具提示控件才会弹出,默认的超时时间为鼠标双击的时间,即GetDoubleClickTime()函数返回的值,通常为500ms。要指定非默认超时值,可以向工具提示控件发送TTM_SETDELAYTIME消息。

  • TTM_SETDELAYTIME消息的wParam参数用于指定要设置哪个超时时间值,可以是表8.33所示的值之一。

表8.33

常量 含义
TTDT_INITIAL 设置在工具提示控件窗口出现之前鼠标光标必须在"工具"的边界矩形内保持静止的时间。要将该超时值恢复为默认值,可以把lParam参数设置为−1
TTDT_AUTOPOP 设置如果鼠标光标在"工具"的边界矩形内静止,工具提示控件窗口保持可见的时间(保持显示的时间)。要将该超时值恢复为默认值,可以把lParam参数设置为−1
TTDT_RESHOW 设置当鼠标光标从一个"工具"移动到另一个"工具"时,后面这个"工具"显示工具提示控件所需的时间。要将该超时值恢复为默认值,可以把lParam参数设置为−1
TTDT_AUTOMATIC 默认情况下,AUTOPOP时间是INITIAL时间的10倍,RESHOW时间是INITIAL时间的1/5。如果设置了该标志,则lParam参数可以指定为以毫秒为单位的正值表示INITIAL时间,如果lParam参数指定为负值,则将所有3个超时时间恢复为默认值。通常不需要指定该标志。默认比例如下:nInitial = GetDoubleClickTime(); nAutoPop = GetDoubleClickTime() * 10; nReShow = GetDoubleClickTime() / 5;
  • lParam参数指定超时时间,以毫秒为单位。

例如下面的代码:

复制代码
// 设置超时时间
SendMessage(hwndTip, TTM_SETDELAYTIME, TTDT_INITIAL, 100);
SendMessage(hwndTip, TTM_SETDELAYTIME, TTDT_AUTOPOP, 10000);

工具提示控件可以是活动的也可以是非活动的。当它处于活动状态且鼠标光标位于"工具"上时,会显示"工具"提示文本;当它处于非活动状态时,即使鼠标光标位于"工具"上,也不会显示"工具"提示文本,可以通过发送TTM_ACTIVATE消息来激活或停用工具提示控件。TTM_ACTIVATE消息的wParam参数可以指定为TRUE表示激活控件,或FALSE表示停用控件;lParam参数没有用到。

8.11.2 跟踪工具提示

"工具"提示文本可以是固定的,也可以随鼠标光标的移动而移动,称为跟踪工具提示。要创建跟踪工具提示,发送TTM_ADDTOOL消息注册"工具"的时候需要在TOOLINFO结构的uFlags字段中包含TTF_TRACK标志;还需要通过向工具提示控件发送TTM_TRACKACTIVATE消息手动激活(显示)或停用(隐藏)跟踪工具提示;跟踪工具提示处于激活状态时,还需要通过向工具提示控件发送TTM_TRACKPOSITION消息来指定跟踪工具提示的位置。

跟踪工具提示的位置需要程序手动控制,因此在发送TTM_ADDTOOL消息注册工具时TOOLINFO结构的uFlags字段中不需要包含TTF_SUBCLASS标志,也不需要通过发送TTM_RELAYEVENT将鼠标消息转发到工具提示控件上。

发送TTM_ADDTOOL消息注册"工具"的时候需要在TOOLINFO结构的uFlags字段中包含TTF_TRACK标志。在指定该标志后,工具提示控件窗口显示在与其对应的"工具"旁边;如果同时指定TTF_ABSOLUTE标志,则可以将工具提示控件窗口显示在TTM_TRACKPOSITION消息提供的坐标处(屏幕坐标),该标志必须与TTF_TRACK标志一起使用。即指定了TTF_ABSOLUTE标志可以将工具提示控件窗口显示在TTM_TRACKPOSITION消息提供的坐标处(屏幕坐标),而不是"工具"旁边,因此通常都是同时指定这两个标志:TTF_TRACK和TTF_ABSOLUTE。接下来实现一个跟踪工具提示的例子,当鼠标光标在客户区中移动时实时显示光标位置处的坐标,TrackTool.cpp源文件的部分内容如下所示:

复制代码
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    HINSTANCE hInstance;
    static HWND hwndTip;
    static BOOL bTracking = FALSE;
    static int oldX, oldY;
    int newX, newY;
    static TOOLINFO ti = { sizeof(TOOLINFO) };
    TRACKMOUSEEVENT tme = { sizeof(TRACKMOUSEEVENT) };
    POINT pt;
    TCHAR szBuf[24] = { 0 };

    switch (uMsg)
    {
    case WM_CREATE:
        hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
        // 创建工具提示控件
        hwndTip = CreateWindowEx(WS_EX_TOPMOST, TEXT("tooltips_class32"), NULL,
            WS_POPUP | TTS_ALWAYSTIP,
            CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
            hwnd, NULL, hInstance, NULL);

        // 添加跟踪工具,客户区域
        ti.uFlags = TTF_TRACK | TTF_ABSOLUTE;
        ti.hwnd = hwnd;
        ti.uId = 1001;
        GetClientRect(hwnd, &ti.rect);
        SendMessage(hwndTip, TTM_ADDTOOL, 0, (LPARAM)&ti);
        return 0;

    case WM_MOUSEMOVE:
        // 程序可以通过调用TrackMouseEvent函数让系统发送另外两条消息:
        // 当鼠标光标悬停在客户区一段时间后发送WM_MOUSEHOVER消息;
        // 当光标离开客户区时发送WM_MOUSELEAVE消息
        if (!bTracking)
        {
            tme.dwFlags = TME_LEAVE;
            tme.hwndTrack = hwnd;
            TrackMouseEvent(&tme);

            bTracking = TRUE;
        }

        // 激活跟踪工具提示
        SendMessage(hwndTip, TTM_TRACKACTIVATE, (WPARAM)TRUE, (LPARAM)&ti);

        newX = GET_X_LPARAM(lParam);
        newY = GET_Y_LPARAM(lParam);
        if ((newX != oldX) || (newY != oldY))
        {
            oldX = newX;
            oldY = newY;

            // 更改"工具"的提示文本
            wsprintf(szBuf, TEXT("鼠标的客户区坐标:%d, %d"), newX, newY);
            ti.lpszText = szBuf;
            SendMessage(hwndTip, TTM_SETTOOLINFO, 0, (LPARAM)&ti);

            // 移动跟踪工具提示的位置
            pt = { newX, newY };
            ClientToScreen(hwnd, &pt);
            SendMessage(hwndTip, TTM_TRACKPOSITION, 0, MAKELPARAM(pt.x, pt.y));
        }
        return 0;

    case WM_MOUSELEAVE:
        // 停用跟踪工具提示
        SendMessage(hwndTip, TTM_TRACKACTIVATE, FALSE, (LPARAM)&ti);
        bTracking = FALSE;
        return 0;

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

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

完整代码参见Chapter8\TrackTool项目。

另外,常用的消息还有设置工具提示控件窗口中背景颜色的TTM_SETTIPBKCOLOR消息、设置工具提示控件窗口中文本颜色的TTM_SETTIPTEXTCOLOR消息等。

8.12 列表视图

列表视图控件和列表框控件类似,但是列表视图控件提供了多种排列和显示列表项的方法,比列表框控件更灵活。列表视图控件有图标、小图标、列表、报表(也称详情视图,有列标题)等排列显示方式,如原书的图8.15所示。

图标视图的每一个列表项通常会显示一个32像素 32像素的图标,图标下方显示列表项文本;小图标视图的每一个列表项通常会显示一个16像素 16像素的图标,图标右侧显示列表项文本;列表视图的每一个列表项通常会显示一个16像素 16像素的图标,图标右侧显示列表项文本,列表项按列排序;报表视图的每一个列表项的最左一列通常会显示一个16像素 16像素的图标,图标右侧显示列表项文本,每一个列表项还可以选择显示其他列(也称子项),用于显示一些附加信息,按列排序,每一列的顶部会显示一个列标题。另外,和列表框控件一样,每个列表项都可以关联一个项目数据。

列表项文本实际上是一个编辑控件,报表视图的列标题是一个标题控件,类名是SysHeader32。标题控件用得不多,本书不作介绍,需要的读者请自行参考MSDN。

在通用控件版本6及以后的版本中,新增了平铺视图。平铺视图的列表项可以显示多行标签文本,如图8.16所示。实际上这5种视图,读者应该都很熟悉,因为桌面用的就是一个列表视图控件。另外,资源管理器中也用到了这些视图,Windows 7和Windows 10的资源管理器中用于显示文件和文件夹的控件是DirectUIHWND类,实际上就是列表视图控件的扩展。

列表视图控件的相关知识点比较多,让我们先看一下常用的列表视图控件样式,如表8.34所示。

表8.34

样式 含义
LVS_ICON 图标视图
LVS_SMALLICON 小图标视图
LVS_LIST 列表视图
LVS_REPORT 报表视图,第一列始终是左对齐的
LVS_SHOWSELALWAYS 即使控件失去输入焦点,也始终显示选择
LVS_SINGLESEL 一次只能选择一个列表项,默认情况下,列表项可以多选
LVS_SORTASCENDING 列表项按列表项文本升序排列
LVS_SORTDESCENDING 列表项按列表项文本降序排列
LVS_EDITLABELS 默认情况下列表项文本不能编辑,在指定该标志后列表项的文本可以被编辑。当用户完成列表项文本的编辑以后,父窗口会收到包含LVN_ENDLABELEDIT通知码的WM_NOTIFY消息,处理该通知码只需要简单地返回TRUE表示接受编辑,或FALSE表示拒绝编辑即可
LVS_NOLABELWRAP 对于图标视图,如果列表项文本比较长,默认情况下会分多行显示。在指定该样式后,列表项文本单行显示
LVS_ALIGNLEFT 对于图标视图和小图标视图,在默认情况下,在列表视图控件的矩形范围内,所有列表项在宽度范围内排列完一行后,会换到下一行。在指定该样式后,先在高度范围内排列完一列,然后换到下一列
LVS_NOCOLUMNHEADER 对于报表视图,在默认情况下每一列的顶部都会显示一个标题。在指定该样式后,则不显示标题
LVS_AUTOARRANGE 对于图标、小图标视图自动排列
LVS_OWNERDRAWFIXED 自绘报表视图中的列表项,当需要重绘时会收到WM_DRAWITEM消息,程序应该处理该消息绘制每个列表项,系统不会为每个列表项发送单独的WM_DRAWITEM消息

没有平铺视图样式,如果需要平铺视图,可以发送LVM_SETVIEW消息。wParam参数可以指定为LV_VIEW_ICON、LV_VIEW_SMALLICON、LV_VIEW_LIST、LV_VIEW_DETAILS、LV_VIEW_TILE分别表示图标、小图标、列表、报表和平铺视图。lParam参数没有用到。

再看一下常用的列表视图控件扩展样式(见表8.35)。扩展样式不能直接设置,可以通过发送LVM_SETEXTENDEDLISTVIEWSTYLE消息来进行设置。wParam参数用于指定lParam参数中的哪些样式将受到影响。该参数可以是扩展样式的组合,只会更改wParam参数中指定的扩展样式。如果该参数设置为0,则lParam参数中的所有样式都将受到影响,该参数通常可以设置为0。lParam参数指定要设置的列表视图控件扩展样式,可以是扩展样式的组合,lParam参数中未指定但在wParam参数中指定的样式将被删除。如果执行成功,则返回包含先前列表视图控件扩展样式的DWORD值。

表8.35

样式 含义
LVS_EX_CHECKBOXES 列表项前面显示一个复选框
LVS_EX_AUTOCHECKSELECT 单击列表项即自动选中复选框。对于报表视图,在设置该样式后,第一列的列标题前面会显示一个复选框,单击复选框可以在全部选中和全部取消选中之间切换
LVS_EX_LABELTIP 如果列表项文本因为宽度或高度限制没有完全显示出来,则当鼠标在列表项上时会弹出一个提示文本以显示完整的列表项文本
LVS_EX_FULLROWSELECT 用于报表视图,默认情况下选择一个列表项只会突出显示第一列,指定该样式并选择一个列表项后会整行突出显示(包括子项)
LVS_EX_GRIDLINES 用于报表视图,列表项之间以及列与列之间会显示网格线,整个列表视图控件(报表视图)就像是一个表格
LVS_EX_HEADERDRAGDROP 用于报表视图,可以通过拖动列标题来调整列的顺序
LVS_EX_SUBITEMIMAGES 用于报表视图,在指定该样式后,子项文本前面也可以显示一个图像

下面的代码创建了一个小图标视图列表视图控件,即使列表视图控件失去输入焦点也始终显示选择,列表项的文本可以编辑:

复制代码
hwndListView = CreateWindowEx(0, TEXT("SysListView32"), NULL,
    WS_CHILD | WS_VISIBLE | LVS_SMALLICON | LVS_SHOWSELALWAYS | LVS_EDITLABELS,
    10, 0, 300, 200, hwnd, (HMENU)IDC_LISTVIEW, hInstance, NULL);

可以通过发送LVM_SETEXTENDEDLISTVIEWSTYLE消息来设置列表视图控件的扩展样式,例如:

复制代码
SendMessage(hwndListView, LVM_SETEXTENDEDLISTVIEWSTYLE, 0, LVS_EX_CHECKBOXES | LVS_EX_ AUTOCHECKSELECT);

8.12.1 添加列标题和列表项

在创建列表视图控件后,可以通过发送LVM_INSERTITEM消息添加列表项。如果是LVS_REPORT报表视图样式,还必须首先通过发送LVM_INSERTCOLUMN消息来添加列标题。

LVM_INSERTCOLUMN消息的wParam参数指定为列的索引(第一列为0,第二列为1,依次类推),lParam参数是一个指向包含列属性的LVCOLUMN结构的指针。如果执行成功,则返回新列的索引;如果执行失败,则返回−1。只有在报表视图中才会显示列标题,在其他视图中即使添加了列也不会显示。LVCOLUMN结构在CommCtrl.h头文件中定义如下:

复制代码
typedef struct _LVCOLUMN {
    UINT   mask;        // 掩码标志,指定哪个字段有效
    int    fmt;         // 列标题和列表项子项的文本在列中的对齐方式,但最左一列始终左对齐
    int    cx;          // 列的宽度,以像素为单位
    LPTSTR pszText;     // 列标题字符串指针,如果获取列信息,则是字符串缓冲区地址
    int    cchTextMax;  // pszText指向的缓冲区的大小,以字符为单位,如果是设置列信息,则忽略该字段
    int    iSubItem;    // 列的索引,通常和wParam参数使用相同的值
    int    iImage;      // 图像列表中图像的从0开始的索引,指定的图像将显示在列标题左侧
    int    iOrder;      // 按从左到右的顺序排列的列偏移,例如,0表示最左边的列,通常不设置
    int    cxMin;       // 列的最小宽度,以像素为单位,通常不设置
    int    cxDefault;   // 一般不使用
    int    cxIdeal;     // 列的理想宽度,只读(用于获取,而不是设置)
} LVCOLUMN, *LPLVCOLUMN;
  • mask字段是掩码标志,指定哪个字段有效,该字段可以是表8.36所示的一个或多个值。

表8.36

标志 含义
LVCF_FMT fmt字段有效
LVCF_WIDTH cx字段有效
LVCF_TEXT pszText字段有效
LVCF_SUBITEM iSubItem字段有效
LVCF_IMAGE iImage字段有效
LVCF_ORDER iOrder字段有效
LVCF_MINWIDTH cxMin字段有效
LVCF_DEFAULTWIDTH cxDefault字段有效
LVCF_IDEALWIDTH cxIdeal字段有效
  • fmt字段指定列标题和列表项子项的文本在列中的对齐方式。最左列始终是LVCFMT_LEFT左对齐方式,这无法改变。常用的值如表8.37所示。

表8.37

常量 含义
LVCFMT_LEFT 文本左对齐
LVCFMT_RIGHT 文本右对齐
LVCFMT_CENTER 文本居中对齐
LVCFMT_SPLITBUTTON 列标题显示为拆分按钮

例如下面的代码,为报表视图设置了2列,第1列宽度为160像素,第2列宽度为100像素,每一列的列标题前面会显示一个图标:

复制代码
// 如果是报表视图样式,必须先设置列标题
lvc.mask = LVCF_SUBITEM | LVCF_TEXT | LVCF_WIDTH | LVCF_IMAGE;
lvc.iSubItem = 0; lvc.cx = 160; lvc.pszText = TEXT("专业名称"); lvc.iImage = 0;
SendMessage(hwndListView, LVM_INSERTCOLUMN, 0, (LPARAM)&lvc);
lvc.iSubItem = 1; lvc.cx = 100; lvc.pszText = TEXT("价格"); lvc.iImage = 1;
SendMessage(hwndListView, LVM_INSERTCOLUMN, 1, (LPARAM)&lvc);

图像列表的代码如下:

复制代码
// 图像列表
hImagListNormal = ImageList_Create(GetSystemMetrics(SM_CXICON), 
    GetSystemMetrics(SM_CYICON), ILC_MASK | ILC_COLOR32, 10, 0);
hImagListSmall = ImageList_Create(GetSystemMetrics(SM_CXSMICON),
    GetSystemMetrics(SM_CYSMICON), ILC_MASK | ILC_COLOR32, 10, 0);
for (int i = 0; i < 10; i++)
{
    // 资源文件中定义了10个图标,IDI_ICON1~IDI_ICON10
    hiconItem = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1 + i));
    ImageList_AddIcon(hImagListNormal, hiconItem);
    ImageList_AddIcon(hImagListSmall, hiconItem);
    DestroyIcon(hiconItem);
}
SendMessage(hwndListView, LVM_SETIMAGELIST, LVSIL_SMALL, (LPARAM)hImagListSmall);

LVM_SETIMAGELIST消息用于把图像列表分配给列表视图控件,wParam参数指定图像列表的类型,可以是表8.38所示的值之一(常用的)。

表8.38

类型 含义
LVSIL_NORMAL 大图标的图像列表
LVSIL_SMALL 小图标的图像列表
LVSIL_STATE 带状态图像的图像列表

lParam参数指定为图像列表句柄,如果执行成功,则返回值为先前与控件关联的图像列表的句柄。

图标视图、平铺视图中的大图标,不一定就是GetSystemMetrics(SM_CXICON) GetSystemMetrics (SM_CYICON)的大小,还可以设置得更大或更小;小图标、列表视图、报表视图中的小图标也不一定就是GetSystemMetrics(SM_CXSMICON) GetSystemMetrics(SM_CYSMICON)的大小,也可以设置得更大或更小。

LVM_INSERTITEM消息用于向列表视图控件中添加列表项,wParam参数没有用到,lParam参数是一个指向LVITEM结构的指针。如果执行成功,则返回值为新添加列表项的索引;如果执行失败,则返回−1。LVITEM结构在CommCtrl.h头文件中定义如下:

复制代码
typedef struct {
    UINT   mask;        // 掩码标志,设置或获取哪些字段的值
    int    iItem;       // 新列表项的从0开始的索引,如果该值大于控件中的列表项总数,则插入末尾
    int    iSubItem;    // 列表项的子项的索引,通常设置为0(第一列)
    UINT   state;       // 指定列表项的状态、状态图像和叠加图像,stateMask字段指定该字段的有效位
    UINT   stateMask;   // 指定state字段的有效位
    LPTSTR pszText;     // 列表项文本字符串指针,如果获取列表项信息,则是字符串缓冲区地址
    int    cchTextMax;  // pszText缓冲区的大小,以字符为单位,获取列表项信息时需要该字段
    int    iImage;      // 图像列表中图像的从0开始的索引,指定的图像将显示在列表项左侧
    LPARAM lParam;      // 与列表项相关联的项目数据
    int    iIndent;     // 列表项缩进数,设置为1表示1个图像宽度,2表示2个图像宽度...
    int    iGroupId;    // 列表项所属组的ID
    UINT   cColumns;
    PUINT  puColumns;
    int    *piColFmt;
    int    iGroup;      // 列表项所属的组索引
} LVITEM, *LPLVITEM;
  • mask字段是掩码标志,表示要设置或获取哪些字段的值,可以是表8.39所示的值的组合(常用的)。

表8.39

标志 含义
LVIF_IMAGE iImage字段有效
LVIF_INDENT iIndent字段有效
LVIF_PARAM lParam字段有效
LVIF_STATE state字段有效
LVIF_TEXT pszText字段有效
  • state字段指定列表项的状态、状态图像索引或叠加图像索引。该字段的第0~7位包含列表项状态标志,可以设置为LVIS_CUT、LVIS_DROPHILITED、LVIS_FOCUSED、LVIS_SELECTED(具体含义参见stateMask字段);该字段的第8~11位指定列表项的叠加图像索引,叠加图像叠加在列表项的图标图像上执行掩码运算,要设置叠加图像可以使用INDEXTOOVERLAYMASK(图像索引)宏;该字段的第12~15位指定状态图像索引,状态图像显示在列表项的图标旁边,要设置状态图像可以使用INDEXTOSTATEIMAGEMASK(图像索引)宏。如果是获取指定列表项的状态、状态图像索引或叠加图像索引,则不需要设置该字段(mask字段需要指定LVIF_STATE标志)。

  • stateMask字段指定state字段的有效位,也就是指定state字段的状态位、状态图像索引位或叠加图像索引位中哪一个有效,可以是表8.40所示的值的组合。

表8.40

标志 含义
LVIS_CUT state字段的第0~7位有效,列表项为剪切状态
LVIS_DROPHILITED state字段的第0~7位有效,列表项突出显示为拖放目标
LVIS_FOCUSED state字段的第0~7位有效,列表项具有焦点(要为列表项设置焦点,应首先保证列表视图控件具有焦点)
LVIS_SELECTED state字段的第0~7位有效,列表项为已选中状态
LVIS_OVERLAYMASK state字段的第8~11位有效,state字段应使用INDEXTOOVERLAYMASK(图像索引)宏设置叠加图像索引
LVIS_STATEIMAGEMASK state字段的第12~15位有效,state字段应使用INDEXTOSTATEIMAGEMASK(图像索引)宏设置状态图像索引

要获取或设置所有状态,可以将该字段设置为(UINT)−1。

例如,要设置列表项为选中状态,可以把state和stateMask字段都设置为LVIS_SELECTED。关于状态图像或叠加图像的用法,请读者自行阅读相关文章。

  • iGroupId字段表示列表项所属组的ID,或指定为I_GROUPIDCALLBACK表示列表视图控件会向父窗口发送LVN_GETDISPINFO通知码以获取组的索引,也可以指定为I_GROUPIDNONE表示该列表项不属于任何组。此处就不详细介绍分组了。

例如下面的代码添加了两个列表项,每个列表项前面显示一个图标:

复制代码
LVITEM lvi;
ZeroMemory(&lvi, sizeof(LVITEM));
lvi.mask = LVIF_TEXT | LVIF_IMAGE;
lvi.iItem = 0; lvi.pszText = TEXT("汇编语言程序设计"); lvi.iImage = 0;
SendMessage(hwndListView, LVM_INSERTITEM, 0, (LPARAM)&lvi);
lvi.iItem = 1; lvi.pszText = TEXT("Windows程序设计"); lvi.iImage = 1;
SendMessage(hwndListView, LVM_INSERTITEM, 0, (LPARAM)&lvi);

对于报表视图,在添加一个列表项后,接着发送LVM_SETITEM或LVM_SETITEMTEXT消息为该列表项添加子项(其他列)。wParam参数没有用到,lParam参数同样是一个指向LVITEM结构的指针。例如:

复制代码
LVITEM lvi;
ZeroMemory(&lvi, sizeof(LVITEM));
lvi.mask = LVIF_TEXT | LVIF_IMAGE;
lvi.iItem = 0; lvi.pszText = TEXT("汇编语言程序设计"); lvi.iImage = 0;
SendMessage(hwndListView, LVM_INSERTITEM, 0, (LPARAM)&lvi);

// 添加一个子项,也就是第2列,索引为1
lvi.iSubItem = 1; lvi.pszText = TEXT("18000");
SendMessage(hwndListView, LVM_SETITEM, 0, (LPARAM)&lvi);

平铺视图的列表项可以显示多行标签文本,例如把报表视图的子项全部显示到平铺视图列表项文本的后续行中,如原书的图8.17所示。

可以通过发送LVM_SETTILEINFO消息设置平铺视图列表项信息来实现。LVM_SETTILEINFO消息的wParam参数没有用到。lParam参数是一个指向包含要设置的信息的LVTILEINFO结构的指针,该结构在CommCtrl.h头文件中定义如下:

复制代码
typedef struct tagLVTILEINFO
{
    UINT    cbSize;     // 该结构的大小
    int     iItem;      // 列表项索引
    UINT    cColumns;   // 列数,不包括第一列
    PUINT   puColumns;  // 列索引(或者说子项索引)数组,指定要显示的列以及这些列的顺序
    int*    piColFmt;   // 列格式数组(例如LVCFMT_LEFT),对应于puColumns中指定的每个列
} LVTILEINFO, *PLVTILEINFO;

这里我写了一个可以切换各种视图的示例程序ListViewDemo,具体代码参见Chapter8\ListViewDemo项目。

8.12.2 列表视图控件消息和通知码

有很多关于列表视图控件的知识点,本节介绍一些常用的消息(见表8.41)和通知码。完整介绍请参考MSDN。

表8.41

消息类型 含义
LVM_DELETEITEM 删除一个列表项,wParam参数指定为列表项索引,lParam参数没有用到。例如:SendMessage(hwndListView, LVM_DELETEITEM, 1, 0);
LVM_DELETEALLITEMS 删除所有列表项,wParam和lParam参数都没有用到
LVM_DELETECOLUMN 删除一列,wParam参数指定为列索引,lParam参数没有用到
LVM_SETITEMSTATE 更改指定列表项的状态。wParam参数指定为列表项的索引,如果指定为−1,则表示更改所有列表项的状态;lParam参数指定为指向LVITEM结构的指针,stateMask字段指定要更改的状态位,state字段指定列表项的新状态(可以设置为LVIS_CUT、LVIS_DROPHILITED、LVIS_FOCUSED、LVIS_SELECTED),其他字段将被忽略
LVM_GETCOUNTPERPAGE 计算在列表视图或报表视图中可以垂直放置在列表视图控件的可见区域中的列表项个数(仅完全可见的列表项)。wParam和lParam参数都没有用到。如果执行成功,则返回完全可见的列表项个数。如果当前视图是图标视图或小图标视图,则返回值是列表视图控件中的列表项总数
LVM_APPROXIMATEVIEWRECT 计算显示给定数量的列表项所需的大约宽度和高度。wParam参数指定为要显示的列表项个数,如果设置为−1,表示使用控件中的列表项总数;LOWORD(lParam)和HIWORD(lParam)指定为建议的控件宽度和高度值,宽度和高度值通常都可以设置为−1,表示使用当前宽度和高度值。如果执行成功,则返回一个DWORD值,该值包含显示指定数量的列表项所需的近似宽度(在LOWORD中)和高度(在HIWORD中),以像素为单位
LVM_SCROLL 滚动列表视图控件的内容。wParam参数指定为要滚动的水平距离,lParam参数指定为要滚动的垂直距离,以像素为单位
LVM_FINDITEM 搜索具有指定特征的列表项。wParam参数指定开始搜索的列表项索引(不包括指定项),指定为−1表示从头开始搜索;lParam参数是一个指向LVFINDINFO结构的指针,该结构包含有关要搜索的内容信息。如果执行成功,则返回第一个符合条件的列表项的索引;否则返回−1
LVM_GETITEMCOUNT 用于获取列表视图控件中的列表项的总个数,wParam和lParam参数都没用到
LVM_GETSELECTIONMARK 通常用于单选列表视图控件。wParam和lParam参数都没有用到,该消息返回选中列表项的索引;如果当前没有选中项,则返回−1
LVM_GETSELECTEDCOUNT 获取已选中列表项的总个数。wParam和lParam参数都没有用到。如果执行成功,则返回总数目;如果没有已选中项,则返回0
LVM_GETNEXTITEM 搜索与wParam参数指定的列表项有一定关系的列表项,wParam参数指定开始搜索的列表项索引(不包括指定项),指定为−1 表示从头开始搜索。lParam参数用于说明与wParam中指定项的关系,可以是以下值中的一个或多个。(1)按索引搜索● LVNI_ALL------默认值,从wParam参数指定的索引开始往后搜索(搜索大于指定索引的)● LVNI_PREVIOUS------从wParam参数指定的索引开始往前搜索(搜索小于指定索引的)(2)按坐标关系搜索(物理位置)● LVNI_ABOVE------从wParam参数指定的列表项的坐标处开始向上搜索● LVNI_BELOW------从wParam参数指定的列表项的坐标处开始向下搜索● LVNI_TOLEFT------从wParam参数指定的列表项的坐标处开始向左搜索● LVNI_TORIGHT------从wParam参数指定的列表项的坐标处开始向右搜索(3)按列表项状态搜索● LVIS_CUT------列表项为剪切状态● LVIS_DROPHILITED------列表项突出显示为拖放目标● LVIS_FOCUSED------列表项具有焦点● LVIS_SELECTED------列表项为已选中状态
LVM_GETITEM 用于获取列表项的信息。wParam参数没有用到,lParam参数是一个指向LVITEM结构的指针。该结构在讲解LVM_INSERTITEM消息时已经介绍过。该消息可以获取列表项或子项的信息(通过iItem和iSubItem字段指定)。如果消息执行成功,则返回TRUE;否则返回FALSE

当用户鼠标右键单击列表项时,列表视图控件的父窗口会收到包含NM_RCLICK通知码的WM_NOTIFY消息,lParam参数是一个指向NMITEMACTIVATE结构的指针。

对于报表视图,当用户单击列标题时会收到包含LVN_COLUMNCLICK通知码的WM_NOTIFY消息,消息的lParam参数是一个指向NMLISTVIEW结构的指针,NMLISTVIEW.iSubItem字段是列的索引,这时程序可以通过发送LVM_SORTITEMS消息根据所单击的列进行排序(如果指定了LVS_SORTASCENDING和LVS_SORTDESCENDING样式,则只可以根据列表项文本进行排序)。

8.12.3 选中项的获取

对于单选列表视图控件,可以发送LVM_GETSELECTIONMARK消息获取选中项。wParam和lParam参数都没有用到。如果有选中项,则返回选中列表项的索引;如果当前没有选中项,则返回−1。

如果多选列表视图控件,没有获取所有选中列表项索引的相关消息或函数,则可以通过类似下面的代码循环获取每个已选中列表项的信息:

复制代码
// 获取已选中列表项总数
int nCount = SendMessage(hwndListView, LVM_GETSELECTEDCOUNT, 0, 0);
if (nCount > 0)
{
    LPTSTR pBuf = new TCHAR[nCount * 128];
    ZeroMemory(pBuf, sizeof(TCHAR) * nCount * 128);
    TCHAR szText[128] = { 0 };

    lvi.mask = LVIF_TEXT;
    lvi.iSubItem = 0; lvi.pszText = szText; lvi.cchTextMax = _countof(szText);

    // 先搜索出第一个选中项
    int nIndex = SendMessage(hwndListView, LVM_GETNEXTITEM, -1, LVIS_SELECTED);
    // 获取列表项的信息
    lvi.iItem = nIndex;
    SendMessage(hwndListView, LVM_GETITEM, 0, (LPARAM)&lvi);
    StringCchCopy(pBuf, nCount * 128, lvi.pszText);
    StringCchCat(pBuf, nCount * 128, TEXT("\n"));

    for (int i = 0; i < nCount - 1; i++)
    {
        // 按索引往后搜索
        nIndex = SendMessage(hwndListView, LVM_GETNEXTITEM, nIndex, LVIS_SELECTED);
        // 获取列表项的信息
        lvi.iItem = nIndex;
        SendMessage(hwndListView, LVM_GETITEM, 0, (LPARAM)&lvi);
        StringCchCat(pBuf, nCount * 128, lvi.pszText);
        StringCchCat(pBuf, nCount * 128, TEXT("\n"));
    }
    MessageBox(hwnd, pBuf, TEXT("已选中列表项"), MB_OK);
    delete[] pBuf;
}

8.13 树视图

树视图控件通常用于显示一些具有层次关系的项目,例如资源管理器左侧的导航窗格用的就是SysTreeView32树视图控件。每个项目都有一个标签文本和一个可选的图像,每个项目都可以有一个与之关联的项目数据。每个项目都可以包含与之关联的一系列子项(子节点),具有一个或多个子项的项目称为父项(父节点)。子项显示在其父项下方,并适当缩进以表示它从属于父项。双击父项,可以展开或折叠关联的子项列表。没有父项的项目出现在层次结构的顶部,称为根项(根节点)。每个项目的标签文本实际上是一个编辑控件。

树视图控件常用的样式如表8.42所示。

表8.42

样式 含义
TVS_CHECKBOXES 树视图项前面显示一个复选框。虽然可以显示复选框,但树视图控件并不能多选
TVS_EDITLABELS 允许用户编辑树视图项的标签文本,在用户完成对标签文本的编辑后,树视图控件的父窗口会收到包含TVN_ENDLABELEDIT通知码的WM_NOTIFY消息,处理该通知码只需要简单地返回TRUE表示接受编辑,或FALSE表示拒绝编辑
TVS_HASLINES 树视图项的前面会显示一个线条,表明项目的层次结构关系
TVS_HASBUTTONS 父节点前面显示加号(+)或减号(−)按钮,用户单击按钮可以展开或折叠父项的子项列表。要在树视图控件的根节点中包含按钮,还必须指定TVS_LINESATROOT
TVS_LINESATROOT 表明项目层次结构关系的线条链接到根节点。如果未指定TVS_HASLINES,则忽略该样式
TVS_SHOWSELALWAYS 当树视图控件失去焦点时,选中的项目也保持选中状态
TVS_TRACKSELECT 在树视图控件中启用热跟踪,即当鼠标悬停在树视图项上时,光标形状变为小手

关于表明项目层次结构关系的线条,以及父节点前面显示的加号(+)或减号(−)按钮,请参照原书的图8.18。

图中有图书和世界两个根节点。图书下面有平面设计和程序设计两个子节点,世界下面有美国和中国两个子节点;中国下面又有北京市和上海市两个子节点。

8.13.1 项目的添加

创建树视图控件以后,可以通过发送TVM_INSERTITEM消息在树视图控件中插入新项。wParam参数没有用到。lParam参数是一个指向TVINSERTSTRUCT结构的指针,该结构指定树视图项的属性。如果执行成功,则返回新项的HTREEITEM类型的句柄;否则返回NULL。TVINSERTSTRUCT结构定义如下:

复制代码
typedef struct {
    HTREEITEM hParent;      // 父项句柄,如果设置为TVI_ROOT或NULL,表示添加一个根节点
    HTREEITEM hInsertAfter; // 在hInsertAfter指定的项目后面插入新项目
    union {
        TVITEMEX itemex;    // TVITEMEX结构,包含要添加的项目的信息
        TVITEM   item;      // TVITEM结构,包含要添加的项目的信息
    } DUMMYUNIONNAME;
} TVINSERTSTRUCT, *LPTVINSERTSTRUCT;
  • 如果hInsertAfter字段指定了一个项目句柄,则在hInsertAfter指定的项目后面插入新项目,可以指定为表8.43所示的值。

表8.43

常量 含义
TVI_FIRST 将项目插入列表的开头
TVI_LAST 将项目插入列表的末尾
TVI_ROOT 将项目添加为根节点
TVI_SORT 按字母顺序将项目插入列表
  • TVITEMEX比TVITEM结构多了几个字段,通常使用TVITEM结构即可,该结构也用于设置、获取树视图项信息的TVM_SETITEM、TVM_GETITEM消息。TVITEM结构定义如下:

    typedef struct tagTVITEM {
    UINT mask; // 掩码标志,指定哪个字段有效
    HTREEITEM hItem; // 项目句柄,设置、获取项目信息时需要该字段
    UINT state;
    UINT stateMask; // 这两个字段的用法和列表视图控件LVITEM结构的同名字段类似
    LPTSTR pszText; // 项目的文本
    int cchTextMax; // 项目的文本缓冲区长度,以字符为单位
    int iImage; // 当项目处于非选定状态时使用的图像列表索引
    int iSelectedImage;// 当项目处于选定状态时使用的图像列表索引
    int cChildren; // 通常不用
    LPARAM lParam; // 与项目关联的数据
    } TVITEM, *LPTVITEM;

mask字段是掩码标志,指定哪个字段有效,可以是以下一个或多个值(见表8.44)。

表8.44

标志 含义
TVIF_HANDLE hItem字段有效
TVIF_STATE state和stateMask字段有效
TVIF_TEXT pszText和cchTextMax字段有效
TVIF_IMAGE iImage字段有效
TVIF_SELECTEDIMAGE iSelectedImage字段有效
TVIF_CHILDREN cChildren字段有效
TVIF_PARAM lParam字段有效

例如下面的代码:

复制代码
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    HINSTANCE hInstance;
    static HWND hwndTreeView;               // 树视图控件
    static HTREEITEM htrBook, htrWorld;     // 根节点:图书、世界
    static HTREEITEM htrAmerican, htrChina; // 世界:美国、中国

    static HIMAGELIST hImagList;
    HBITMAP    hbmImage;

    TVINSERTSTRUCT tvi = { 0 };

    switch (uMsg)
    {
    case WM_CREATE:
        hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
        // 创建树视图控件
        hwndTreeView = CreateWindowEx(0, TEXT("SysTreeView32"), NULL,
            WS_CHILD | WS_VISIBLE | WS_BORDER | 
            TVS_HASLINES | TVS_LINESATROOT | TVS_HASBUTTONS |
            TVS_EDITLABELS | TVS_SHOWSELALWAYS | TVS_TRACKSELECT,
            10, 0, 200, 200, hwnd, (HMENU)IDC_TREEVIEW, hInstance, NULL);

        // 图像列表
        hImagList = ImageList_Create(24, 24, ILC_COLOR32, 8, 0);
        for (int i = 0; i < 8; i++)
        {
            // 资源文件中定义了8个位图
            hbmImage = (HBITMAP)LoadImage(hInstance, MAKEINTRESOURCE(IDB_BMP_BOOK + i),
                IMAGE_BITMAP, 24, 24, 0);
            ImageList_Add(hImagList, hbmImage, NULL);
            DeleteObject(hbmImage);
        }
        SendMessage(hwndTreeView, TVM_SETIMAGELIST, TVSIL_NORMAL, (LPARAM)hImagList);

        // 为树视图控件添加项目
        // 根节点:图书、世界
        tvi.item.mask = TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE;
        tvi.hInsertAfter = TVI_LAST;

        tvi.hParent = TVI_ROOT;
        tvi.item.pszText = TEXT("图书"); tvi.item.iImage = tvi.item. iSelectedImage = 0;
        htrBook = (HTREEITEM)SendMessage(hwndTreeView, TVM_INSERTITEM, 0, (LPARAM)&tvi);
        tvi.item.pszText = TEXT("世界"); tvi.item.iImage = tvi.item. iSelectedImage = 1;
        htrWorld = (HTREEITEM)SendMessage(hwndTreeView, TVM_INSERTITEM, 0, (LPARAM)&tvi);

        // 图书(平面设计、程序设计)
        tvi.hParent = htrBook;
        tvi.item.pszText = TEXT("平面设计"); tvi.item.iImage = tvi.item. iSelectedImage = 2;
        SendMessage(hwndTreeView, TVM_INSERTITEM, 0, (LPARAM)&tvi);
        tvi.item.pszText = TEXT("程序设计"); tvi.item.iImage = tvi.item. iSelectedImage = 3;
        SendMessage(hwndTreeView, TVM_INSERTITEM, 0, (LPARAM)&tvi);

        // 世界(美国、中国)
        tvi.hParent = htrWorld;
        tvi.item.pszText = TEXT("美国"); tvi.item.iImage = tvi.item. iSelectedImage = 4;
        htrAmerican = (HTREEITEM)SendMessage(hwndTreeView, TVM_INSERTITEM, 0, (LPARAM)&tvi);
        tvi.item.pszText = TEXT("中国"); tvi.item.iImage = tvi.item. iSelectedImage = 5;
        htrChina = (HTREEITEM)SendMessage(hwndTreeView, TVM_INSERTITEM, 0, (LPARAM)&tvi);

        // 中国(北京市、上海市)
        tvi.hParent = htrChina;
        tvi.item.pszText = TEXT("北京市"); tvi.item.iImage = tvi.item. iSelectedImage = 6;
        SendMessage(hwndTreeView, TVM_INSERTITEM, 0, (LPARAM)&tvi);
        tvi.item.pszText = TEXT("上海市"); tvi.item.iImage = tvi.item. iSelectedImage = 7;
        SendMessage(hwndTreeView, TVM_INSERTITEM, 0, (LPARAM)&tvi);
        return 0;

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

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

完整代码参见Chapter8\TreeViewDemo项目。

8.13.2 选中项的获取

要获取选中项很简单,在用户将选中项从一个项目更改为另一个项目后,树视图控件的父窗口会收到包含TVN_SELCHANGED通知码的WM_NOTIFY消息,lParam参数是一个指向NMTREEVIEW结构的指针。NMTREEVIEW结构的itemOld和itemNew字段都是TVITEM结构,包含先前所选项目和新选择项目的信息,但请注意,只有结构的mask、hItem、state和lParam字段有效。NMTREEVIEW结构的定义如下:

复制代码
typedef struct tagNMTREEVIEW {
    NMHDR  hdr;     // NMHDR结构
    UINT   action;  // 导致选择更改的操作类型,可以是:
                    // TVC_BYKEYBOARD(通过击键)、TVC_BYMOUSE(通过单击鼠标)或TVC_UNKNOWN(未知)
    TVITEM itemOld; // TVITEM结构,包含先前所选项目的信息
    TVITEM itemNew; // TVITEM结构,包含新选择项目的信息
    POINT  ptDrag;  // 事件发生时鼠标的坐标(相对于客户区)
} NMTREEVIEW, *LPNMTREEVIEW;

例如下面处理TVN_SELCHANGED通知码的代码:

复制代码
case WM_NOTIFY:
    switch (((LPNMHDR)lParam)->code)
    {
    case TVN_SELCHANGED:
        pnmTV = (LPNMTREEVIEW)lParam;
        wsprintf(szBuf, TEXT("项目句柄:0x%X"), pnmTV->itemNew.hItem);
        SetDlgItemText(hwnd, IDC_STATICSELECTED, szBuf);   // 选中的项目句柄显示到静态控件中
        break;
    }
    return 0;

如果不是在处理TVN_SELCHANGED通知码的情况下,可以通过发送TVM_GETNEXTITEM消息来获取具有某种特征的树视图项。wParam参数指定要获取的项目,可以是表8.45所示的值之一。

表8.45

常量 含义
TVGN_CARET 获取当前选中的项目
TVGN_CHILD 获取lParam参数指定的项的第一个子项
TVGN_NEXTSELECTED 获取lParam参数指定的项后面的一个选中的项目
TVGN_PARENT 获取lParam参数指定的项的父项目
TVGN_PREVIOUS 获取lParam参数指定的项的上一个兄弟项目
TVGN_NEXT 获取lParam参数指定的项的下一个兄弟项目
TVGN_ROOT 获取树视图控件的最顶部或第一项

lParam参数指定为一个项目句柄,如果不需要,可以设置为0。如果执行成功,则返回符合条件项目的句柄。

例如下面的代码:

复制代码
HTREEITEM htrSelected;
htrSelected = (HTREEITEM)SendMessage(hwndTreeView, TVM_GETNEXTITEM, TVGN_CARET, 0);
wsprintf(szBuf, TEXT("项目句柄:0x%X"), htrSelected);
SetDlgItemText(hwnd, IDC_STATICSELECTED, szBuf);

8.13.3 其他消息和通知码

常用的其他消息如表8.46所示。

表8.46

消息类型 含义
TVM_DELETEITEM 删除一个树视图项及其所有子项。wParam参数没有用到,lParam参数指定为项目句柄,如果设置为TVI_ROOT或NULL,则删除所有项目
TVM_GETCOUNT 获取树视图控件中的项目总数。wParam和lParam参数都没有用到
TVM_SETITEM 设置树视图项的部分或全部属性。wParam参数没有用到,lParam参数是一个指向包含新项属性的TVITEM结构的指针
TVM_GETITEM 获取树视图项的部分或全部属性。wParam参数没有用到,lParam参数是一个指向TVITEM结构的指针

当用户鼠标右键单击树视图项时,树视图控件的父窗口会收到包含NM_RCLICK通知码的WM_NOTIFY消息,lParam参数是一个指向NMHDR结构的指针。

8.14 状态栏

状态栏是显示在窗口底部的一个子窗口控件,可以在其中显示各种状态信息,状态栏可以分为多个部分以显示不同类型的信息。调用CreateWindowEx函数创建状态栏的时候,状态栏的窗口过程会自动设置窗口的初始位置和大小,状态栏的宽度与父窗口的客户区宽度相同,高度则基于状态栏DC所选择字体的高度。另外,当父窗口的大小更改时,只需要简单地向状态栏发送一个WM_SIZE消息(wParam和lParam参数都设置为0即可),状态栏就能自动调整大小。

例如,下面的代码创建了一个状态栏,当父窗口的窗口大小更改时,状态栏会自动调整大小:

复制代码
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    HINSTANCE hInstance;
    static HWND hwndStatus;

    switch (uMsg)
    {
    case WM_CREATE:
        hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
        hwndStatus = CreateWindowEx(0, TEXT("msctls_statusbar32"), NULL, WS_CHILD |WS_ VISIBLE,
            0, 0, 0, 0, hwnd, (HMENU)IDC_STATUSBAR, hInstance, NULL);
        return 0;

    case WM_SIZE:
        SendMessage(hwndStatus, WM_SIZE, 0, 0);
        return 0;

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

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

如果只是简单地创建一个状态栏,也可以调用DrawStatusText函数:

复制代码
void DrawStatusText(
    HDC     hdc,    // 设备环境句柄
    LPCRECT lprc,   // 状态栏所属的矩形区域,相对于父窗口客户区
    LPCTSTR pszText,// 状态栏文本字符串,可以使用制表符\t
    UINT    uFlags);// 文本绘制标志,可以设置为0,或设置为SBT_NOBORDERS表示不使用边框

该函数使用和状态栏窗口过程相同的技术绘制状态栏,但是不会自动设置状态栏的大小和位置。例如下面的代码:

复制代码
case WM_SIZE:
    hdc = GetDC(hwnd);
    hFont = CreateFont(18, 0, 0, 0, 0, 0, 0, 0, GB2312_CHARSET, 0, 0, 0, 0, TEXT ("微软雅黑"));
    SelectObject(hdc, hFont);
    SetRect(&rect, 0, HIWORD(lParam) - 20, LOWORD(lParam), HIWORD(lParam));
    DrawStatusText(hdc, &rect, TEXT("这是DrawStatusText函数设置的状态栏"), 0);
    ReleaseDC(hwnd, hdc);
    return 0;

效果如原书的图8.19所示。

DrawStatusText函数的pszText字符串中可以使用制表符(\t),默认情况下,文本在状态栏的指定矩形范围内左对齐,可以在文本中嵌入制表符以使其居中或右对齐。第1个制表符左侧的文本左对齐,第1个制表符右侧的文本居中对齐,第2个制表符字符右侧的文本右对齐。例如:

复制代码
DrawStatusText(hdc, &rect, TEXT("这是\tDrawStatusText\t函数设置的状态栏"), 0);

效果如下所示:

状态栏常见的样式如表8.47所示。

表8.47

样式 含义
SBARS_SIZEGRIP 状态栏的右端包含一个大小调整手柄
CCS_TOP 显示在父窗口客户区的顶部
CCS_LEFT 显示在父窗口客户区的左侧
CCS_RIGHT 显示在父窗口客户区的右侧
CCS_VERT 垂直显示
SBT_TOOLTIPS 启用工具提示,需要发送SB_SETTIPTEXT消息

8.14.1 为状态栏分栏

一个状态栏最多可以包含256个部分,每个部分称之为"指示器",可以通过发送SB_SETPARTS消息来为状态栏添加指示器,或者说为状态栏分栏。SB_SETPARTS消息的wParam参数指定为指示器的数量,lParam参数是一个指定每个指示器右边缘坐标的数值型数组,如果把某个数组元素设置为−1,则表示把状态栏的剩余部分全部分配给该指示器。

例如,下面的代码把状态栏分为3栏,第1栏、第2栏的宽度分别为150、230,第3栏则使用客户区剩余的宽度:

复制代码
INT nArrParts[] = { 150, 380, -1 };
......
SendMessage(hwndStatus, SB_SETPARTS, _countof(nArrParts), (LPARAM)nArrParts);

在分栏后,通过发送SB_SETTEXT消息为每一栏设置文本。wParam参数的高位字没有用到,LOBYTE(LOWORD(wParam))指定为栏的索引号,栏索引从0开始;HIBYTE(LOWORD(wParam))指定绘制类型,通常指定为0即可。如果不需要解析制表符,可以指定SBT_NOTABPARSING。lParam参数指定为要设置的文本字符串指针。例如:

复制代码
SendMessage(hwndStatus, SB_SETTEXT, 
    MAKEWORD(0, 0), (LPARAM)TEXT("鼠标位置:(1000, 1000)"));
SendMessage(hwndStatus, SB_SETTEXT, 
    MAKEWORD(1, 0), (LPARAM)TEXT("鼠标点的颜色:RGB(100, 100, 100)"));
SendMessage(hwndStatus, SB_SETTEXT, 
    MAKEWORD(2, 0), (LPARAM)TEXT("6-20 23:56:31"));

效果如下所示:

要获取栏目的文本,可以发送SB_GETTEXTLENGTH和SB_GETTEXT消息。如果状态栏只有一栏,可以使用WM_SETTEXT、WM_GETTEXTLENGTH、WM_GETTEXT消息来执行文本操作,因为这些消息仅处理栏索引为0的部分。

关于状态栏的示例程序参见Chapter8\StatusBarDemo项目。

8.14.2 MenuHelp

在本节中,我们为Chapter6\HelloWindows项目添加状态栏菜单提示功能。当用户选择一个菜单项时,很多程序都会在底部的状态栏中显示该菜单项的帮助提示信息,可以响应WM_MENUSELECT消息并从消息参数中获取用户所选择的菜单项ID,根据菜单项ID获取对应的提示信息字符串,然后将其显示到状态栏中。但是还有更简单的,系统提供了一个专用函数MenuHelp,可以在处理WM_MENUSELECT或WM_COMMAND消息时调用该函数,在指定的状态栏窗口中显示当前菜单项的帮助提示文本。

复制代码
void MenuHelp(
    UINT      uMsg,      // 正在处理的消息,可以是WM_MENUSELECT或WM_COMMAND
    WPARAM    wParam,    // 消息的wParam参数
    LPARAM    lParam,    // 消息的lParam参数
    HMENU     hMainMenu, // 程序的主菜单句柄
    HINSTANCE hInst,     // 包含字符串资源的模块句柄
    HWND      hwndStatus,// 状态栏句柄
    UINT      *lpwIDs);
  • 通常是在WM_MENUSELECT消息中调用MenuHelp函数,将函数的前3个参数uMsg、wParam、lParam设置为窗口过程函数的相应参数即可。

  • WM_MENUSELECT消息的lParam参数就是用户正在浏览的菜单句柄,因此MenuHelp函数的hMainMenu参数直接设置为lParam即可。

  • 要配合MenuHelp函数,需要把每个菜单项对应的帮助提示文本定义在字符串表中,函数会自动调用LoadString函数载入正确的字符串,参数hInst指定为包含字符串资源的模块句柄。

  • hwndStatus参数指定为状态栏窗口的句柄。

如原书的图8.20所示,我为每个子菜单项和主菜单中的菜单项创建了字符串表。

在这里,我定义子菜单项所用的字符串ID值和菜单资源中子菜单项的ID值相同,而主菜单中的菜单项所用的字符串ID值分别为ID_FILE(20010)、ID_EDIT (20011)和ID_HELP(20012)。lpwIDs参数指定为一个数值型数组,第1个数组元素指定为子菜单项的基数,第2个数组元素指定为弹出菜单的基数,基数加上用户当前浏览的菜单项ID得到的数值就是MenuHelp函数要载入的字符串ID。例如:

复制代码
UINT uArrIDs[] = { 0, ID_FILE };
......
case WM_MENUSELECT:
    MenuHelp(uMsg, wParam, lParam, (HMENU)lParam, g_hInstance, hwndStatus, uArrIDs);
    return 0;

对于子菜单项,基数指定为0即可;弹出菜单没有ID,系统按照菜单项的索引号加上基数当作字符串ID。对于第1个弹出菜单"文件",函数加载的是ID为ID_FILE(20010)的字符串;对于第2个弹出菜单"编辑",菜单索引为1,所以函数加载的是ID为ID_EDIT(20011)的字符串,以此类推。

具体代码参见Chapter8\HelloWindows项目。

8.15 工具栏

工具栏一般位于菜单栏的下方,包含一个或多个按钮。这些按钮实际上都是一副图像,所以说它们只是仿真按钮,而不是真正的按钮子窗口控件。每个按钮对应于程序菜单中的某个菜单项,为用户提供了一种更直观的方式来访问应用程序的菜单项。当用户按下按钮的时候窗口过程会收到WM_COMMAND消息,这是为了和菜单、加速键使用同一份代码来处理用户按下工具栏按钮的操作。在工具栏中,有的按钮按下后会自动弹起;有的按钮按下后保留在"选中"状态,再按一次后恢复弹起状态,按钮的"选中"状态可以是互斥或不互斥的,按钮也可以被灰化或隐藏,所有这些属性和菜单项的属性类似。

要创建工具栏,首先需要使用Photoshop等工具制作一副位图,例如对于HelloWindows程序可以制作一副如原书的图8.21所示的.bmp位图,宽高为450像素 50像素,表示9个50像素 50像素的按钮,需要注意的是,每一个按钮图片的大小必须相同,然后有序摆放到一副大位图中。

上面的每个位图分别对应于新建、打开、保存、另存为、退出、剪切、复制、粘贴和关于菜单项。

创建工具栏的代码很简单,例如:

复制代码
hwndToolBar = CreateWindowEx(0, TEXT("ToolbarWindow32"), NULL,
    WS_CHILD | WS_VISIBLE | TBSTYLE_FLAT,
    0, 0, 0, 0, hwnd, (HMENU)IDC_TOOLBAR, g_hInstance, NULL);

稍后再讲工具栏的样式。

在创建工具栏后,可以通过发送TB_ADDBITMAP消息将一个或多个图像添加到工具栏的按钮图像列表中。wParam参数指定位图中按钮图像的数量,如果lParam参数指定的是系统预定义的位图,则忽略wParam参数;lParam参数是一个指向TBADDBITMAP结构的指针,该结构包含位图资源的ID以及包含位图资源的可执行文件的模块句柄。如果执行成功,则返回第一个新图像的索引;否则返回−1。TBADDBITMAP结构在CommCtrl.h头文件中定义如下:

复制代码
typedef struct tagTBADDBITMAP {
    HINSTANCE       hInst;  // 包含位图资源的可执行文件的模块句柄
    UINT_PTR        nID;    // 位图资源的ID
} TBADDBITMAP, *LPTBADDBITMAP;

在发送TB_ADDBITMAP消息添加图像前,还应该发送TB_BUTTONSTRUCTSIZE和TB_SETBITMAPSIZE消息到工具栏(见表8.48)。

表8.48

消息类型 含义
TB_BUTTONSTRUCTSIZE 指定TBBUTTON结构的大小。wParam参数指定为TBBUTTON结构的大小,以字节为单位。lParam参数没有用到
TB_SETBITMAPSIZE 设置要添加到工具栏中的每个位图图像的大小(如果不设置,默认情况下是24像素 22像素)。wParam参数没有用到。LOWORD(lParam)指定位图图像的宽度,HIWORD (lParam)指定位图图像的高度,以像素为单位

例如下面的代码:

复制代码
#define NUMBTNS 11
......
// 为工具栏添加图像
SendMessage(hwndToolBar, TB_BUTTONSTRUCTSIZE, sizeof(TBBUTTON), 0);
SendMessage(hwndToolBar, TB_SETBITMAPSIZE, 0, MAKELPARAM(50, 50));
TBADDBITMAP tbab;
tbab.hInst = g_hInstance;
tbab.nID = IDB_BITMAP;      // 工具栏图片已经添加到位图资源
SendMessage(hwndToolBar, TB_ADDBITMAP, NUMBTNS - 2, (LPARAM)&tbab);

只有9个按钮图像,但是我还想在"退出"和"粘贴"后各添加一个竖分隔符,所以NUMBTNS定义为11。

添加图像也可以通过创建图像列表的方式,例如下面的代码:

复制代码
hImagList = ImageList_Create(50, 50, ILC_MASK | ILC_COLOR32, NUMBTNS, 0);
ImageList_Add(hImagList, LoadBitmap(g_hInstance, MAKEINTRESOURCE(IDB_BITMAP)), NULL);
SendMessage(hwndToolBar, TB_SETIMAGELIST, 0, (LPARAM)hImagList);

接下来还需要将图像与按钮相关联,在将位图添加到按钮图像列表后,可以通过发送TB_ADDBUTTONS消息把按钮添加到工具栏中。将wParam参数设置为要添加的按钮个数,lParam参数是一个指向TBBUTTON结构数组的指针,其中包含要添加的按钮的信息,数组中的元素个数必须与wParam参数指定的按钮数相同。发送TB_ADDBUTTONS消息,一次可以添加多个按钮,也可以通过发送TB_INSERTBUTTON消息在工具栏中插入一个按钮。wParam参数指定为按钮从0开始的索引;lParam参数是一个指向TBBUTTON结构的指针,其中包含要插入的按钮的信息。TBBUTTON结构在CommCtrl.h头文件中定义如下:

复制代码
typedef struct {
    int       iBitmap;      // 按钮图像的索引
    int       idCommand;    // 按钮命令ID,通常指定为某个菜单项ID
    BYTE      fsState;      // 按钮状态
    BYTE      fsStyle;      // 按钮样式
    BYTE      bReserved[2]; // 保留字段
    DWORD_PTR dwData;       // 自定义数据
    INT_PTR   iString;      // 按钮文本,显示在按钮图像的下边,如果不需要显示按钮文本,则可以设置为-1
} TBBUTTON, *PTBBUTTON, *LPTBBUTTON;

例如下面的代码:

复制代码
TBBUTTON tbButtons[NUMBTNS] = {
    { 0, ID_FILE_NEW,    TBSTATE_ENABLED, BTNS_BUTTON, { 0 }, 0, (INT_PTR)TEXT("新建") },
    { 1, ID_FILE_OPEN,   TBSTATE_ENABLED, BTNS_BUTTON, { 0 }, 0, (INT_PTR)TEXT("打开") },
    { 2, ID_FILE_SAVE,   TBSTATE_ENABLED, BTNS_BUTTON, { 0 }, 0, (INT_PTR)TEXT("保存") },
    { 3, ID_FILE_SAVEAS, TBSTATE_ENABLED, BTNS_BUTTON, { 0 }, 0, (INT_PTR)TEXT("另存为") },
    { 4, ID_FILE_EXIT,   TBSTATE_ENABLED, BTNS_BUTTON, { 0 }, 0, (INT_PTR)TEXT("退出") },
    { 0, 0,              TBSTATE_ENABLED, TBSTYLE_SEP, { 0 }, 0, -1 },
    { 5, ID_EDIT_CUT,    TBSTATE_ENABLED, BTNS_BUTTON, { 0 }, 0, (INT_PTR)TEXT("剪切") },
    { 6, ID_EDIT_COPY,   TBSTATE_ENABLED, BTNS_BUTTON, { 0 }, 0, (INT_PTR)TEXT("复制") },
    { 7, ID_EDIT_PASTE,  TBSTATE_ENABLED, BTNS_BUTTON, { 0 }, 0, (INT_PTR)TEXT("粘贴") },
    { 0, 0,              TBSTATE_ENABLED, TBSTYLE_SEP, { 0 }, 0, -1 },
{ 8, ID_HELP_ABOUT,  TBSTATE_ENABLED, BTNS_BUTTON, { 0 }, 0, (INT_PTR)TEXT("关于") } };
......
SendMessage(hwndToolBar, TB_ADDBUTTONS, NUMBTNS, (LPARAM)&tbButtons);

TBBUTTON结构并没有一个size字段,因此在发送TB_ADDBUTTONS消息前,应该确保已经发送TB_BUTTONSTRUCTSIZE消息。发送TB_ADDBUTTONS消息添加按钮后,可以通过发送TB_SETBUTTONSIZE消息设置工具栏上按钮的大小(不设置也可以),系统会根据按钮图像的大小自动调整按钮大小。wParam参数没有用到,LOWORD(lParam)指定按钮的宽度,HIWORD(lParam)指定按钮的高度,以像素为单位。

工具栏窗口过程会在收到WM_SIZE或TB_AUTOSIZE消息时自动调整工具栏的大小,每当父窗口的大小发生变化时,或者在发送调整工具栏大小的消息(例如TB_SETBUTTONSIZE消息)后,程序都应向工具栏发送一个WM_SIZE或TB_AUTOSIZE消息(wParam和lParam参数都设置为0即可)。

每一个TBBUTTON结构都设置了图像索引,以及与各菜单项对应的ID,现在工具栏可以正常工作了,如原书的图8.22所示。

具体代码参见Chapter8\HelloWindows2项目。

接下来,看一下TBBUTTON结构的fsState按钮状态和fsStyle按钮样式字段。fsState字段指定按钮状态,常用的值如表8.49所示。

表8.49

按钮状态 含义
TBSTATE_ENABLED 按钮可用
TBSTATE_HIDDEN 按钮隐藏
TBSTATE_INDETERMINATE 按钮灰化
TBSTATE_CHECKED 按钮处于选中状态
TBSTATE_PRESSED 按钮被按下
TBSTATE_WRAP 该按钮后面的按钮换行到下一行中

fsStyle字段指定按钮样式,常用的值如表8.50所示。

表8.50

按钮样式 含义
BTNS_BUTTON 创建一个标准按钮
BTNS_AUTOSIZE 按钮自动调整其宽度,系统会根据按钮图像的宽度加上文本的宽度来计算
BTNS_CHECK 创建双状态按钮,当用户单击按钮时在按下和弹起状态之间切换
BTNS_SEP 创建一个分隔符

常用的工具栏样式如表8.51所示。

表8.51

工具栏样式 含义
CCS_BOTTOM 工具栏显示在父窗口客户区的底部
CCS_LEFT 工具栏显示在父窗口客户区的左侧
CCS_RIGHT 工具栏显示在父窗口客户区的右侧
CCS_VERT 垂直显示。要创建一个垂直显示的工具栏,可以在调用CreateWindowEx函数创建工具栏的时候指定CCS_VERT样式,并在发送TB_ADDBUTTONS消息时,为每个TBBUTTON结构指定TBSTATE_ENABLED
CCS_ADJUSTABLE 启用工具栏的自定义功能,允许用户将按钮拖动到新位置或通过将其拖离工具栏来删除按钮。此外,用户可以双击工具栏以显示"自定义工具栏"对话框,该对话框允许用户添加、删除和重新排列工具栏按钮
TBSTYLE_ALTDRAG 允许用户在按住Alt键的同时拖动按钮来更改其位置。如果未指定该样式,则是在按住Shift键的同时拖动按钮来更改位置。请注意,必须同时指定CCS_ADJUSTABLE样式
TBSTYLE_FLAT 创建一个平面工具栏,在平面工具栏中,工具栏和按钮的背景都是透明的,按钮文本显示在按钮图像的下方
TBSTYLE_LIST 按钮文本显示在按钮图像的右侧
TBSTYLE_TOOLTIPS 当鼠标悬停在工具栏按钮上时显示工具提示控件
TBSTYLE_WRAPABLE 当工具栏太窄而不能在一行显示所有按钮时,工具栏按钮可以换行到下一行显示

8.15.1 为按钮显示工具提示

要为按钮显示工具提示,在调用CreateWindowEx函数创建工具栏的时候必须指定TBSTYLE_TOOLTIPS样式,可以通过以下几种方式为按钮设置工具提示文本。

  • 通过TBBUTTON结构的iString字段为每个按钮设置工具提示文本,还必须发送TB_SETMAXTEXTROWS消息将最大文本行设置为0,以使iString字段指定的文本显示为工具提示而不是按钮文本。

  • 使用TBSTYLE_LIST样式创建工具栏,通过TBBUTTON结构的iString字段为每个按钮设置工具提示文本,然后设置TBSTYLE_EX_MIXEDBUTTONS工具栏扩展样式SendMessage (hwndToolBar, TB_SETEXTENDEDSTYLE, 0, TBSTYLE_EX_MIXEDBUTTONS);。

  • 处理TTN_NEEDTEXT通知码(等于TTN_GETDISPINFO,#define TTN_NEEDTEXT TTN_GETDISPINFO)的WM_NOTIFY消息。lParam参数是一个指向NMTTDISPINFO结构的指针,在CommCtrl.h头文件中定义如下:

    复制代码
      typedef struct {
      NMHDR     hdr;       // NMHDR结构
      LPTSTR    lpszText;  // 指定为工具提示文本字符串指针,如果hinst是模块句柄,则为字符串资源ID
      TCHAR     szText[80];// 工具提示文本的缓冲区,可以将文本复制到该缓冲区,而不是指定字符串指针或
                           // 字符串资源ID
      HINSTANCE hinst;     // 包含字符串资源的模块句柄
      UINT      uFlags;    // 标志 
      LPARAM    lParam;    // 自定义数据
      } NMTTDISPINFO, *LPNMTTDISPINFO;

设置NMTTDISPINFO结构的方法有3种,可以任选其一。

1)字符串包含在资源中,这时可以将hinst字段设置为包含资源的模块句柄,并把lpszText字段设置为字符串ID,其他字段保持为NULL,工具提示控件会调用LoadString函数载入字符串。

2)字符串在内存中,将字符串指针放入lpszText字段中,其他字段保持为NULL。

3)将字符串复制到szText字段中,其他字段保持为NULL。

在HelloWindows2程序中,为了配合MenuHelp函数,已经把每个菜单项对应的帮助提示文本定义在字符串表中,定义子菜单项所用的帮助提示文本字符串ID值和菜单资源中子菜单项的ID值相同,hdr.idFrom字段正好是按钮的命令ID,因此可以使用方法1。例如下面的代码:

复制代码
    case TTN_NEEDTEXT:
    LPNMTTDISPINFO lpnmTDI;
    lpnmTDI = (LPNMTTDISPINFO)lParam;
    lpnmTDI->hinst = g_hInstance;
    lpnmTDI->lpszText = (LPTSTR)lpnmTDI->hdr.idFrom;
    return 0;
  • 处理TBN_GETINFOTIP通知码的WM_NOTIFY消息。lParam参数是一个指向NMTBGETINFOTIP结构的指针,该结构和上面的NMTTDISPINFO结构类似。

8.15.2 自定义工具栏

工具栏具有简单、直接、美观的特点,但是如果菜单项比较多,想要全部显示可能会占据大量的空间。为此可以在调用CreateWindowEx函数创建工具栏时指定CCS_ADJUSTABLE样式启用工具栏的自定义功能,用户可以自由排列工具栏按钮,或者仅选择显示用户感兴趣的按钮。

在指定CCS_ADJUSTABLE样式后,用户可以将按钮拖动到新位置或通过将其拖离工具栏来删除按钮(按住Shift键)。此外,当用户双击工具栏的时候,会弹出一个自定义工具栏对话框,如原书的图8.23所示。

右侧的列表框是当前工具栏上显示的所有按钮。在选择一个列表项(按钮)后,用户可以单击上移下移按钮调整列表项(按钮)的顺序,也可以单击"删除"按钮删除不需要的列表项(按钮)。删除一个列表项(按钮)以后,该列表项(按钮)会显示到左边的可用工具栏按钮列表框中。即左边的列表框显示的是可以插入工具栏的按钮,右边的列表框显示的是当前工具栏上显示的所有按钮。除了可以通过双击工具栏弹出一个自定义工具栏对话框,程序还可以通过向工具栏发送TB_CUSTOMIZE消息以打开自定义工具栏对话框。

当用户通过自定义工具栏对话框来插入、删除一个列表项(按钮)时,工具栏的父窗口会收到包含TBN_QUERYINSERT、TBN_QUERYDELETE通知码的WM_NOTIFY消息,程序可以处理这两个通知码并返回TRUE表示允许插入、删除操作,或返回FALSE表示拒绝插入、删除操作。例如下面的代码:

复制代码
case WM_NOTIFY:
    switch (((LPNMHDR)lParam)->code)
    {
    case TBN_QUERYINSERT:
    case TBN_QUERYDELETE:
        return TRUE;

自定义工具栏对话框中两个列表框的维护需要处理TBN_GETBUTTONINFO通知码,当工具栏需要全部按钮的信息时,会多次发送包含TBN_GETBUTTONINFO通知码的WM_NOTIFY消息。lParam参数是一个指向NMTOOLBAR结构的指针,该结构在CommCtrl.h头文件中定义如下:

复制代码
typedef struct tagNMTOOLBAR {
    NMHDR    hdr;       // NMHDR结构
    int      iItem;     // 按钮从0开始的索引
    TBBUTTON tbButton;  // 用于每个按钮的TBBUTTON结构
    int      cchText;   // 按钮文本中的字符数
    LPTSTR   pszText;   // 按钮文本的字符串缓冲区地址
    RECT     rcButton;  // 按钮所属的矩形区域范围
} NMTOOLBAR, *LPNMTOOLBAR;

通常,处理TBN_GETBUTTONINFO通知码的代码如下:

复制代码
case TBN_GETBUTTONINFO:
    LPNMTOOLBAR lpnmTB;
    lpnmTB = (LPNMTOOLBAR)lParam;
    // 如果不是最后一个按钮
    if (lpnmTB->iItem < NUMBTNS)
    {
        lpnmTB->tbButton = tbButtons[lpnmTB->iItem];
        return TRUE;
    } 
    return FALSE;

工具栏控件每次都会发送一个TBN_GETBUTTONINFO通知码,并且NMTOOLBAR结构中的iItem字段会依次递增,每次需要在lpnmTB->tbButton字段中返回一个按钮的信息,如果还有剩余的按钮信息没有告诉工具栏(比如可用按钮和已显示按钮加起来总共有9个,现在返回了8个,那么还剩1个按钮的信息没有告诉工具栏),则应该在消息的返回值中返回TRUE。工具栏根据这个返回值知道还有多余的按钮没有处理,于是马上将iItem字段加1并再次发送TBN_GETBUTTONINFO通知码,如此循环,直到消息的返回值是FALSE为止。为什么工具栏不知道需要获取的按钮数量,而需要由父窗口来确定呢?这是因为工具栏只维护目前显示的按钮。假设工具栏上当前显示了8个按钮,而程序处理TBN_GETBUTTONINFO通知码共返回了9个按钮的信息。这9个按钮中包括了已经显示在工具栏上的8个按钮和可以添加到工具栏上的另外1个按钮。工具栏就会对这9个按钮和工具栏上目前已显示的所有按钮进行比较,把已经显示的8个按钮放在自定义工具栏对话框右边的列表框中,把剩余的1个放在对话框左边的列表框中。

到现在为止,添加、删除以及两个列表框已经可以正常工作了,"上移""下移"按钮不需要额外处理就可以正常工作。当用户单击"帮助""重置"按钮的时候,工具栏的父窗口会收到包含TBN_CUSTHELP、TBN_RESET通知码的WM_NOTIFY消息,程序需要自行处理这两个通知码,感兴趣的读者请自行阅读MSDN。

8.16 进度条控件

进度条控件通常用于一些需要长时间操作的任务以实时显示进度。进度条控件常用的样式如表8.52所示。

表8.52

样式 含义
PBS_VERTICAL 进度条从下到上垂直显示进度状态(默认情况下是从左到右)
PBS_MARQUEE 进度指示条的大小不会增加,而是沿着进度条的长度方向重复移动

进度条控件常用的消息类型如表8.53所示。

表8.53

消息类型 含义
PBM_SETRANGE、
PBM_SETRANGE32 设置进度条的最小值和最大值(默认情况下范围值为0~100),并重绘以反映新范围
PBM_SETPOS 设置进度条的当前位置,并重绘以反映新位置
PBM_GETPOS 获取进度条的当前位置
PBM_SETSTEP 指定进度条的步长增量,步长增量是进度条在收到PBM_STEPIT消息时增加其当前位置的量,默认情况下步长增量为10
PBM_STEPIT 把进度条的当前位置增加一个步长增量,并重绘以反映新位置
PBM_DELTAPOS 把进度条的当前位置增加指定的量,并重绘以反映新位置

关于进度条控件的示例程序参见Chapter8\ProgressDemo项目。

8.17 日期控件

日期控件可以让用户选择年月日,如原书的图8.24所示。

可以通过指定MCS_WEEKNUMBERS样式在日期控件左侧显示周数,如图8.24(右图)所示。

通常可以按如下方式创建一个日期控件:

复制代码
// 日期控件
hwndMonthCal = CreateWindowEx(0, TEXT("SysMonthCal32"), NULL,
    WS_CHILD | WS_VISIBLE | MCS_WEEKNUMBERS,
    0, 0, 0, 0, hwnd, (HMENU)IDC_MONTHCAL, hInstance, NULL);

// 根据日期控件所需的最小大小调整其位置
SendMessage(hwndMonthCal, MCM_GETMINREQRECT, 0, (LPARAM)&rect);
MoveWindow(hwndMonthCal, 10, 0, rect.right, rect.bottom, FALSE);

日期控件常用的消息如表8.54所示。

表8.54

消息类型 含义
MCM_GETCURSEL 获取当前选定的日期。wParam参数没有用到,lParam参数指定为一个指向接收当前所选日期信息的SYSTEMTIME结构的指针。该消息不能用于MCS_MULTISELECT样式的日期控件。例如:SendMessage(hwndMonth, MCM_GETCURSEL, 0, (LPARAM)&st);wsprintf(szBuf, TEXT("%d年%0.2d月%0.2d日"), st.wYear, st.wMonth, st.wDay);MessageBox(hwnd, szBuf, TEXT("提示"), MB_OK);
MCM_GETTODAY 获取今天的日期,该消息的用法和MCM_GETCURSEL消息相同
MCM_SETCURSEL 设置日期控件的当前选定日期,如果指定的日期不在视图中,控件会更新显示视图。wParam参数没有用到,lParam参数是一个指向要设置为当前所选日期信息的SYSTEMTIME结构的指针。该消息不能用于MCS_MULTISELECT样式的日期控件
MCM_SETTODAY 设置今天的日期,该消息的用法和MCM_SETCURSEL消息相同
MCM_GETMINREQRECT 根据当前字体、控件样式等计算日期控件所需的最小容量。wParam参数没有用到,lParam参数是一个指向接收边界矩形信息的RECT结构的指针,矩形坐标相对于父窗口客户区的左上角

关于日期控件的示例程序参见Chapter8\MonthDemo项目。

还有一个日期时间控件,可以显示时间,类名是SysDateTimePick32。日期时间控件的日期控件部分平时是收起的,单击右侧的下拉按钮可以展开,如原书的图8.25所示。感兴趣的读者请自行参阅MSDN。

8.18 Tab选项卡控件

通过使用选项卡控件,当用户单击不同的选项卡时,可以显示不同的页面(子窗口或对话框),如原书的图8.26所示。

在图8.26中,程序具有4个选项卡,选项卡的文本称为文本标签。选项卡是指包含文本标签的那个矩形区域。当用户单击不同的选项卡时,下方会显示不同的子窗口,子窗口通常使用对话框。前面说过,在对话框中摆放子窗口控件很简单。如图8.26所示,当鼠标悬停在选项卡上时,可以选择显示工具提示。每个选项卡的文本标签前面还可以选择显示一个图标,每个选项卡都可以设置一个与之关联的自定义数据。

在原书的图8.27中,程序的整个客户区是一个选项卡控件。4个选项卡下面是用于显示不同子窗口的区域,叫作显示区域。显示区域用于显示不同的子窗口,图中框选出来的矩形区域就是显示区域。

选项卡控件常用的样式如表8.55所示。

表8.55

样式 含义
TCS_FIXEDWIDTH 所有选项卡的宽度相同(默认情况下每个选项卡的宽度会根据图标和文本标签的宽度自动调整)。另外,可以通过发送TCM_SETITEMSIZE消息设置选项卡的宽度和高度
TCS_MULTILINE 默认情况下所有选项卡显示在一行,如果显示不完全,会在右侧显示一个Up-Down控件以滚动选项卡:指定该样式以后,如果多个选项卡在一行显示不完全,可以显示为多行
TCS_VERTICAL 选项卡显示在程序窗口的左侧,选项卡文本标签垂直显示,通用控件版本6可能不支持该样式
TCS_RIGHT 与TCS_VERTICAL样式一起使用,选项卡显示在程序窗口的右侧,通用控件版本6可能不支持该样式
TCS_TOOLTIPS 选项卡显示工具提示,需要处理TTN_NEEDTEXT消息(等于TTN_GETDISPINFO)

通常可以按如下方式创建一个选项卡控件:

复制代码
// 根据父窗口客户区的大小创建选项卡控件
GetClientRect(hwnd, &rect);
hwndTabControl = CreateWindowEx(0, TEXT("SysTabControl32"), NULL,
    WS_CHILD | WS_VISIBLE | TCS_MULTILINE | TCS_TOOLTIPS,
    0, 0, rect.right, rect.bottom, hwnd, (HMENU)IDC_TABCONTROL, hInstance, NULL);

创建选项卡控件以后,可以通过发送TCM_INSERTITEM消息添加选项卡。wParam参数指定为新选项卡中从0开始的索引。lParam参数指定为一个指向TCITEM结构的指针,该结构指定选项卡的属性。TCITEM结构还可以用于TCM_SETITEM、TCM_GETITEM消息设置、获取指定选项卡的属性,该结构在CommCtrl.h头文件中定义如下:

复制代码
typedef struct {
    UINT   mask;        // 掩码标志,指定要设置或获取哪些字段的值
    DWORD  dwState;     // 一般不用
    DWORD  dwStateMask; // 一般不用
    LPTSTR pszText;     // 选项卡文本标签的字符串指针,如果是获取信息,则是缓冲区地址
    int    cchTextMax;  // pszText字段指向的缓冲区的大小(字符),如果不是获取信息,则忽略该字段
    int    iImage;      // 图像列表的索引
    LPARAM lParam;      // 自定义数据
} TCITEM, *LPTCITEM;

mask字段是掩码标志,指定要设置或获取哪些字段的值,可以是表8.56中的一个或多个值。

表8.56

标志 含义
TCIF_STATE dwState字段有效
TCIF_TEXT pszText字段有效
TCIF_IMAGE iImage字段有效
TCIF_PARAM lParam字段有效

例如下面的代码:

复制代码
// 添加选项卡
TCITEM tci;
tci.mask = TCIF_TEXT;
tci.pszText = TEXT("平面设计");
SendMessage(hwndTabControl, TCM_INSERTITEM, 0, (LPARAM)&tci);
tci.pszText = TEXT("Web开发");
SendMessage(hwndTabControl, TCM_INSERTITEM, 1, (LPARAM)&tci);
tci.pszText = TEXT("Windows程序设计");
SendMessage(hwndTabControl, TCM_INSERTITEM, 2, (LPARAM)&tci);
tci.pszText = TEXT("加密解密");
SendMessage(hwndTabControl, TCM_INSERTITEM, 3, (LPARAM)&tci);

要想在用户单击不同的选项卡时显示不同的页面(子窗口或对话框),就需要创建这些子窗口或对话框,通常是使用对话框。但是因为还没有学习对话框,所以这里以普通的子窗口为例。因为是4个选项卡,所以需要创建4个子窗口。为了简单起见,我基于同一窗口类创建了4个子窗口,子窗口的创建及其窗口过程如下所示:

复制代码
// 全局变量
HWND hwndChild[4];

// 创建4个选项卡对应的4个子窗口
wndclass.cbSize = sizeof(WNDCLASSEX);
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WindowProcChild;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = TEXT("ChildWindow");
wndclass.hIconSm = NULL;
RegisterClassEx(&wndclass);
for (int i = 0; i < 4; i++)
    hwndChild[i] = CreateWindowEx(0, TEXT("ChildWindow"), NULL, WS_CHILD | WS_VISIBLE,
        0, 0, 0, 0, hwnd, NULL, hInstance, NULL);

LRESULT CALLBACK WindowProcChild(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    HDC hdc;
    PAINTSTRUCT ps;
    RECT rect;

    switch (uMsg)
    {
    case WM_PAINT:
        hdc = BeginPaint(hwnd, &ps);
        GetClientRect(hwnd, &rect);
        if (hwnd == hwndChild[0])
            DrawText(hdc, TEXT("平面设计 价格1万元"), _tcslen(TEXT("平面设计 价格1万元")),
                &rect, DT_SINGLELINE | DT_VCENTER | DT_CENTER);
        else if (hwnd == hwndChild[1])
            DrawText(hdc, TEXT("Web开发 价格2万元"), _tcslen(TEXT("Web开发 价格2万元")),
                &rect, DT_SINGLELINE | DT_VCENTER | DT_CENTER);
        else if (hwnd == hwndChild[2])
            DrawText(hdc, TEXT("Windows程序设计 价格3万元"), _tcslen(TEXT ("Windows程序设计  
                         价格3万元")),
                &rect, DT_SINGLELINE | DT_VCENTER | DT_CENTER);
        else
            DrawText(hdc, TEXT("加密解密 价格4万元"), _tcslen(TEXT("加密解密 价格4万元")),
                &rect, DT_SINGLELINE | DT_VCENTER | DT_CENTER);
        EndPaint(hwnd, &ps);
        return 0;

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

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

添加选项卡以后,初始情况下第一个(即索引为0)选项卡处于选中状态。在用户选择了其他选项卡后,选项卡控件的父窗口会收到包含TCN_SELCHANGE通知码的WM_NOTIFY消息,lParam参数是指向NMHDR结构的指针。处理该通知码就是通过发送TCM_GETCURSEL消息(wParam和lParam参数都没有用到)获取选项卡控件的当前选中项,销毁或隐藏不相关的子窗口,并显示需要的子窗口。例如下面的代码:

复制代码
case TCN_SELCHANGE:
    nIndex = SendMessage(hwndTabControl, TCM_GETCURSEL, 0, 0);
    for (int i = 0; i < 4; i++)
        ShowWindow(hwndChild[i], SW_HIDE);
    ShowWindow(hwndChild[nIndex], SW_SHOW);
    break;

选项卡控件指定TCS_TOOLTIPS样式以后可以显示工具提示,需要处理TTN_NEEDTEXT消息。该消息在学习工具栏按钮工具提示的时候已经详细讲解过了。此处NMTTDISPINFO结构的hdr.idFrom字段是选项卡的索引。例如下面的代码:

复制代码
case TTN_NEEDTEXT:
    LPNMTTDISPINFO lpnmTDI;
    lpnmTDI = (LPNMTTDISPINFO)lParam;
    switch (lpnmTDI->hdr.idFrom)
    {
    case 0:
        StringCchCopy(lpnmTDI->szText, 80, TEXT("这是平面设计的工具提示文本"));
        break;
    case 1:
        StringCchCopy(lpnmTDI->szText, 80, TEXT("这是Web开发的工具提示文本"));
        break;
    case 2:
        StringCchCopy(lpnmTDI->szText, 80, TEXT("这是Windows程序设计的工具提示文本"));
        break;
    case 3:
        StringCchCopy(lpnmTDI->szText, 80, TEXT("这是加密解密的工具提示文本"));
        break;
    }
    break;

除此之外,还需要处理WM_SIZE消息。在WM_SIZE消息中,根据父窗口客户区的大小调整选项卡控件的大小。可以根据选项卡控件的大小计算出显示区域的大小,然后利用显示区域的大小调整子窗口的大小。例如下面的代码:

复制代码
case WM_SIZE:
    // 根据父窗口客户区的大小调整选项卡控件的大小
    MoveWindow(hwndTabControl, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);

    // 根据选项卡控件的窗口矩形大小计算显示区域的大小,然后调整子窗口的大小
    SetRect(&rect, 0, 0, LOWORD(lParam), HIWORD(lParam));
    SendMessage(hwndTabControl, TCM_ADJUSTRECT, FALSE, (LPARAM)&rect);
    for (int i = 0; i < 4; i++)
        MoveWindow(hwndChild[i], 
            rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, TRUE);
    return 0;

TCM_ADJUSTRECT消息可以根据选项卡控件的窗口大小计算出显示区域的大小,wParam设置为FALSE,lParam参数指定为选项卡控件窗口大小的RECT结构,并在这个RECT结构中返回显示区域的大小;TCM_ADJUSTRECT消息也可以根据显示区域的大小计算选项卡控件的窗口大小,此时wParam参数应该设置为TRUE,lParam参数指定为选项卡控件显示区域大小的RECT结构,并在这个RECT结构中返回选项卡控件的大小。该消息仅用于选项卡位于顶部的选项卡控件。

完整代码参见Chapter8\TabControlDemo项目。

关于选项卡控件的消息,我们已经学习了TCM_INSERTITEM、TCM_GETCURSEL、TCM_ADJUSTRECT,以及TCN_SELCHANGE通知码等。其他常用的消息如表8.57所示。

表8.57

消息类型 含义
TCM_SETCURSEL 选择选项卡控件中的一个选项卡
TCM_SETITEM 设置选项卡控件中选项卡的信息
TCM_GETITEM 获取选项卡控件中选项卡的信息
TCM_SETITEMSIZE 在固定宽度(TCS_FIXEDWIDTH)或程序自绘(TCS_OWNERDRAWFIXED)的选项卡控件中设置选项卡的宽度和高度
TCM_SETMINTABWIDTH 设置选项卡控件中选项卡的最小宽度
TCM_SETIMAGELIST 将图像列表分配给选项卡控件
TCM_DELETEITEM 从选项卡控件中删除一个选项卡
TCM_DELETEALLITEMS 从选项卡控件中删除所有选项卡
TCM_GETITEMCOUNT 获取选项卡控件中的选项卡数

8.19 动画控件

动画控件是一个用于显示AVI(Audio-Video Interleaved,音频视频交错格式)剪辑的窗口,动画控件通常用于在漫长的操作期间指示系统活动,例如,Windows XP系统在查找文件的过程中会显示一个手电筒形状的动画。不过动画控件只能显示不包含音频的AVI剪辑,并且应该是未压缩的AVI文件或使用BI_RLE8编码压缩的AVI文件。动画控件的功能非常有限,如果需要多媒体播放和录制功能,可以使用MCIWnd控件。

动画控件常用的样式有:ACS_AUTOPLAY打开AVI剪辑后立即开始播放动画,ACS_CENTER将动画居中放在动画控件的窗口中。如果未指定ACS_CENTER样式,调用CreateWindowEx函数时可以不指定宽度参数和高度参数,动画控件会根据AVI剪辑中帧的尺寸设置控件的宽度和高度。动画控件的创建如下所示:

复制代码
hwndAnimate = CreateWindowEx(0, TEXT("SysAnimate32"), NULL,
    WS_CHILD | WS_VISIBLE | WS_BORDER | ACS_CENTER,
    10, 0, 330, 330, hwnd, (HMENU)IDC_ANIMATE, hInstance, NULL);

动画控件可以对AVI文件执行打开、播放、停止、关闭等操作,相关的消息如表8.58所示。

表8.58

消息类型 含义
ACM_OPEN 打开AVI剪辑并在动画控件中显示第一帧。wParam参数指定为AVI资源所属的模块句柄,如果设置为NULL,表示使用创建动画控件时指定的HINSTANCE值;如果lParam参数指定为AVI文件路径,则wParam参数也可以设置为NULL。lParam参数指定为AVI文件路径或AVI资源名称(MAKEINTRESOURCE宏),如果该参数设置为NULL,则系统将关闭动画控件中先前打开的AVI文件或从内存中删除AVI资源
ACM_PLAY 在动画控件中播放AVI剪辑。wParam参数指定为重播AVI剪辑的次数,指定为−1表示无限重播剪辑;LOWORD(lParam)指定从哪一帧开始播放,HIWORD(lParam)指定播放到哪一帧结束,指定为−1表示播放到AVI剪辑的最后一帧,例如MAKELPARAM(0, −1)
ACM_STOP 停止在动画控件中播放AVI剪辑。wParam和lParam参数都没有用到

例如下面的代码:

复制代码
case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case IDC_BTNOPEN:
        SendMessage(hwndAnimate, ACM_OPEN, NULL, (LPARAM)TEXT("Colck.avi"));
        break;
    case IDC_BTNPLAY:
        SendMessage(hwndAnimate, ACM_PLAY, -1, MAKELPARAM(0, -1));
        break;
    case IDC_BTNSTOP:
        SendMessage(hwndAnimate, ACM_STOP, NULL, NULL);
        break;
    case IDC_BTNCLOSE:
        SendMessage(hwndAnimate, ACM_OPEN, NULL, NULL);
        break;
    }
    return 0;

关于动画控件的示例程序参见Chapter8\AnimateDemo项目。

相关推荐
M78佐菲2 小时前
HTML学习笔记
linux·笔记·学习·tcp/ip·html
其实防守也摸鱼3 小时前
OpenClaw Windows 踩坑实战指南
运维·服务器·数据库·windows·github·copilot
我还可以再学点3 小时前
QUIC学习笔记
笔记·学习
点心的游戏开发世界3 小时前
GDScript 入门笔记:目录
开发语言·笔记·游戏引擎·godot
山岚的运维笔记3 小时前
mysql 专业笔记 -- 第 17 章:连接:连接三个具有相同名称 ID 的表
运维·数据库·笔记·后端·学习·mysql·dba
又見山3 小时前
用 Obsidian 写了两年的笔记,怎么备份?怎么同步到其他设备上
笔记·obsidian·obsidian插件·obsidian同步·obsidian教程
zihan5184 小时前
理论:构建“基于客观事实、可量化、闭环自给自足”的个人操作系统
笔记·职场和发展·学习方法
Z5998178414 小时前
c#软件开发学习笔记--WPF(MVVM框架)
笔记·学习·c#
hgfsjk4 小时前
跨境电商ERP系统软件哪个好?2026年选型指南
大数据·经验分享·笔记·其他