You can extend the PROPSHEETPAGE structure with your own bonus data


... for when regular strength lParam just isn't enough.

A little-known and even less-used feature of the shell property sheet is that you can hang custom data off the end of the PROPSHEETPAGE structure, and the shell will carry it around for you. Mind you, the shell carries it around by means of memcpy and destroys it by just freeing the underlying memory, so whatever you stick on the end needs to be plain old data. (Though you do get an opportunity to "construct" and "destruct" if you register a PropSheetPageProc callback, during which you are permitted to modify your bonus data and the lParam field of the PROPSHEETPAGE.)

Here's a program that illustrates this technique. It doesn't do much interesting, mind you, but maybe that's a good thing: Makes for fewer distractions.

#include <windows.h>
#include <prsht.h>

HINSTANCE g_hinst;

struct ITEMPROPSHEETPAGE : public PROPSHEETPAGE
{
 int cWidgets;
 TCHAR szItemName[100];
};

ITEMPROPSHEETPAGE is a custom structure that appends our bonus data (an integer and a string) to the standard PROPSHEETPAGE. This is the structure that our property sheet page will use.

INT_PTR CALLBACK DlgProc(HWND hwnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
 switch (uiMsg) {
 case WM_INITDIALOG:
  {
   ITEMPROPSHEETPAGE *ppsp =
      reinterpret_cast<ITEMPROPSHEETPAGE*>(lParam));
   SetDlgItemText(hwnd, 100, ppsp->szItemName);
   SetDlgItemInt(hwnd, 101, ppsp->cWidgets, FALSE);
  }
  return TRUE;
 }
 return FALSE;
}

The lParam passed to WM_INITDIALOG is a pointer to the shell-managed copy of the PROPSHEETPAGE structure. Since we associated this dialog procedure with a ITEMPROPSHEETPAGE structure, we can cast it to the larger structure to get at our bonus data (which the shell happily memcpy'd from our copy into the shell-managed copy).

HPROPSHEETPAGE CreateItemPropertySheetPage(
    int cWidgets, PCTSTR pszItemName)
{
 ITEMPROPSHEETPAGE psp;
 ZeroMemory(&psp, sizeof(psp));
 psp.dwSize = sizeof(psp);
 psp.hInstance = g_hinst;
 psp.pszTemplate = MAKEINTRESOURCE(1);
 psp.pfnDlgProc = DlgProc;
 psp.cWidgets = cWidgets;
 lstrcpyn(psp.szItemName, pszItemName, 100);
 return CreatePropertySheetPage(&psp);
}

It is here that we associate the DlgProc with the ITEMPROPSHEETPAGE. Just to highlight that the pointer passed to the DlgProc is a copy of the ITEMPROPSHEETPAGE used to create the property sheet page, I created a separate function to create the property sheet page, so that the ITEMPROPSHEETPAGE on the stack goes out of scope, making it clear that the copy passed to the DlgProc is not the one we passed to CreatePropertySheetPage.

Note that you must set the dwSize of the base PROPSHEETPAGE to the size of the PROPSHEETPAGE plus the size of your bonus data. In other words, set it to the size of your ITEMPROPSHEETPAGE.

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst,
                   LPSTR lpCmdLine, int nCmdShow)
{
 g_hinst = hinst;
 HPROPSHEETPAGE hpage =
   CreateItemPropertySheetPage(42, TEXT("Elmo"));
 if (hpage) {
  PROPSHEETHEADER psh = { 0 };
  psh.dwSize = sizeof(psh);
  psh.dwFlags = PSH_DEFAULT;
  psh.hInstance = hinst;
  psh.pszCaption = TEXT("Item Properties");
  psh.nPages = 1;
  psh.phpage = &hpage;
  PropertySheet(&psh);
 }
 return 0;
}

Here is where we display the property sheet. It looks just like any other code that displays a property sheet. All the magic happens in the way we created the HPROPSHEETPAGE.

If you prefer to use the PSH_PROPSHEETPAGE flag, then the above code could have been written like this:

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst,
                   LPSTR lpCmdLine, int nCmdShow)
{
 ITEMPROPSHEETPAGE psp;
 ZeroMemory(&psp, sizeof(psp));
 psp.dwSize = sizeof(psp);
 psp.hInstance = g_hinst;
 psp.pszTemplate = MAKEINTRESOURCE(1);
 psp.pfnDlgProc = DlgProc;
 psp.cWidgets = cWidgets;
 lstrcpyn(psp.szItemName, pszItemName, 100);

 PROPSHEETHEADER psh = { 0 };
 psh.dwSize = sizeof(psh);
 psh.dwFlags = PSH_PROPSHEETPAGE;
 psh.hInstance = hinst;
 psh.pszCaption = TEXT("Item Properties");
 psh.nPages = 1;
 psh.ppsp = &psp;
 PropertySheet(&psh);
 return 0;
}

