Skip to content

MS_WindowSystem

nishi_74322014 edited this page Aug 3, 2026 · 1 revision

ウィンドウ・システム

概要

ウィンドウ・システムの詳細を理解して、

「バックグラウンド・スレッドから上げた
モーダル・ダイアログがモーダルにならない理由」

などのさまざまな挙動を理解できるようにする。

テンプレート

Windowsプログラムは、

  • エントリポイント(プログラムの開始)である (1)WinMain と、
  • Windowsから呼び出されて渡されたメッセージを処理する (2)ウィンドウプロシージャ(通常WndProcと書く)

から成り立つ。

そして (1)WinMain の中は、

  • (A) ウィンドウクラスの登録
  • (B) ウィンドウの作成
  • (C) ウィンドウの表示
  • (D) メッセージループ

の4つのパートから成り立つ。

// (1) WinMain
WinMain(
  HINSTANCE hInstance,
  HINSTANCE hPrevInstance,
  LPSTR lpCmdLine,
  int nCmdShow)
{
  // ウィンドウクラス構造体を設定

  // (A) ウィンドウクラスの登録
  RegisterClassEx

  // (B) ウィンドウを作成
  CreateWindow

  // (C) ウィンドウを表示
  ShowWindow
  UpdateWindow

  // (D) メッセージループ
}

// (2) ウィンドウプロシージャ
WndProc(
  HWND hWnd,
  UINT message,
  WPARAM wParam,
  LPARAM lParam)
{
  ・・・
}

移行メモ(誤記): 原文コード中の UNIT messageUINT message の誤記、 「ウインドウプロシ-ジャ(通常WinProcと書く)」は本文中の他の記述と合わせて 「WndProc」に統一した。

Windowsメッセージキューとメッセージループ

Windowsメッセージキュー

MSMQではなく、ウィンドウ メッセージ用のキュー。
ウィンドウ・システム(WindowsのGUI)の根幹をなすメカニズム。

作成方法

  • Windowsメッセージキューは、1つのスレッドが1つだけ保持できる。
    • ウィンドウを作成したスレッドには、Windowsメッセージキューが与えられる。
    • スレッドからメッセージ系のAPIを使用した場合も、Windowsメッセージキューが与えられる。
  • この際、システムは、
    • THREADINFO構造体を作成してスレッドに割り当てる。
    • THREADINFO構造体はWindowsメッセージキューに関する情報を持っている。
項目(THREADINFO構造体) APIとの関連
ポストメッセージ PostMessage() で送信されるメッセージ
送信メッセージキュー SendMessage() で送信されるメッセージ、別スレッドの場合
応答メッセージキュー SendMessage() で送信されるメッセージ、別スレッドの場合
仮想入力キュー(ハードウェア入力)
ウェイクフラグ PostQuitMessage()
nExitCode PostQuitMessage()
スレッドローカル・入力状態管理変数

作成単位

  • Windowsメッセージキューは、複数のウィンドウ間で共有される。
    • 通常、1つのプロセスは、1つのUIスレッドで、複数(全て)のウィンドウを処理する。
      このため、通常、1プロセス、1UIスレッド、1Windowsメッセージキューになる。
    • この理由は、1プロセスで、2つ以上のUIスレッドを持つ場合、
      画面間の通信をスレッドセーフに実装する必要があり実装し難くなる。

利用方法

後述のWin32APIを使用する。

  • PostMessage() — メッセージ・ループにキューイングする。
  • SendMessage() — ウィンドウ・プロシージャ(WndProc)を直接呼び出す。

その他の利用方法

  • プロセス間通信
  • スレッド間通信
  • COMのSTA(呼び出しの直列化)

などでも利用される。

メッセージループ

GetMessage関数でWindowsメッセージキューから取り出したMSG 構造体を
DispatchMessage関数でウィンドウ・プロシージャに渡す。

while (GetMessage(&msg, NULL, 0, 0)) {  /* メッセージループ */
  TranslateMessage(&msg);
  DispatchMessage(&msg);
}

