65.9K
CodeProject 正在变化。 阅读更多。
Home

Win32 SDK 属性网格变得简单

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (161投票s)

2010年4月30日

CPOL

14分钟阅读

viewsIcon

274746

downloadIcon

16649

本文介绍如何创建一个非 MFC 的自定义 PropertyGrid 控件。

目录

引言

我曾编写过一个实用程序,需要处理与远程设备相关的众多参数。我使用了属性表样式的标签页,每个属性都有一个单独的字段,控件字段的数量不断增加,直到应用程序不堪重负。从那时起,我就成了 DatagridPropertygrid 风格 UI 的忠实粉丝。理想情况下,任何时候您都会有一个窗口显示数据,另一个窗口编辑数据,同时保持一个拥有大量控件并组织良好的丰富界面的假象。我想为我的 Win32 项目拥有这样一个界面,它易于使用,极其轻量级,而且看起来专业。

背景

在开始编写这个 Propertygrid 之前,我做了一些背景研究,看看其他人想出了什么样的解决方案。我注意到 Noel Ramathal 的一个有前途的属性 Listbox [^] 以及 Runming Yan 的另一个属性 Listbox [^],它似乎基于 Noel 的工作。我从这些示例开始,但努力使它具有与 Visual Studio Propertygrid 以及 Pelles C IDE 中使用的那个相似的外观和感觉。此外,我想将这个 Propertygrid 编写成一个基于消息的自定义 Win32/64 控件。

使用 PropertyGrid

首先,在项目中包含 Propertygrid 控件的头文件

#include "propertyGrid.h"

这个 Propertygrid 控件是一个基于消息的自定义控件,因此必须在使用前进行初始化。一种处理方法是在应用程序的 WinMain() 方法中调用初始化程序,紧跟在调用 InitCommonControlsEx() 之后。

int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
           LPSTR lpszCmdLine, int nCmdShow)
{
    INITCOMMONCONTROLSEX icc;
    WNDCLASSEX wcx;

    ghInstance = hInstance;

    /* Initialize common controls. Also needed for MANIFEST's */

    icc.dwSize = sizeof(icc);
    icc.dwICC = ICC_WIN95_CLASSES/*|ICC_COOL_CLASSES|ICC_DATE_CLASSES|
                   ICC_PAGESCROLLER_CLASS|ICC_USEREX_CLASSES*/;
    InitCommonControlsEx(&icc);

    InitPropertyGrid(hInstance);

然而,为了简化起见,我将这个步骤合并到控件的伪构造函数中。它只调用一次,即在第一次实例化一个新的 Propertygrid 控件时。

HWND New_PropertyGrid(HWND hParent, DWORD dwID)
{
    static ATOM aPropertyGrid = 0;
    static HWND hPropertyGrid;

    HINSTANCE hinst = (HINSTANCE)GetWindowLongPtr(hParent, GWLP_HINSTANCE);

    //Only need to register the property grid once
    if (!aPropertyGrid)
        aPropertyGrid = InitPropertyGrid(hinst);

    hPropertyGrid = CreateWindowEx(0, g_szClassName, _T(""),
                        WS_CHILD, 0, 0, 0, 0, hParent, (HMENU)dwID, hinst, NULL);

    return hPropertyGrid;
}

接下来,声明一个 Propertygrid 项,然后对其进行初始化。一个项必须属于一个组或目录,由 lpszCatalog 参数标识。下面是一个演示如何将属性加载到网格中的代码片段。

BOOL Main_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam)
{
    HWND hPropGrid = GetDlgItem(hwnd,IDC_PG);
    PROPGRIDITEM Item;

    //Initialize Item in order to prevent unassigned pointers
    PropGrid_ItemInit(Item);

    Item.lpszCatalog = _T("Edit, Static, and Combos"); //Static text
    Item.lpszPropName = _T("Edit Field"); //Static text
    Item.lpCurValue = (LPARAM) gDemoData.strProp1; //Depends on ItemType
    Item.lpszPropDesc = _T("This field is editable"); //Static text
    Item.iItemType = PIT_EDIT;
    PropGrid_AddItem(hPropGrid, &Item);

    //
    //Add other items
    //

    PropGrid_ShowToolTips(hPropGrid,TRUE); //Show Tool Tips (Default = no tool tips)
    PropGrid_ExpandAllCatalogs(hPropGrid); //Load all properties in display

    return TRUE;
}

下图标识了 Propertygrid 的各个字段,这些字段由这些 PROPGRIDITEM 结构字段填充。

Demo screenshot

这是另一个显示文件对话框项的代码片段

