「マイクロソフト系技術情報 Wiki」は、「Open棟梁Project」,「OSSコンソーシアム .NET開発基盤部会」によって運営されています。
目次 †
概要 †
ウィンドウ・システムの詳細を理解して、
「バックグラウンド・スレッドから上げた
モーダル・ダイアログがモーダルにならない理由」
などのさまざまな挙動を理解できるようにする。
テンプレート †
Windowsプログラムは、
- エントリポイント(プログラムの開始)である(1)WinMain?と、
- Windowsから呼び出されて渡されたメッセージを処理する
(2)ウインドウプロシ-ジャ(通常WinProc?と書く)
から成り立つ。
そして(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,
UNIT message,
WPARAM wParam,
LPARAM lParam )
{
・・・
}
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を使用する。
その他の利用方法 †
- プロセス間通信
- スレッド間通信
- 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的な)を貼り付けたければ、
自前で CreateWindow?す(自前のウィンドウクラス の hwnd,...)する。
ダイアログ †
VC++のダイアログエディタで、ダイアログリソースを作ってCreateDialog?する。
ダイアログのイベントは、WndProc?ではなく、DlgProc? へ来る(これについては後述)。
ウィンドウプロシージャ †
イベントハンドラみたいなもの。
以下のように、イベントハンドラを実装する。
- Control的なウィンドウクラスの標準WndProc?は、
親ウィンドウクラスに対してSendMessageでWM_Commandを送るようになっている。
- Form的な親ウィンドウのカスタムWndProc?に、
WM_Commandを処理するカバレージをcase WM_COMMAND:等を使用して追加する。
WndProc? †
//(2) ウィンドウプロシージャ
WndProc (
HWND hWnd,
UNIT message,
WPARAM wParam,
LPARAM lParam )
{
・・・
}
DlgProc? †
???
サブクラス化 †
- GetWindowLong?(hwnd, GWL_WNDPROC) を使って、今あるWndProc? を取得して、
- SetWindowLong?(hwnd, GWL_WNDPROC, 自前WndProc?) で置き換えて、
- 元あったWndProc?を呼ばなければ、そのイベントの処理を置き換え。
- 自前WndProc? の中で、 CallWndProc?(取得したWndProc?, pMsg)し、
元のWndProc?を呼ぶことで、WndProc?のチェインに割って入る。
この既定のWndProc?を置き換えることで、
イベントの挙動(≠イベント・ハンドラ、ボタン押下時にボタンが凹む等)
をカスタマイズすることが可能である。
Win32PI †
ウィンドウの生成 †
メッセージの送信 †
PostMessageはメッセージ・ループにキューイングする。
SendMessageはウィンドウ・プロシージャ(WndProc?)を直接呼び出す。
- 基本的には、サブルーチン的にイベント実装を処理するためのもの。
- スレッド跨ぎか否かで動作が大きく異なる。
- スレッド跨ぎでない場合、サブルーチン的にイベント実装を処理
- スレッド跨ぎの場合、送信・応答メッセージキューを使用する(同期呼出の実現)。
以下は、SendMessageの同期呼出ハングの問題を回避するための関数(送信側)。
以下は、SendMessageの同期呼出ハングの問題を回避するための関数(受信側)。
メッセージ・ループで使用 †
GetMessage? †
while (GetMessage (&msg,NULL,0,0)) { /* メッセージループ */
TranslateMessage(&msg);
DispatchMessage(&msg);
}
- WaitForMultipleObjectsEx?
ウィンドウプロシージャで使用 †
WindowProc? †
- WM_DESTROY メッセージ
- ウィンドウプロシージャはその全てのメッセージに対応できなければならないため。
サブクラス化 †
ハードウェア入力モデル †
- 用語
- SHIQ(system hardware input queue:システムハードウェア入力キー)
- RIT(raw input thread:生入力スレッド)/(SHIQを待機、取出、変換)
- VIQ(virtual input queue:仮想入力キュー)/(THREADINFO構造体内)
- RITが接続するVIQのスレッドを識別する
- マウスポインタ:マウスカーソルの下のウィンドウのスレッド
- キーストローク:フォアグラウンド・ウィンドウのスレッド
- その他、Alt+Tabなどの特殊キー・シーケンスを処理する。
- スレッドローカル・入力状態管理変数(THREADINFO構造体内)
- キーボード入力、ウィンドウフォーカス情報
- フォーカス・ウィンドウ
- アクティブ・ウィンドウ
- 押下されているキー
- キャレット(入力カーソル)の状態
関連するWin32API †
フォーカス †
アクティブ化 †
フォアグラウンド化 †
キー入力状態 †
カーソル入力状態 †
VIQの共有 †
- ただし、THREADINFO構造体の、
- ポストメッセージ
- 送信メッセージキュー
- 応答メッセージキュー
- ウェイクフラグ
については自分のものを使い続ける。
- 用途としてはジャーナルの記録・再生フックをインストールした場合。
DLL注入とAPIフック †
UIオートメーションの裏側。
- ちなみに、操作ログの取得って
- GetMessege?のフックと
- AttachThreadInput?の
2つの方法がありそうですが、
現行はどっちを使用しているんでしょうか?
- 答えは、GetMessege?のフックらしい。
例えばAttachThreadInput?でSetWindowLongPtr?を使用して
別プロセスのウィンドウをサブクラス化できるか?と言うとこれは当然できない。
これは、呼び出し元のWndProc?のアドレスが呼び出し先のプロセスで有効でないから。
しかし、これを可能にする方法がある。
「DLL注入」と「APIフック」である。
DLL注入 †
APIフック †
別プロセスのウィンドウにGetMsgProc?フックプロシージャをインストールしてメッセージを監視する。
GetMsgProc?フックプロシージャは、前述のDLL注入のDLL中に実装しておく。
SetWindowsHookEx? 関数 †
http://msdn.microsoft.com/ja-jp/library/cc430103.aspx
- 第一引数:インストール対象のフックタイプ
- 第二引数:関数ポインタ(フックタイプにより可変)
- 第三引数:上記関数を格納するDLLのモジュールハンドル
- 第四引数:フックすべきスレッド
- ウィンドウのスレッドを取得するには、
FindWindow?→GetWindowThreadProcessId? 関数を使用する。
フックタイプと関数ポインタ †
- フックタイプにはWH_GETMESSAGEを指定する。
- メッセージキューへポストされた
メッセージを監視する1個のフックプロシージャをインストール。
- フックは、メッセージ系を処理するものが多数である。
その他、シェル、デバッガ、CBT用途のフックなどがある。
- 関数ポインタには、GetMsgProc?フックプロシージャを指定する。
詳細については、GetMsgProc?フックプロシージャの説明を参照。
- GetMsgProc?フックプロシージャ
http://msdn.microsoft.com/ja-jp/library/cc429822.aspx
- システムは、 または関数がアプリケーションのメッセージキューからメッセージを取得するときに、必ずこの関数を呼び出す。
- システムは、取得したメッセージを目的のウィンドウプロシージャへ渡す前に、そのメッセージをこのフックプロシージャに渡す。
- GetMsgProc?を実装したDLLは、呼出元プロセスと同じ位置にロードされるよう努力されるが、
同じ位置でなければ、システムが自動的に呼出先プロセスの関数アドレスを算出する。
- これにより、「呼出先プロセス」でSetWindowLongPtr?を実行できる。
- (ただし本来の用途とは異なる。通常は、メッセージを処理する追加処理を実装する)
スパイ †
Spy++やWinspector Spyを使用すると、ウィンドウのメッセージの分析が可能。
- イベントの順番を確認する。
- 欲しいイベントのメッセージが来ているか?調べる。
- 当該イベントではどのようなメッセージが来るのか?調べる。
- SetCapture?により別のマウスイベントが来てしまっていないか?確認する。
- SendMessage/PostMessageどちらで送られてきているか?確認する。
- IMEの漢字入力で挙動不振の場合、イベントの順番、WM_CHAR WM_KEYDOWN を調べる。
- 当該ウィンドウの
- ウィンドウ名を確認する。
- ウィンドウクラス名を確認する。
- ウィンドウ位置、大きさを確認する。
- WndProc? の値が変わっているかを確認する。
Spy++ †
Winspector Spy †
別スレッド †
別スレッドでウィンドウが起動する例 †
- スレッドを明示的に起動して、そこからForm等、新規ウィンドウを生成する。
- 以下は、暗黙的に、別スレッドからウィンドウを起動する例。
- async/awaitを使用する。
- System.Timers.Timerを使用する。
発生する問題の例 †
ここまで、色々と説明してきたように、スレッド関係で色々な問題が発生する。
サーバーの同期呼出でUIがハングする。 †
メッセージループを持つスレッドで同期呼出を行うと、
同一のスレッド(メッセージループ)を共有する全てのスレッドがハングする。
そのため、サーバー呼出を別スレッドで処理を行うことがあるが、
以下の様な問題が発生するため、基本的に、UI処理はUIスレッドで処理するようにする。
モーダル・ダイアログがモーダルにならない。 †
バックグラウンド・スレッドから上げたモーダル・ダイアログがモーダルにならない。
これは、UIスレッドと別スレッドのメッセージループが異なるので、
スレッド間のウィンドウはモーダル・ダイアログ的な動作にならないため。
IME制御がおかしくなる。 †
UI処理をUIスレッドで処理する方法 †
非同期処理が必要になっても、UI処理はUIスレッドで処理するようにする。
それには以下の技術を使用できる。
参考 †
書籍 †
msdn †
標準 Windows API †
Win32 API 階梯 †
Windowsプログラムの正しい雛形 †
WindowsAPI Programming †
EternalWindows? †
http://eternalwindows.jp/index.html
Tags: :Windows, :ウィンドウ・システム, :プログラミング