パソコンでプログラミングしよう ウィンドウズC++プログラミング環境の構築
1.6.3.6(15)
wxWidgetsとメッセージループ

wxWidgetsライブラリウィンドウズ実装に置けるイベント駆動メッセージループを考察する。

ウィンドウズのメッセージループ

メッセージループとウィンドウプロシージャ

GUIを持つウィンドウズアプリケーションはメッセージループを持ち、ユーザーGUI操作などをメッセージとして受け取る。メッセージループは受け取ったメッセージをターゲットとなるウィンドウ(トップレベルウィンドウあるいはコントロール)へ送付(ディスパッチ)し、ウィンドウがメッセージを処理する(メッセージハンドラ)。アプリケーションは最大一つのメッセージループ(正確にはスレッド毎に最大一つ)を駆動する。モーダルダイアログは専用のメッセージループを駆動する。メッセージループ中でモーダルダイアログを起動すると、メッセージループ(外側)はモーダルダイアログのメッセージループ(内側)の終了を待つ。全てのウィンドウはメッセージハンドラを持つ。このメッセージハンドラはウィンドウプロシージャと呼ばれ、アプリケーション定義あるいはデフォルト設定による。ウィンドウプロシージャは第一仮引数をウィンドウハンドルとする関数で、複数ウィンドウで共有できる。ダイアログはダイアログプロシージャと呼ばれる別のメッセージハンドラを持つが、こちらは次項目で説明する。

以下にメッセージループを構成する主なウィンドウズAPI関数を示す。

以下にウィンドウプロシージャを構成する主なAPI関数を示す。なおWindowProcはアプリケーション定義のコールバック関数で要件(仮引数型、戻り値型、呼出し規約)が記載されている(名前もWindowProcに限定されない)。

以下にメッセージ送出に関する主なAPI関数を示す。

ウィンドウズAPI関数による標準的なメッセージループ実装をメインウィンドウのウィンドウプロシージャと共に例示する。ほぼC言語の範疇で書かれたソースコードであるが、API関数を強調するためC++のスコープ解決演算子::を使っている。

  1. GetMessageはメッセージmsgがポストされるまでスリープする。
  2. モードレスダイアログg_hModelessDlgがあればIsDialogMessageでmsgのターゲットがg_hModelessDlgまたはそのコントロールであるかを確認する。そうであればmsgをターゲットへ送付しtrueを返してループ先頭へ戻り、そうでなければfalseを返し処理継続する。
  3. TranslateAcceleratorはmsgがアクセラレータキー入力であれば処理してtrueを返してループ先頭へ戻り、そうでなければfalseを返し処理継続する。
  4. TranslateMessageはmsgがキー入力であれば対応する文字入力に変換する。
  5. DispatchMessageはmsgをターゲットへ送付する。
  6. GetMessageはmsgが終了メッセージ(WM_QUIT)の場合のみfalseを返しメッセージループを終了する。
#include <tchar.h>
#include <windows.h>
LRESULT CALLBACK WindowProc(HWND,UINT,WPARAM,LPARAM);
HWND g_hModelessDlg=NULL;
int WINAPI WinMain(HINSTANCE hThisInstance,HINSTANCE hPrevInstance
,LPSTR lpszArgument,int nCmdShow) // アプリケーションエントリポイント
{
WNDCLASSEX wincl; // メインウィンドウのウィンドウクラス
wincl.hInstance=hThisInstance;
wincl.lpszClassName=_T("MyToplevelWindowClass");
wincl.lpfnWndProc=WindowProc; // ウィンドウプロシージャ定義
...
::RegisterClassEx(&wincl),true); // ウィンドウクラス登録
HWND hwnd=::CreateWindowEx(0,_T("MyToplevelWindowClass"),...); // ウィンドウ作成
::ShowWindow(hwnd,nCmdShow); // ウィンドウ表示
HACCEL hacc=::LoadAccelerators(...); // アクセラレータ取得
MSG msg;
while(::GetMessage(&msg,NULL,0,0)) // (1) メッセージループ
{ //
if (!(g_hModelessDlg&&::IsDialogMessage(g_hModelessDlg,&msg)) // (2)
&&!::TranslateAccelerator(hwnd,hacc,&msg)) // (3)
{ //
::TranslateMessage(&msg); // (4)
::DispatchMessage(&msg); // (5)
} //
} // (6)
return msg.wParam;
}
LRESULT CALLBACK WindowProc // メインウィンドウのウィンドウプロシージャ
(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam)
{
switch (msg)
{
case WM_COMMAND: // コマンド処理(メニュー、アクセラレータなど)
...
break;
...
case WM_DESTROY: // ウィンドウ解体時処理
::PostQuitMessage(0); // 終了メッセージ(WM_QUIT)をポスト
break;
default:
return ::DefWindowProc(hwnd,msg,wParam,lParam); // デフォルト処理
}
return 0;
}

