Appending additional payload to a PROPSHEETPAGE structure

Raymond

A not-well-known feature of the common controls property sheet is that you can append your own custom data to the end of the PROPSHEETPAGE structure, and the system will carry it around for you.

The traditional way of setting up a PROPSHEETPAGE is to use the lParam member to point to a structure containing additional data that is used by the property sheet:

struct WidgetNameData
{
    HWIDGET widget;
    bool uppercaseOnly;
    int renameCount;
};

void ShowWidgetProperties(HWIDGET widget, HWND hwndOwner)
{
    WidgetNameData nameData;
    nameData.widget = widget;
    nameData.uppercaseOnly = IsPolicyEnabled(Policy::UppercaseNames);
    nameData.renameCount = 0;

    PROPSHEETPAGE pages[1] = {};

    pages[0].dwSize = sizeof(pages[0]);
    pages[0].hInstance = g_hinstThisDll;
    pages[0].pszTemplate = MAKEINTRESOURCE(IDD_WIDGETNAMEPROP);
    pages[0].pfnDlgProc = WidgetNameDlgProc;
    pages[0].lParam = (LPARAM)&nameData;

    PROPSHEETHEADER psh = { sizeof(psh) };
    psh.dwFlags = PSH_WIZARD | PSH_PROPSHEETPAGE;
    psh.hInstance = g_hinstThisDll;
    psh.hwndParent = hwndOwner;
    psh.pszCaption = MAKEINTRESOURCE(IDS_WIDGETPROPTITLE);
    psh.nPages = ARRAYSIZE(pages);
    psh.ppsp = pages;
    PropertySheet(&psh);
}

For simplicity, this property sheet has only one page. This page needs a WidgetData worth of extra state, so we allocate that state (on the stack, in this case) and put a pointer to int in the PROPSHEETPAGE‘s lParam for the dialog procedure to fish out:

INT_PTR CALLBACK WidgetNameDlgProc(
    HWND hdlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    auto pageData = (WidgetNameData*)GetWindowLongPtr(
        hdlg, DWLP_USER);
    switch (uMsg)
    {
    case WM_INITDIALOG:
        {
            auto page = (PROPSHEETPAGE*)lParam;
            pageData = (WidgetNameData*)page->lParam;
            SetWindowLongPtr(hdlg, DWLP_USER, (LONG_PTR)pageData);

            ... initialize the page ...
            return TRUE;
        }

    ... other message handlers ...
    }
    return FALSE;
}

For a property sheet dialog procedure, the lParam of the WM_INIT­DIALOG points to a PROPSHEETPAGE structure, and you can pull out the lParam to access your private data.

Now, this gets kind of complicated if the property sheet page was created via Create­Prop­Sheet­Page, say, because it is a plug-in that is added dynamically into an existing widget property sheet.

HPROPSHEETPAGE CreateWidgetNamePage(HWIDGET widget)
{
    PROPSHEETPAGE page = {};

    page.dwSize = sizeof(page);
    page.hInstance = g_hinstThisDll;
    page.pszTemplate = MAKEINTRESOURCE(IDD_WIDGETNAMEPROP);
    page.pfnDlgProc = WidgetNameDlgProc;
    page.lParam = (LPARAM)&(what goes here?);

    return CreatePropertySheetPage(&page);
}

You can’t use a stack-allocated Widget­Name­Data because that will disappear once the Create­Widget­Name­Page function returns. You probably have to create a separate heap allocation for it, and pass a pointer to the heap allocation as the lParam, but now you also need to add a property sheet callback so you can remember to free the data when you get a PSPCB_RELEASE callback.

Exercise: Why is WM_DESTROY the wrong place to free the data? (Answer below.)

To avoid this extra hassle, you can use this one weird trick: Append your private data to the PROPSHEETPAGE, and the system will carry it around for you.

struct WidgetNameData : PROPSHEETPAGE
{
    HWIDGET widget;
    bool uppercaseOnly;
    int renameCount;
};

