-
Notifications
You must be signed in to change notification settings - Fork 0
MS_WindowSystem
-
TOP > インフラストラクチャ > Windows > Windows OSの基礎的トピック
- カーネルモード・ユーザモード
- メモリ管理
- ファイルシステム
- ウィンドウ・システム
ウィンドウ・システムの詳細を理解して、
「バックグラウンド・スレッドから上げた
モーダル・ダイアログがモーダルにならない理由」
などのさまざまな挙動を理解できるようにする。
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 messageはUINT messageの誤記、 「ウインドウプロシ-ジャ(通常WinProcと書く)」は本文中の他の記述と合わせて 「WndProc」に統一した。
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スレッドを持つ場合、
画面間の通信をスレッドセーフに実装する必要があり実装し難くなる。
- 通常、1つのプロセスは、1つの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:ウィンドウハンドルを持つ)。
-
用語:ウィンドウクラスって?
ウィンドウクラスとは、ウィンドウの雛形(テンプレート)のようなもので、
対応するウィンドウ関数・アイコン・カーソル・背景色など基本的な情報が登録されている。
自前のウィンドウクラス名を持つウィンドウクラス(.NETで言う、Form的なもの)は、
WndProcを用意して、WNDCLASS 構造体にまとめて、RegisterClassExで登録する。
その時に自前のウィンドウクラス名を登録し、CreateWindowExで、ウィンドウを作る。
そのイベントは、すべて、WNDCLASS で登録した WndProc へ来る(これについては後述)。
あらかじめ登録されている既定のウィンドウクラスとしては
- "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:等を使用して追加する。 - 参考
// (2) ウィンドウプロシージャ
WndProc(
HWND hWnd,
UINT message,
WPARAM wParam,
LPARAM lParam)
{
・・・
}(元 Wiki では「???」のまま未記載)
-
GetWindowLong(hwnd, GWL_WNDPROC)を使って、今あるWndProc を取得して、 -
SetWindowLong(hwnd, GWL_WNDPROC, 自前WndProc)で置き換えて、- 元あったWndProcを呼ばなければ、そのイベントの処理を置き換え。
- 自前WndProc の中で、
CallWindowProc(取得したWndProc, ...)し、
元のWndProcを呼ぶことで、WndProcのチェインに割って入る。
この既定のWndProcを置き換えることで、
イベントの挙動(≠イベント・ハンドラ、ボタン押下時にボタンが凹む等)
をカスタマイズすることが可能である。
移行メモ(誤記): 原文の
CallWndProcはCallWindowProcの誤記 (本ページ後半の Win32API 節ではCallWindowProcと記載されている)。 なお 64bit 環境ではGetWindowLong/SetWindowLongではなくGetWindowLongPtr/SetWindowLongPtrを使う必要がある。
- WNDCLASS 構造体 — ウィンドウクラスの属性を表します。
- RegisterClassEx 関数 — ウィンドウクラスを登録します。
- CreateWindow 関数 — メモリ上にウィンドウを生成します。
- ShowWindow 関数 — ウィンドウを表示します。
- UpdateWindow 関数 — ウィンドウを更新します。
- GetWindowThreadProcessId 関数 — 指定されたウィンドウを作成したスレッドの ID を取得します。
移行メモ(誤記): 原文の見出しは「Win32PI」だったため「Win32API」に整えた。
PostMessageはメッセージ・ループにキューイングする。
- PostMessage 関数
- 指定されたウィンドウのメッセージキューに 1 つのメッセージをポストします。
- 対応するスレッドがメッセージを処理するのを待たずに制御を返します。
- PostThreadMessage 関数
- 指定されたスレッドのメッセージキューに 1 つのメッセージをポストします。
- この関数は、スレッドがメッセージを処理するのを待たず、即座に制御を返します。
- PostQuitMessage 関数
- WM_NCLBUTTONDOWN メッセージなどを取得した場合に、WM_QUIT メッセージをメッセージキューにポストする。
SendMessageはウィンドウ・プロシージャ(WndProc)を直接呼び出す。
- SendMessage 関数
- 1 つまたは複数のウィンドウへ、指定されたメッセージを送信します。
- 指定されたウィンドウのウィンドウプロシージャを呼び出します。
- ウィンドウプロシージャがメッセージを処理し終わった後で、制御を返します。
- 基本的には、サブルーチン的にイベント実装を処理するためのもの。
- スレッド跨ぎか否かで動作が大きく異なる。
- スレッド跨ぎでない場合、サブルーチン的にイベント実装を処理
- スレッド跨ぎの場合、送信・応答メッセージキューを使用する(同期呼出の実現)。
- なお、処理の優先度は、PostMessageより高い。
以下は、SendMessageの同期呼出ハングの問題を回避するための関数(送信側)。
- SendMessageTimeout 関数
- SendMessage のタイムアウト機能付きバージョン。
- 同一スレッドからの場合はタイムアウト値を無視する。
- SendMessageCallback 関数
- SendMessage のCallback機能付き非同期版(即座に制御を返す)
- オーバーラップウィンドウにメッセージをブロードキャストできる。
- SendNotifyMessage 関数
- PostMessageに近いが、以下の点がPostMessageと異なる。
- PostMessageよりも優先度が高い(先にキューから取り出される)。
- スレッド跨ぎでない場合、SendMessage的に動作する。
- PostMessageに近いが、以下の点がPostMessageと異なる。
以下は、SendMessageの同期呼出ハングの問題を回避するための関数(受信側)。
- ReplyMessage 関数
- SendMessage送信側の応答メッセージキューに結果をポストし、制御を返す。
- スレッド跨ぎでない場合、何もしない(falseを返す)。
- InSendMessage 関数
- スレッド跨ぎでない場合、falseを返す。
- スレッド跨ぎの場合、trueを返す。
移行メモ(誤記): 原文の「タイムアウト地」は「タイムアウト値」の誤記と解して整えた。
- 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 関数 — ウィンドウへ送信されたメッセージを処理する、アプリケーション定義のコールバック関数です。
- 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)と合わないため「入力キュー」に整えた。
- 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 関数 — 上記のマウスのキャプチャを停止する。
- AttachThreadInput 関数 — 複数のスレッドに複数のVIQとローカル入力状態を共有させる。
-
ただし、THREADINFO構造体の、
- ポストメッセージ
- 送信メッセージキュー
- 応答メッセージキュー
- ウェイクフラグ
については自分のものを使い続ける。
-
このAPIはシステムの堅牢性を損なう可能性がある。
-
用途としてはジャーナルの記録・再生フックをインストールした場合。
AttachThreadInput API関数で無理やり現在アクティブなスレッドに自分のスレッドをアタッチして、
その間にBringWindowToTop API関数で自分のフォームを前面に出します。
その後もう一度AttachThreadInput API関数でデタッチして解除すれば可能です。
もちろん、Windowsの仕様に反しますが。
-
UIオートメーションの裏側。
-
ちなみに、操作ログの取得って
- GetMessage のフックと
- AttachThreadInput の
2つの方法がありそうですが、現行はどっちを使用しているんでしょうか?
-
答えは、GetMessage のフックらしい。
例えばAttachThreadInputでSetWindowLongPtrを使用して
別プロセスのウィンドウをサブクラス化できるか?と言うとこれは当然できない。
これは、呼び出し元のWndProcのアドレスが呼び出し先のプロセスで有効でないから。
移行メモ(誤記): 原文の「GetMessege」は「GetMessage」の誤記。
-
レジストリエントリを使用してDLLを注入する。
ただし、問題としては汎用性がない点が挙げられる。
DLLを注入してSetWindowLongPtrを実行する。
※ AppInit_DLLs は、セキュア ブート有効時は無視される(Windows 8 以降)。
現在この手法は実質使えない。
別プロセスのウィンドウにGetMsgProcフックプロシージャをインストールしてメッセージを監視する。
GetMsgProcフックプロシージャは、前述のDLL注入のDLL中に実装しておく。
- 第一引数:インストール対象のフックタイプ
- 第二引数:関数ポインタ(フックタイプにより可変)
- 第三引数:上記関数を格納するDLLのモジュールハンドル
- 第四引数:フックすべきスレッド
ウィンドウのスレッドを取得するには、FindWindow → GetWindowThreadProcessId 関数を使用する。
-
フックタイプにはWH_GETMESSAGEを指定する。
- メッセージキューへポストされたメッセージを監視する1個のフックプロシージャをインストール。
- フックは、メッセージ系を処理するものが多数である。
その他、シェル、デバッガ、CBT用途のフックなどがある。
-
関数ポインタには、GetMsgProcフックプロシージャを指定する。
- システムは、アプリケーションのメッセージキューからメッセージを取得するときに、必ずこの関数を呼び出す。
- システムは、取得したメッセージを目的のウィンドウプロシージャへ渡す前に、そのメッセージをこのフックプロシージャに渡す。
- GetMsgProcを実装したDLLは、呼出元プロセスと同じ位置にロードされるよう努力されるが、
同じ位置でなければ、システムが自動的に呼出先プロセスの関数アドレスを算出する。 - これにより、「呼出先プロセス」でSetWindowLongPtrを実行できる。
- (ただし本来の用途とは異なる。通常は、メッセージを処理する追加処理を実装する)
-
・所定のプロセスに対する Windows メッセージの監視・捕捉
・所定のプロセスでの特定のイベントに呼応する自作コードの注入
・既存のアプリケーションの所作を変更 .etc -
また、フックチェーンを実装することもできる(CallNextHookEx)。
Spy++やWinspector Spyを使用すると、ウィンドウのメッセージの分析が可能。
- イベントの順番を確認する。
- 欲しいイベントのメッセージが来ているか?調べる。
- 当該イベントではどのようなメッセージが来るのか?調べる。
- SetCaptureにより別のマウスイベントが来てしまっていないか?確認する。
- SendMessage/PostMessageどちらで送られてきているか?確認する。
- IMEの漢字入力で挙動不審の場合、イベントの順番、WM_CHAR WM_KEYDOWN を調べる。
- 当該ウィンドウの
- ウィンドウ名を確認する。
- ウィンドウクラス名を確認する。
- ウィンドウ位置、大きさを確認する。
- WndProc の値が変わっているかを確認する。
移行メモ(誤記): 原文の「挙動不振」は「挙動不審」の誤記と解して整えた。
- スレッドを明示的に起動して、そこからForm等、新規ウィンドウを生成する。
- 以下は、暗黙的に、別スレッドからウィンドウを起動する例。
- async/await(非同期処理:
MS_AsynchronousProcessing.md)を使用する。 - System.Timers.Timerを使用する。
- System.Timers.Timer は精度が高い反面、別スレッドで実行される。
- [参考] C# の Timer 種類別 特徴 と 使い方
- async/await(非同期処理:
移行メモ(誤記): 原文の「制度が高い」は「精度が高い」の誤記と解して整えた。
ここまで、色々と説明してきたように、スレッド関係で色々な問題が発生する。
メッセージループを持つスレッドで同期呼出を行うと、
同一のスレッド(メッセージループ)を共有する全てのウィンドウがハングする。
そのため、サーバー呼出を別スレッドで処理を行うことがあるが、
以下の様な問題が発生するため、基本的に、UI処理はUIスレッドで処理するようにする。
移行メモ(正誤): 原文は「同一のスレッド(メッセージループ)を共有する 全てのスレッドがハングする」となっていたが、 1 スレッド = 1 メッセージキューであり「スレッドを共有するスレッド」は成立しない。 直前の作成単位の記述(1 UIスレッドが複数のウィンドウを処理する)から、 ハングするのは同じメッセージループを共有するウィンドウと解して整えた。
バックグラウンド・スレッドから上げたモーダル・ダイアログがモーダルにならない。
これは、UIスレッドと別スレッドのメッセージループが異なるので、
スレッド間のウィンドウはモーダル・ダイアログ的な動作にならないため。
- IMEはスレッド単位の初期化が必要になる。
- 基本的には、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版)。
- メッセージ / ウィンドウプロシージャ / 終了処理
- C による GUI アプリケーション開発(ウィンドウの生成、メッセージループ、ウィンドウプロシージャ ほか)
- コントロール基礎(子ウィンドウ、サブクラス化、オーナードロー、カスタムドロー ほか)
Tags: Windows, ウィンドウ・システム, プログラミング
このWikiは「Open棟梁Project」,「OSSコンソーシアム 開発基盤部会」によって運営されています。