ダイアログプロシージャ

ダイアログはメインウィンドウから独立してポップアップするトップレベルウィンドウで、モードレス(ダイアログ表示中もメインウィンドウが操作できる)あるいはモーダル(ダイアログ表示中はメインウィンドウが操作できない)いずれかの動作を持つ。全てのウィンドウ(トップレベルウィンドウあるいはコントロール)はイベントハンドラとしてウィンドウプロシージャを持つが、ダイアログは加えてダイアログプロシージャを持つ。ウィンドウプロシージャとダイアログプロシージャは類似の名称だが異なった特徴を持ち混乱を招く。ウィンドウプロシージャとダイアログプロシージャを理解するため、まずはウィンドウズAPIレベルでウィンドウを理解する。

ウィンドウはウィンドウクラスが定義する。ウィンドウクラスはウィンドウズAPI用語でC++のクラスと無関係だが、オブジェクト指向プログラミングをC言語で実現するものとして概念は似る。例えばあるボタンコントロールはButtonウィンドウクラスのあるインスタンスと見なせる。wxWidgetsのようなC++ラッパーライブラリのクラス構成も多くはこれをベースとする。ウィンドウクラスはWNDCLASSEX構造体で定義してRegisterClassExで登録する。Button、Edit、ListBoxなどのウィンドウクラスは事前に定義登録されている。ウィンドウはウィンドウクラスを指定してCreateWindowExで作成しハンドルと呼ばれる整数値(HWND型)で識別する。各ウィンドウはウィンドウクラスのWNDCLASSEXコピーを保持し、メンバの一部はGetWindowLongPtr/SetWindowLongPtrで取得/設定できる。ウィンドウプロシージャはウィンドウクラス共通にWNDCLASSEXのlpfnWndProcメンバで設定するが、ウィンドウ個別にはGetWindowLongPtr/SetWindowLongPtrのGWLP_WNDPROCインデックスで取得/設定できる。

通常のウィンドウにダイアログプロシージャは不要でその記憶領域さえ持たない。ダイアログはWNDCLASSEXのcbWndExtraメンバにDLGWINDOWEXTRA(=30)を設定して記憶領域を拡張しダイアログプロシージャなどを記憶する。ダイアログプロシージャはGetWindowLongPtr/SetWindowLongPtrのDWLP_DLGPROCインデックスで取得/設定できる。ただし普通はダイアログをCreateWindowExでなく専用関数で作成するのでこれらを意識する必要は無い。専用関数はモードレスダイアログ(CreateDialogXXX)、モーダルダイアログ(DialogBoxXXX)それぞれに複数(XXX、XXXParam、XXXIndirect、XXXIndirectParam)用意するが全てダイアログプロシージャを仮引数に受ける。CreateDialogXXXはモードレスダイアログを作成してそのハンドルを返す。DialogBoxXXXはモーダルダイアログを作成してそのメッセージループを起動し、メッセージループが終了したら返る。