HPROPSHEETPAGE CreateWidgetNamePage(HWIDGET widget)
{
    WidgetNameData page = {};

    page.dwSize = sizeof(page);
    page.hInstance = g_hinstThisDll;
    page.pszTemplate = MAKEINTRESOURCE(IDD_WIDGETNAMEPROP);
    page.pfnDlgProc = WidgetNameDlgProc;

    // store the extra data in the extended page
    page.widget = widget;
    page.uppercaseOnly = IsWidgetPolicyEnabled(WidgetPolicy::UppercaseNames);
    page.renameCount = 0;

    return CreatePropertySheetPage(&page);
}

We append the data to the PROPSHEETPAGE by deriving from PROPSHEETPAGE and adding our extra data to it. The trick is that by setting the page.dwSize to the size of the entire larger structure, we tell the property sheet manager that our private data is part of the page. When the property sheet manager creates a page, it copies all of the bytes described by the dwSize member into its private storage (referenced by the returned HPROPSHEETPAGE), and by increasing the value of page.dwSize, we get our data copied there too.

Recall that the lParam parameter to the WM_INIT­DIALOG message is a pointer to a PROPSHEETPAGE. In our case, it’s a pointer to our custom PROPSHEETPAGE structure, and we can downcast to the specific type to access the extra data.

INT_PTR CALLBACK WidgetNameDlgProc(
    HWND hdlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    auto pageData = (WidgetNameData*)GetWindowLongPtr(
        hdlg, DWLP_USER);
    switch (uMsg)
    {
    case WM_INITDIALOG:
        {
            // the lParam points to our extended page
            pageData = (WidgetNameData*)lParam;
            SetWindowLongPtr(hdlg, DWLP_USER, (LONG_PTR)pageData);

            ... initialize the page ...
            return TRUE;
        }

    ... other message handlers ...
    }
    return FALSE;
}

In pictures: The traditional way creates a separate allocation that the PROPSHEETPAGE‘s lParam points to. When the system copies the PROPSHEETPAGE into private storage, the lParam is copied with it.

    passed to Create-
Prop­Sheet­Page
      copy passed to
WM_INITDIALOG
dwSize   PROPSHEETPAGE dwSize
dwFlags
      PROPSHEETPAGE dwSize
dwFlags
  dwSize
  lParam widget lParam
    uppercaseOnly  
          renameCount    

The new way makes the whole thing a giant Widget­Name­Data with a PROPSHEETPAGE at the top and bonus data at the bottom.

    passed to Create-
Prop­Sheet­Page
  copy passed to
WM_INITDIALOG
dwSize   PROPSHEETPAGE dwSize
dwFlags

lParam
  PROPSHEETPAGE dwSize
dwFlags

lParam
  dwSize
    widget
uppercaseOnly
renameCount
    widget
uppercaseOnly
renameCount

The catch here is that the bonus data is copied to the internal storage via memcpy, so it must be something like a POD type which can be safely copied byte-by-byte.

If you didn’t know about this trick, you would wonder why the lParam of the WM_INIT­DIALOG message points to the full PROPSHEETPAGE. After all, without this trick, the only thing you could do with the PROPSHEETPAGE pointer was access the lParam; all the other fields are explicitly documented as off-limits¹ during the handling of the WM_INIT­DIALOG message. Why bother giving you a pointer to a structure where you’re allowed to access only one member? Why not just pass that one member?

And now you know why: Because you actually can access more than just the lParam. If you hung extra data off the end of the PROPSHEETPAGE, then that data is there for you too.

Extending the PROPSHEETPAGE structure means that each PROPSHEETPAGE can be a different size, which makes it tricky to pass an array of them, which is something you need to do if you’re using the PSH_PROP­SHEET­PAGE flag. We’ll look at that problem next time.

Bonus chatter: This is an expansion of a previous discussion of the same topic. In that earlier topic, I said that the array technique requires all of the elements to be the same size. But next time, I’ll show how to create an array of heterogeneous types.

