クラス内にウインドウを持ったときに、WndProc内でthisが見えずクラスメンバ関数が呼べないのを何とかする(GetWindowLongPtr/SetWindowLongPtrとGWLP_USERDATAを使う)

もくじ
https://tera1707.com/entry/2022/02/06/144447

やりたいこと

ウインドウプロシージャの中で、クラスのthisポインターを使いたい。(thisポインタを使って、クラスのメソッドを呼び出したい)

・・・

C++のウインドウをもつデスクトップアプリだと、下記のようなウインドウプロシージャを持ってて、

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_COMMAND:
        {
            int wmId = LOWORD(wParam);
            // 選択されたメニューの解析:
            switch (wmId)
            {
            case IDM_EXIT:
                DestroyWindow(hWnd);
                break;
            default:
                return DefWindowProc(hWnd, message, wParam, lParam);
            }
        }
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

そのプロシージャ―を下記のように、ウインドウクラスに登録すると思う。

ATOM MyRegisterClass(HINSTANCE hInstance)
{
    WNDCLASSEXW wcex;    
    wcex.cbSize = sizeof(WNDCLASSEX);
    wcex.style          = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc    = WndProc;                              // ←★★コレで登録!★★
   ・
   ・
   ・
    return RegisterClassExW(&wcex);
}

ただ今回、自作クラスの中にウインドウを持たせて、そのウインドウのウインドウプロシージャ―から、自作クラスの関数を呼びたかった。

で、こういうコードを書いて、WndProcの登録をラムダ式にして、そのラムダでthisをキャプチャして、自作クラスの関数を呼べるようにしようとした。

// WNDCLASSEXW に登録する部分をラムダ式にした
wcex.lpfnWndProc = [this](HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -> LRESULT
    {
        switch (message)
        {
        case WM_COMMAND:
        {
            int wmId = LOWORD(wParam);
            // 選択されたメニューの解析:
            switch (wmId)
            {
            case IDM_EXIT:
                DestroyWindow(hWnd);
                break;
            default:
                return DefWindowProc(hWnd, message, wParam, lParam);
            }
        }
        break;
        case WM_DESTROY:
            PostQuitMessage(0);
            break;
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
        }
        return 0;
    };

そうすると、下図のようなエラーが出て、ビルドできなかった。

なぜそうなるかを調べたところ、

関数ポインタに代入できるラムダ式は、

  • staticな関数のみである。
  • キャプチャしないもののみである。

参考:C++のラムダ式の説明

なので、staticではなくキャプチャもしてしまうような今回のラムダ式は、関数ポインタには渡せない。

・・・

これの壁を越えられる方法が(staticな関数からクラスのthisを直接見る方法が)みつからなかったので、別の方法を探したところ、CreateWindowW()でウインドウを作るときに、関数の最後の引数に、ウインドウプロシージャ―側でWM_CREATE到来時に受け取ることができるパラメータを載せることができるというのを知った。

それを使ってthisポインタを渡す形で、ウインドウプロシージャ内でthisとメンバ関数を使えるようにできないか、試してみる。

やったこと

今回作成したコードはこちら。

github.com

上のアプリの簡易説明
今回作ってみたWindowMessageHandlerというクラスを使えば、

  • WindowMessageHandlerクラスをnewして、
  • 受信したいウインドウメッセージの番号と受信時に実行してほしいfunctionを登録すれば、
  • 「ウインドウ」の存在を意識しなくて済む(ウインドウを作ったり、表示したりしなくて済む)

という意図の実験アプリ。

やったのは、SetWindowLongPtr()とその引数GWLP_USERDATAを使って、ウインドウハンドルに紐づく情報を覚えておくという方法なので、要点は「GetWindowLongPtr(hwnd, GWLP_USERDATA);」と「SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pThis);」の部分。

実験コード(要点だけ抜粋)