ウィンドウ的なもの

ウィンドウクラス

.NETで言う、FormもControlも全てが
ウィンドウ(hwnd:ウィンドウハンドルを持つ)。

  • 用語:ウィンドウクラスって?

    ウィンドウクラスとは、ウィンドウの雛形(テンプレート)のようなもので、
    対応するウィンドウ関数・アイコン・カーソル・背景色など基本的な情報が登録されている。

Form的なもの

自前のウィンドウクラス名を持つウィンドウクラス(.NETで言う、Form的なもの)は、
WndProcを用意して、WNDCLASS 構造体にまとめて、RegisterClassExで登録する。
その時に自前のウィンドウクラス名を登録し、CreateWindowExで、ウィンドウを作る。

そのイベントは、すべて、WNDCLASS で登録した WndProc へ来る(これについては後述)。

Control的なもの

あらかじめ登録されている既定のウィンドウクラスとしては

  • "BUTTON"
  • "COMBOBOX"
  • "EDIT"
  • "LISTBOX"
  • "MDICLIENT"
  • "RichEdit"
  • "RICHEDIT_CLASS"
  • "SCROLLBAR"
  • "STATIC"

がある(.NETで言う、Control的なもの)。

これらは登録済みで個別用途のウィンドウクラスであるため
(例えばボタンウィンドウやエディットボックスなど)、
RegisterClassEx関数を使用せずに作成可能。

自前のウィンドウクラス(Form的な)の中へ、
既定のウィンドウクラス(Control的な)を貼り付けたければ、
自前のウィンドウクラスの hwnd を親として CreateWindow する。

ダイアログ

VC++のダイアログエディタで、ダイアログリソースを作ってCreateDialogする。

ダイアログのイベントは、WndProcではなく、DlgProc へ来る(これについては後述)。

ウィンドウプロシージャ

イベントハンドラみたいなもの。

以下のように、イベントハンドラを実装する。

  • Control的なウィンドウクラスの標準WndProcは、
    親ウィンドウクラスに対してSendMessageでWM_COMMANDを送るようになっている。
  • Form的な親ウィンドウのカスタムWndProcに、
    WM_COMMANDを処理するカバレージを case WM_COMMAND: 等を使用して追加する。
  • 参考

WndProc

// (2) ウィンドウプロシージャ
WndProc(
  HWND hWnd,
  UINT message,
  WPARAM wParam,
  LPARAM lParam)
{
  ・・・
}

DlgProc

(元 Wiki では「???」のまま未記載)

サブクラス化

  • GetWindowLong(hwnd, GWL_WNDPROC) を使って、今あるWndProc を取得して、
  • SetWindowLong(hwnd, GWL_WNDPROC, 自前WndProc) で置き換えて、
    • 元あったWndProcを呼ばなければ、そのイベントの処理を置き換え。
    • 自前WndProc の中で、CallWindowProc(取得したWndProc, ...) し、
      元のWndProcを呼ぶことで、WndProcのチェインに割って入る。

この既定のWndProcを置き換えることで、
イベントの挙動(≠イベント・ハンドラ、ボタン押下時にボタンが凹む等)
をカスタマイズすることが可能である。

移行メモ(誤記): 原文の CallWndProcCallWindowProc の誤記 (本ページ後半の Win32API 節では CallWindowProc と記載されている)。 なお 64bit 環境では GetWindowLong / SetWindowLong ではなく GetWindowLongPtr / SetWindowLongPtr を使う必要がある。

Win32API

ウィンドウの生成

移行メモ(誤記): 原文の見出しは「Win32PI」だったため「Win32API」に整えた。

メッセージの送信

PostMessage

