3

This article explains brilliantly the options to call a class member WndProc. I've seen this response in stackoverflow but the main problem associating class member WndProc after CreateWindow is that some messages will be lost (including the important WM_CREATE) as explained in the mentioned article.

My question: I would like to hear the opinion from an expert on which of the methods exposed below or new one is the best one (performance, maintanability, ...) to create a class member WndProc.

Briefing the two final solutions exposed in the article (suposing that it exists a Window class with WndProc method):

  1. Per-window data with this global pointer storage, protecting it with CRITICAL_SECTION to make it thread safe (extracted from here):

    // The helper window procedure
    // It is called by Windows, and thus it's a non-member function
    // This message handler will only be called after successful SetWindowLong call
    // We can assume that pointer returned by GetWindowLong is valid
    // It will route messages to our member message handler
    LRESULT CALLBACK WndProc2(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
      // Get a window pointer associated with this window
      Window *w = (Window *) GetWindowLong(hwnd, GWL_USERDATA);
      // It should be valid, assert so
      _ASSERT(w);
      // Redirect messages to the window procedure of the associated window
      return w->WndProc(hwnd, msg, wp, lp);
    }
    // The temporary global this pointer
    // It will be used only between CreateWindow is called and the first message is processed by WndProc
    // WARNING: it is not thread-safe.
    Window *g_pWindow;
    
    // Critical section protecting the global Window pointer
    CRITICAL_SECTION g_WindowCS;
    
    // The helper window procedure
    LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
      // Stash global Window pointer into per-window data area
      SetWindowLong(hwnd, GWL_USERDATA, (long) g_pWindow);
      // Unlock global critical section
      g_pWindow->HaveCSLock = false;
      LeaveCriticalSection(&g_WindowCS);
      // Reset the window message handler
      SetWindowLong(hwnd, GWL_WNDPROC, (long) WndProc2);
      // Dispatch first message to the member message handler
      return WndProc2(hwnd, msg, wp, lp);
    }
    

    And now we can create the window:

    InitializeCriticalSection(&g_WindowCS);
    // Enter the critical section before you write to protected data
    EnterCriticalSection(&g_WindowCS);
    
    // Set global Window pointer to our Window instance
    // Moved the assignment here, where we have exclusive access to the pointer
    g_pWindow = &w;
    
    // Set a flag indicating that the window has the critical section lock
    // Note: this must be executed after the above assignment
    g_pWindow->HaveCSLock = true;
    
    // Create window
    // Note: lpParam is not used
    HWND hwnd = CreateWindow(TEXT("BaseWnd"), TEXT("Hello, World!"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, 0, 0, hinst, 0);
    
    // Leave critical section if window creation failed and our window procedure hasn't released it
    if (g_pWindow->HaveCSLock)
      LeaveCriticalSection(&g_WindowCS);
    // Destroy critical section
    // In production code, you'd do this when application terminates, not immediately after CreateWindow call
    DeleteCriticalSection(&g_WindowCS);
    
  2. Using CBT hook procedure (extracted from here):

    // The helper window procedure
    // It is called by Windows, and thus it's a non-member function
    // This message handler will only be called after successful SetWindowLong call from the hook
    // We can assume that pointer returned by GetWindowLong is valid
    // It will route messages to our member message handler
    LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
    {
      // Get a window pointer associated with this window
      Window *w = (Window *) GetWindowLong(hwnd, GWL_USERDATA);
      // It should be valid, assert so
      _ASSERT(w);
      // Redirect messages to the window procedure of the associated window
      return w->WndProc(hwnd, msg, wp, lp);
    }
    
    // The CBT hook procedure
    // It is called during CreateWindow call before WndProc receives any messages
    // Its job is to set per-window Window pointer to the one passed through lpParam to CreateWindow
    LRESULT CALLBACK CBTProc(int code, WPARAM wp, LPARAM lp)
    {
      if (code != HCBT_CREATEWND) {
        // Ignore everything but create window requests
        // Note: generally, HCBT_CREATEWND is the only notification we will get,
        // assuming the thread is hooked only for the duration of CreateWindow call.
        // However, we may receive other notifications, in which case they will not be passed to other CBT hooks.
        return 0;
      }
      // Grab a pointer passed to CreateWindow as lpParam
      std::pair<Window *, HHOOK> *p = (std::pair<Window *, HHOOK> *) LPCBT_CREATEWND(lp)->lpcs->lpCreateParams;
      // Only handle this window if it wasn't handled before, to prevent rehooking windows when CreateWindow is called recursively
      // ie, when you create windows from a WM_CREATE handler
      if (p->first) {
        // Stash the associated Window pointer, which is the first member of the pair, into per-window data area
        SetWindowLong((HWND) wp, GWL_USERDATA, (long) p->first);
        // Mark this window as handled
        p->first = 0;
      }
      // Call the next hook in chain, using the second member of the pair
      return CallNextHookEx(p->second, code, wp, lp);
    }
    

    And now we can create the window:

    // Install the CBT hook
    // Note: hook the thread immediately before, and unhook it immediately after CreateWindow call.
    // The hook procedure can only process window creation nofitications, and it shouldn't be called for other types of notifications
    // Additionally, calling hook for other events is wasteful since it won't do anything useful anyway
    HHOOK hook = SetWindowsHookEx(WH_CBT, CBTProc, 0, GetCurrentThreadId());
    _ASSERT(hook);
    
    // Create window
    // Pass a pair consisting of window object pointer and hook as lpParam
    std::pair<Window *, HHOOK> p(&w, hook);
    HWND hwnd = CreateWindow(TEXT("BaseWnd"), TEXT("Hello, World!"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, 0, 0, hinst, &p);
    
    // Unhook first
    UnhookWindowsHookEx(hook);
    
Community
  • 1
  • 1
Miquel
  • 8,339
  • 11
  • 59
  • 82
  • Related: [Why cant my wndproc be in a class](https://stackoverflow.com/questions/17221815/why-cant-my-wndproc-be-in-a-class)? – Laurie Stearn Mar 29 '21 at 06:20

1 Answers1

5

I personally would not use either of these methods. The global variable approach works, but feels dirty. Especially with the lock. And the CBT hook is, well over the top. Although it points in the right direction.

The standard way to pass state information to your window procedure during creation is through lpParam parameter of CreateWindow or CreateWindowEx. So the technique is as follows:

  1. Pass your instance pointer in the lpParam parameter of CreateWindow or CreateWindowEx.
  2. Read this value in your WM_NCCREATE handler. That message supplies the information as part of the CREATESTRUCT struct.
  3. Still in WM_NCCREATE call SetWindowLongPtr to set the user data of the window to the instance pointer.
  4. All future calls to the window procedure can now obtain the instance pointer by calling GetWindowLongPtr.

Raymond Chen illustrates the details here: How can I make a WNDPROC or DLGPROC a member of my C++ class?

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • The Raymond Chen article is pretty much the same as my solution to this problem. +1. The only difference is that I have the `pThis->WndProc` function return a flag to indicate if `DefWindowProc` should be called (OK, it actually uses a struct to pass data into the function and back out again, but you get the idea) – Skizz Feb 12 '14 at 12:10
  • @DavidHeffernan thanks, I had the intuition that first method would be a little `horrible`. Correct me if I'm wrong: I initially discarted this solution as it will be always calling first the `static WndProc`, then `GetWindowLongPtr` and finally the `class member WndProc`. I suposed that the exposed methods in the question would perform better as they make windows to call directly the class member WndProc. Is it worth to do CBT hook in terms of performance? – Miquel Feb 12 '14 at 12:16
  • You cannot use a non-static class member as the `WndProc`. It is simply the wrong solution. So all solutions involve reading the instance with `GetWindowLongPtr`, and forwarding the call to a method of that instance. They will all perform exactly the same. – David Heffernan Feb 12 '14 at 12:30
  • Mmm I see my mistake: all three methods call `static WndProc` to `GetWindowLongPtr` and then `class member WndProc`. Unfortunate my last comment. – Miquel Feb 12 '14 at 12:36
  • The global variable method works fine. The value of the variable will be consumed when the first message arrives, well before the WM_CREATE handler runs. Hence it doesn't get clobbered by nested window creation. – arx Feb 12 '14 at 16:16
  • 2
    @Miguel: DavidHeffernan is wrong when he says it **IS NOT** possible to make a window message directly call a non-static class method as a handler. It **IS** possible, but it involves dynamically allocating small thunks to translate the call parameters. Borland/CodeGear/Embarcadero's VCL framework, and also the ATL framework, use this technique and it works very well. But it is an advanced technique that most people forget about, or choose not to mention, because there are simpler solutions, like the `Get/SetWindowLongPtr()` technique. – Remy Lebeau Feb 12 '14 at 16:50
  • @RemyLebeau could you provide a link or example for this? – Miquel Feb 12 '14 at 16:53
  • @RemyLebeau Is wrong when he says it is possible to do that. Windows calls a method of type `WNDPROC`. That is categorically not a non-static class method. The thunk that the VCL uses presents a function with `WNDPROC` signature to Windows and that function unpacks the instance, and forwards the call to the non-static class method. Remy perhaps doesn't fully understand how the VCL thunk works. – David Heffernan Feb 12 '14 at 16:56
  • @DavidHeffernan: Fine, using a thunk is not **direct** access, it is a proxy, but it does offer functionality that is very close to direct access. I know EXACTLY how the VCL's thunks work as I wrote a series of articles titled "Using Class Methods as API Callbacks" for [C++Builder Journal](http://bcbj.org/) explaining in detail exactly how they work (I never finished the final part of the series due to a system crash that wiped out the companion code I was writing for the last article). Links to the articles are on the "Articles" page of [my website](http://www.lebeausoftware.org). – Remy Lebeau Feb 12 '14 at 18:55
  • @Remy Perhaps you could delete the comment which erroneously said I was wrong – David Heffernan Feb 12 '14 at 19:38
  • FWIW, MFC uses the CBT method because that ensures that every window message can be hooked. – user1793036 Feb 13 '14 at 01:48
  • An implementation of Chen's article to the Pudeyev's "Hullo World" example posted as a question [here](http://stackoverflow.com/questions/36111009/lnk2001-in-how-can-i-make-a-wndproc-or-dlgproc-a-member-of-my-c-class). – Laurie Stearn Mar 20 '16 at 09:00
  • `WM_NCCREATE` may not be the first message sent to a window about to be created. Some windows receive `WM_GETMINMAXINFO` first, for example. Installing a CBT hook is guaranteed to pass **all** messages to a non-static window procedure, irrespective of message ordering. – IInspectable Feb 14 '17 at 16:28