If you want to create a property sheet with more than one page, then you would pass an array of ITEMPROPSHEETPAGEs. Note that passing an array requires all the pages in the array to use the same custom structure (because that's how arrays work; all the elements of an array are the same type).

Finally, here's the dialog template. Pretty anticlimactic.

1 DIALOG 0, 0, PROP_SM_CXDLG, PROP_SM_CYDLG
STYLE WS_CAPTION | WS_SYSMENU
CAPTION "General"
FONT 8, "MS Shell Dlg"
BEGIN
    LTEXT "Name:",-1,7,11,42,14
    LTEXT "",100,56,11,164,14
    LTEXT "Widgets:",-1,7,38,42,14
    LTEXT "",101,56,38,164,14
END

And there you have it. Tacking custom data onto the end of a PROPSHEETPAGE, an alternative to trying to cram everything into a single lParam.

Exercise: Observe that the size of the PROPSHEETPAGE structure has changed over time. For example, the original PROPSHEETPAGE ends at the pcRefParent. In Windows 2000, there are two more fields, the pszHeaderTitle and pszHeaderSubTitle. Windows XP added yet another field, the hActCtx. Consider a program written for Windows 95 that uses this technique. How does the shell know that the cWidgets is really bonus data and not a pszHeaderTitle?

Comments (15)
  1. Niels says:

    From the documentation for pszHeaderTitle: "If dwFlags does not include the PSP_USEHEADERTITLE value, this member is ignored."

    What I'm guessing is that the system doesn't even attempt to access the fields if the appropriate flags aren't set, and an application built against an old version of the struct won't have those flags set.

  2. Niels says:

    As an aside, does this trick work for all structs that have a dwSize, cbSize or similar member? Or should we assume that the size field is generally used for struct versioning?

    [The trick does not work in general. The property sheet manager gets away with it because it uses a different method for version checking. -Raymond]
  3. configurator says:

    So, same as OVERLAPPED, really, except it's being copied around and not remaining at the same address. Are there other commonly-used structures that enable the same technique? It seems very useful to be able to provide extra data with everything, although it does remind me of the .net Tag property which I never ever use.

    To answer your exercise: Id guess there's a flag specifying if pszHeaderTitle, pszHeaderSubTitle, and hActCtx are being used, possibly in the dwFlags you mentioned.

  4. Dan Bugglin says:

    Niels: I always assumed that the size would determine which fields would be accessed by the OS.  And in fact this seems to be the underlying assumption of Raymond's Exercise.

  5. Dan Bugglin says:

    @configurator I use Tag all the time, usually to link UI items with the underlying code objects they represent.

  6. Alex Grigoriev says:

    I can't believe my eyes. Raymond recommends using a kludge that potentially breaks future compatibility, and depends generally on internal implementation and non-guaranteed behavior.

  7. laonianren says:

    +1 Alex.  Not only is this undocumented (as far as I can tell), the PROPSHEETPAGE documentation even says that you can't do this: "Size, in bytes, of this structure. The property sheet manager uses this member to determine which version of the PROPSHEETHEADER structure you are using."  (Assuming that PROPSHEETHEADER was intended to read PROPSHEETPAGE; it makes no sense at all as it is.)

    [Clearly a documentation error (bad copy/paste from PROPSHEETHEADER). The correct text is in this version of PROPSHEETPAGE. -Raymond]
  8. Adam Rosenfield says:

    @Alex, couldn't agree more. The documentation for lstrcpyn even says to use StringCchCopy instead of it, which I know I've seen Raymond use before. Are we sure that Raymond hasn't secretly been replaced by an imposter?

    [Dude, calm down. It's just a sample. Using StringCchCopy would have made the sample bigger and distracted from the point. -Raymond]
  9. Typical Microsoft Dev says:

    "Consider a program written for Windows 95 that uses this technique. How does the shell know that the cWidgets is really bonus data and not a pszHeaderTitle?"

    Because it's using the "MS Shell" Font and not Tahoma?

  10. Absotively says:

    The first search result for PROPSHEETPAGE is msdn.microsoft.com/…/aa815151%28VS.85%29.aspx for some reason.  It says dwSize is "Size, in bytes, of the PROPSHEETPAGE structure. The size includes any extra application-defined data at the end of the structure."  But it also links to the main PROPSHEETPAGE documentation quoted by laonianren.  Perhaps the main documentation is a victim of careless copying and pasting from the documentation for PROPSHEETHEADER?

  11. configurator says:

    Your reasoning for why the trick doesn't work in general makes perfect sense – it can cause bugs. So why enable it in this case? Was there a distinct advantage that doesn't exist elsewhere? Or perhaps just because it already existed this way and is now needed for compatibility?

    [There was already a different version-detection algorithm in place (flags) so the size was no longer meaningful for version detection. Remember, this stuff was designed back in the days when saving 4KB of memory was a huge deal. -Raymond]
  12. Ben says:

    COMCTL32 uses a different method of versioning, you have to request which behaviour/version you want, using InitCommonControls.

    That's how it knows whether that extra data is yours, or whether it is pszHeaderTitle, not the size.

  13. dllver says:

    Some Shell functions (methods) also reads the calling assembly's DLLVERSIONINFO and exposes different behaviour depending on what the caller happens to be compiled with.

  14. Anonymous says:

    Could be through the size of the header:

     PROPSHEETHEADER psh = { 0 };

     psh.dwSize = sizeof(psh);

  15. Roman says:

    So I guess TRWTF is that there are two descriptions of PROPSHEETPAGE in the MSDN library, that contradict each other. And the one that seems more legitimate is wrong.

    [My theory is that when I originally reported the error in PROPSHEETPAGE to the documentation team, they fixed only one of the copies of PROPSHEETPAGE. -Raymond]

Comments are closed.

Skip to main content