PostMessageはメッセージ・ループにキューイングする。

  • PostMessage 関数
    • 指定されたウィンドウのメッセージキューに 1 つのメッセージをポストします。
    • 対応するスレッドがメッセージを処理するのを待たずに制御を返します。
  • PostThreadMessage 関数
    • 指定されたスレッドのメッセージキューに 1 つのメッセージをポストします。
    • この関数は、スレッドがメッセージを処理するのを待たず、即座に制御を返します。
  • PostQuitMessage 関数
    • WM_NCLBUTTONDOWN メッセージなどを取得した場合に、WM_QUIT メッセージをメッセージキューにポストする。

SendMessage

SendMessageはウィンドウ・プロシージャ(WndProc)を直接呼び出す。

  • SendMessage 関数
    • 1 つまたは複数のウィンドウへ、指定されたメッセージを送信します。
    • 指定されたウィンドウのウィンドウプロシージャを呼び出します。
    • ウィンドウプロシージャがメッセージを処理し終わった後で、制御を返します。
    • 基本的には、サブルーチン的にイベント実装を処理するためのもの。
    • スレッド跨ぎか否かで動作が大きく異なる。
      • スレッド跨ぎでない場合、サブルーチン的にイベント実装を処理
      • スレッド跨ぎの場合、送信・応答メッセージキューを使用する(同期呼出の実現)。
    • なお、処理の優先度は、PostMessageより高い。

以下は、SendMessageの同期呼出ハングの問題を回避するための関数(送信側)。

  • SendMessageTimeout 関数
    • SendMessage のタイムアウト機能付きバージョン。
    • 同一スレッドからの場合はタイムアウト値を無視する。
  • SendMessageCallback 関数
    • SendMessage のCallback機能付き非同期版(即座に制御を返す)
    • オーバーラップウィンドウにメッセージをブロードキャストできる。
  • SendNotifyMessage 関数
    • PostMessageに近いが、以下の点がPostMessageと異なる。
      • PostMessageよりも優先度が高い(先にキューから取り出される)。
      • スレッド跨ぎでない場合、SendMessage的に動作する。

以下は、SendMessageの同期呼出ハングの問題を回避するための関数(受信側)。

  • ReplyMessage 関数
    • SendMessage送信側の応答メッセージキューに結果をポストし、制御を返す。
    • スレッド跨ぎでない場合、何もしない(falseを返す)。
  • InSendMessage 関数
    • スレッド跨ぎでない場合、falseを返す。
    • スレッド跨ぎの場合、trueを返す。

移行メモ(誤記): 原文の「タイムアウト」は「タイムアウト」の誤記と解して整えた。

メッセージ・ループで使用

GetMessage

  • MSG 構造体 — メッセージのデータを表します。
  • GetMessage 関数 — Windowsメッセージキューからメッセージを MSG 構造体として取得
  • PeekMessage 関数 — GetMessage 関数とは異なり、
    メッセージキューにメッセージがなかった場合、メッセージがポストされるのを待たずに制御を返す。
  • TranslateMessage 関数 — 仮想キーメッセージを文字メッセージへ変換(ショートカットの変換)
  • DispatchMessage 関数 — GetMessage関数で取得したMSG 構造体をウィンドウ・プロシージャに渡す。
while (GetMessage(&msg, NULL, 0, 0)) {  /* メッセージループ */
  TranslateMessage(&msg);
  DispatchMessage(&msg);
}
  • GetQueueStatus 関数 — 当該スレッドのWindowsメッセージキューの状態確認
  • WaitForMultipleObjects / WaitForMultipleObjectsEx(元 Wiki では説明が未記載)

ウィンドウプロシージャで使用

WindowProc

  • WindowProc 関数 — ウィンドウへ送信されたメッセージを処理する、アプリケーション定義のコールバック関数です。
  • DefWindowProc 関数
    • 既定のウィンドウプロシージャを呼び出して、既定の処理を提供します。
    • この関数は、すべてのメッセージが処理されることを保証します。
  • WM_DESTROY メッセージ
    • ウィンドウプロシージャはその全てのメッセージに対応できなければならないため。
  • PostQuitMessage 関数
    • WM_NCLBUTTONDOWN メッセージなどを取得した場合に、WM_QUIT メッセージをメッセージキューにポストする。