覚え書き
CreateDialogとCreateDialogIndirectのリンク先マイクロソフトドキュメントは戻り値をvoid型とするがこれは誤りで、どちらも作成されたダイアログのハンドル(HWND型)を返す。

ダイアログはウィンドウプロシージャでなくダイアログプロシージャがメッセージを処理するが、実はウィンドウプロシージャがダイアログプロシージャをコールしているに過ぎない。ダイアログに定義されているウィンドウプロシージャはDefDlgProcで、これは両者のアドレス値を比較すれば容易に確認できる。DefDlgProcはダイアログプロシージャをコールして、ダイアログプロシージャがfalseを返す場合にだけデフォルト処理を行う。DefWindowProcをウィンドウプロシージャのデフォルト処理と理解するならば、DefDlgProcをダイアログプロシージャのデフォルト処理と理解するのは誤りで、そもそもDefDlgProcとダイアログプロシージャは戻り値が異なる。ウィンドウプロシージャがDefWindowProcをコールするのと逆にDefDlgProcがダイアログプロシージャをコールするため、ダイアログプロシージャがDefDlgProcをコールすれば無限再帰となる。レイモンド・チェン(Raymond Chen)によるDefDlgProcを示すが、引用元のリンクはダイアログプロシージャの戻り値がLRESULTではなくINT_PTRである理由も説明する。

LRESULT CALLBACK DefDlgProc(
HWND hdlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
DLGPROC dp = (DLGPROC)GetWindowLongPtr(hdlg, DWLP_DLGPROC);
SetWindowLongPtr(hdlg, DWLP_MSGRESULT, 0);
BOOL_PTR fResult = dp(hdlg, uMsg, wParam, lParam);
if (fResult) return GetWindowLongPtr(hdlg, DWLP_MSGRESULT);
else ... do default behavior ...
}

モードレスダイアログ表示中はメッセージループ先頭でIsDialogMessageでダイアログをターゲットとするメッセージを先に処理する。これはダイアログのキーボードフォーカス移動(タブキーなどによるフォーカス移動)を行うためであるが、それ以外は通常のウィンドウとしてメッセージを受け取る。

IsDialogMessageの必要性や働きに関する記述は無数にあるが、IsDialogMessageが何をするかを記述するものはほとんど見当たらないため、以下にサイト作成者の想像するコードを示す。ターゲットとなるウィンドウがダイアログに帰属するかどうかを確認し、キーボードフォーカス移動を行うメッセージのみを処理して他は通常のメッセージ処理関数に委ねる。ターゲットがダイアログ自身の場合もメッセージ処理関数に委ね、ウィンドウプロシージャであるDefDlgProcを介してダイアログプロシージャをコールする。

// サイト作成者の想像コードなのであまり信用しないように
// ダイアログを子ウィンドウとするなどでキーボードフォーカス移動がネストする場合を無視している
// https://devblogs.microsoft.com/oldnewthing/20040730-00/?p=38293
BOOL IsDialogMessage(HWND hDlg,LPMSG lpMsg)
{
if (::GetAncestor(lpMsg->hwnd,GA_ROOT)==hDlg)
{
if (lpMsg->message==WK_KEYDOWN) // キーが押下された
{
switch ( lpMsg->wParam )
{
case VK_TAB: // タブキー
... // フォーカス移動
return true
case ... : // その他のフォーカス移動キー(矢印、リターンなど)
... // フォーカス移動
return true
default:
break;
}
}
::TranslateMessage(lpMsg);
::DispatchMessage(lpMsg);
return true;
}
return false;
}

モーダルダイアログはDialogBoxXXXで作成する。DialogBoxXXXはメッセージループを起動して先頭でIsDialogMessageをコールし、モードレスダイアログ同等の処理を行う。モーダルダイアログをDialogBoxXXXによらず作成しようとすれば、こういった処理を自らコーディングする事になる。

wxWidgetsの実装

