Contents
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.
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.
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;
icc.dwSize = sizeof(icc);
icc.dwICC = ICC_WIN95_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);
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;
PropGrid_ItemInit(Item);
Item.lpszCatalog = _T("Edit, Static, and Combos"); Item.lpszPropName = _T("Edit Field"); Item.lpCurValue = (LPARAM) gDemoData.strProp1; Item.lpszPropDesc = _T("This field is editable"); Item.iItemType = PIT_EDIT;
PropGrid_AddItem(hPropGrid, &Item);
PropGrid_ShowToolTips(hPropGrid,TRUE); PropGrid_ExpandAllCatalogs(hPropGrid);
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:
PROPGRIDFDITEM ItemFd = {0};
ItemFd.lpszDlgTitle = _T("Choose File"); ItemFd.lpszFilePath = gDemoData.strProp8; ItemFd.lpszFilter = _T("Text files (*.txt)\0*.txt\0All Files (*.*)\0*.*\0");
ItemFd.lpszDefExt = _T("txt");
Item.lpszPropName = _T("Choose file"); 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:
_tmemset(gDemoData.strProp3,_T('\0'),NELEMS(gDemoData.strProp3));
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);
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;
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.
The PROPGRIDITEM structure specifies or receives attributes for a Propertygrid item.
typedef struct tagPROPGRIDITEM {
LPTSTR lpszCatalog;
LPTSTR lpszPropName;
LPTSTR lpszzCmbItems;
LPTSTR lpszPropDesc;
LPARAM lpCurValue;
INT iItemType;
} PROPGRIDITEM, *LPPROPGRIDITEM;
lpszCatalog: The catalog (group) name
lpszPropName: The property (item) name
lpszzCmbItems: A double null terminated list of strings (combo items). This field is only valid for items of type PIT_COMBO and PIT_EDITCOMBO
lpszPropDesc: The property (item) description
iItemType: The property (item) type identifier. The value may be one of the following:
PIT_EDIT - Property item type: Edit
PIT_COMBO - Property item type: Dropdown list
PIT_EDITCOMBO - Property item type: Dropdown(editable)
PIT_STATIC - Property item type: Not editable text
PIT_COLOR - Property item type: Color
PIT_FONT - Property item type: Font
PIT_FILE - Property item type: File select dialog
PIT_FOLDER - Property item type: Folder select dialog
PIT_CHECK - Property item type: Boolean
PIT_IP - Property item type: IP Address
PIT_DATE - Property item type: Date
PIT_TIME - Property item type: Time
PIT_DATETIME - Property item type: Date & Time
PIT_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 and PIT_FOLDER - Text
PIT_COLOR - A COLORREF value
PIT_FONT - Pointer to a PROPGRIDFONTITEM struct
PIT_FILE - Pointer to a PROPGRIDFDITEM struct
PIT_CHECK - A BOOL value
PIT_IP - A DWORD value
PIT_DATE, PIT_TIME and PIT_DATETIME - Pointer to a SYSTEMTIME struct
The PROPGRIDFONTITEM structure specifies or receives attributes for a Propertygrid item of type PIT_FONT.
typedef struct tagPROPGRIDFONTITEM {
LOGFONT logFont;
COLORREF crFont;
} PROPGRIDFONTITEM, *LPPROPGRIDFONTITEM;
logFont: Logical font struct
crFont: Text color
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;
lpszDlgTitle: The font dialog title
lpszFilePath: Initial path
lpszFilter: A double null terminated list of strings (filter items)
lpszDefExt: The default extension
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.
Add an item to a Propertygrid. Items are appended to their respective catalogs.
INT PropGrid_AddItem(
HWND hwndCtl
LPPROPGRIDITEM lpItem
);
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.*/
Deletes the item at the specified location in a Propertygrid.
INT PropGrid_DeleteItem(
HWND hwndCtl
INT index
);
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.*/
Enables or disables a Propertygrid control.
VOID PropGrid_Enable(
HWND hwndCtl
BOOL fEnable
);
hwndCtl
Handle to the Propertygrid control.
fEnable
TRUE to enable the control, or FALSE to disable it.
Return Values
No return value.*/
Gets the number of items in a Propertygrid.
INT PropGrid_GetCount(
HWND hwndCtl
);
hwndCtl
Handle to the Propertygrid control.
Return Values
The number of items.*/
Gets the index of the currently selected item in a Propertygrid.
INT PropGrid_GetCurSel(
HWND hwndCtl
);
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.*/
Gets the width that a Propertygrid can be scrolled horizontally (the scrollable width).
INT PropGrid_GetHorizontalExtent(
HWND hwndCtl
);
hwndCtl
Handle to the Propertygrid control.
Return Values
The scrollable width, in pixels, of the Propertygrid.*/
Gets the PROPGRIDITEM associated with the specified Propertygrid item.
LPPROPGRIDITEM PropGrid_GetItemData(
HWND hwndCtl
INT index
);
hwndCtl
Handle to the Propertygrid control.
index
The zero-based index of the item.
Return Values
A pointer to a PROPGRIDITEM object.*/
Retrieves the height of all items in a Propertygrid.
INT PropGrid_GetItemHeight(
HWND hwndCtl
);
hwndCtl
Handle to the Propertygrid control.
Return Values
The height, in pixels, of the items, or LB_ERR if an error occurs.*/
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
);
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.*/
Gets the selection state of an item.
INT PropGrid_GetSel(
HWND hwndCtl
INT index
);
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.*/
Removes all items from a Propertygrid.
INT PropGrid_ResetContent(
HWND hwndCtl
);
hwndCtl
Handle to the Propertygrid control.
Return Values
The return value is not meaningful.*/
Sets the currently selected item in a Propertygrid.
INT PropGrid_SetCurSel(
HWND hwndCtl
INT index
);
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.*/
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
);
hwndCtl
Handle to the Propertygrid control.
cxExtent
The number of pixels by which the grid can be scrolled.
Return Values
No return value.*/
Sets the PROPGRIDITEM associated with the specified Propertygrid item.
INT PropGrid_SetItemData(
HWND hwndCtl
INT index
LPPROPGRIDITEM data
);
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.*/
Sets the height of all items in a Propertygrid.
INT PropGrid_SetItemHeight(
HWND hwndCtl
INT cy
);
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.*/
Expand certain specified catalogs in a Propertygrid.
VOID PropGrid_ExpandCatalogs(
HWND hwndCtl
LPTSTR lpszzCatalogs
);
hwndCtl
Handle to the Propertygrid control.
lpszzCatalogs
The list of catalog names each terminated by a null (\0).
Return Values
No return value.*/
Expand all catalogs in a Propertygrid.
VOID PropGrid_ExpandAllCatalogs(
HWND hwndCtl
);
hwndCtl
Handle to the Propertygrid control.
Return Values
No return value.*/
Collapse certain specified catalogs in a Propertygrid.
VOID PropGrid_CollapseCatalogs(
HWND hwndCtl
LPTSTR lpszzCatalogs
);
hwndCtl
Handle to the Propertygrid control.
lpszzCatalogs
The list of catalog names each terminated by a null (\0).
Return Values
No return value.*/
Collapse all catalogs in a Propertygrid.
VOID PropGrid_CollapseAllCatalogs(
HWND hwndCtl
);
hwndCtl
Handle to the Propertygrid control.
Return Values
No return value.*/
Show or hide tooltips in the Propertygrid.
VOID PropGrid_ShowToolTips(
HWND hwndCtl
BOOL fShow
);
hwndCtl
Handle to the Propertygrid control.
fShow
TRUE for tooltips; FALSE do not show tooltips.
Return Values
No return value.*/
Show or hide the property description pane in the Propertygrid.
VOID PropGrid_ShowPropertyDescriptions(
HWND hwndCtl
BOOL fShow
);
hwndCtl
Handle to the Propertygrid control.
fShow
TRUE for descriptions; FALSE do not show description pane
Return Values
No return value.*/
Initialize an item struct.
VOID PropGrid_ItemInit(
PROPGRIDITEM pgi
);
pgi
The newly declared PROPGRIDITEM struct.
Return Values
No return value.*/
The Propertygrid control provides notifications via WM_NOTIFY. The lParam parameter of these notification messages points to an NMPROPGRID structure.
The NMPROPGRID structure contains information about a Propertygrid control notification message.
typedef struct tagNMPROPGRID {
NMHDR hdr;
INT iIndex;
} NMPROPGRID, *LPNMPROPGRID;
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;
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 Listboxes 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.
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).
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)
{
SetBkMode(lpDIS->hDC, TRANSPARENT);
FillRect(lpDIS->hDC, &rectPart1,
CreateSolidBrush(nIndex ==
(UINT)ListBox_GetCurSel(lpDIS->hwndItem) ?
RGB(0,0,255) : RGB(255,255,255)));
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));
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));
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)
{
SetBkMode(lpDIS->hDC, TRANSPARENT);
FillRect(lpDIS->hDC, &rectPart1,
GetSysColorBrush(nIndex == (UINT)ListBox_GetCurSel(lpDIS->hwndItem) ?
COLOR_HIGHLIGHT : COLOR_WINDOW));
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!
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 Comboboxes, a single method worked for everything.
BOOL Editor_OnPaint(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
HDC hdc = GetWindowDC(hwnd);
RECT rect;
CallWindowProc((WNDPROC)GetProp(hwnd, TEXT("Wprc")), hwnd, msg, wParam, lParam);
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));
Control_GetInstanceData(hGParent, &g_lpInst);
if (WM_DESTROY == msg) {
SetWindowLongPtr(hDate, GWLP_WNDPROC, (DWORD)GetProp(hDate, TEXT("Wprc")));
RemoveProp(hDate, TEXT("Wprc"));
return 0;
}
else if (WM_PAINT == msg) {
return Editor_OnPaint(hDate, msg, wParam, lParam);
}
As I said, the Comboboxes were a bit different but the applied principle is the same. Here's how it's done for the Combobox.
else if (WM_PAINT == msg) {
CallWindowProc((WNDPROC)GetProp(hwnd, TEXT("Wprc")), hwnd, msg, wParam, lParam);
HDC hdc = GetWindowDC(hwnd);
RECT rect;
GetClientRect(hwnd, &rect);
rect.bottom -= 2;
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);
return TRUE;
}
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));
FrameRect(lpDIS->hDC, &rect3, GetSysColorBrush(COLOR_BTNFACE));
InflateRect(&rect3, -1, -1);
FrameRect(lpDIS->hDC, &rect3, GetSysColorBrush(COLOR_WINDOW));
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); if (pItem->fCollapsed) DrawLine(lpDIS->hDC, ptCtr.x, rect2.top, ptCtr.x, rect2.bottom);
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)
{
else {
if (WM_PAINT == msg) {
return Editor_OnPaint(hwnd, msg, wParam, lParam);
}
else if (WM_COMMAND == msg)
{
HWND hwndCtl = GET_WM_COMMAND_HWND(wParam, lParam);
{
WNDPROC lpfn = (WNDPROC)GetProp(hwndCtl, TEXT("Wprc"));
if (NULL == lpfn)
{
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. Here is how it's done for the Comboboxes.
static LRESULT CALLBACK ComboBox_Proc
(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
static TCHAR buf[MAX_PATH];
HWND hGParent = GetParent(GetParent(hwnd));
GetClassName(hwnd, buf, NELEMS(buf));
BOOL fEdit = (0 == _tcsicmp(buf, WC_EDIT));
if (fEdit)
Control_GetInstanceData(GetParent(hGParent), &g_lpInst);
else
Control_GetInstanceData(hGParent, &g_lpInst);
if (WM_DESTROY == msg) {
SetWindowLongPtr(hwnd, GWLP_WNDPROC, (DWORD)GetProp(hwnd, TEXT("Wprc")));
RemoveProp(hwnd, TEXT("Wprc"));
return 0;
}
else if (WM_COMMAND == msg)
{
HWND hwndCtl = GET_WM_COMMAND_HWND(wParam, lParam);
{
WNDPROC lpfn = (WNDPROC)GetProp(hwndCtl, TEXT("Wprc"));
if (NULL == lpfn)
{
GetClassName(hwndCtl, buf, NELEMS(buf));
if (0 == _tcsicmp(buf, WC_EDIT))
{
SetProp(hwndCtl, TEXT("Wprc"),
(HANDLE)GetWindowLongPtr(hwndCtl, GWLP_WNDPROC));
SubclassWindow(hwndCtl, ComboBox_Proc);
}
}
}
}
return CallWindowProc((WNDPROC)GetProp(hwnd, TEXT("Wprc")),
hwnd, msg, wParam, lParam);
}
I ran into a gotcha when initially using this method for a Combobox. I inadvertently subclassed the drop down list. Understand that its parent is not actually the Combobox! The reason for this is that the drop down would be clipped by the client rectangle of a parent Combobox. The list box does send WM_COMMAND notifications to the Combobox as if it were the parent control. But if you use GetParent() the HWND that is returned is the desktop. This meant that a call to Control_GetInstanceData() returned NULL since it could not find the property stored with this instance of the Propertygrid.
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.
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);
Control_GetInstanceData(hParent, &g_lpInst);
switch (msg)
{
case WM_MBUTTONDOWN:
case WM_NCLBUTTONDOWN:
ListBox_OnBeginScroll(hList);
g_lpInst->fScrolling = TRUE;
break;
case WM_SETCURSOR:
if (g_lpInst->fScrolling)
{
ListBox_OnEndScroll(hList);
g_lpInst->fScrolling = FALSE;
}
break;
I documented this source with Doxygen [^] comments for those that might find it helpful or useful. Your feedback is appreciated.
- 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 type PIT_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 the PROPGRIDITEM returned by PropGrid_GetItemData() for an item of type PIT_FILE is now a pointer to a PROPGRIDFDITEM struct in accordance with the documentation instead of just the file path string.