サブクラス化

  • IsWindowUnicode 関数 — ウィンドウが Unicode のネイティブウィンドウであるかどうかをチェック
  • SetWindowLongPtr 関数 — WndProcのサブクラス化
  • CallWindowProc 関数 — 元WndProcにメッセージを送信
  • .etc

ハードウェア入力モデル

  • 用語
    • SHIQ(system hardware input queue:システムハードウェア入力キュー)
    • RIT(raw input thread:生入力スレッド)/(SHIQを待機、取出、変換)
    • VIQ(virtual input queue:仮想入力キュー)/(THREADINFO構造体内)
  • RITが接続するVIQのスレッドを識別する
    • マウスポインタ:マウスカーソルの下のウィンドウのスレッド
    • キーストローク:フォアグラウンド・ウィンドウのスレッド
    • その他、Alt+Tabなどの特殊キー・シーケンスを処理する。
  • スレッドローカル・入力状態管理変数(THREADINFO構造体内)
    • キーボード入力、ウィンドウフォーカス情報
      • フォーカス・ウィンドウ
      • アクティブ・ウィンドウ
      • 押下されているキー
      • キャレット(入力カーソル)の状態
    • マウスカーソル管理情報
      • 表示/非表示の状態
      • キャプチャ
  • マウスによる切り替え
    • WM_KILLFOCUS

移行メモ(誤記): 原文の「システムハードウェア入力キー」は SHIQ の展開(input queue)と合わないため「入力キュー」に整えた。

関連するWin32API

フォーカス

  • SetFocus 関数
    • WM_SETFOCUS
    • VIQの切り替えはできない。
    • 通常、RITに接続されているときだけ呼び出す。
    • スレッド1がRITに接続しているときにスレッド2が自身のウィンドウにSetFocusを
      呼び出すと2つのフォーカス矩形が現れ問題(RITに接続するVIQは切り替わらないので)。
  • GetFocus 関数 — ローカル入力状態からフォーカスを持つウィンドウのHWNDを返す。

アクティブ化

  • SetActiveWindow 関数
    • ウィンドウをアクティブにする。
    • RITに接続するVIQの切り替えはできない。
    • 通常、RITに接続されているときだけ呼び出す。
  • GetActiveWindow 関数 — ローカル入力状態からアクティブなウィンドウのHWNDを返す。

フォアグラウンド化

  • BringWindowToTop 関数 — HWND_TOPを指定しSetWindowPosを呼び出す(RITに接続するVIQを変更可能)。
  • SetWindowPos 関数 — ウィンドウをアクティブにしてサイズ、位置、Zオーダを変更(RITに接続するVIQを変更可能)。
  • GetForegroundWindow 関数 — フォアグラウンド・ウィンドウのHWNDを返す。
  • SetForegroundWindow 関数
    • ウィンドウをフォアグラウンドに移動する(RITに接続するVIQを変更可能)
    • このAPIは、プログラマによって乱用されたため、
      MSはRITに接続されているか、RITに接続されている別スレッドが一定時間以上、
      入力を受け取っていない場合にのみ成功するように機能を追加した。
    • 失敗の際は、タスクバーをフラッシュさせる。
  • AllowSetForegroundWindow 関数 — 指定したプロセスのスレッドがSetForegroundWindowを呼び出すことを許可する。
  • LockSetForegroundWindow 関数 — 他のSetForegroundWindow呼出をロックする(Windowsスタートメニューなどで使用されている)。

移行メモ(誤記): 原文は一貫して「ForgroundWindow」と綴られていたが、 正しい API 名は ForegroundWindow(GetForegroundWindow 等)。

キー入力状態

  • GetKeyState 関数
    • 最後のキー入力を返す。
    • 入力フォーカスを持ったウィンドウを持っている必要はない。
  • GetAsyncKeyState 関数
    • キーが押されているか?前回の呼び出し以降にキーが押されたか?を返す。
    • 入力フォーカスを持ったウィンドウを持っていること。

