Win32 SDK PropertyGrid Made Easy






4.97/5 (161 votes)
This article describes the creation of a non-MFC custom PropertyGrid control.
Contents
- Introduction
- Background
- Using the Propertygrid
- Public Data Structures
- Messages and Macros
- 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
- Design considerations
- Tips and Tricks for the Win32/64 SDK Developer
- Final Comments
- History
Introduction
I once wrote a utility application that needed to handle a multitude of parameters associated with a remote device. I used property sheet style tab pages with individual fields for each property and the number of control fields multiplied until the application teetered under the weight of all those windows. Since then, I have become a big fan of Datagrid
and Propertygrid
style UIs. Ideally, at any one time you have one window displaying data and another to edit the data, yet maintain the illusion of a rich interface with many controls well organized. I wanted to have such an interface for my Win32 projects, something easy to use, ultra light weight, and professional looking.
Background
Before starting to write this Propertygrid
, I did a little background research to see what kinds of solutions others had come up with. I noticed a promising property Listbox
by Noel Ramathal [^] as well as another by Runming Yan [^] that appears to be based upon Noel's work. I started from these examples but strove for something that had a similar look and feel to the Visual Studio Propertygrid
as well as the one used in the Pelles C IDE. In addition to this, I wanted to write this Propertygrid
as a message based custom Win32/64 control.
Using the Propertygrid
To begin, include the Propertygrid
control's header file in the project:
#include "propertyGrid.h"
This Propertygrid
control is a message based, custom control, and as such must be initialized before use. One way to handle this is to call the initializer in the WinMain()
method of the application just after the call to 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);
To make things simple though, I combined this step in the control's pseudo constructor. It is called only once, the first time a new Propertygrid
control is instantiated.
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;
}
Next declare a Propertygrid
item and then initialize it. An item must be part of a group or catalog as identified by the lpszCatalog parameter. Here is a snippet that demonstrates how properties are loaded into the grid.
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;
}
The following figure identifies the various fields of the Propertygrid
that are populated by these PROPGRIDITEM struct fields.
Here is another snippet showing a file dialog item:
//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);
In addition to declaring a Propertygrid
item and then initializing it. It is also necessary to declare and initialize a PROPGRIDFDITEM. The following figure identifies the various fields of the file dialog popup that are populated by these PROPGRIDFDITEM struct fields.
And finally a snippet showing an editable combo item:
//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);
In addition to declaring a Propertygrid
item and then initializing it. It is also necessary to populate the lpszzCmbItems parameter. In the demo, I want to be able to add items to this list so I created the buffer via malloc()
. The following figure shows the dropdown populated by the list.
Application property values can be updated dynamically, this is the technique used by the demo application in the WM_NOTIFY
handler.
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
//
Each time an item value changes, the Propertygrid
sends a notification to the control's parent. If dynamic updates are not necessary, then ignore the notifications and simply request the data when desired.
These examples show some of the ways that the control can be simply implemented in a Win32 project. To demonstrate the class in a useful context, I put together a demo that includes code for each supported item type.
What follows is a programming reference for the Propertygrid
control class.
Public Data Structures
PROPGRIDITEM
The PROPGRIDITEM
structure specifies or receives attributes for a Propertygrid
item.
typedef struct tagPROPGRIDITEM {
LPTSTR lpszCatalog;
LPTSTR lpszPropName;
LPTSTR lpszzCmbItems;
LPTSTR lpszPropDesc;
LPARAM lpCurValue;
LPVOID lpUserData;
INT iItemType;
} PROPGRIDITEM, *LPPROPGRIDITEM;
Members
lpszCatalog
: The catalog (group) namelpszPropName
: The property (item) namelpszzCmbItems
: A doublenull
terminated list of strings (combo items). This field is only valid for items of typePIT_COMBO
andPIT_EDITCOMBO
lpszPropDesc
: The property (item) descriptioniItemType
: The property (item) type identifier. The value may be one of the following:PIT_EDIT
- Property item type: EditPIT_COMBO
- Property item type: Dropdown listPIT_EDITCOMBO
- Property item type: Dropdown(editable)PIT_CHECKCOMBO
- Property item type: Dropdown checked listPIT_STATIC
- Property item type: Not editable textPIT_COLOR
- Property item type: ColorPIT_FONT
- Property item type: FontPIT_FILE
- Property item type: File select dialogPIT_FOLDER
- Property item type: Folder select dialogPIT_CHECK
- Property item type: BooleanPIT_IP
- Property item type: IP AddressPIT_DATE
- Property item type: DatePIT_TIME
- Property item type: TimePIT_DATETIME
- Property item type: Date & TimePIT_CATALOG
- Property item type: Catalog
lpCurValue
: The property (item) value. The data type depends on the item type as follows:PIT_EDIT
,PIT_COMBO
,PIT_EDITCOMBO
,PIT_STATIC
andPIT_FOLDER
- TextPIT_COLOR
- ACOLORREF
valuePIT_FONT
- Pointer to aPROPGRIDFONTITEM
structPIT_FILE
- Pointer to aPROPGRIDFDITEM
structPIT_CHECK
- ABOOL
valuePIT_IP
- ADWORD
valuePIT_DATE
,PIT_TIME
andPIT_DATETIME
- Pointer to aSYSTEMTIME
struct
PROPGRIDFONTITEM
The PROPGRIDFONTITEM
structure specifies or receives attributes for a Propertygrid
item of type PIT_FONT
.
typedef struct tagPROPGRIDFONTITEM {
LOGFONT logFont;
COLORREF crFont;
} PROPGRIDFONTITEM, *LPPROPGRIDFONTITEM;
Members
logFont
: Logical font structcrFont
: Text color
PROPGRIDFDITEM
The PROPGRIDFDITEM
structure specifies or receives attributes for a Propertygrid
item of type PIT_FILE
.
typedef struct tagPROPGRIDFDITEM {
LPTSTR lpszDlgTitle;
LPTSTR lpszFilePath;
LPTSTR lpszFilter;
LPTSTR lpszDefExt;
} PROPGRIDFDITEM, *LPPROPGRIDFDITEM;
Members
lpszDlgTitle
: The font dialog titlelpszFilePath
: Initial pathlpszFilter
: A doublenull
terminated list of strings (filter items)lpszDefExt
: The default extension
Messages and Macros
Configure the control to do what you want using Windows messages. To make this easy and as a way of documenting the messages, I created macros for each message. If you prefer to call SendMessage()
or PostMessage()
explicitly, please refer to the macro defs in the header for usage.
PropGrid_AddItem
Add an item to a Propertygrid
. Items are appended to their respective catalogs.
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
Deletes the item at the specified location in a 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
Enables or disables a Propertygrid
control.
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
Gets the number of items in a Propertygrid
.
INT PropGrid_GetCount(
HWND hwndCtl
);
/*Parameters
hwndCtl
Handle to the Propertygrid control.
Return Values
The number of items.*/
PropGrid_GetCurSel
Gets the index of the currently selected item in a 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
Gets the width that a Propertygrid
can be scrolled horizontally (the scrollable width).
INT PropGrid_GetHorizontalExtent(
HWND hwndCtl
);
/*Parameters
hwndCtl
Handle to the Propertygrid control.
Return Values
The scrollable width, in pixels, of the Propertygrid.*/
PropGrid_GetItemData
Gets the PROPGRIDITEM associated with the specified Propertygrid
item.
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
Retrieves the height of all items in a 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
Gets the dimensions of the rectangle that bounds a Propertygrid
item as it is currently displayed in the 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
Gets the selection state of an item.
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
Removes all items from a Propertygrid
.
INT PropGrid_ResetContent(
HWND hwndCtl
);
/*Parameters
hwndCtl
Handle to the Propertygrid control.
Return Values
The return value is not meaningful.*/
PropGrid_SetCurSel
Sets the currently selected item in a 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
Set the width by which a Propertygrid
can be scrolled horizontally (the scrollable width). If the width of the Propertygrid
is smaller than this value, the horizontal scroll bar horizontally scrolls items in the Propertygrid
. If the width of the Propertygrid
is equal to or greater than this value, the horizontal scroll bar is hidden.
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
Sets the PROPGRIDITEM associated with the specified Propertygrid
item.
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
Sets the height of all items in a 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
Expand certain specified catalogs in a 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
Expand all catalogs in a Propertygrid
.
VOID PropGrid_ExpandAllCatalogs(
HWND hwndCtl
);
/*Parameters
hwndCtl
Handle to the Propertygrid control.
Return Values
No return value.*/
PropGrid_CollapseCatalogs
Collapse certain specified catalogs in a 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
Collapse all catalogs in a Propertygrid
.
VOID PropGrid_CollapseAllCatalogs(
HWND hwndCtl
);
/*Parameters
hwndCtl
Handle to the Propertygrid control.
Return Values
No return value.*/
PropGrid_ShowToolTips
Show or hide tooltips in the 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
Show or hide the property description pane in the 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
Sets the appearance of the checkboxes.
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
Initialize an item struct
.
VOID PropGrid_ItemInit(
PROPGRIDITEM pgi
);
/*Parameters
pgi
The newly declared PROPGRIDITEM struct.
Return Values
No return value.*/
Notifications
The Propertygrid
control provides notifications via WM_NOTIFY
. The lParam
parameter of these notification messages points to an NMPROPGRID
structure.
NMPROPGRID
The NMPROPGRID
structure contains information about a Propertygrid
control notification message.
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
The PGN_PROPERTYCHANGE
notification message notifies a Propertygrid
control's parent window that an item's data has changed. This notification message is sent in the form of a WM_NOTIFY
message.
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.*/
Design Considerations
During a recent break in the school year, two of my kids decided that they wanted to spend a day with Dad at work to see all of the cool stuff he gets to do at the office. I agreed to take them on separate days and planned work related activities on those days that I thought would interest each boy. At lunch time, I took them to an interesting exhibit hosted by a firm located in a business park near work, The Craftsmanship Museum [^].
One of the things that impressed me about this exhibit was the attention to detail and many hours of patient work the hobbyists invested in the miniature replicas and indeed, most of the mechanical engines run. The skill and craftsmanship invested in the projects are evident even at a casual glance.
Something I learned there was that for a mechanical engine to run well (or at all for that matter) parts have to be machined to exact tolerances and tolerances stack up. Some of the most reliable engine designs tend to be simple but well thought out.
Ideally, with a Propertygrid
, you have only 2 windows to work with, one to display data and one to edit data. Initially I used a link list to store pointers to all item data and the Listbox
displayed the catalogs and the visible data. Later as I began to flesh things out, I realized that I kept adding features to the internal data structure that mimicked Listbox
features such as indexing. At that point I substituted my list with a second, minimal instance of the windows Listbox
class and simplified the code considerably. I realized that I could transparently support a greater subset of existing Listbox
messages giving the user greater flexibility with reduced development effort.
This Propertygrid
uses seven different windows controls to edit properties. I took a cue from the Listview
control which creates an Editbox
only when an item is being edited and then destroys it when the edit is done. Consequently the Propertygrid
has only one instance of an editor most of the time. The exceptions being the date and time field with two editors, or the static and check box fields which do not require any editor. This cuts out some overhead when it comes to managing the editors and makes for a cleaner more elegant implementation in the code.
In addition to the Listbox
es and the editors, there are two optional components - a static
description field and tooltips. These are created if the user configures the Propertygrid
to display them.
Tips and Tricks for the Win32/64 SDK Developer
There are a lot of things I won't cover here that I cover in some detail in previous articles. For those interested in how I approach the overall structure of a custom control, as well as the basics of subclassing windows, check out Win32 SDK Data Grid View Made Easy [^]. The tips I want to share here, with a few exceptions, have to do with aspects of drawing the control(s).
Owner Draw Tip
Most of the look and feel of the grid is achieved by owner drawing a Listbox
. One mistake that I have seen developers make when owner drawing something is not making use of the stock objects (pens and brushes) and system colors. The following is an example of how NOT to draw your control.
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
What's wrong with the above code snippet? A little test will show the error quickly enough. With the application running in the debugger, I change the display properties of my desktop to Plum...
Yuck! That's not what I wanted. Let's draw it the right way and let the user decide how the control should look.
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));
Plum perfect!
Borderless Controls
They said it couldn't be done...and yet that date-time picker appears to be nearly flat, actually it is nearly invisible, but how?
The various editors are subclassed primarily to gain access to keypress events but why not override WM_PAINT
as well? That's exactly what I did, and with the exception of the Combobox
es, a single method worked for everything.
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;
}
Here I call the method from the 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
//
As I said, the Combobox
es were a bit different but the applied principle is the same. Here's how it's done for the 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);
}
Easy Check Boxes
The check boxes are not controls at all but are drawn using DrawFrameControl()
which draws a bitmap representation of a classic styled checkbox like this:, yet with three additional lines of code I turn it into a nice flat checkbox like this:
. Here's how it's done.
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));
The Catalog Toggle
No bitmap or resources necessary, just drew the little box like so (rect2
defines a square with an odd number of pixels on a side).
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);
Subclassing a Composite Control
Subclassing an edit control is fairly straight forward. It consists of only a single window, and its messages are all routed through the same proc. Subclassing a Combobox
or IPedit
is another matter entirely. Keyboard messages get routed through child controls and the parent control's proc will never see them if we don't subclass the children too. If only there were an easy way to subclass those children... Well there is. When a control is created, right off it sends a WM_COMMAND
message to its parent usually with an EN_SETFOCUS
notification code if the child is an edit or an LBN_SETFOCUS
for a Listbox
. We don't care about the notification, we want the child's handle the first time, to subclass it.
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);
}
I subclassed the edit controls to the same proc as their parent. In doing so, I need to take care to differentiate between child and parent when messages route through this proc.
Mouse Wheel Bug
It turns out that a Listbox
control created with the LBS_OWNERDRAWVARIABLE
style does not handle the mouse wheel correctly. The scroll effect is very jumpy; so bad in fact, that if you want to use that style, it is advisable to intercept WM_MOUSEWHEEL
to either disable it or write your own handler.
Detecting Begin and End Scroll Events in a Listbox
The Listbox
doesn't have a scrollbar component, instead it draws a scroll bar in the non-client area of the control probably using DrawFrameControl()
. Consequently one cannot subclass the scrollbar in order to detect mouse events. The following snippet demonstrates one way to work around this and detect begin and end scroll.
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
//
Final Comments
I documented this source with Doxygen [^] comments for those that might find it helpful or useful. Your feedback is appreciated.
History
- April 30, 2010: Version 1.0.0.0
- August 3, 2010: Version 1.1.0.0 - Fixed Bug with
PropGrid_GetItemData()
where an item of typePIT_FILE
returned an empty string instead of the file path - October 28, 2010: Version 1.2.0.0 - Several bug fixes and improvements (annotated in the source code)
- December 09, 2010: Version 1.3.0.0 - Improved tabbing behavior with the control, the
lpCurValue
member of thePROPGRIDITEM
returned byPropGrid_GetItemData()
for an item of typePIT_FILE
is now a pointer to aPROPGRIDFDITEM
struct in accordance with the documentation instead of just the file path string. - November 11, 2013: Version 1.4.0.0 - Modified how the editors were drawn in order to maintain consistent look and feel between XP and Win7.
- November 16, 2013: Version 1.5.0.0 - Fixed bug condition under X64 operation.
- November 27, 2013: Version 1.6.0.0 - Fixed some bugs related to data persistance if the grid is resized during a field edit. Fixed a bug in the date field.
- September 14, 2014: Version 1.7.0.0 - Some minor bug fixes.
- February 27, 2016: Version 1.8.0.0 - Added PG_FLATCHECKS message and PropGrid_SetFlatStyleChecks() macro.
- March 30, 2016: Version 1.9.0.0 - Some bug fixes related to the IP address field. Added MSVC project download.
- November 18, 2018: Version 2.0.0.0 - Fixed crash issue related to PropGrid_ResetContent(). Fixed the firing of WM_NOTIFY messages, limiting them to once per field edit. Added some requested features - factory created window now default visible, added scrollbar to drop downs, keyboard shortcuts, etc...
- November 21, 2018: Version 2.1.0.0 - Encorporated two more bug fixes and support for user data member in the item struct. Thanks to Jakob for contributing these fixes/features.
- May 4, 2021: Version 2.2.0.0 - rewrote the section of code related to the comboboxes adding a checked combobox. Improved keyboard and tabbing related to the comboboxes, added bug fix suggestions, and cleaned up unnecessary comments.
- May 9, 2021: Version 2.3.0.0 - fixed bug preventing editor window from hiding during mouse wheel scroll in windows 10.
- Nov 23, 2021: Version 2.4.0.0 - Added missing GetProp to ComboList_OnRButtonDown