ウィンドウズのC++ラッパーライブラリはメッセージループを含むウィンドウズAPIコールをC++オブジェクトで隠蔽する。wxWidgetsはマルチプラットフォーム対応でウィンドウズターゲットと他のターゲットでソースコードの大部分を共通化できるが、そのウィンドウズ実装がウィンドウズAPIコールを隠蔽するのは同様である。wxWidgetsのソースコードを解析しウィンドウズのメッセージループがどのように実装されているかを調査した。wxWidgetsはwxApp派生クラスでアプリケーションをC++オブジェクトとして扱い、wxWindow派生クラスでウィンドウ(トップレベルウィンドウあるいはコントロール)を扱う。アプリケーションインスタンスがメッセージループを実装し、ウィンドウインスタンスがメッセージハンドラを実装する。

メッセージループ

wxWidgetsアプリケーションのソースコードは表面的にはメインエントリ(ウィンドウズデスクトップアプリケーションならWinMain)を持たず、wxApp::OnInit仮想メンバ関数のオーバーライドが相当してメインウィンドウとなるwxWindow継承トップレベルウィンドウを構築表示する。メインエントリはwxIMPLEMENT_APPマクロあるいはIMPLEMENT_APPマクロに隠される。IMPLEMENT_APPはwxIMPLEMENT_APP;(末尾セミコロンに注目)に#defineされている。wxIMPLEMENT_APPはWinMainとアプリケーションインスタンスの構築関数(アプリケーションクラスコンストラクタをコール)を展開する。

WinMainはアプリケーションインスタンスを構築して以下の仮想メンバ関数をコールする。

wxApp仮想メンバ関数 機能 備考
OnInit メインウィンドウ構築 必ずオーバーライドする
OnRun メッセージループ実行 MainLoop仮想メンバ関数をコールする(オーバーライドしない)
OnExit 終了処理 必要ならオーバーライドする

MainLoopの処理は非常に複雑なので単純化した概略を示す。ウィンドウズAPI関数のPeekMessageはポストされたメッセージを取得するが、GetMessageと異なりメッセージが無い場合にスリープせずfalseを返す。MsgWaitForMultipleObjectsはメッセージがポストされるか他スレッドからウェイクアップ要求されるまでスリープする。