カーソル入力状態

  • SetCursor 関数 — マウス・カーソルのセット(砂時計など)
  • ShowCursor 関数 — マウス・カーソルの表示・非表示
  • ClipCursor 関数 — マウス・カーソルの範囲を指定する(非同期的なアクティブ化イベントで中止)。
  • SetCapture 関数
    • マウス・カーソルがウィンドウの範囲内にあるかどうかに関わり無く、すべてのマウス入力を受け取る。
    • 一度に 1 つのウィンドウだけがマウスをキャプチャできる。
    • 通常はマウスのボタン押下後に呼び出す。
  • ReleaseCapture 関数 — 上記のマウスのキャプチャを停止する。

VIQの共有

  • AttachThreadInput 関数 — 複数のスレッドに複数のVIQとローカル入力状態を共有させる。
    • ただし、THREADINFO構造体の、

      • ポストメッセージ
      • 送信メッセージキュー
      • 応答メッセージキュー
      • ウェイクフラグ

      については自分のものを使い続ける。

    • このAPIはシステムの堅牢性を損なう可能性がある。

    • 用途としてはジャーナルの記録・再生フックをインストールした場合。

      AttachThreadInput API関数で無理やり現在アクティブなスレッドに自分のスレッドをアタッチして、
      その間にBringWindowToTop API関数で自分のフォームを前面に出します。
      その後もう一度AttachThreadInput API関数でデタッチして解除すれば可能です。
      もちろん、Windowsの仕様に反しますが。

DLL注入とAPIフック

UIオートメーションの裏側。

  • ちなみに、操作ログの取得って

    • GetMessage のフックと
    • AttachThreadInput の

    2つの方法がありそうですが、現行はどっちを使用しているんでしょうか?

  • 答えは、GetMessage のフックらしい。

    例えばAttachThreadInputでSetWindowLongPtrを使用して
    別プロセスのウィンドウをサブクラス化できるか?と言うとこれは当然できない。
    これは、呼び出し元のWndProcのアドレスが呼び出し先のプロセスで有効でないから。

    しかし、これを可能にする方法がある。
    DLL注入」と「APIフック」である。

移行メモ(誤記): 原文の「GetMessege」は「GetMessage」の誤記。

DLL注入

AppInit_DLLs は、セキュア ブート有効時は無視される(Windows 8 以降)。 現在この手法は実質使えない。

APIフック

別プロセスのウィンドウにGetMsgProcフックプロシージャをインストールしてメッセージを監視する。
GetMsgProcフックプロシージャは、前述のDLL注入のDLL中に実装しておく。

SetWindowsHookEx 関数

  • 第一引数:インストール対象のフックタイプ
  • 第二引数:関数ポインタ(フックタイプにより可変)
  • 第三引数:上記関数を格納するDLLのモジュールハンドル
  • 第四引数:フックすべきスレッド

ウィンドウのスレッドを取得するには、FindWindow → GetWindowThreadProcessId 関数を使用する。

フックタイプと関数ポインタ

  • フックタイプにはWH_GETMESSAGEを指定する。

    • メッセージキューへポストされたメッセージを監視する1個のフックプロシージャをインストール。
    • フックは、メッセージ系を処理するものが多数である。
      その他、シェル、デバッガ、CBT用途のフックなどがある。
  • 関数ポインタには、GetMsgProcフックプロシージャを指定する。

    • システムは、アプリケーションのメッセージキューからメッセージを取得するときに、必ずこの関数を呼び出す。
    • システムは、取得したメッセージを目的のウィンドウプロシージャへ渡す前に、そのメッセージをこのフックプロシージャに渡す。
    • GetMsgProcを実装したDLLは、呼出元プロセスと同じ位置にロードされるよう努力されるが、
      同じ位置でなければ、システムが自動的に呼出先プロセスの関数アドレスを算出する。
    • これにより、「呼出先プロセス」でSetWindowLongPtrを実行できる。
    • (ただし本来の用途とは異なる。通常は、メッセージを処理する追加処理を実装する)
  • Windowsに土足で乱入?! ~ フック関数の使い方

    ・所定のプロセスに対する Windows メッセージの監視・捕捉
    ・所定のプロセスでの特定のイベントに呼応する自作コードの注入
    ・既存のアプリケーションの所作を変更 .etc

  • また、フックチェーンを実装することもできる(CallNextHookEx)。

スパイ

Spy++やWinspector Spyを使用すると、ウィンドウのメッセージの分析が可能。

  • イベントの順番を確認する。
  • 欲しいイベントのメッセージが来ているか?調べる。
  • 当該イベントではどのようなメッセージが来るのか?調べる。
  • SetCaptureにより別のマウスイベントが来てしまっていないか?確認する。
  • SendMessage/PostMessageどちらで送られてきているか?確認する。
  • IMEの漢字入力で挙動不審の場合、イベントの順番、WM_CHAR WM_KEYDOWN を調べる。
  • 当該ウィンドウの
    • ウィンドウ名を確認する。
    • ウィンドウクラス名を確認する。
    • ウィンドウ位置、大きさを確認する。
    • WndProc の値が変わっているかを確認する。

Spy++

Winspector Spy

移行メモ(誤記): 原文の「挙動不」は「挙動不」の誤記と解して整えた。

別スレッド

別スレッドでウィンドウが起動する例

  • スレッドを明示的に起動して、そこからForm等、新規ウィンドウを生成する。
  • 以下は、暗黙的に、別スレッドからウィンドウを起動する例。
    • async/await(非同期処理: MS_AsynchronousProcessing.md)を使用する。
    • System.Timers.Timerを使用する。

移行メモ(誤記): 原文の「制度が高い」は「精度が高い」の誤記と解して整えた。

発生する問題の例

ここまで、色々と説明してきたように、スレッド関係で色々な問題が発生する。

サーバーの同期呼出でUIがハングする。

メッセージループを持つスレッドで同期呼出を行うと、
同一のスレッド(メッセージループ)を共有する全てのウィンドウがハングする。

そのため、サーバー呼出を別スレッドで処理を行うことがあるが、
以下の様な問題が発生するため、基本的に、UI処理はUIスレッドで処理するようにする

移行メモ(正誤): 原文は「同一のスレッド(メッセージループ)を共有する 全てのスレッドがハングする」となっていたが、 1 スレッド = 1 メッセージキューであり「スレッドを共有するスレッド」は成立しない。 直前の作成単位の記述(1 UIスレッドが複数のウィンドウを処理する)から、 ハングするのは同じメッセージループを共有するウィンドウと解して整えた。

モーダル・ダイアログがモーダルにならない。

バックグラウンド・スレッドから上げたモーダル・ダイアログがモーダルにならない。

これは、UIスレッドと別スレッドのメッセージループが異なるので、
スレッド間のウィンドウはモーダル・ダイアログ的な動作にならないため。

IME制御がおかしくなる。

  • IMEはスレッド単位の初期化が必要になる。
  • 基本的には、UI処理は、UIスレッドから処理するようにする。
  • 止む無く、別スレッドから処理する場合。

UI処理をUIスレッドで処理する方法

非同期処理が必要になっても、UI処理はUIスレッドで処理するようにする。

それには以下の技術を使用できる。

  • Control.Invoke、.BeginInvoke(非同期処理: MS_AsynchronousProcessing.md
  • async/await(非同期処理: MS_AsynchronousProcessing.md

参考

書籍

  • Advanced Windows 改訂第4版(Jeffrey Richter 著)
    • 第6部:ウィンドウシステム

※ 絶版。後継は『Windows via C/C++』(改訂第5版)。

Microsoft Learn

標準 Windows API

Win32 API 階梯

EternalWindows

  • コントロール基礎(子ウィンドウ、サブクラス化、オーナードロー、カスタムドロー ほか)

Tags: Windows, ウィンドウ・システム, プログラミング

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally