How can I make a WNDPROC or DLGPROC a member of my C++ class?


Continuing my discussion of How can I make a callback function a member of my C++ class?

Common special cases for wanting to use a member function as a callback function are the window procedure and its cousin the dialog procedure. The question, then, is where to put the reference data.

Let's start with window procedures. The Create­Window function and its close friend Create­Window­Ex let you pass your reference data as the final parameter, prototyped as LPVOID lpParam. As noted in the documentation, that value is passed back to the window procedure by the WM_NC­CREATE and WM_CREATE messages as part of the CREATE­STRUCT structure. One of the first messages passed to a window is WM_NC­CREATE, so that's where we'll grab the reference data and save it for later.

You can follow along in this simple C++ program: The static window procedure handles the WM_NC­CREATE message by extracting the lpCreate­Params from the CREATE­STRUCT and saving it in the GWLP_USER­DATA window bytes. That value is a special per-window storage location provided for the benefit of the window procedure, and most people use it to store their context parameter for safekeeping.

If the message is not WM_NC­CREATE, then we retrieve the context parameter from where we had stashed it.

Either way, we end up with a copy of the context parameter. If you want your window procedure to be a member function, the natural choice for the context parameter is the this pointer for the instance. The static window procedure therefore tends to look like this:

LRESULT CALLBACK MyWindowClass::s_WndProc(
    HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
 MyWindowClass *pThis; // our "this" pointer will go here

 if (uMsg == WM_NCCREATE) {
  // Recover the "this" pointer which was passed as a parameter
  // to CreateWindow(Ex).
  LPCREATESTRUCT lpcs = reinterpret_cast<LPCREATESTRUCT>(lParam);
  pThis = static_cast<MyWindowClass*>(lpcs->lpCreateParams);
  // Put the value in a safe place for future use
  SetWindowLongPtr(hwnd, GWLP_USERDATA,
                   reinterpret_cast<LONG_PTR>(pThis));
 } else {
  // Recover the "this" pointer from where our WM_NCCREATE handler
  // stashed it.
  pThis = reinterpret_cast<MyWindowClass*>(
              GetWindowLongPtr(hwnd, GWLP_USERDATA));
 }

 if (pThis) {
  // Now that we have recovered our "this" pointer, let the
  // member function finish the job.
  return pThis->WndProc(hwnd, uMsg, wParam, lParam);
 }

 // We don't know what our "this" pointer is, so just do the default
 // thing. Hopefully, we didn't need to customize the behavior yet.
 return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

You pass the the this pointer to Create­Window as the last parameter, so that the window procedure can pick it up.

hwnd = CreateWindow(... other parameters..., this);

For dialog boxes, you can do basically the same thing. It's just that the bookkeeping is slightly different.

  • The ...Param versions of the dialog box functions are the ones which let you pass reference data.

  • The dialog procedure receives the reference data in the lParam passed with the WM_INIT­DIALOG.

  • The system-provided secret hiding place for dialog boxes is called DWLP_USER.
INT_PTR CALLBACK MyDialogClass::s_DlgProc(
    HWND hdlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
 MyDialogClass *pThis; // our "this" pointer will go here

 if (uMsg == WM_INITDIALOG) {
  // Recover the "this" pointer which was passed as the last parameter
  // to the ...Dialog...Param function.
  pThis = reinterpret_cast<MyDialogClass*>(lParam);
  // Put the value in a safe place for future use
  SetWindowLongPtr(hdlg, DWLP_USER,
                   reinterpret_cast<LONG_PTR>(pThis));
 } else {
  // Recover the "this" pointer from where our WM_INITDIALOG handler
  // stashed it.
  pThis = reinterpret_cast<MyDialogClass*>(
              GetWindowLongPtr(hdlg, DWLP_USER));
 }

 if (pThis) {
  // Now that we have recovered our "this" pointer, let the
  // member function finish the job.
  return pThis->DlgProc(hwnd, uMsg, wParam, lParam);
 }

 // We don't know what our "this" pointer is, so just do the default
 // thing. Hopefully, we didn't need to customize the behavior yet.
 return FALSE; // returning FALSE means "do the default thing"
}

The above code should look really familiar, since it's the same as the window procedure case, just with slightly different bookkeeping.

The resulting classes look like this:

class MyWindowClass
{
 ... other class stuff goes here ...

 // This is the static callback that we register
 static LRESULT CALLBACK s_WndProc(
    HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

 // The static callback recovers the "this" pointer and then
 // calls this member function.
 LRESULT WndProc(
    HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
};

void MyWindowClass::SomeMemberFunction()
{
 // to register the class
 WNDCLASS wc;
 ... fill out the window class as normal ...
 wc.lpfnWndProc = MyWindowClass::s_WndProc;
 wc.lpszClassName = TEXT("MyWindowClass");
 RegisterClass(&wc);

 // to create a window
 hwnd = CreateWindow(TEXT("MyWindowClass"),
                     ... other parameters as usual ...,
                     this);
}

class MyDialogClass
{
 ... other class stuff goes here ...

 // This is the static callback that we register
 static INT_PTR CALLBACK s_DlgProc(
    HWND hdlg, UINT uMsg, WPARAM wParam, LPARAM lParam);

 // The static callback recovers the "this" pointer and then
 // calls this member function.
 INT_PTR DlgProc(
    HWND hdlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
};

void MyDialogClass::SomeMemberFunction()
{
 // to create the dialog box
 DialogBoxParam(...  other parameters as usual ...,
                reinterpret_cast<LPARAM>(this));
}

Okay, I'll try to write something more interesting for next week. But at least I wrote this part down so I can point people at it in the future.

Bonus chatter: As commenter Ben noted last week, DDEML is another component that uses the implicit reference data model. In the DDEML case, you use Dde­Set­User­Handle to set the reference data and Dde­Query­Conv­Info to retrieve it.

(Various errors have been corrected based on comments, thanks everybody!)

Comments (22)
  1. Joshua says:

    I have a circa 1994 library that does this. The comments say this is not documented that the initial value is zero. I suppose it might as well be now.

  2. skSdnW says:

    There is also the (ATL) thunking model where a small assembly stub sets things up and just jumps to the correct place. Too bad Windows never provided a helper function to build those, it would have avoided the DEP ATL compatibility shim and probably saved a few pages since each toolkit would not have to create its own pages to store them…

  3. AC says:

    "Windows XP/2000:   The SetWindowLongPtr function fails if the window specified by the hWnd parameter does not belong to the same process as the calling thread."

    Does this mean calling from a different process succeeds on Vista+? That sounds bad. Or does this only include versions prior to XP/2000?

  4. David says:

    Since CreateWindow returns the window handle, is it not possible to immediately call SetWindowLongPtr to set "this" and avoid potentially dispatching early messages to DefWindowProc?

  5. @David: No, even the CreateWindow itself, before it returns, sends quite an amount of messages to this newly created window.

  6. IInspectable says:

    @David: If you want to make sure that your class member window procedure sees all messages, you need to set up a CBT hook, and initialize your reference data in the HCBT_CREATEWND event handler. This is essentially how MFC implements this pattern. Added benefit: This implementation is immune to changes in message ordering.

  7. Zenju says:

    > But how do you handle the error?

    Throw an exception, as always :)

    > The call cannot fail if parameters are valid

    Uhm… as an average dev with no internal knowledge of Win32 API implementation, is this something I am supposed to know? The conditions for errors are usually not documented on MSDN, so I treat API calls as a black box. Consequently I check even simple functions for errors to be on the safe side, whenever MSDN documents potential for failure.

    I think this is more of a style question: "How far would Raymond go with regards to error checking if he did not know any Windows internals."?

    [You can't throw an exception because that crosses a foreign stack frame. Even if you didn't know that the function succeeds with valid parameters, I would put it in the category of "don't check for errors you don't know how to handle." -Raymond]
  8. AsmGuru62 says:

    The 'hwnd' parameter used here:

    if (pThis) {

     // Now that we have recovered our "this" pointer, let the

     // member function finish the job.

     return pThis->WndProc(hwnd, uMsg, wParam, lParam);

    }

    it must be inside 'this' (must be done together with SetWindowLong call), so the code below slightly better:

    if (pThis) {

     // Now that we have recovered our "this" pointer, let the

     // member function finish the job.

     return pThis->WndProc(uMsg, wParam, lParam);

    }

    [But how does the class know what window handle to use if you don't pass it? -Raymond]
  9. Zenju says:

    > You can't throw an exception because that crosses a foreign stack frame

    I'm sorry, I should have pointed out that my question was more general:

    E.g. in my code the SetWindowLongPtr() is not called from the Win32 callback but directly after CreateWindow() which happens in a C++ class' constructor. So in my case it's safe to throw an exception. OTOH SetWindowLongPtr() must not be called any earlier, e.g. in the callback, because well… "this" is still under construction.

    [But if you wait until CreateWindow returns, then you lose the ability to respond to creation messages. -Raymond]
  10. IInspectable says:

    @Zenju: The call to SetWindowLongPtr() is called in the message handler for WM_NCCREATE. The window procedure is called with this message as part of the call to CreateWindow. If you throw an exception from the code that saves the pThis pointer you are crossing a foreign stack frame (namely that of CreateWindow, and APIs it calls).

    As for the "still under construction" bit: The moment you enter you constructor's body the object pointed to by "this" is fully constructed.

  11. Joshua says:

    [But how does the class know what window handle to use if you don't pass it? -Raymond]

    I can't imagine not already having it as a class level variable.

    [But how do you handle the error?]

    Return fail from WM_NCCREATE.

    [Sure, you can make it a class variable, and you need to remember to set it in your WM_NCCREATE handler, which must be done in the static part. The idea here was to make the member function have the same prototype as the original callback. If you want to fancy it up, then you can do that as a second step of the transformation. -Raymond]
  12. Zenju says:

    > if you wait until CreateWindow returns, then you lose the ability to respond to creation

    Yes, I don't need this msg type. Actually my scenario is to just create some invisible dummy window in order to respond to a single message type: DBT_DEVICEQUERYREMOVE. In high-level terms, I'm watching for the USB-unmount event. Buy hey, I was asking a general question… why are you dissecting my scenario? :)

  13. Zenju says:

    @IInspectable:

    What I meant with "still under construction" is that the class' invariants do not hold until the constructor finishes – in C++ terms, the object's life time has not yet begun. In my example this means it's not safe to call a member function that expects all invariants to be established.

  14. Zenju says:

    Is it paranoid to call SetWindowLongPtr() like the following?

    ::SetLastError(0);

    if (::SetWindowLongPtr(windowHandle, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this)) == 0)

       if (::GetLastError() != ERROR_SUCCESS)

           … handle error …

    [But how do you handle the error? (The call cannot fail if parameters are valid. If the window handle is invalid, then it means that the window was destroyed while being created, at which point it's going to be destroyed anyway so it doesn't matter that we couldn't set our "this" pointer.) -Raymond]
  15. Neil says:

    When I first needed to store user data for a window it surprised me that WM_NCCREATE wasn't the first message sent to the window, and in particular you could get window messages that you wouldn't be able to respond effectively to. (As I recall, the one I wanted to be able to handle turned out to be one of the ones that gets sent too early. Fortunately later versions of Windows provide another message which allows you similar functionality, but is processed much later.)

    [I noted this some time ago. -Raymond]
  16. Steve says:

    What ever happened to the discussion here: blogs.msdn.com/…/384285.aspx

    MSDN says GWLP_USERDATA "is intended for use by the application that created the window."

    You said guidance is unclear and for safety's sake avoid it.

    So, should we be using the window extra bytes or is GWLP_USERDATA fine?

    [Read the whole sentence. "For safety's sake, then, you should just avoid it unless you can establish clear ownership." In most cases, clear ownership is easy to establish because the code that registered the class is the same as the code that created the window. The issue only gets murky if you have a control implemented in one component but consumed in another. -Raymond]
  17. Anonymous Coward says:

    @Zenju/Raymond:

    How's this for a tradeoff?

    if(::SetWindowLongPtr(windowHandle, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this)) == 0)

       abort();

  18. IInspectable says:

    @AC: Seriously!? If GWLP_USERDATA hasn't been changed yet, terminate the process. If someone else already changed it, destroy it and continue. I can see why you prefer to remain anonymous.

  19. Anonymous Coward says:

    @IInspectable: The point is that you can't handle the error in any meaningful way. MSDN says the initial value is 0. If the initial value is not zero, either something went horribly wrong in the OS, or your program already set it (which means something went horribly wrong in your program).

    And oh, yes. I mistakenly didn't copy the other lines from Zenju. Here's the fixed version:

    ::SetLastError(0);

    if (::SetWindowLongPtr(windowHandle, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this)) == 0)

      if (::GetLastError() != ERROR_SUCCESS)

         abort();

  20. IInspectable says:

    @AC: If SetWindowLongPtr() returns a value != 0 it means one thing, and one thing only: *Someone* changed the value stored in the window memory area at offset GWLP_USERDATA. There are no restrictions at all, any program can change the stored value of any window of the same desktop. Your conclusion, that something went horribly wrong in your program is unfounded.

    Worse, though, your conclusion that – if you cannot handle an error in any meaningful way – one should take the most extreme measure and terminate the calling process, without doing any cleanup, or running any destructors. This is certainly not anything one should do. Instead, you could quit the window creating process and report an error by pass NULL to the caller of CreateWindow().

    [There's no point trying to detect somebody maliciously attacking your window. They can screw with you in plenty of other more effective ways beyond tweaking your window bytes. -Raymond]
  21. Patrick says:

    To make the link between the HWND and 'this' in your own window class, you can stash 'this' with SetMessageExtraInfo before calling CreateWindowEx(). Use a window procedure that just picks-up the 'this' pointer with GetMessageExtraInfo, tucks it away with GWL_USERDATA, and forwards handling the fist message to whatever instance method will handle it. Before returning from the fist message, swap out the window procedure for one that skips the housekeeping (since it's all done by now) for one that just picks-up 'this' from GWL_USERDATA and does whatever processing is required. Doing it this way gets the link between 'this' and HWND established on the first message to the window procedure without, whatever it might be.

    [SetMessageExtraInfo is too volatile for my tastes. Suppose there is a message hook, and the hook calls SetMessageExtraInfo too. Oops. -Raymond]
  22. Patrick says:

    "SetMessageExtraInfo is too volatile for my tastes …"

    Fair enough. Ones own TLS would work too, I suppose.

Comments are closed.