Click here to Skip to main content
Click here to Skip to main content

Win32 SDK PropertyGrid Made Easy

, 27 Nov 2013 CPOL
Rate this:
Please Sign up or sign in to vote.
This article describes the creation of a non-MFC custom PropertyGrid control.
Demo screenshot

Contents

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.

Demo screenshot

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.

Demo screenshot

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.

Demo screenshot

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;
    INT    iItemType;
} PROPGRIDITEM, *LPPROPGRIDITEM;

Members

  • 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

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 struct
  • crFont: 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 title
  • lpszFilePath: Initial path
  • lpszFilter: A double null 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_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 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.

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...

Demo screenshot

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));

Demo screenshot

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 Comboboxes, 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 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) // Obliterate border (differs from standard method)
{
    // First let the system do its thing
    CallWindowProc((WNDPROC)GetProp(hwnd, TEXT("Wprc")), hwnd, msg, wParam, lParam);

    // Next obliterate the border
    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;
}

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:Standard check, yet with three additional lines of code I turn it into a nice flat checkbox like this:Flat check. 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. 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));

    // Note: Instance data is attached to combo's grandparent
    //  or the edit field's greatgrandparent
    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)  //Unsubclass the combobox or child edit control
    {
        SetWindowLongPtr(hwnd, GWLP_WNDPROC, (DWORD)GetProp(hwnd, TEXT("Wprc")));
        RemoveProp(hwnd, TEXT("Wprc"));
        return 0;
    }

    //
    // Skip some stuff
    //

    else if (WM_COMMAND == msg)
    {
        // The editable combo's edit box posts a notification on loading
        //  the first time it does so we'll grab and subclass it.
        HWND hwndCtl = GET_WM_COMMAND_HWND(wParam, lParam);
        {
            WNDPROC lpfn = (WNDPROC)GetProp(hwndCtl, TEXT("Wprc"));
            if (NULL == lpfn)
            {
                // Do not subclass the drop down list
                GetClassName(hwndCtl, buf, NELEMS(buf));
                if (0 == _tcsicmp(buf, WC_EDIT))
                {
                    //Subclass edit and save the old proc
                    SetProp(hwndCtl, TEXT("Wprc"),
			(HANDLE)GetWindowLongPtr(hwndCtl, GWLP_WNDPROC));
                    SubclassWindow(hwndCtl, ComboBox_Proc);
                }
            }
        }
    }

    //
    // Skip some more stuff
    //

    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.

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 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.
  • 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

David MacDermot

United States United States
No Biography provided

Comments and Discussions

 
QuestionGood Work - Date issue PinmemberBrianCharles25-Nov-13 7:52 
AnswerRe: Good Work - Date issue PinmemberDavid MacDermot25-Nov-13 10:19 
QuestionCompier error in VC6.0 PinmemberTonyJingLi22-Nov-13 15:49 
AnswerRe: Compier error in VC6.0 PinmemberDavid MacDermot23-Nov-13 5:54 
Question5 stars any day PinmemberCristian Amarie16-Nov-13 9:26 
AnswerRe: 5 stars any day PinmemberDavid MacDermot16-Nov-13 17:33 
QuestionC++ PinmemberMember 1012278125-Jun-13 16:48 
GeneralMy vote of 5 Pingrouplyricc3-Jan-13 15:57 
GeneralMy vote of 5 Pinmembermaplewang10-Jul-12 22:23 
GeneralMy vote of 5 PinmemberMihai MOGA18-Dec-11 3:34 
QuestionKick-ass work PinmemberAHPlankton7-Sep-11 3:23 
AnswerRe: Kick-ass work PinmemberDavid MacDermot7-Sep-11 10:28 
GeneralMy vote of 5 PinmemberAHPlankton25-Aug-11 19:08 
David Rocks
QuestionProblem with MAKE_PRECT PinmemberG2F26-Jul-11 15:36 
AnswerRe: Problem with MAKE_PRECT PinmemberDavid MacDermot7-Jul-11 5:41 
GeneralRe: Problem with MAKE_PRECT PinmemberG2F27-Jul-11 7:41 
QuestionAm I missing something? Pinmemberjlemon12-Feb-11 13:02 
AnswerRe: Am I missing something? PinmemberDavid MacDermot14-Feb-11 4:37 
QuestionRe: Am I missing something? Pinmemberjlemon15-Feb-11 5:26 
AnswerRe: Am I missing something? PinmemberDavid MacDermot16-Feb-11 4:59 
GeneralMy vote of 5 Pinmemberflyhigh13-Dec-10 18:21 
GeneralMy vote of 5 PinmemberS.H.Bouwhuis1-Nov-10 22:41 
GeneralMy vote of 5 Pinmembermaplewang31-Oct-10 16:26 
GeneralMy vote of 5 Pinmemberpestis8-Sep-10 2:49 
GeneralRe: My vote of 5 PinmemberDavid MacDermot15-Sep-10 8:14 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.141015.1 | Last Updated 27 Nov 2013
Article Copyright 2010 by David MacDermot
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid