マイクロソフト系技術情報 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を使用する。

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

その他の利用方法

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

などでも利用される。

メッセージループ

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

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

ウィンドウ的なもの

ウィンドウクラス

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

  • 用語(7):ウィンドウクラスって?
    http://yonex1.cis.ibaraki.ac.jp/~yonekura/winclass.html

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

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

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

  • PostThreadMessage? 関数
    http://msdn.microsoft.com/ja-jp/library/cc410979.aspx
    • 指定されたスレッドのメッセージキューに 1 つのメッセージをポストします。
    • この関数は、スレッドがメッセージを処理するのを待たず、即座に制御を返します。

SendMessage

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

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

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

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

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

GetMessage?

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

  • WaitForMultipleObjects?
  • WaitForMultipleObjectsEx?

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

WindowProc?

  • WM_DESTROY メッセージ
    • ウィンドウプロシージャはその全てのメッセージに対応できなければならないため。

サブクラス化

  • .etc

ハードウェア入力モデル

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

関連するWin32API

フォーカス

  • SetFocus? 関数
    http://msdn.microsoft.com/ja-jp/library/cc411074.aspx
    • WM_SETFOCUS
    • VIQの切り替えはできない。
    • 通常、RITに接続されているときだけ呼び出す。   
    • スレッド1がRITに接続しているときに
      スレッド2が自身のウィンドにSetFocus?
      呼び出すと2つのフォーカス矩形が現れ問題。
      (RITに接続するVIQは切り替わらないので)

アクティブ化

フォアグラウンド化

  • SetForgroundWindow? 関数
    http://msdn.microsoft.com/ja-jp/library/cc411039.aspx
    • ウィンドウをフォアグラウンドに移動する(RITに接続するVIQを変更可能)
    • このAPIは、プログラマによって乱用されたため、
      MSはRITに接続されているか、RITに接続されている別スレッドが一定時間以上、
      入力を受け取っていない場合にのみ成功するように機能を追加した。
    • 失敗の際は、タスクバーをフラッシュさせる。

キー入力状態

カーソル入力状態

  • SetCapture? 関数
    http://msdn.microsoft.com/ja-jp/library/cc411051.aspx
    • マウス・カーソルがウィンドウの範囲内にあるかどうか
      に関わり無く、すべてのマウス入力を受け取る。 
    • 一度に 1 つのウィンドウだけがマウスをキャプチャできる。
    • 通常はマウスのボタン押下後に呼び出す。

VIQの共有

  • ただし、THREADINFO構造体の、
    • ポストメッセージ
    • 送信メッセージキュー
    • 応答メッセージキュー
    • ウェイクフラグ

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

  • このAPIはシステムの強度を損なう可能性がある。
  • 用途としてはジャーナルの記録・再生フックをインストールした場合。
    • RE#2347:アプリケーションの切替方法について
      http://www.gizcollabo.jp/vbtomo/log/archive/vbqanda_2302_2.html#Num2360-0

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

DLL注入とAPIフック

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

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

GetMessege?のフックと

AttachThreadInput?

2つの方法がありそうですが、

現行はどっちを使用しているんでしょうか?

答えは、GetMessege?のフックらしい。

↓↓↓

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

しかし、これを可能にする方法がある。

DLL注入」と「APIフック」である。

DLL注入

  • AppInit_DLLs による DLL インジェクション
    http://keicode.com/windows/win08.php

    レジストリエントリを使用してDLLを注入する。
    ただし、問題としては汎用性がない点が挙げられる。
    DLLを注入してSetWindowLongPtr?を実行する。

APIフック

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

SetWindowsHookEx? 関数

http://msdn.microsoft.com/ja-jp/library/cc430103.aspx

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

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

  • フックタイプにはWH_GETMESSAGEを指定する。
  • メッセージキューへポストされた
    メッセージを監視する1個のフックプロシージャをインストール。 
  • フックは、メッセージ系を処理するものが多数である。
    その他、シェル、デバッガ、CBT用途のフックなどがある。
  • 関数ポインタには、GetMsgProc?フックプロシージャを指定する。
    詳細については、GetMsgProc?フックプロシージャの説明を参照。  
    • GetMsgProc?フックプロシージャ
      http://msdn.microsoft.com/ja-jp/library/cc429822.aspx
      • システムは、 または関数がアプリケーションのメッセージキューからメッセージを取得するときに、必ずこの関数を呼び出す。
      • システムは、取得したメッセージを目的のウィンドウプロシージャへ渡す前に、そのメッセージをこのフックプロシージャに渡す。
      • GetMsgProc?を実装したDLLは、呼出元プロセスと同じ位置にロードされるよう努力されるが、
        同じ位置でなければ、システムが自動的に呼出先プロセスの関数アドレスを算出する。
      • これにより、「呼出先プロセス」でSetWindowLongPtr?を実行できる。
      • (ただし本来の用途とは異なる。通常は、メッセージを処理する追加処理を実装する)
  • Windowsに土足で乱入?! ~ フック関数の使い方
    http://dsas.blog.klab.org/archives/50829204.html

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

スパイ

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スレッドで処理するようにする。

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

Control.Invoke、.BeginInvoke

async/await

参考

書籍

msdn

標準 Windows API

Win32 API 階梯

Windowsプログラムの正しい雛形

WindowsAPI Programming

EternalWindows?

http://eternalwindows.jp/index.html


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


トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2018-09-07 (金) 12:52:54 (378d)