// 説明用概略コード
// 実際はwxAppではなく基底wxAppConsoleBaseクラスがwxGUIEventLoopクラスインスタンスで実装する
int wxApp::MainLoop()
{
MSG msg={0};
for (;;) // メッセージループ
{
while (!::PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // メッセージポストの確認
{
DWORD rc=::MsgWaitForMultipleObjects(...); // メッセージポストかウェイクアップがあるまでスリープ
switch (rc)
{
case WAIT_OBJECT_0: // 他スレッドからのウェイクアップ
continue;
case WAIT_OBJECT_0+1: // メッセージポスト
break;
}
if (msg->message==WM_QUIT)
{
break;
}
wxWindow* wndThis=wxGetWindowFromHWND(msg.hwnd); // ウィンドウハンドルからwxWindowポインタを取得
bool bProcess=false;
for (wxWindow* wnd=wndThis;wnd&&!bProcess;wnd=wnd->GetParent()) // キーボードフォーカス移動処理ウィンドウを探索
{
bProcess=wnd->MSWProcessMessage((WXMSG*)&msg); // キーボードフォーカス移動処理ウィンドウならIsDialogMessage相当処理
}
if (!bProcess) // メッセージがキーボードフォーカス移動処理ウィンドウで処理されない
{
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
}
return msg.wParam;
}

キーボードフォーカス移動処理ウィンドウは多くはダイアログ(wxWidgetsならwxDialog)だが一般にはWS_EX_CONTROLPARENT拡張スタイルを持つウィンドウで、wxWidgetsではwxDialog以外にwxPanelなどが相当する。wxWindow::MSWProcessMessageは名称から想像されるものと異なりウィンドウズAPIのIsDialogMessage相当の処理を行うが以下の差異がある。

  • IsDialogMessageはダイアログを第一実引数、任意のメッセージを第二実引数に与え、メッセージがダイアログをターゲットとする場合にのみ処理してtrueを返し、そうでない場合はfalseを返す。
  • MSWProcessMessageはインスタンス自ら(wnd)がキーボードフォーカス移動処理ウィンドウである場合にのみ処理してtrueを返し、そうでない場合はfalseを返す。

上記コードは最初にメッセージターゲットウィンドウ(wndThis)のMSWProcessMessageをコールし、処理されない場合は親ウィンドウを再帰コールする。MSWProcessMessageの概略を示す。

// 説明用概略コード
// 実際はwxWindowではなくwxWindowMSWクラスで定義されている
bool wxWindow::MSWProcessMessage(WXMSG* pMsg)
{
if (m_hWnd&&
HasFlag(wxTAB_TRAVERSAL)&&(wxGetWindowExStyle(this)&WS_EX_CONTROLPARENT)) // キーボードフォーカス移動処理ウィンドウ
{
MSG* msg=(MSG*)pMsg;
if (msg->message==WM_KEYDOWN )
{
bool bProcess=true;
switch (msg->wParam)
{
case VK_TAB: // タブキーの処理
...
break;
... // 矢印キー、リターンキーなどの処理
default:
bProcess=false;
break;
}
if (bProcess)
{
if (...) // キーボードフォーカス移動イベント(wxNavigationKeyEvent)送出
{
return true; // イベントがハンドラされた場合
}
}
}
// 実コードはバグパッチ処理してIsDialogMessageをコールするがここでは以下の理解で十分
::TranslateMessage(msg);
::DispatchMessage(msg);
return true;
}
return false;
}

ウィンドウズAPIによる標準的な実装との違いはIsMessageDialogの代わりにMSWProcessMessageを用いる点にある。API標準的実装はキーボードフォーカス移動イベントをモードレスダイアログからトップダウンで捉えるが、wxWidgets実装はメッセージからボトムアップで捉える。API標準的実装はメッセージループの外側でモードレスダイアログを管理するため不定数のモードレスダイアログを構築/解体する場合に面倒だが、wxWidgets実装のメッセージループはモードレスダイアログの存在を意識する必要が無い。

メッセージハンドラ

wxWidgetsにおいてトップレベルウィンドウ(wxFrameとwxDialog)はwxTopLevelWindowクラスを継承し、コントロールはwxControlクラスを継承し、どちらのクラスもwxWindowクラスを継承する。ウィンドウズ実装はwxTopLevelWindowをwxTopLevelWindowMSWクラスに、wxWindowをwxWindowMSWクラスに#defineする。これらのクラスはウィンドウズAPIのウィンドウハンドルを作成したらそのウィンドウプロシージャを共通のwxWinProc関数に置換し、置換する前の古いウィンドウプロシージャはメンバ変数に記憶する。wxWinProcはウィンドウハンドルからwxWindowポインタを取得してwxWindow::MSWWindowProcをコールする。wxWindow::MSWWindowProcはwxWindow::MSWHandleMessageに処理を委ね、MSWHandleMessageが処理しないメッセージはwxWindow::MSWDefWindowProcを介してメンバ変数に記憶された古いウィンドウプロシージャに渡す。MSWHandleMessageはウィンドウメッセージに従う処理を行うが、多くはwxWidgetsイベントを生成送出する。

// 説明用概略コード
// 実際はwxWindowではなくwxWindowMSWクラスで定義されている
WXLRESULT wxWindow::MSWWindowProc(WXUINT message,WXWPARAM wParam,WXLPARAM lParam)
{
WXLRESULT result;
if (!MSWHandleMessage(&result,message,wParam,lParam))
{
result=MSWDefWindowProc(message,wParam,lParam); // 古いウィンドウプロシージャをコール
}
return result;
}
bool wxWindow::MSWHandleMessage(WXLRESULT *result,WXUINT message,WXWPARAM wParam,WXLPARAM lParam)
{
bool processed = false;
...
switch ( message )
{
... // 多数のウィンドウズメッセージ処理(case WM_XXX: ... break;)、処理したらprocessed=true
}
if ( !processed ) return false;
...
return true;
}

ダイアログ(wxDialog)もウィンドウプロシージャが置換されるため、MSWHandleMessageがメッセージ処理すると古いウィンドウプロシージャDefDlgProcはコールせず結果としてダイアログプロシージャもコールしない。メッセージ処理せずDefDlgProcをコールした場合もダイアログプロシージャとしてwxWidgetsの与えたwxDlgProc関数は何もせず、falseを返してDefDlgProcのデフォルト処理のみとなる。結局wxDialogのメッセージ処理は他のウィンドウと何ら変わらない。

ウィンドウズAPIの標準的な実装でモードレスダイアログはCreateDialogXXX、モーダルダイアログはDialogBoxXXXで作成するため、両者で同一のインスタンス(ウィンドウハンドル)を共有できない(DialogBoxXXXは開く際にウィンドウハンドルを作成し閉じる際に破棄する)。同一のウィンドウハンドルを共有するにはモードレスダイアログとして作成したウィンドウハンドルを用いてメッセージループをマニュアル実装する。wxDialogはこれを実装し、構築後にwxDialog::Showメンバ関数をコールすればモードレス動作、wxDialog::ShowModalメンバ関数をコールすればモーダル動作となり、後者はダイアログ以外のトップレベルウィンドウを無効化して一時的なメッセージループを起動する。いずれもキーボードフォーカス移動処理は前述のMSWProcessMessageが行う。

wxWindow::MSWXXX仮想メンバ関数

ウィンドウの基底クラスであるwxWindowはウィンドウズ実装でwxWindowMSWに#defineされる。wxWindowMSWはマニュアルに記載されないメッセージを扱う仮想メンバ関数(MSWXXX)をいくつか持ち、派生クラスはこれをカスタマイズできる。主要なものを以下に示す。

分類 仮想メンバ関数 機能 備考
メッセージループ WXLRESULT MSWWindowProc (WXUINT,WXWPARAM,WXLPARAM) ウィンドウプロシージャ MSWHandleMessageをコールする
bool MSWHandleMessage (WXLRESULT*,WXUINT,WXWPARAM,WXLPARAM) ウィンドウプロシージャ実装 -
WXLRESULT MSWDefWindowProc (WXUINT,WXWPARAM,WXLPARAM) デフォルトウィンドウプロシージャ MSWHandleMessageが処理しない場合のデフォルト処理
bool MSWShouldPreProcessMessage (WXMSG*) 何もせずtrueを返す falseならMSWProcessMessageとMSWTranslateMessageを無効化
bool MSWProcessMessage (WXMSG*) メッセージ処理前にキーボードフォーカス移動 ウィンドウズAPIのIsDialogMessage相当
bool MSWTranslateMessage (WXMSG*) メッセージ処理前にアクセラレータキーを処理 ウィンドウズAPIのTranslateAccelerator相当
メッセージハンドラ bool MSWOnScroll (int,WXWORD,WXWORD,WXHWND) WM_HSCROLLとWM_VSCROLLメッセージ処理 -
bool MSWOnNotify (int,WXLPARAM,WXLPARAM*) WM_NOTIFYメッセージ処理 -
bool MSWOnDrawItem (int,WXDRAWITEMSTRUCT*) WM_DRAWITEMメッセージ処理 -
bool MSWOnMeasureItem (int,WXMEASUREITEMSTRUCT*) WM_MEASUREITEMメッセージ処理 -
ウィンドウ解体 void MSWDestroyWindow () 何もしない wxMDIChildFrame以外は無関係
覚え書き
MSWDestroyWindowは定義されているもののwxMDIChildFrameクラス以外は何もせず誰もコールしない。wxMDIChildFrameのみデストラクタでコールしてwxMDIChildFrame::MSWDestroyWindowがMDI子ウィンドウ解体時処理を行う。