//Declare and initialize a prop grid file dialog item
PROPGRIDFDITEM ItemFd = {0};
ItemFd.lpszDlgTitle = _T("Choose File"); //Static text
ItemFd.lpszFilePath = gDemoData.strProp8; //Text
// Standard file dialog filter string array (a double null terminated string)
ItemFd.lpszFilter = _T("Text files (*.txt)\0*.txt\0All Files (*.*)\0*.*\0");
ItemFd.lpszDefExt = _T("txt"); //Static text

//Prop grid item current value takes a pointer to a  prop grid file dialog item struct
Item.lpszPropName = _T("Choose file"); //Static text
Item.lpCurValue = (LPARAM) &ItemFd;
Item.iItemType = PIT_FILE;

PropGrid_AddItem(hPropGrid, &Item);

除了声明一个 Propertygrid 项并对其进行初始化之外,还需要声明和初始化一个 PROPGRIDFDITEM。下图标识了文件对话框弹出窗口的各个字段,这些字段由这些 PROPGRIDFDITEM 结构字段填充。

Demo screenshot

最后是一个显示可编辑组合框项的代码片段

//Initialize Demo Data
_tmemset(gDemoData.strProp3,_T('\0'),NELEMS(gDemoData.strProp3));

// Combo choices string array (a double null terminated string)
TCHAR szzChoices[] = _T("Robert\0Wally\0Mike\0Vickie\0Leah\0Arthur\0");
gDemoData.dwChoicesCount = NELEMS(szzChoices);
gDemoData.szzChoices = (LPTSTR) malloc(gDemoData.dwChoicesCount * sizeof(TCHAR));
_tmemmove(gDemoData.szzChoices, szzChoices, gDemoData.dwChoicesCount);

//
// Skip some stuff
//

Item.lpszPropName = _T("Editable Combo Field");
Item.lpCurValue = (LPARAM) gDemoData.strProp3;
Item.lpszzCmbItems = gDemoData.szzChoices;
Item.lpszPropDesc = _T("Press F4 to view drop down.");
Item.iItemType = PIT_EDITCOMBO;
PropGrid_AddItem(hPropGrid, &Item);

除了声明一个 Propertygrid 项并对其进行初始化之外,还需要填充 lpszzCmbItems 参数。在演示中,我想向此列表添加项,因此我通过 malloc() 创建了缓冲区。下图显示了由列表填充的下拉列表。

Demo screenshot

应用程序属性值可以动态更新,这是演示应用程序在 WM_NOTIFY 处理程序中使用的技术。

