Win32 SDK 属性网格变得简单






4.97/5 (161投票s)
本文介绍如何创建一个非 MFC 的自定义 PropertyGrid 控件。
目录
- 引言
- 背景
- 使用 PropertyGrid
- 公共数据结构
- 消息和宏
- PropGrid_AddItem
- PropGrid_DeleteItem
- PropGrid_Enable
- PropGrid_GetCount
- PropGrid_GetCurSel
- PropGrid_GetHorizontalExtent
- PropGrid_GetItemData
- PropGrid_GetItemHeight
- PropGrid_GetItemRect
- PropGrid_GetSel
- PropGrid_ResetContent
- PropGrid_SetCurSel
- PropGrid_SetHorizontalExtent
- PropGrid_SetItemData
- PropGrid_SetItemHeight
- PropGrid_ExpandCatalogs
- PropGrid_ExpandAllCatalogs
- PropGrid_CollapseCatalogs
- PropGrid_CollapseAllCatalogs
- PropGrid_ShowToolTips
- PropGrid_ShowPropertyDescriptions
- PropGrid_SetFlatStyleChecks
- PropGrid_ItemInit
- Notifications
- 设计考量
- Win32/64 SDK 开发人员的技巧与窍门
- 最终评论
- 历史
引言
我曾编写过一个实用程序,需要处理与远程设备相关的众多参数。我使用了属性表样式的标签页,每个属性都有一个单独的字段,控件字段的数量不断增加,直到应用程序不堪重负。从那时起,我就成了 Datagrid
和 Propertygrid
风格 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 结构字段填充。
这是另一个显示文件对话框项的代码片段
//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 结构字段填充。
最后是一个显示可编辑组合框项的代码片段
//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()
创建了缓冲区。下图显示了由列表填充的下拉列表。
应用程序属性值可以动态更新,这是演示应用程序在 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_COMBO
和PIT_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_EDIT
、PIT_COMBO
、PIT_EDITCOMBO
、PIT_STATIC
和PIT_FOLDER
- 文本PIT_COLOR
-COLORREF
值PIT_FONT
- 指向PROPGRIDFONTITEM
结构的指针PIT_FILE
- 指向PROPGRIDFDITEM
结构的指针PIT_CHECK
-BOOL
值PIT_IP
-DWORD
值PIT_DATE
、PIT_TIME
和PIT_DATETIME
- 指向SYSTEMTIME
结构的指针
PROPGRIDFONTITEM
PROPGRIDFONTITEM
结构指定或接收类型为 PIT_FONT
的 Propertygrid
项的属性。
typedef struct tagPROPGRIDFONTITEM {
LOGFONT logFont;
COLORREF crFont;
} PROPGRIDFONTITEM, *LPPROPGRIDFONTITEM;
成员
logFont
: 逻辑字体结构crFont
: 文本颜色
PROPGRIDFDITEM
PROPGRIDFDITEM
结构指定或接收类型为 PIT_FILE
的 Propertygrid
项的属性。
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
上面的代码片段有什么问题?稍作测试就能很快发现错误。在调试器中运行应用程序时,我将桌面的显示属性更改为梅红色...
呃!那不是我想要的。让我们用正确的方式绘制它,让用户决定控件应该是什么样子。
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));
完美无瑕!
无边框控件
他们说这是不可能的……然而,那个日期时间选择器看起来几乎是平坦的,实际上它几乎是看不见的,但是怎么做到的?
各种编辑器主要被子类化以访问按键事件,但为什么不重写 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 进行路由。子类化一个 Combobox
或 IPedit
是另一回事。键盘消息会通过子控件进行路由,如果我们不子类化子控件,父控件的 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
类型项的PROPGRIDITEM
的lpCurValue
成员现在是指向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