Bonus bonus chatter: This is similar to, but not the same as, the trick of adding extra information to the end of the OVERLAPPED structure. In the case of overlapped I/O, the same OVERLAPPED pointer is used; there is no copying. You can therefore put arbitrary complex data after the OVERLAPPED; they don’t have to be POD types. Of course, you have to be careful to destruct them properly when the I/O is complete.

In the case of a PROPSHEETPAGE, the memory is copied, so the data needs to be memcpy-safe. You could still use it to hold non-POD types, though, by treating it as uninitialized memory that the system conveniently preallocates for you. You’ll have to placement-construct the objects in their copied location, and manually destruct them when the property sheet page is destroyed.

Answer to exercise: If the property sheet page is never created (because the user never clicks on the tab for the page), then the dialog is never created, and therefore is never destroyed either. In that case, you don’t get any WM_DESTROY message, and the memory ends up leaked.

¹ The documentation cheats a bit and says that you cannot modify anything except for the lParam, when what it’s really saying is that you cannot modify any system-defined things except for the lParam. Your private things are yours, and you can modify them at will.

7 comments

Leave a comment

  • skSdnW

    It is perhaps not well-known because the documentation says otherwise. For PROPSHEETPAGE::dwSize MSDN currently says: Size, in bytes, of this structure. Since this struct has grown over time this is clearly not 100% correct but using a size larger than what Windows itself knows about is risky when the official documentation does not say you can use a larger size.

    Something like the OPENFILENAME struct does the complete opposite, not only can you not make it larger than what Windows expects but you also have to match the actual Windows version you are on and be mindful that your compiler defines/SDK version determines how large the struct is. Specifically, the size changed in Windows 2000 and older versions need the old size. This has been covered here on this blog before where the excuse is that people fail to initialize the size member so functions are coded defensively with == instead of >= size checks.

  • Martin Soles

    I always thought that the size parameters were there for internal purposes and should be treated as opaque. I had the notion that the kernel used this value to figure out versioning. As in, under 3.11, the size parameter was “10” to indicate that the structure contained 10 bytes in a specific format. Under Windows 2000, the parameter was “24” and indicated that the structure could be a completely different sequence of values. It never occurred to me to lie to the OS and say the size was bigger than the structure was and stuff extra goodness in there.

    I would hope that there was some minimal validation in there to make sure the size parameter meets some minimum. Imagine the fun that happens if you passed a value smaller than the structure size really was. That could be crash waiting to happen at some random point in the future.

    • acampbell

      That was also my understanding. It provides “mostly free compatibility” when Windows adds new fields. If you add your own fields in the place where Microsoft’s new fields would go, you’ll cause exactly the same kinds of compatibility headaches that Raymond often talks about. You might even have an oldnewthing blog post written about your application.

    • Raymond ChenMicrosoft employee

      PROPSHEETPAGE uses a different pattern, where you use the PSP_HAS* flags to indicate which fields contain information. This avoids the forward-compatibility problem, because if you extend the structure and add some custom thing, and then later a standard thing shows up at the same offset, your custom thing won’t set the PSP_HASNEWTHING flag, so the system ignores the custom thing.

      Maybe it was wrong for PROPSHEETPAGE to use this different pattern, but it does, so you have to deal with it.

  • 紅樓鍮

    …each PROPSHEETPAGE can be a different size, which makes it tricky to pass an array of them

    Your article at 20110318-00/?p=11183 states 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).

    I guess the solution is to use a std::tuple to hold the heterogeneous array of differently sized elements, each tagged with its own size, making it effectively an intrusive linked list that the framework code can walk. But what if the n’th element has a size that’s not a whole multiple of the (n+1)’st element’s alignment? The framework determines the latter’s address by adding the former’s tagged size to the former’s address, but that would be incorrect due to padding. (The framework has no way of knowing the (n+1)’st object’s alignment; it may well be 16 bytes.)