|
本サイトは移転しました。旧アドレスからのリダイレクトは2025年03月31日(月)まで有効です。
|
🛈 | ✖ |
wxWidgetsライブラリのウィンドウズ実装に置けるイベント駆動メッセージループを考察する。
GUIを持つウィンドウズアプリケーションはメッセージループを持ち、ユーザーGUI操作などをメッセージとして受け取る。メッセージループは受け取ったメッセージをターゲットとなるウィンドウ(トップレベルウィンドウあるいはコントロール)へ送付(ディスパッチ)し、ウィンドウがメッセージを処理する(メッセージハンドラ)。アプリケーションは最大一つのメッセージループ(正確にはスレッド毎に最大一つ)を駆動する。モーダルダイアログは専用のメッセージループを駆動する。メッセージループ中でモーダルダイアログを起動すると、メッセージループ(外側)はモーダルダイアログのメッセージループ(内側)の終了を待つ。全てのウィンドウはメッセージハンドラを持つ。このメッセージハンドラはウィンドウプロシージャと呼ばれ、アプリケーション定義あるいはデフォルト設定による。ウィンドウプロシージャは第一仮引数をウィンドウハンドルとする関数で、複数ウィンドウで共有できる。ダイアログはダイアログプロシージャと呼ばれる別のメッセージハンドラを持つが、こちらは次項目で説明する。
以下にメッセージループを構成する主なウィンドウズAPI関数を示す。
以下にウィンドウプロシージャを構成する主なAPI関数を示す。なおWindowProcはアプリケーション定義のコールバック関数で要件(仮引数型、戻り値型、呼出し規約)が記載されている(名前もWindowProcに限定されない)。
以下にメッセージ送出に関する主なAPI関数を示す。
ウィンドウズAPI関数による標準的なメッセージループ実装をメインウィンドウのウィンドウプロシージャと共に例示する。ほぼC言語の範疇で書かれたソースコードであるが、API関数を強調するためC++のスコープ解決演算子::を使っている。
ダイアログはメインウィンドウから独立してポップアップするトップレベルウィンドウで、モードレス(ダイアログ表示中もメインウィンドウが操作できる)あるいはモーダル(ダイアログ表示中はメインウィンドウが操作できない)いずれかの動作を持つ。全てのウィンドウ(トップレベルウィンドウあるいはコントロール)はイベントハンドラとしてウィンドウプロシージャを持つが、ダイアログは加えてダイアログプロシージャを持つ。ウィンドウプロシージャとダイアログプロシージャは類似の名称だが異なった特徴を持ち混乱を招く。ウィンドウプロシージャとダイアログプロシージャを理解するため、まずはウィンドウズ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はモーダルダイアログを作成してそのメッセージループを起動し、メッセージループが終了したら返る。
ダイアログはウィンドウプロシージャでなくダイアログプロシージャがメッセージを処理するが、実はウィンドウプロシージャがダイアログプロシージャをコールしているに過ぎない。ダイアログに定義されているウィンドウプロシージャはDefDlgProcで、これは両者のアドレス値を比較すれば容易に確認できる。DefDlgProcはダイアログプロシージャをコールして、ダイアログプロシージャがfalseを返す場合にだけデフォルト処理を行う。DefWindowProcをウィンドウプロシージャのデフォルト処理と理解するならば、DefDlgProcをダイアログプロシージャのデフォルト処理と理解するのは誤りで、そもそもDefDlgProcとダイアログプロシージャは戻り値が異なる。ウィンドウプロシージャがDefWindowProcをコールするのと逆にDefDlgProcがダイアログプロシージャをコールするため、ダイアログプロシージャがDefDlgProcをコールすれば無限再帰となる。レイモンド・チェン(Raymond Chen)によるDefDlgProcを示すが、引用元のリンクはダイアログプロシージャの戻り値がLRESULTではなくINT_PTRである理由も説明する。
モードレスダイアログ表示中はメッセージループ先頭でIsDialogMessageでダイアログをターゲットとするメッセージを先に処理する。これはダイアログのキーボードフォーカス移動(タブキーなどによるフォーカス移動)を行うためであるが、それ以外は通常のウィンドウとしてメッセージを受け取る。
IsDialogMessageの必要性や働きに関する記述は無数にあるが、IsDialogMessageが何をするかを記述するものはほとんど見当たらないため、以下にサイト作成者の想像するコードを示す。ターゲットとなるウィンドウがダイアログに帰属するかどうかを確認し、キーボードフォーカス移動を行うメッセージのみを処理して他は通常のメッセージ処理関数に委ねる。ターゲットがダイアログ自身の場合もメッセージ処理関数に委ね、ウィンドウプロシージャであるDefDlgProcを介してダイアログプロシージャをコールする。
モーダルダイアログはDialogBoxXXXで作成する。DialogBoxXXXはメッセージループを起動して先頭でIsDialogMessageをコールし、モードレスダイアログ同等の処理を行う。モーダルダイアログをDialogBoxXXXによらず作成しようとすれば、こういった処理を自らコーディングする事になる。
ウィンドウズの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はメッセージがポストされるかウェイクアップ要求されるまでスリープする。アプリケーション終了によるループ終了はWM_QUITメッセージでなく、m_shouldExitフラグをセットしてウェイクアップ要求する。
キーボードフォーカス移動処理ウィンドウは多くはダイアログ(wxWidgetsならwxDialog)だが一般にはWS_EX_CONTROLPARENT拡張スタイルを持つウィンドウで、wxWidgetsではwxDialog以外にwxPanelなどが相当する。wxWindow::MSWProcessMessageは名称から想像されるものと異なりウィンドウズAPIのIsDialogMessage相当の処理を行うが以下の差異がある。
上記コードは最初にメッセージターゲットウィンドウ(wndThis)のMSWProcessMessageをコールし、処理されない場合は親ウィンドウを再帰コールする。MSWProcessMessageの概略を示す。
ウィンドウズ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イベントを生成送出する。
ダイアログ(wxDialog)もウィンドウプロシージャが置換されるため、MSWHandleMessageがメッセージ処理すると古いウィンドウプロシージャDefDlgProcはコールせず結果としてダイアログプロシージャもコールしない。メッセージ処理せずDefDlgProcをコールした場合もダイアログプロシージャとしてwxWidgetsの与えたwxDlgProc関数は何もせず、falseを返してDefDlgProcのデフォルト処理のみとなる。結局wxDialogのメッセージ処理は他のウィンドウと何ら変わらない。
ウィンドウズAPIの標準的な実装でモードレスダイアログはCreateDialogXXX、モーダルダイアログはDialogBoxXXXで作成するため、両者で同一のインスタンス(ウィンドウハンドル)を共有できない(DialogBoxXXXは開く際にウィンドウハンドルを作成し閉じる際に破棄する)。同一のウィンドウハンドルを共有するにはモードレスダイアログとして作成したウィンドウハンドルを用いてメッセージループをマニュアル実装する。wxDialogはこれを実装し、構築後にwxDialog::Showメンバ関数をコールすればモードレス動作、wxDialog::ShowModalメンバ関数をコールすればモーダル動作となり、後者はダイアログ以外のトップレベルウィンドウを無効化して一時的なメッセージループを起動する。いずれもキーボードフォーカス移動処理は前述のMSWProcessMessageが行う。
ウィンドウの基底クラスである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以外は無関係 |