/// <summary>
/// 指定のウインドウタイトル、ウインドウクラス名でウインドウを作成し、見えない状態で立ち上げる
/// </summary>
HWND WindowMessageHandler::CreateSpecifiedTitleClassWindow(const wchar_t* windowTitle, const wchar_t* className)
{
    const auto hInstance2 = GetModuleHandleW(nullptr);

    MsgLoopThread = std::thread([=]
        {
            WNDCLASS wc = { };
            wc.lpfnWndProc = WndProcForGetThisPtr;// ★ここで、staticなクラスメンバ関数を登録する
            wc.hInstance = hInstance2;
            wc.lpszClassName = className;

            RegisterClass(&wc);

            hWnd = CreateWindowEx(
                0,   
                className,
                windowTitle,
                WS_OVERLAPPEDWINDOW,       
                CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
                NULL,     
                NULL,     
                hInstance2, 
                (WindowMessageHandler*)this
            );

            ShowWindow(hWnd, SW_HIDE); // ★Hideにしているので見えないウインドウとなる

            MSG msg = { };
            while (GetMessage(&msg, NULL, 0, 0) > 0)
            {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
            OutputDebugString(L"Thread Finished.");
        });

    return 0;
}

/// <summary>
/// ★★★ここが要点★★★
/// クラス情報(thisポインタ)を取得するためにWndProcにかぶせたstaticな関数(.hに「static」と書いている)
/// WM_CREATEでlParamとしてわたってきたthisポインタをGWLP_USERDATAで保存する
/// 保存以降は、そのthisポインタを使う。
/// </summary>
LRESULT CALLBACK WindowMessageHandler::WndProcForGetThisPtr(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    WindowMessageHandler* pThis = (WindowMessageHandler*)GetWindowLongPtr(hwnd, GWLP_USERDATA);

    if (uMsg == WM_CREATE)
    {
        pThis = (WindowMessageHandler*)(((CREATESTRUCT*)lParam)->lpCreateParams);
        SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pThis);
        return pThis->WndProc(hwnd, uMsg, wParam, lParam);
    }

    if (pThis)
    {
        pThis->MyClassInstanceMethod();// ★ここでメンバ関数を呼んだ。クラスのthisポインタを使って、メンバ関数を呼ぶ!
        return pThis->WndProc(hwnd, uMsg, wParam, lParam);
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

/// <summary>
/// ウインドウプロシージャ―(ここは、代り映えしない、一般的なWndProc)
/// </summary>
LRESULT CALLBACK WindowMessageHandler::WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
        case WM_CREATE:
        {
            break;
        }
        case WM_DESTROY:
        {
            PostQuitMessage(0);
            return (LRESULT)0;
        }
    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

/// <summary>
/// 呼びたいクラスメソッド
/// </summary>
void WindowMessageHandler::MyClassInstanceMethod()
{
    // ・・・・・
}

WndProcForGetThisPtr()関数でGetWindowLongPtr()/SetWindowLongPtr()を使ってthisポインタを受けとって、それを使ってメンバ関数を呼んだ。これで、一旦やりたいことはできた。

思ったこと

そこから呼び出すWndProc()は、普通のWndProcと同じように

LRESULT WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

としてるが、

LRESULT WndProc(WindowMessageHandler* t, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

のように、引数でthis相当ポインタをもらって、WndProcの中でメンバメソッドを呼んで色々処理を行う、でもよいのかも。

今回作った実験アプリについて

今回、ウインドウやウインドウメッセージを意識せずにイベントを拾えるようにしたのだが、

今回作ったWindowMessageHandlerクラスは、まだWM_XXXXXというウインドウの世界の情報が見えてしまっているので、 例えば「電池残量変化」とか「ログイン」「ログアウト」などのイベントの「意味」を純粋に扱いたいのなら、WindowMessageHandlerの上にもう一つクラスをかぶせて、「ログイン時」「ログアウト時」などのイベントを生やしてやる形にした方がよいかもしれない。

github.com

参考

作成する際に元にしたstackoverflow

https://stackoverflow.com/questions/14292803/can-i-have-main-window-procedure-as-a-lambda-in-winmain

もっとわかりやすい日本語ページがあった。

https://akatukisiden.wordpress.com/2016/02/14/c-%E3%81%AB%E3%82%88%E3%82%8B-windows-%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%81%AE%E5%AD%A6%E7%BF%92-1-6-%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7/

SetWindowLongPtrA

https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-setwindowlongptra

GetWindowLongPtrA

https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-getwindowlongptra

C#で、見えないウインドウを作るときに参考にさせて頂いたページ

https://qiita.com/ikuzak/items/6feba393150b9fbec708