次の方法で共有


Managed Spy

Spy++ の能力を新規ツールとともに Windows Forms に提供する

Benjamin Wulfe


この記事で取り上げる話題:

  • 新しい ManagedSpy ユーティリティ
  • デバッグ時の ManagedSpy の動作方法およびどのように役立つかの理解
  • ManagedSpyLib の内部処理の概要
  • 単体テスト時の ManagedSpyLib の使用

この記事で使用する技術:

  • .NET Framework 2.0

Download the code for this article: ManagedSpy.exe (284KB)
翻訳元: Deliver The Power Of Spy++ To Windows Forms With Our New Tool (英語)


目次

  1. UI を見張る
  2. ManagedSpy の内部
  3. ManagedSpyLib の使用
  4. 基底コントロール プロパティへのアクセス
  5. 他の ControlProxy メソッド
  6. ウィンドウ フックの使用
  7. メモリマップド ファイルの使用
  8. ControlProxy の作成およびハンドル再作成
  9. 単体テストのための ManagedSpyLib
  10. まとめ

多くの開発者は Visual Studio で提供される Spy++ ツールを使用します。Spy++ を使用すると、実行しているアプリケーションのウィンドウ レイアウトを理解して、不具合を引き起こす、ある種のウィンドウ メッセージを特定することができます。ただし、Microsoft .NET Framework ベースのアプリケーションを作成する場合、Spy++ はさほど便利ではなくなります。それは、Spy++ により横取りされたウィンドウ メッセージおよびクラスが、開発者が使用するものと、あるいは確認するものとでさえも対応しないためです。開発者が本当に確認したいのは、マネージ イベントおよびプロパティ値です。

この記事は、ManagedSpy と呼ばれる新しいユーティリティおよびこれに関連するライブラリである ManagedSpyLib の使用方法について説明します。両方とも MSDNマガジン Web サイトからダウンロードすることができます。Spy++ によるウィンドウ クラス、スタイル、およびメッセージなどの Win32 の情報の表示方法と同様に、ManagedSpy は、マネージ コントロール、プロパティ、およびイベントを表示します。ManagedSpyLib により、別の処理内の Windows Forms コントロールにプログラムでアクセスすることができます。自分で作成したコードでプロパティの設定および取得と、イベントの同期を行うことができます。ManagedSpyLib はまた、テストの装置を作成してウィンドウ、メッセージ、およびイベントのログを取得することができます。

1. UI を見張る

クライアント アプリケーションを記述しているとき、従来のデバッガが便利ではない場合が多くあります。たとえば、不具合がフォーカスあるいは他の UI の特徴に伴って生じる場合、ブレークポイントに到達するたびにデバッガがこの状態を編集するためデバッグするのが難しくなります。デバッグが難しくなるもう 1 つの問題点は、レイアウトです。もしフォームに複雑で動的に変化するレイアウトがある場合、レイアウトのロジックが複数回呼び出されているかどうかは常に明らかであるわけではありません。これらの問題をデバッグするには、通常はイベントあるいはメッセージのログを用いて、どのような入力が UI に投入されているかを知る必要があります。

複雑な UI では、ウィンドウおよび関連する状態のビューがあると便利です。たとえば、関連するコントロール オブジェクトの位置をデバッガで特定するのは困難です。多くの場合は、あるデバッガの変数が UI で表示されている変数であることを推測しなければなりません。

図 1 は、いくつかのネストされたコントロールを持つダイアログ ボックスを示します。この例の目的としては、不具合がどのようなものであるかはさほど問題ではありませんが、このアプリケーションの右上部のテキストボックスには不具合があります。赤いテキストボックスがどのメンバであるかだけではなく、親階層および関連するコントロールのレイアウトについても特定しておくと便利です。

図 1 問題のあるダイアログ ボックス
図 1 問題のあるダイアログ ボックス

ManagedSpy は、このシナリオおよび他のシナリオに役立ちます。ManagedSpy は、作成した .NET ベースのクライアント アプリケーションにあるコントロールのツリービューを表示します。コントロールを選択して、そのプロパティを取得あるいは設定することができます。また、コントロールが発生するイベントをフィルタしたものをログに記録することもできます。デバッグに優れている一方、コントロールの互換性テストにも役立ちます。実際のアプリケーションおよびログ イベントを使用して、イベントの順序が次のバージョンのコントロールでも保持されていることを確認することができます。

初めて ManagedSpy を実行すると、ウィンドウの左側にツリービューで処理の一覧が表示され、右側に PropertyGrid が表示されます。処理を展開して、処理の上位レベルのウィンドウを表示することができます。

コントロールを選択すると、そのコントロールのプロパティが PropertyGrid に表示されます。プロパティの値の調査あるいは変更は、ここで行います。カスタム タイプは、バイナリで直列化可能である限りサポートされることに注意すべきです (基本的なシリアル化を参照)。

ツールバーには、どのイベントをイベント ペインにログ記録するかを選択したり、新しいウィンドウが作成されたときに TreeView をリフレッシュしたり、イベント ペインへのイベントのログ記録を開始あるいは終了したり、イベント ペインをクリアしたりするコマンドが含まれています。

図 1 に示すダイアログ ボックスについて、ManagedSpy は図 2 に示す情報を表示します。ManagedSpy によると、textBox1 は、SplitContainer (SplitContainer2) の子となり、SplitContainer は次に TableLayoutPanel (tableLayoutPanel1) の子となります。TableLayoutPanel の親は TabControl ですが、これは別の SplitContainer に含まれています。また、ManagedSpy は BackColor が赤色であることを示していることに注意してください。

図 2 ManagedSpy でのコントロールのデバッグ
図 2 ManagedSpy でのコントロールのデバッグ

[イベント] タブをクリックすると、ツリービューで現在選択されているコントロールでの MouseMove などのイベントが表示されます。イベントのログ記録を開始するには、[ログの開始] ボタンをクリックします。出力結果が図 3 に示されるように表示されます。

図 3 イベントのログ記録
図 3 イベントのログ記録

通常多くのマウス イベントが発生しています。これらのイベントあるいは他のイベントから、ログ記録が行われているイベントをフィルタして選択するには、[イベントのフィルタ] ボタンをクリックします。すると、どのイベントをログに記録するかを指定することができるダイアログ ボックスが表示されます。イベント フィルタ ダイアログには、Type コントロールの全イベントが一覧表示されます。派生クラスで宣言されるイベントは、カスタム イベントの選択により制御されます。

ページのトップへ


2. ManagedSpy の内部

ManagedSpy の主要なメソッドは、RefreshWindows と呼ばれます。このメソッドの役目は、TreeView にデスクトップ上で実行しているすべてのウィンドウおよび処理を設定することです。まず、TreeView をクリアし、システムの上位レベルのウィンドウのすべてについて再問い合わせを行います。

private void RefreshWindows() {
this.treeWindow.BeginUpdate();
this.treeWindow.Nodes.Clear();

ControlProxy[] topWindows =
Microsoft.ManagedSpy.
ControlProxy.TopLevelWindows;
    ...

上位レベルのウィンドウのコレクションが用意できたら、ManagedSpy は各ウィンドウを列挙し、マネージ ウィンドウの場合は、ツリービューに追加します。

if (topWindows != null && topWindows.Length > 0) {
    foreach (ControlProxy cproxy in topWindows) {
        TreeNode procnode;

        //マネージ ウィンドウのみを表示する
        if (cproxy.IsManaged) {

ここで、ManagedSpy は ManagedSpyLib で定義される ControlProxy クラスを使用しています。ControlProxy は、別の処理で実行しているウィンドウを表します。ウィンドウが実際は System.Windows.Forms.Control である場合、IsManaged は True になります。ManagedSpy は、.NET Framework ベースのコントロールの情報を表示するだけであるため、他のウィンドウ タイプは表示しません。

ManagedSpy は、上位レベルのマネージされる ControlProxy ごとに、所有している処理を検索します。処理が TreeView のノードを持つと、ManagedSpy はそれを新しい ControlProxy エントリの親 TreeNode として使用します。

Process proc = cproxy.OwningProcess;
if (proc.Id != Process.GetCurrentProcess().Id) {
    procnode = treeWindow.Nodes[proc.Id.ToString()];
    if (procnode == null) {
        procnode = treeWindow.Nodes.Add(proc.Id.ToString(),
            proc.ProcessName + "  " + 
            proc.MainWindowTitle + 
            " [" + proc.Id.ToString() + "]");
        procnode.Tag = proc;
    }
    ...

この時点で、procnode は所有している処理の TreeNode です。このタイトルは、System.Diagnostics.Process からの情報を使用して生成されます。ここで他に興味深い点といえば、ManagedSpy は、それ自身からウィンドウを表示しないようにしていることだけです。

最後に、ManagedSpy は procnode 配下に別の TreeNode を追加して、ウィンドウを表します (図 4 を参照)。ManagedSpy は ControlProxy.GetComponentName および ControlProxy.GetClassName を TreeNode のタイトルとして使用します。GetClassName は、Spy++ が表示するウィンドウ クラスではなく、リモート コントロールの System.Type を参照します。

図 4 ウィンドウ ノードを追加する

string name = String.IsNullOrEmpty(cproxy.GetComponentName()) ?
    "<noname>" :cproxy.GetComponentName();
TreeNode node = procnode.Nodes.Add(cproxy.Handle.ToString(), 
    name + "     [" + cproxy.GetClassName() + "]");
node.Tag = cproxy;
...
if (treeWindow.Nodes.Count == 0) 
{
    treeWindow.Nodes.Add("No managed processes running.");
    treeWindow.Nodes.Add("Select View->Refresh.");
}
this.treeWindow.EndUpdate();

TreeNode をいつ選択しても、ManagedSpy はそのツリーノードのタグを、右側に表示される PropertyGrid に配置します。このようにして、リモート コントロールにプロパティを表示します。以下のコードは、ManagedSpy が TreeView およびそのプロパティのすべてを表示する方法を示します。

private void treeWindow_AfterSelect(object sender, TreeViewEventArgs e)
{
this.propertyGrid.SelectedObject = this.treeWindow.SelectedNode.Tag;
this.toolStripStatusLabel1.Text = treeWindow.SelectedNode.Text;
StopLogging();
this.eventGrid.Rows.Clear();
StartLogging();
}

イベントがログに記録される方法について 1 ステップずつ説明しませんが、プロパティの表示が複雑ではないのと同様にこの処理も複雑ではありません。ManagedSpy は、選択された ControlProxy の EventFired イベントに登録します。このイベントが実行されると、新しい行が DataGridView コントロールに追加されて、データが表示されます (DataGridView コントロールは、.NET Framework 2.0 で新たに追加されたコントロールです)。

ページのトップへ


3. ManagedSpyLib の使用

ManagedSpy は、ManagedSpyLib と呼ばれるマネージ C++ ライブラリの上位に記述されます。ManagedSpyLib の目的は、別の処理の .NET Framework ベースのウィンドウにプログラム的にアクセスできるようにすることです。ManagedSpyLib は、別の処理内のコントロールを表す、ControlProxy と呼ばれるクラスを公開します。これは現実に存在するコントロールではありませんが、このコントロールが表すすべてのプロパティおよびイベントにアクセスすることができます。

ManagedSpyLib は、メモリマップド ファイルを使用して、見張っている側の処理と見張られている側の処理間でデータを転送することで動作します。これが動作するには、処理間で転送されたすべてのデータがバイナリで直列化可能でなければなりません。処理間の通信で使用される主要な方法は、カスタム ウィンドウ メッセージおよび SetWindowsHookEx です。これは、送信先のコードが、問い合わせを行う必要があるウィンドウを所有するスレッド上で確実に実行するようにします。ウィンドウを所有するスレッドから呼ばれたときのみ動作する操作が多くあるため、これは重要です。

ControlProxy を作成する方法は 2 つあります。1 番目は、ControlProxy.FromHandle を使用して、送信先の HWND を表す IntPtr をこのメソッドに渡す方法です。こうすると、送信先の ControlProxy が返されます。通常ウィンドウの HWND は、EnumWindows などの Win32 メソッドあるいは Spy++ などのアプリケーションを使用するときに見かけます。また、コントロールの Handle プロパティにアクセスすることによって HWND を取得できます。

2 番目は、ControlProxy.TopLevelWindows を使用する方法です。この静的メソッドを呼び出して、ControlProxy クラスの配列を取得します。デスクトップ上のすべての上位レベルのウィンドウについて、ControlProxy を取得することができます。ただし、これらのウィンドウのすべてが、マネージ コントロールにより表されるわけではありません。これを決定するには、ControlProxy のプロパティを調べて、これが確かにマネージ ウィンドウなのかどうかを確認します。取得可能な情報に関する詳細については、以下の [プロパティ] セクションを確認してください。図 5 は、処理ごとの上位レベルのウィンドウの数を一覧表示する例を提供します。

図 5 処理ごとにウィンドウを一覧する

using System;
using System.Text;
using System.Diagnostics;
using System.Windows.Forms;
using System.Collections.Generic;
using Microsoft.ManagedSpy;

class Program 
{
    static void Main(string[] args) 
    {
        Dictionary<int, int> topWindowCounts = 
            new Dictionary<int, int>();

        foreach (ControlProxy proxy in ControlProxy.TopLevelWindows) 
        {
            if (!topWindowCounts.ContainsKey(proxy.OwningProcess.Id)) 
            {
                topWindowCounts.Add(proxy.OwningProcess.Id, 0);
            }
            topWindowCounts[proxy.OwningProcess.Id]++;
        }

        foreach (int pid in topWindowCounts.Keys) 
        {
            Process p = Process.GetProcessById(pid);
            Console.WriteLine("Process:" + p.ProcessName + 
                " has " + topWindowCount[pid].ToString() + 
                " top level windows");
        }
    }
}

ページのトップへ


4. 基底コントロール プロパティへのアクセス

ControlProxy を使用する主要な理由の 1 つは、別の処理のコントロールからプロパティにアクセスするためです (プロパティについては、図 6 に記述されています)。これらのプロパティにアクセスするには、ControlProxy.FromHandle あるいは ControlProxy.TopLevelWindows を使用して ControlProxy を作成し、2 つのメソッドを呼び出して値にアクセスするだけです。見張られている処理の基底コントロールからプロパティ値を取得するには、GetValue を呼び出します。たとえば、このコードで GetValue を呼び出して、Size プロパティを取得します。

controlproxy.GetValue("Size")

図 6 ControlProxy プロパティ

プロパティ 説明
IntPtr ハンドル コントロールの基底 HWND を返します。これはコントロール上の Handle プロパティへのアクセスと同じです。
array<ControlProxy^>^ Children コントロールの子ウィンドウを表す ControlProxy クラスの配列を返します。
Process^ OwningProcess コントロールが実行している処理のオブジェクトを返します。
bool IsManaged ControlProxy が調査しているウィンドウがマネージ System.Windows.Forms.Control を表す場合、返される値は True です。マネージでなければ False です。ウィンドウが System.Windows.Forms.Control である場合、ほとんどの ControlProxy メソッドのみが動作します。
Type^ ComponentType コントロールの種類を返します。これは、見張られている処理にロードされるアセンブリから元々由来する種類であることに注意してください。アセンブリおよび種類は、ControlProxy が初めて作成されたときに見張っている処理に再読み込みされます。
ControlProxy^ Parent 親ウィンドウを ControlProxy として返します。親および子を使用して、処理のウィンドウ チェーンを移動します。

監視しているプロセスの基底コントロールのプロパティ値を変更するには、SetValue を呼び出します。たとえば、以下のコードで背景色を青色に設定します。

controlproxy.SetValue("BackColor", "Color.Blue")

プロセス間でプロパティを編集するときの ManagedSpyLib の便利さを説明するために、簡単な C# アプリケーションを作成します。textBox1 というテキストボックスと button1 というボタンを追加します。そして、ボタンをダブルクリックして、button1_Click ハンドラを作成し、図 7 に示すように引用を含むコードをいくつか追加します。

図 7 アプリケーションの他のインスタンスを編集する

private void button1_Click(object sender, EventArgs e) 
{
    foreach (Process p in 
        Process.GetProcessesByName("WindowsApplication1")) 
    {
        if (p.Id != Process.GetCurrentProcess().Id) 
        {
            ControlProxy proxy = 
                ControlProxy.FromHandle(p.MainWindowHandle);
            string val = (string)proxy.GetValue("MyStringValue");
            MessageBox.Show("Changing " + val + " to " + MyStringValue);
            proxy.SetValue("MyStringValue", (object)MyStringValue);
        }
    }
}

public string MyStringValue 
{
    get { return this.textBox1.Text; }
    set { this.textBox1.Text = value; }
}

このアプリケーションの 2 つのインスタンスを実行すると、図 8 に示すように、一方のインスタンスの textBox1 にあるテキストを入力して button1 をクリックすると、このアプリケーションの他に実行中のインスタンスすべてを検索してテキストボックスの文字列が同じになるように変更します。

図 8 インスタンス
図 8 インスタンス

別の処理のコントロールでの Click あるいは MouseMove などのイベントに登録することができます。イベントの登録は、2 つの手順による処理です。まず、SubscribeEvent をイベント名とともに呼び出して、ControlProxy にそのイベントについてリッスンさせなくてはなりません。それから、EventFired と呼ばれる ControlProxy イベントに登録します。

private void SubscribeMainWindowClick(ControlProxy proxy) 
{
proxy.SubscribeEvent("Click");
proxy.EventFired += new ControlProxyEventHandler(
Program.ProxyEventFired);
}

void ProxyEventFired(object sender, ProxyEventArgs args) 
{
System.Windows.Forms.MessageBox.Show(args.eventDescriptor.Name 
+ " event fired!");
}

ControlProxy での作業が完了したら、以前に登録したイベントのすべてを登録解除しなければならないことに注意してください。

ManagedSpy 自体は、ControlProxy クラスを使用してプロパティ値を取得します。たとえば、FlashCurrentWindow は選択されたウィンドウを数秒間強調表示します。また、ログ機能のイベントにも登録します。

ページのトップへ


5. 他の ControlProxy メソッド

ControlProxy で検討に値するメソッドが他にもいくつかあります。SendMessage メソッドを呼び出して、ウィンドウ メッセージをコントロールに送信します。テストの装置を作成したい場合に、これは有用です。たとえば、WM_CLICK あるいは WM_KEYDOWN メッセージを送信して、入力をシミュレーションすることができます。このように ManagedSpyLib を使用したい場合は、編集して、ウィンドウ フック プロシージャが常にオンとなり、プログラムしたメッセージを除くすべてのウィンドウ メッセージをフィルタして選択するようにします。こうすることで、他の入力を無効にする自動化ドライバを作成します。

PointToClient および PointToScreen は、スクリーン座標をクライアント座標に変換します。SetEventWindow および RaiseEvent メソッドは、ユーザー コードから使用されることを意図していません。これらは、処理間のイベント管理のため内部的に使用されます。ICustomTypeDescriptor により、オブジェクトは動的にプロパティおよびイベントを指定することができます。ControlProxy は、PropertyGrid のサポートのためにこのインターフェイスを実装します。これらのメソッドをユーザー コードから直接呼び出すことができますが、通常必要ありません。プロパティにアクセスするには、GetValue および SetValue メソッドを使用します。

ページのトップへ


6. ウィンドウ フックの使用

以前にお話ししたように、ManagedSpyLib はプロセス間でデータの転送を行うことで動作します。ウィンドウ フックは、WM_SETTEXT のようなウィンドウ メッセージを横取りする 1 つの方法です。ウィンドウ フックを作成するメソッドは 2 つあります。SetWindowLong は、同じ処理内の特定のウィンドウのウィンドウ メッセージを横取りすることができます。SetWindowsHookEx は、現在のデスクトップのすべての処理について、すべてのウィンドウのメッセージをフックする機能を含む、広範囲のメッセージ フックを行うことができます。

ネイティブ コードを使用する多くの開発者は、SetWindowLong を、ウィンドウをサブクラス化する Win32 の関数として認識しています。ウィンドウをサブクラス化すると、Windows は、宛先が指定したウィンドウ ハンドルである Win32 メッセージのすべてをコールバック メソッドに送信します。これにより、メッセージを編集するか、単に調査します。

SetWindowLong はサブクラス化しているウィンドウと同じ処理内にいることを要求します。この種類のサブクラス化を行いたい場合、.NET Framework によって System.Windows.Forms.NativeWindow と呼ばれるクラスが提供されているため非常に簡単です。この時点で 2 つの疑問が生じるかもしれません。

  1. ウィンドウ メッセージを表示したいが対象ウィンドウと同じ処理内にいない場合はどうなるでしょうか。
  2. とにかくマネージ情報を表示してしまうのであれば、ウィンドウ メッセージのフックは、ManagedSpyLib とどのように関連するのでしょうか。

ウィンドウ メッセージを表示したいが対象ウィンドウと同じ処理内で実行していない場合、SetWindowLong を使用することはできません。1 つの注意とともに SetWindowsHookEx を使用することができますが、ほとんどの種類のフックについては、コールバック メソッドは DLL エクスポートとして公開される必要があります。これは、コールバックをネイティブ DLL 内あるいは混在モードの C++ DLL 内に記述しなければならないことを意味します。ManagedSpyLib は、まさにこの理由から、マネージ C++ を使用して記述されました。ManagedSpyLib は、Visual Studio 2005 の C++/CLI サポートを使用します。

ManagedSpyLib がウィンドウ メッセージ フックを使用する理由は 2 つあります。送信先処理で要求を受け取るには、送信先処理のコードを実行できることが必要です。SetWindowsHookEx を使用すると、これが可能となります。また ManagedSpyLib は、処理間でデータの送受信を行うためにカスタム ウィンドウ メッセージを使用します。これは、ManagedSpyLib が要求を送信するとき (別の処理のコントロールの BackColor を取得するなど)、そのウィンドウ フックがアクティブになっていなければならないことを意味します。

ページのトップへ


7. メモリマップド ファイルの使用

しかし、ManagedSpyLib はどの程度正確に処理間のデータ転送を行うのでしょうか。確かに、WM_SETMGDPROPERTY などのカスタム ウィンドウ メッセージを送信して、プロパティの値を設定することができます。しかしたとえば、プロパティが BackColor である場合、どのように BackColor.Red を送信するのでしょうか。ウィンドウ メッセージは、パラメータとして DWORD 値を 2 つしか取れません。

その答えは、メモリマップド ファイルを使用することです。これは、正確にはディスク上のファイルではなく、複数の処理間で共有可能なメモリの領域です。そのメモリを処理自体のアドレス空間にマップします。ただしこの結果、共有された領域は異なる開始アドレスを持つことになります。したがって、ポインタが分からなくなるため、この中にデータを格納するときは注意する必要があります。また、メモリマップド ファイル内にマネージ オブジェクトを持つことはできません。これは、共通言語ランタイム (CLR) がそのメモリを管理することができないためです。これは、raw byte データのみを格納できることを意味します。

この理由から、ManagedSpyLib はバイナリの直列化データのみを格納します。これが、ManagedSpyLib によってサポートされるために、プロパティ (および EventArgs) が直列化されていなければならない理由です。ManagedSpyLib は、すべてのトランザクション用のメモリマップド ファイルを作成するために CAtlFileMapping を使用します。

ManagedSpyLib はバイナリ ストリームのサイズを計算し、適切なサイズのメモリマップド ファイルを作成してから、データをそのファイルにコピーします。これで、ManagedSpyLib がウィンドウ フックを使用して、ManagedSpyLib 自体とメモリマップド ファイルをインストールしてデータを送信する方法に関する概念を理解したので、ControlProxy クラスの作成および管理方法について詳しく見ていきましょう。

ページのトップへ


8. ControlProxy の作成およびハンドル再作成

図 9 は、ControlProxy の作成方法 (赤色の矢印) およびハンドルが変更されたときの ControlProxy の管理方法 (青色の矢印) について示します。ユーザーは初めに ControlProxy.FromHandle あるいは ControlProxy.TopLevelWindows を呼び出します。TopLevelWindows は EnumWindows を呼び出し、それから列挙されたウィンドウごとに FromHandle を呼び出します。なので、TopLevelWindows をより複雑な FromHandle 呼び出しと考えることができます。

図 9 ControlProxy の作成
図 9 ControlProxy の作成

ManagedSpyLib は、送信先ウィンドウを所有するスレッドに対するウィンドウ フックをオンにします。そして、ManagedSpyLib は WM_GETPROXY メッセージを送信先ウィンドウに送信します (このメッセージが処理されたら、ウィンドウ フックをオフにします)。受信側では、メッセージを受け取り、コマンド ライブラリが Control.FromHandle を呼び出して、見張られている処理内で実行しているマネージ コントロールを取得します。このコントロールを使用して、ManagedSpyLib は新たに ControlProxy を作成します。この ControlProxy は、現在の AppDomain に読み込まれているすべてのアセンブリの Assembly.Location と同じように、コントロールの Type.FullName を格納します。

ControlProxy は、コントロールの HandleCreated および HandleDestroyed イベントに登録します。ControlProxy は、適切なウィンドウ ハンドルの状態を管理するためにこれを後で使用します。ControlProxy は、見張られている処理の ProxyCache に格納され、バイナリ直列化を使用して見張っている処理に返送されます。見張っている処理は、ControlProxy の直列化を解除し、ローカルの ProxyCache に追加します。それから、ControlProxy をユーザーに返します。

見張られている処理がコントロールのハンドルを再作成するときに、ManagedSpyLib は適切な状態を管理します。HandleDestroyed が、見張られている処理の ControlProxy から返されます。ControlProxy は、Control.RecreatingHandle を確認して、コントロールがハンドルの再作成を行っているかどうかを確認します。ハンドルが再作成されていれば、ControlProxy は対応する HandleCreated を待ちます。ControlProxy は、ローカルの ProxyCache を更新して、WM_HANDLECHANGED を見張っている処理の EventWindow へ送信します。見張っている処理は、古いウィンドウ ハンドルで検索することで正しい ControlProxy を ProxyCache から探します。それから、ControlProxy および見張っている処理の ProxyCache を更新します。

図 10 は、ControlProxy のプロパティ取得方法 (赤色の矢印) およびイベントの受信方法 (青色の矢印) を示します。ManagedSpyLib は、ControlProxy.GetValue(propertyName) を使用してプロパティの値を取得するときに、次の順序で実行します。まず、見張っている処理がプロパティ名とともに ControlProxy.GetValue を呼び出します。ManagedSpyLib は、送信先ウィンドウを所有するスレッドに対するウィンドウ フックをオンにします。メッセージが処理されると、ウィンドウ フックはオフにされます。ManagedSpyLib は、メモリマップド ファイルに入れるプロパティ名を格納します (呼び出し元処理のメモリ ストアの [パラメータ] セクション)。これを行うには、バイナリの直列化を使用します。

図 10 プロキシおよび受信イベントを取得する
図 10 プロキシおよび受信イベントを取得する

ManagedSpyLib は WM_SETMGDPROPERTY メッセージを送信先ウィンドウに送信します。ウィンドウ フック プロシージャ (MessageHookProc) が見張られている処理内で呼び出され、ウィンドウ メッセージを処理します。MessageHookProc はコマンドを処理し、戻り値を取得するためにリフレクションを使用します。戻り値を呼び出し元処理のメモリ ストアに格納します。SendMessage が完了すると、見張っている処理はメモリ ストアの戻り値の直列化を解除します。WM_RELEASEMEM を同じ送信先ウィンドウに送信して、マップド ファイルの参照が解除されることを通知します。最後に、値を返します。

イベントの登録と取得は似ています。見張っている処理は、EventWindow ハンドル、登録するイベントの名前、およびこのウィンドウ内のこのイベントに特有なイベント コード (通常イベント リストのイベントに対するインデックス) を、見張っている処理のメモリ ストアの [パラメータ] セクションに格納している SubscribeEvent を呼び出します。

SubscribeEvent は、WM_SUBSCRIBEEVENT を送信先のコントロールに送信します。見張られている処理の WM_SUBSCRIBEEVENT を受け取ると、ManagedSpyLib は、イベントに登録する EventRegister オブジェクトを作成し、登録したイベントを追跡します。イベントが実行されると、EventRegister は WM_EVENTFIRED メッセージを、送信元ウィンドウ、イベントコード、および見張られている処理のメモリ ストア内に格納されている EventArgs とともに Event ウィンドウに送信します。

見張っている処理が WM_EVENTFIRED の処理、送信元ウィンドウ、イベント コード、EventArgs の解析、および正確な ControlProxy の RaiseEvent の呼び出し (正確なイベントおよび EventArg 情報とともに) を行います。RaiseEvent は ControlProxy の EventFired イベントを発生させます。

ページのトップへ


9. 単体テストのための ManagedSpyLib

ManagedSpyLib を使用して、フックをアプリケーションから公開せずにテストを行うことができます。これを説明するために、Multiply という名前の C# Window Forms ベースのアプリケーションを新たに作成しました。3 つのテキストボックスと、1 つのボタンを追加して、そのボタンをダブルクリックし、以下のコードをこの Click イベントに追加しました。

private void button1_Click(object sender, EventArgs e) 
{
    int n1 = Convert.ToInt32(this.textBox1.Text);
    int n2 = Convert.ToInt32(this.textBox2.Text);
    this.textBox3.Text = (n1 * n2).ToString();
}

このアプリケーションが行うことは、2 つのテキストボックスを計算して、結果を 3 番目のテキストボックスに表示するだけです。本当の目的は、この簡単なサンプルとともに動作する単体テスト アプリケーションを作成することです。

次の手順に向けて、C# Windows ベースのアプリケーションを解決法に新たに追加して、UnitTest という名前にしました。図 11 に示すコードに付随するフォームには 1 つのボタンしかありません。

図 11 テスト コード

private void button1_Click(object sender, EventArgs e) 
{
    Process[] procs = Process.GetProcessesByName("Multiply");
    if (procs.Length != 1) return;
    ControlProxy proxy =
        ControlProxy.FromHandle(procs[0].MainWindowHandle);
    if (proxy == null) return;

    // 興味があるコントロールを検索する...
    if (cbutton1 == null) 
    {
        foreach (ControlProxy child in proxy.Children) 
        {
            if (child.GetComponentName() == "textBox1") {
                textBox1 = child;
            }
            else if (child.GetComponentName() == "textBox2") {
                textBox2 = child;
            }
            else if (child.GetComponentName() == "textBox3") {
                textBox3 = child;
            }
            else if (child.GetComponentName() == "button1") {
                cbutton1 = child;
            }
        }

        // 変更されたかどうかが分かるように、textbox3 上の testchanged を同期する。
        textBox1.SetValue("Text", "5");
        textBox2.SetValue("Text", "7");
        textBox3.SetValue("Text", "");
        textBox3.EventFired += 
            new ControlProxyEventHandler(textBox3_EventFired);
        textBox3.SubscribeEvent("TextChanged");
    }
    else textBox3.SetValue("Text", "");

    // ここでボタンをクリックしてテストを開始する...
    if (cbutton1 != null) 
    {
        cbutton1.SendMessage(WM_LBUTTONDOWN, IntPtr.Zero, IntPtr.Zero);
        cbutton1.SendMessage(WM_LBUTTONUP, IntPtr.Zero, IntPtr.Zero);
        Application.DoEvents();
    }

    if (result == 35) MessageBox.Show("Passed!");
    else MessageBox.Show("Fail!");
}

void textBox3_EventFired(object sender, ProxyEventArgs ed) 
{
    int val;
    if (int.TryParse((string)textBox3.GetValue("Text"), out val) 
    {
        result = val;
    }
}

単体テスト アプリケーションを実行すると、1 番目のテキストボックスが 5 に変更され、2 番目が 7 に変更されます。そして、クリックが (Mousedown および Mouseup を使用して) ボタンに送信され、最後の結果が表示されます (これがイベント コールバックに設定されます)。

ページのトップへ


10. まとめ

ManagedSpy は、Spy++ と同様の診断ツールです。ManagedSpy はマネージ プロパティを表示して、イベントのログ記録を可能とし、ManagedSpyLib を使用する良い例となります。ManagedSpyLib は、ControlProxy と呼ばれるクラスを導入します。ControlProxy は、別の処理内の System.Windows.Forms.Control を表すものです。ControlProxy により、送信先の処理内部で実行しているかのように、プロパティの取得あるいは設定およびイベントへの登録を行うことができます。自動化テスト、互換性に関するイベント ログ記録、クロス処理通信、あるいはホワイトボックス テストに ManagedSpyLib を使用します。

ページのトップへ


Benjamin Wulfe は、マイクロソフトで 6 年間、.NET Framework および .NET Compact Framework 向けの Visual Studio、Windows Forms デザイナに取り組んでおり、ComboBox や NativeWindow などの Framework クラスのいくつかにも取り組んでいます。


この記事は、MSDN マガジン - 2006 年 4 月からの翻訳です。

QJ: 060401

ページのトップへ