もくじ
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とメンバ関数を使えるようにできないか、試してみる。
やったこと
今回作成したコードはこちら。
上のアプリの簡易説明
今回作ってみた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の上にもう一つクラスをかぶせて、「ログイン時」「ログアウト時」などのイベントを生やしてやる形にした方がよいかもしれない。
参考
作成する際に元にしたstackoverflow
https://stackoverflow.com/questions/14292803/can-i-have-main-window-procedure-as-a-lambda-in-winmain
もっとわかりやすい日本語ページがあった。
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#で、見えないウインドウを作るときに参考にさせて頂いたページ