static BOOL Main_OnNotify(HWND hwnd, INT id, LPNMHDR pnm)
{
    if(IDC_PG == id)
    {
        LPNMPROPGRID lpnmp = (LPNMPROPGRID)pnm;
        switch(lpnmp->iIndex)
        {
            case 0:
                _stprintf(gDemoData.strProp1, NELEMS(gDemoData.strProp1),
#ifdef _UNICODE
                    _T("%ls"),
#else
                    _T("%s"),
#endif
                    (LPTSTR)(PropGrid_GetItemData
			(pnm->hwndFrom,lpnmp->iIndex)->lpCurValue));
                break;

                //
                // Other cases follow
                //

每次项值更改时,Propertygrid 都会向控件的父窗口发送通知。如果不需要动态更新,则忽略通知,并在需要时请求数据。

这些示例展示了控件如何在 Win32 项目中简单实现的一些方法。为了在有用的上下文中演示该类,我组织了一个演示,其中包含支持的每种项类型的代码。

以下是 Propertygrid 控件类的编程参考。

公共数据结构

PROPGRIDITEM

PROPGRIDITEM 结构指定或接收 Propertygrid 项的属性。

typedef struct tagPROPGRIDITEM {
    LPTSTR lpszCatalog;
    LPTSTR lpszPropName;
    LPTSTR lpszzCmbItems;
    LPTSTR lpszPropDesc;
    LPARAM lpCurValue;
    LPVOID lpUserData;
    INT    iItemType;
} PROPGRIDITEM, *LPPROPGRIDITEM;

成员

  • lpszCatalog 目录(组)名称
  • lpszPropName 属性(项)名称
  • lpszzCmbItems 一个双 null 终止的字符串列表(组合项)。此字段仅对 PIT_COMBOPIT_EDITCOMBO 类型的项有效。
  • lpszPropDesc 属性(项)描述
  • iItemType 属性(项)类型标识符。该值可以是以下之一:
    • PIT_EDIT - 属性项类型:编辑
    • PIT_COMBO - 属性项类型:下拉列表
    • PIT_EDITCOMBO - 属性项类型:下拉列表(可编辑)
    • PIT_CHECKCOMBO - 属性项类型:下拉列表(已勾选)
    • PIT_STATIC - 属性项类型:不可编辑文本
    • PIT_COLOR - 属性项类型:颜色
    • PIT_FONT - 属性项类型:字体
    • PIT_FILE - 属性项类型:文件选择对话框
    • PIT_FOLDER - 属性项类型:文件夹选择对话框
    • PIT_CHECK - 属性项类型:布尔值
    • PIT_IP - 属性项类型:IP 地址
    • PIT_DATE - 属性项类型:日期
    • PIT_TIME - 属性项类型:时间
    • PIT_DATETIME - 属性项类型:日期和时间
    • PIT_CATALOG - 属性项类型:目录
  • lpCurValue 属性(项)值。数据类型取决于项类型,如下所示:
    • PIT_EDITPIT_COMBOPIT_EDITCOMBOPIT_STATICPIT_FOLDER - 文本
    • PIT_COLOR - COLORREF
    • PIT_FONT - 指向 PROPGRIDFONTITEM 结构的指针
    • PIT_FILE - 指向 PROPGRIDFDITEM 结构的指针
    • PIT_CHECK - BOOL
    • PIT_IP - DWORD
    • PIT_DATEPIT_TIMEPIT_DATETIME - 指向 SYSTEMTIME 结构的指针

PROPGRIDFONTITEM

PROPGRIDFONTITEM 结构指定或接收类型为 PIT_FONTPropertygrid 项的属性。

typedef struct tagPROPGRIDFONTITEM {
    LOGFONT logFont;
    COLORREF crFont;
} PROPGRIDFONTITEM, *LPPROPGRIDFONTITEM;

成员

  • logFont 逻辑字体结构
  • crFont 文本颜色

PROPGRIDFDITEM

PROPGRIDFDITEM 结构指定或接收类型为 PIT_FILEPropertygrid 项的属性。

typedef struct tagPROPGRIDFDITEM {
    LPTSTR lpszDlgTitle;
    LPTSTR lpszFilePath;
    LPTSTR lpszFilter;
    LPTSTR lpszDefExt;
} PROPGRIDFDITEM, *LPPROPGRIDFDITEM;

成员

  • lpszDlgTitle 字体对话框标题
  • lpszFilePath 初始路径
  • lpszFilter 一个双 null 终止的字符串列表(过滤器项)
  • lpszDefExt 默认扩展名

消息和宏

使用 Windows 消息配置控件以满足您的需求。为了简化此过程并记录消息,我为每条消息创建了宏。如果您更喜欢显式调用 SendMessage()PostMessage(),请参阅头文件中的宏定义以了解用法。

PropGrid_AddItem

将项添加到 Propertygrid。项将被附加到其各自的目录。

INT PropGrid_AddItem(
     HWND hwndCtl
     LPPROPGRIDITEM lpItem
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.
lpItem
     Pointer to a Propertygrid item.

Return Values
The zero-based index of the item in the grid. If an error occurs,
 the return value is LB_ERR. If there is insufficient space to store
 the new string, the return value is LB_ERRSPACE.*/

PropGrid_DeleteItem

删除 Propertygrid 中指定位置的项。

INT PropGrid_DeleteItem(
     HWND hwndCtl
     INT index
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.
index
     The zero-based index of the item to delete.

Return Values
A count of the items remaining in the grid. The return value is
 LB_ERR if the index parameter specifies an index greater than the
 number of items in the list.*/

PropGrid_Enable

启用或禁用 Propertygrid 控件。

VOID PropGrid_Enable(
     HWND hwndCtl
     BOOL fEnable
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.
fEnable
     TRUE to enable the control, or FALSE to disable it.

Return Values
No return value.*/

PropGrid_GetCount

获取 Propertygrid 中的项数。

INT PropGrid_GetCount(
     HWND hwndCtl
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.

Return Values
The number of items.*/

PropGrid_GetCurSel

获取 Propertygrid 中当前选定项的索引。

INT PropGrid_GetCurSel(
     HWND hwndCtl
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.

Return Values
The zero-based index of the selected item. If there is no selection,
 the return value is LB_ERR.*/

PropGrid_GetHorizontalExtent

获取 Propertygrid 可水平滚动的宽度(可滚动宽度)。

INT PropGrid_GetHorizontalExtent(
     HWND hwndCtl
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.

Return Values
The scrollable width, in pixels, of the Propertygrid.*/

PropGrid_GetItemData

获取与指定 Propertygrid 项关联的 PROPGRIDITEM

LPPROPGRIDITEM PropGrid_GetItemData(
     HWND hwndCtl
     INT index
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.
index
     The zero-based index of the item.

Return Values
A pointer to a PROPGRIDITEM object.*/

PropGrid_GetItemHeight

检索 Propertygrid 中所有项的高度。

INT PropGrid_GetItemHeight(
     HWND hwndCtl
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.

Return Values
The height, in pixels, of the items, or LB_ERR if an error occurs.*/

PropGrid_GetItemRect

获取 Propertygrid 中当前显示的 Propertygrid 项的边界矩形的尺寸。

INT PropGrid_GetItemRect(
     HWND hwndCtl
     INT index
     RECT* lprc
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.
index
     The zero-based index of the item in the Propertygrid.
lprc
     A pointer to a RECT structure that receives the client
      coordinates for the item in the Propertygrid.

Return Values
If an error occurs, the return value is LB_ERR.*/

PropGrid_GetSel

获取项的选中状态。

INT PropGrid_GetSel(
     HWND hwndCtl
     INT index
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.
index
     The zero-based index of the item.

Return Values
If the item is selected, the return value is greater than zero;
 otherwise, it is zero. If an error occurs, the return value is LB_ERR.*/

PropGrid_ResetContent

Propertygrid 中移除所有项。

INT PropGrid_ResetContent(
     HWND hwndCtl
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.

Return Values
The return value is not meaningful.*/

PropGrid_SetCurSel

设置 Propertygrid 中当前选定的项。

INT PropGrid_SetCurSel(
     HWND hwndCtl
     INT index
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.
index
     The zero-based index of the item to select, or -1 to clear the selection.

Return Values
If an error occurs, the return value is LB_ERR. If the index
 parameter is -1, the return value is LB_ERR even though no error occurred.*/

PropGrid_SetHorizontalExtent

设置 Propertygrid 可水平滚动的宽度(可滚动宽度)。如果 Propertygrid 的宽度小于此值,则水平滚动条将水平滚动 Propertygrid 中的项。如果 Propertygrid 的宽度等于或大于此值,则水平滚动条将隐藏。

VOID PropGrid_SetHorizontalExtent(
     HWND hwndCtl
     INT cxExtent
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.
cxExtent
     The number of pixels by which the grid can be scrolled.

Return Values
No return value.*/

PropGrid_SetItemData

设置与指定 Propertygrid 项关联的 PROPGRIDITEM

INT PropGrid_SetItemData(
     HWND hwndCtl
     INT index
     LPPROPGRIDITEM data
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.
index
     The zero-based index of the item.
data
     The item data to set.

Return Values
If an error occurs, the return value is LB_ERR.*/

PropGrid_SetItemHeight

设置 Propertygrid 中所有项的高度。

INT PropGrid_SetItemHeight(
     HWND hwndCtl
     INT cy
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.
cy
     The height of the items, in pixels.

Return Values
If the height is invalid, the return value is LB_ERR.*/

PropGrid_ExpandCatalogs

展开 Propertygrid 中某些指定的目录。

VOID PropGrid_ExpandCatalogs(
     HWND hwndCtl
     LPTSTR lpszzCatalogs
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.
lpszzCatalogs
     The list of catalog names each terminated by a null (\0).

Return Values
No return value.*/

PropGrid_ExpandAllCatalogs

展开 Propertygrid 中的所有目录。

VOID PropGrid_ExpandAllCatalogs(
     HWND hwndCtl
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.

Return Values
No return value.*/

PropGrid_CollapseCatalogs

折叠 Propertygrid 中某些指定的目录。

VOID PropGrid_CollapseCatalogs(
     HWND hwndCtl
     LPTSTR lpszzCatalogs
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.
lpszzCatalogs
     The list of catalog names each terminated by a null (\0).

Return Values
No return value.*/

PropGrid_CollapseAllCatalogs

折叠 Propertygrid 中的所有目录。

VOID PropGrid_CollapseAllCatalogs(
     HWND hwndCtl
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.

Return Values
No return value.*/

PropGrid_ShowToolTips

Propertygrid 中显示或隐藏工具提示。

VOID PropGrid_ShowToolTips(
     HWND hwndCtl
     BOOL fShow
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.
fShow
     TRUE for tooltips; FALSE do not show tooltips.

Return Values
No return value.*/

PropGrid_ShowPropertyDescriptions

Propertygrid 中显示或隐藏属性描述窗格。

VOID PropGrid_ShowPropertyDescriptions(
     HWND hwndCtl
     BOOL fShow
     );
/*Parameters
hwndCtl
     Handle to the Propertygrid control.
fShow
    TRUE for descriptions; FALSE do not show description pane

Return Values
No return value.*/

  PropGrid_SetFlatStyleChecks

设置复选框的外观。

VOID PropGrid_SetFlatStyleChecks(
   HWND hwndCtl
   BOOL fFlat
     );
/*Parameters
hwndCtl
   Handle to the Propertygrid control.
fFlat
   TRUE for flat checkboxes, or FALSE for standard checkboxes.

Return Values
No return value.*/

PropGrid_ItemInit

初始化一个项 struct

VOID PropGrid_ItemInit(
     PROPGRIDITEM pgi
     );
/*Parameters
pgi
     The newly declared PROPGRIDITEM struct.

Return Values
No return value.*/

Notifications

Propertygrid 控件通过 WM_NOTIFY 提供通知。这些通知消息的 lParam 参数指向一个 NMPROPGRID 结构。

NMPROPGRID

NMPROPGRID 结构包含有关 Propertygrid 控件通知消息的信息。

typedef struct tagNMPROPGRID {
     NMHDR hdr;
     INT iIndex;
} NMPROPGRID, *LPNMPROPGRID;

/*Members
hdr
     Specifies an NMHDR structure. The code member of the NMHDR structure contains
     the following notification code that identifies the message being sent:
          PGN_PROPERTYCHANGE.
iIndex
     Index of a Propertygrid item.

Remarks
     The address of this structure is specified as the lParam parameter of the
     WM_NOTIFY message for all Propertygrid control notification messages.*/

PGN_PROPERTYCHANGE

PGN_PROPERTYCHANGE 通知消息通知 Propertygrid 控件的父窗口项的数据已更改。此通知消息以 WM_NOTIFY 消息的形式发送。

PGN_PROPERTYCHANGE
pnm = (NMPROPGRID *) lParam;

/*Parameters
pnm
     Pointer to an NMPROPGRID structure that specifies
     an item index of the Propertygrid item that has changed.

Return Values
No return value.*/

设计考虑因素

在学年期间的一次休息期间,我的两个孩子决定花一天时间和爸爸一起去工作,看看他办公室里所有酷的东西。我同意在不同日子分别带他们去,并在那些日子里安排了我认为会引起每个男孩兴趣的工作相关活动。午餐时间,我带他们去了一个有趣展览,位于工作地点附近一个商务园区的公司举办的,工艺博物馆 [^]。

这个展览让我印象深刻的一点是细节的关注以及业余爱好者们在微型复制品上投入的大量耐心工作,实际上,大多数机械引擎都在运转。即使是匆匆一瞥,也能看出项目中所投入的技巧和工艺。

我在那里学到的一件事是,要让机械引擎运行良好(或者根本运行起来),零件必须以精确的公差加工,并且公差会累积。一些最可靠的发动机设计往往很简单但构思精良。

理想情况下,使用 Propertygrid,您只需处理 2 个窗口,一个显示数据,一个编辑数据。最初,我使用链表存储指向所有项数据的指针,Listbox 显示目录和可见数据。后来,在我开始充实内容时,我意识到我不断地向内部数据结构添加模仿 Listbox 功能(如索引)的特性。这时,我用一个第二个、最小化的窗口 Listbox 类实例替换了我的列表,并大大简化了代码。我意识到我可以透明地支持更多现有的 Listbox 消息子集,从而为用户提供更大的灵活性,同时减少开发工作。

这个 Propertygrid 使用七种不同的窗口控件来编辑属性。我从 Listview 控件那里得到启发,它只在编辑项时创建一个 Editbox,并在编辑完成后销毁它。因此,Propertygrid 大部分时间只有一个编辑器实例。例外情况是日期和时间字段有两个编辑器,或者静态和复选框字段不需要任何编辑器。这在管理编辑器方面减少了一些开销,并使代码的实现更加简洁优雅。

除了 Listbox 和编辑器之外,还有两个可选组件 - 一个 static 描述字段和工具提示。如果用户配置 Propertygrid 来显示它们,则会创建这些组件。

Win32/64 SDK 开发人员的技巧与窍门

这里有很多我不会详细介绍的内容,我在之前的文章中已经详细介绍过。对于那些对我的自定义控件整体结构方法以及窗口子类化的基础感兴趣的人,请查看 Win32 SDK Data Grid View Made Easy [^]。我想在这里分享的技巧,除了一些例外,都与绘制控件的方面有关。

属主绘制技巧

网格的外观和感觉大部分是通过属主绘制 Listbox 来实现的。我看到开发人员在属主绘制某些内容时犯的一个错误是没有利用标准对象(画笔和画刷)和系统颜色。以下是一个关于如何绘制控件的示例。

VOID Grid_OnDrawItem(HWND hwnd, const DRAWITEMSTRUCT * lpDIS)
{
        //
        // Skip stuff
        //

        SetBkMode(lpDIS->hDC, TRANSPARENT);

        FillRect(lpDIS->hDC, &rectPart1,
			CreateSolidBrush(nIndex ==
				(UINT)ListBox_GetCurSel(lpDIS->hwndItem) ?
			RGB(0,0,255) : RGB(255,255,255)));//Blue and White

        //Write the property name
        oldFont = SelectObject(lpDIS->hDC, Font_SetBold(lpDIS->hwndItem, FALSE));
        SetTextColor(lpDIS->hDC,
            nIndex == (UINT)ListBox_GetCurSel(lpDIS->hwndItem) ?
            RGB(255,255,255) : RGB(0,0,0));//White and Black

        DrawText(lpDIS->hDC, pItem->lpszPropName, _tcslen(pItem->lpszPropName),
            MAKE_PRECT(rectPart1.left + 3, rectPart1.top + 3, rectPart1.right - 3,
            rectPart1.bottom + 3), DT_NOCLIP | DT_LEFT | DT_SINGLELINE);

        DrawBorder(lpDIS->hDC, &rectPart1,
		BF_TOPRIGHT, RGB(192,192,192));//Shade of Grey

上面的代码片段有什么问题?稍作测试就能很快发现错误。在调试器中运行应用程序时,我将桌面的显示属性更改为梅红色...

Demo screenshot

呃!那不是我想要的。让我们用正确的方式绘制它,让用户决定控件应该是什么样子。

VOID Grid_OnDrawItem(HWND hwnd, const DRAWITEMSTRUCT * lpDIS)
{
        //
        // Skip stuff
        //

        SetBkMode(lpDIS->hDC, TRANSPARENT);

        FillRect(lpDIS->hDC, &rectPart1,
            GetSysColorBrush(nIndex == (UINT)ListBox_GetCurSel(lpDIS->hwndItem) ?
            COLOR_HIGHLIGHT : COLOR_WINDOW));

        //Write the property name
        oldFont = SelectObject(lpDIS->hDC, Font_SetBold(lpDIS->hwndItem, FALSE));
        SetTextColor(lpDIS->hDC,
            GetSysColor(nIndex == (UINT)ListBox_GetCurSel(lpDIS->hwndItem) ?
            COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT));

        DrawText(lpDIS->hDC, pItem->lpszPropName, _tcslen(pItem->lpszPropName),
            MAKE_PRECT(rectPart1.left + 3, rectPart1.top + 3, rectPart1.right - 3,
            rectPart1.bottom + 3), DT_NOCLIP | DT_LEFT | DT_SINGLELINE);

        DrawBorder(lpDIS->hDC, &rectPart1, BF_TOPRIGHT,
            GetSysColor(COLOR_BTNFACE));

Demo screenshot

完美无瑕!

无边框控件

他们说这是不可能的……然而,那个日期时间选择器看起来几乎是平坦的,实际上它几乎是看不见的,但是怎么做到的?

各种编辑器主要被子类化以访问按键事件,但为什么不重写 WM_PAINT 呢?这正是我所做的,除了 Combobox 之外,一个方法适用于所有情况。

BOOL Editor_OnPaint(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    HDC hdc = GetWindowDC(hwnd);
    RECT rect;

    // First let the system do its thing
    CallWindowProc((WNDPROC)GetProp(hwnd, TEXT("Wprc")), hwnd, msg, wParam, lParam);

    // Next obliterate the border
    GetWindowRect(hwnd, &rect);
    MapWindowPoints(HWND_DESKTOP, hwnd, (LPPOINT) &rect.left, 2);

    rect.top += 2;
    rect.left += 2;
    DrawBorder(hdc, &rect, BF_RECT, GetSysColor(COLOR_WINDOW));

    rect.top += 1;
    rect.left += 1;
    rect.bottom += 1;
    rect.right += 1;
    DrawBorder(hdc, &rect, BF_RECT, GetSysColor(COLOR_WINDOW));

    ReleaseDC(hwnd, hdc);
    return TRUE;
}

这里我从 DatePicker_Proc() 调用该方法

static LRESULT CALLBACK DatePicker_Proc
	(HWND hDate, UINT msg, WPARAM wParam, LPARAM lParam)
{
    HWND hGParent = GetParent(GetParent(hDate));

    // Note: Instance data is attached to datepicker's grandparent
    Control_GetInstanceData(hGParent, &g_lpInst);

    if (WM_DESTROY == msg)  // Unsubclass the control
    {
        SetWindowLongPtr(hDate, GWLP_WNDPROC, (DWORD)GetProp(hDate, TEXT("Wprc")));
        RemoveProp(hDate, TEXT("Wprc"));
        return 0;
    }
    else if (WM_PAINT == msg)   // Obliterate border
    {
        return Editor_OnPaint(hDate, msg, wParam, lParam);
    }

    //
    // Process other messages
    //

如我所说,Combobox 有点不同,但应用的原理相同。下面是如何为 Combobox 完成的。

void ComboBox_OnPaint(HWND hwnd)
{
    // First let the system do its thing
    FORWARD_WM_PAINT(hwnd, DefProc);

    // Next obliterate the border
    HDC hdc = GetWindowDC(hwnd);
    RECT rect;

    GetClientRect(hwnd, &rect);

    DrawBorder(hdc, &rect, BF_TOPLEFT, GetSysColor(COLOR_WINDOW));

    rect.top += 1;
    rect.left += 1;
    DrawBorder(hdc, &rect, BF_TOPLEFT, GetSysColor(COLOR_WINDOW));

    ReleaseDC(hwnd, hdc);
}

简易复选框

复选框根本不是控件,而是使用 DrawFrameControl() 绘制的,该函数绘制一个经典风格的复选框的位图表示,如下所示:标准复选框,但通过另外三行代码,我将其变成了如下所示的漂亮平坦复选框:平坦复选框。下面是实现方法。

DrawFrameControl(lpDIS->hDC, &rect3, DFC_BUTTON, DFCS_BUTTONCHECK |
                (_tcsicmp(pItem->lpszCurValue, CHECKED) == 0 ? DFCS_CHECKED : 0));

//Make border thin
FrameRect(lpDIS->hDC, &rect3, GetSysColorBrush(COLOR_BTNFACE));
InflateRect(&rect3, -1, -1);
FrameRect(lpDIS->hDC, &rect3, GetSysColorBrush(COLOR_WINDOW));

目录切换

不需要位图或资源,只需像这样绘制小框(rect2 定义了一个边长为奇数像素的正方形)。

FillRect(lpDIS->hDC, &rect2, GetSysColorBrush(COLOR_WINDOW));
FrameRect(lpDIS->hDC, &rect2, GetStockObject(BLACK_BRUSH));

POINT ptCtr;
ptCtr.x = (LONG) (rect2.left + (WIDTH(rect2) * 0.5));
ptCtr.y = (LONG) (rect2.top + (HEIGHT(rect2) * 0.5));
InflateRect(&rect2, -2, -2);

DrawLine(lpDIS->hDC, rect2.left, ptCtr.y, rect2.right, ptCtr.y); //Make a -
if (pItem->fCollapsed) //Convert to +
    DrawLine(lpDIS->hDC, ptCtr.x, rect2.top, ptCtr.x, rect2.bottom);

子类化复合控件

子类化一个编辑控件相当直接。它只包含一个窗口,并且它的消息都通过同一个 proc 进行路由。子类化一个 ComboboxIPedit 是另一回事。键盘消息会通过子控件进行路由,如果我们不子类化子控件,父控件的 proc 将永远不会看到它们。如果能有一种简单的方法来子类化这些子控件就好了……嗯,有办法。当控件创建时,它会立即向其父窗口发送一个 WM_COMMAND 消息,通常带有 EN_SETFOCUS 通知代码(如果子控件是编辑控件)或 LBN_SETFOCUS(对于 Listbox)。我们不关心通知,我们想要第一次获取子控件的句柄,以便对其进行子类化。

static LRESULT CALLBACK IpEdit_Proc
	(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    //
    // Skip a bunch of stuff
    //

    else    //Handle messages (events) in the parent ipedit control
    {
        if (WM_PAINT == msg)    // Obliterate border
        {
            return Editor_OnPaint(hwnd, msg, wParam, lParam);
        }
        else if (WM_COMMAND == msg)
        {
            // Each of the control's edit fields posts notifications on showing
            //  the first time they do so we'll grab and subclass them.
            HWND hwndCtl = GET_WM_COMMAND_HWND(wParam, lParam);
            {
                WNDPROC lpfn = (WNDPROC)GetProp(hwndCtl, TEXT("Wprc"));
                if (NULL == lpfn)
                {
                    //Subclass child and save the OldProc
                    SetProp(hwndCtl, TEXT("Wprc"),
			(HANDLE)GetWindowLongPtr(hwndCtl, GWLP_WNDPROC));
                    SubclassWindow(hwndCtl, IpEdit_Proc);
                }
            }
        }
    }
    return CallWindowProc((WNDPROC)GetProp(hwnd, TEXT("Wprc")),
					hwnd, msg, wParam, lParam);
}

我将编辑控件子类化为与其父控件相同的 proc。这样做时,我需要注意区分子控件和父控件,以便消息能够通过该 proc 进行路由。

鼠标滚轮错误

事实证明,使用 LBS_OWNERDRAWVARIABLE 样式的 Listbox 控件无法正确处理鼠标滚轮。滚动效果非常跳跃,以至于如果您想使用该样式,建议拦截 WM_MOUSEWHEEL 以禁用它或编写自己的处理程序。

检测列表框的开始和结束滚动事件

Listbox 没有滚动条组件,而是会在控件的非客户区绘制一个滚动条,很可能是使用 DrawFrameControl()。因此,无法子类化滚动条以检测鼠标事件。以下代码片段展示了一种解决此问题并检测开始和结束滚动的方法。

static LRESULT CALLBACK ListBox_Proc(HWND hList, UINT msg,
        WPARAM wParam, LPARAM lParam)
{
    HWND hParent = GetParent(hList);

    // Note: Instance data is attached to listbox's parent
    Control_GetInstanceData(hParent, &g_lpInst);

    switch (msg)
    {
        //
        // Skip stuff
        //

        case WM_MBUTTONDOWN:
        case WM_NCLBUTTONDOWN:
            //The listbox doesn't have a scrollbar component, it draws a scroll
            // bar in the non-client area of the control.  A mouse click in the
            // non-client area then, equals clicking on a scroll bar.  A click
            // on the middle mouse button equals pan, we'll handle that as if
            // it were a scroll event.
            ListBox_OnBeginScroll(hList);
            g_lpInst->fScrolling = TRUE;
            break;

        case WM_SETCURSOR:
            //Whenever the mouse leaves the non-client area of a listbox, it
            // fires a WM_SETCURSOR message.  The same happens when the middle
            // mouse button is released.  We can use this behavior to detect the
            // completion of a scrolling operation.
            if (g_lpInst->fScrolling)
            {
                ListBox_OnEndScroll(hList);
                g_lpInst->fScrolling = FALSE;
            }
            break;

            //
            // more stuff
            //

最终评论

我用 Doxygen [^] 注释记录了这些源代码,以便那些觉得它有用或有价值的人能够受益。您的反馈将不胜感激。

历史

  • 2010 年 4 月 30 日:版本 1.0.0.0
  • 2010 年 8 月 3 日:版本 1.1.0.0 - 修复了 PropGrid_GetItemData() 的错误,其中 PIT_FILE 类型项返回空字符串而不是文件路径
  • 2010 年 10 月 28 日:版本 1.2.0.0 - 多项错误修复和改进(在源代码中注释)
  • 2010 年 12 月 9 日:版本 1.3.0.0 - 改进了与控件的制表符行为,根据文档,PropGrid_GetItemData() 返回的 PIT_FILE 类型项的 PROPGRIDITEMlpCurValue 成员现在是指向 PROPGRIDFDITEM 结构的指针,而不是仅仅是文件路径字符串。
  • 2013 年 11 月 11 日:版本 1.4.0.0 - 修改了编辑器的绘制方式,以在 XP 和 Win7 之间保持一致的外观和感觉。
  • 2013 年 11 月 16 日:版本 1.5.0.0 - 修复了 X64 操作下的错误条件。
  • 2013 年 11 月 27 日:版本 1.6.0.0 - 修复了与字段编辑期间网格大小调整有关的数据持久性问题。修复了日期字段中的错误。
  • 2014 年 9 月 14 日:版本 1.7.0.0 - 一些小错误修复。
  • 2016 年 2 月 27 日:版本 1.8.0.0 - 添加了 PG_FLATCHECKS 消息和 PropGrid_SetFlatStyleChecks() 宏。
  • 2016 年 3 月 30 日:版本 1.9.0.0 - 修复了与 IP 地址字段相关的一些错误。添加了 MSVC 项目下载。
  • 2018 年 11 月 18 日:版本 2.0.0.0 - 修复了与 PropGrid_ResetContent() 相关的崩溃问题。修复了 WM_NOTIFY 消息的触发,将其限制为每个字段编辑一次。添加了一些请求的功能 - 工厂创建的窗口现在默认可见,为下拉列表添加了滚动条,键盘快捷键等...
  • 2018 年 11 月 21 日:版本 2.1.0.0 - 结合了另外两项错误修复,并支持项结构中的用户数据成员。感谢 Jakob 贡献了这些修复/功能。
  • 2021 年 5 月 4 日:版本 2.2.0.0 - 重写了与组合框相关的代码部分,添加了勾选组合框。改进了与组合框相关的键盘和制表符行为,添加了错误修复建议,并清理了不必要的注释。
  • 2021 年 5 月 9 日:版本 2.3.0.0 - 修复了在 Windows 10 中鼠标滚轮滚动时阻止编辑器窗口隐藏的错误。
  • 2021 年 11 月 23 日:版本 2.4.0.0 - 在 ComboList_OnRButtonDown 中添加了丢失的 GetProp
© . All rights reserved.