WindowProcの実装とメッセージループの仕組み
Windows GUIプログラムでは、ユーザーの操作やシステムの状態変化がメッセージとして通知されます。ウィンドウプロシージャであるWindowProcは、そのメッセージを受け取り、描画、入力処理、終了処理などを振り分ける中心的な関数です。
WindowProcだけを実装しても、メッセージを取り出して配送する仕組みがなければウィンドウは反応しません。メッセージキュー、メッセージループ、TranslateMessage、DispatchMessageが連携して、イベント駆動型の処理を成立させています。
Win32 APIの関数や定数は数が多く、戻り値や呼び出し順を取り違えると、表示されない、入力を受け取れない、終了できないといった問題につながります。まず各要素の役割を分けて理解すると、デバッグもしやすくなります。
| 要素 | 主な役割 | 代表的な処理 |
|---|---|---|
| メッセージキュー | スレッド宛ての通知を保持する | マウス、キーボード、タイマー |
| GetMessage | キューからメッセージを取得する | 待機しながら取り出す |
| TranslateMessage | キー入力を文字入力へ補助変換する | WM_CHARの生成 |
| DispatchMessage | 対象ウィンドウのWindowProcへ配送する | コールバック呼び出し |
| WindowProc | 個別メッセージに応答する | 描画、生成、破棄、終了 |
ウィンドウ生成からコールバック登録まで
最初にWNDCLASSEXまたはWNDCLASSへ、ウィンドウクラスの情報を登録します。ここでlpfnWndProcにWindowProcのアドレスを指定すると、そのクラスから生成されたウィンドウへ、対応するメッセージを渡せるようになります。クラス名、インスタンスハンドル、背景ブラシ、カーソルなども、この登録時に関連付けます。
その後、CreateWindowExを呼び出して実際のウィンドウを作成します。拡張スタイル、通常のスタイル、位置とサイズ、親ウィンドウ、メニュー、インスタンス、作成時データを指定できるため、引数の意味を整理しておくことが重要です。引数の組み合わせを確認するときは、Windows API資料も参照できます。
作成処理の途中では、WM_NCCREATEやWM_CREATEがWindowProcへ送られます。WM_NCCREATEではCREATESTRUCT構造体を通じてlpCreateParamsの値を受け取れるため、C++のオブジェクトや独自の状態をウィンドウへ結び付ける場面で利用されます。WM_CREATEでは子コントロールの生成や初期化を行うことが一般的です。
メッセージキューとスレッドの関係
メッセージキューは通常、GUI処理を担当するスレッド単位で扱われます。マウス移動やボタン操作のような入力メッセージは、対象ウィンドウを所有するスレッドへ届けられます。別スレッドで重い計算を続けている場合、そのスレッド自身のWindowProcが直接呼び出されるわけではありません。
GetMessageはキューが空の場合に待機し、メッセージが到着するとMSG構造体へ情報を格納します。戻り値が0ならWM_QUITを受け取った状態で、負の値ならエラーです。単純なループでは戻り値を条件式に利用しますが、失敗と終了を区別したい処理では、取得後にエラー状態も確認します。
PostMessageやSendMessageは似た名前を持ちますが、動作は異なります。PostMessageはメッセージをキューへ投入してすぐ戻る非同期処理です。一方、SendMessageは宛先のWindowProcが処理を終えるまで呼び出し元が待機します。異なるスレッド間でSendMessageを使うと、相互待機による停止に注意が必要です。
メッセージループが配送する流れ
典型的なメッセージループは、GetMessage、TranslateMessage、DispatchMessageの順で構成されます。GetMessageで取得したMSGには対象ウィンドウのハンドル、メッセージ番号、wParam、lParamなどが入っています。DispatchMessageはそのhwndを手掛かりに、登録済みのWindowProcを呼び出します。
TranslateMessageは文字入力を補助する関数です。キーボードメッセージを受け取ったとき、現在のキーボード状態を考慮してWM_CHARなどの文字メッセージを生成します。すべてのメッセージを文字に変換するわけではないため、変換後のメッセージをWindowProcで受け取る設計にします。
WindowProcでは、switch文を使ってメッセージを分岐する実装がよく使われます。処理しないメッセージはDefWindowProcへ渡すのが基本です。既定処理を省略すると、タイトルバーやサイズ変更、キーボード操作など、標準的なウィンドウ動作が失われる可能性があります。
WindowProcで扱う代表的な通知
WM_PAINTではBeginPaintとEndPaintを対にして描画領域を処理します。再描画が必要な範囲は更新領域として管理されるため、WM_PAINTを受け取るたびにクライアント領域全体を無条件で描くのではなく、PAINTSTRUCTの情報を意識すると効率を保ちやすくなります。
WM_COMMANDはボタンやメニューなどの操作通知に使われます。通知元のコントロールIDと通知コードをwParamから取り出し、lParamに含まれるコントロールのウィンドウハンドルと合わせて処理を分けます。WM_LBUTTONDOWN、WM_MOUSEMOVE、WM_KEYDOWNなどでは、lParamの座標やキー情報のビット配置にも注意が必要です。
終了時にはWM_CLOSE、WM_DESTROY、WM_QUITの関係を理解しておきます。WM_CLOSEを受けたWindowProcがDestroyWindowを呼ぶと、破棄の過程でWM_DESTROYが通知されます。そこでPostQuitMessageを呼ぶとスレッドのキューへWM_QUITが送られ、GetMessageが0を返してループが終了します。
実装時に確認したい設計上の要点
WindowProcの戻り値は、メッセージをどのように処理したかを示します。独自に処理したメッセージでは適切な値を返し、既定動作が必要なものはDefWindowProcの戻り値を返します。戻り値を常に0に固定すると、APIが期待する通知結果や標準動作を壊すことがあります。
ウィンドウ固有の状態を保持する方法も早い段階で決めます。SetWindowLongPtrでGWLP_USERDATAにポインターを保存する方法、WM_NCCREATEで初期データを関連付ける方法、グローバル変数を使う方法には、それぞれ適用範囲と寿命の違いがあります。64ビット環境ではポインターを格納するため、LONGではなくLONG_PTR系のAPIを使います。
安定した実装に向けて、次の点を確認すると原因の切り分けが容易になります。
- WindowProcのシグネチャをWNDPROCの定義と一致させる
- WNDCLASSEXのcbSizeを設定し、RegisterClassExの戻り値を確認する
- CreateWindowExの戻り値がNULLでないか検査する
- GetMessageの戻り値を終了とエラーに分けて扱う
- WM_PAINTではBeginPaintとEndPaintを必ず対にする
- WM_DESTROYでPostQuitMessageを呼ぶ位置を明確にする
デバッグと拡張処理の考え方
メッセージ処理の不具合は、WindowProcが呼ばれていないのか、呼ばれているが分岐条件が一致しないのか、既定処理へ渡すべきメッセージを失っているのかを分けて調べます。デバッガーでmessage、wParam、lParam、戻り値を確認すると、定数の取り違えやハンドルの誤りを発見しやすくなります。
コントロールや既存ウィンドウの動作を変更したい場合は、サブクラス化を使うことがあります。元のWindowProcを保存しておき、独自処理の後にCallWindowProcで元の処理へ戻す設計です。元のプロシージャを破棄したり、別のウィンドウへ誤って適用したりすると、入力や描画が不安定になります。
Unicode対応では、RegisterClassExW、CreateWindowExW、GetMessageWなどのワイド文字版を基準にすると、文字列の変換箇所を減らせます。CreateWindowExの引数やスタイル指定を詳しく整理する場合は、CreateWindowExの引数を確認し、ウィンドウ生成とメッセージ処理を別々の責務として設計します。
小さなサンプルでは、ウィンドウクラスの登録、ウィンドウ生成、メッセージループ、WindowProcの分岐を一つのファイルにまとめても構いません。ただし機能が増えたら、初期化、入力、描画、終了処理を関数やクラスへ分離し、UIスレッドを長時間ブロックしない構造へ移行します。まず最小構成のプログラムでWM_PAINTとWM_DESTROYの流れを確認し、次にボタン入力を一つ追加してデバッガーで各メッセージを追跡してください。