日本Delphi振興会 Delphiはじめて物語 Delphiつれづれ記 Delphiリンク互助会 Delphiアクセサリ

Delphi壁の穴

その四:アプリケーションを覗く

ここではウィンドウハンドルを使って他のアプリケーションを遠隔操作する方法をご紹介します。


第1章:ウィンドウメッセージとは

 「マウスの左ボタンが押された」「ウィンドウが最小化された」「タイマーを使って時間を計測してる」「画面を再描画する」などなど、Windows内部では様々なメッセージが飛び交っています。このようなメッセージを「ウィンドウメッセージ」と呼びます。Windows自身も各アプリケーションもウィンドウメッセージをやりとりすることで互いを制御しています。
 アプリケーションの遠隔操作はこのウィンドウメッセージを逆手に取ることで行います。例えば任意のアプリケーションのOKボタンを押したいとしましょう。これを行うにはそのアプリケーションのOKボタンに対して「マウスの左ボタンが押されたよ」というメッセージを送ってやればいいのです。OKボタンはマウスの左ボタンが押されたと思い込み、自らをへこませてしまうでしょう。
 それでは具体的に、マウスの左ボタンが押されたメッセージを伝えるにはどうしたらいいのでしょうか。これを行うWindows APIが「SendMessage」です。SendMessageの文法は以下のようになっています。

function SendMessage(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT;

hWndに押したいボタンを、Msgにウィンドウメッセージを、wParam/lParamにはいわばプロパティのようなものを指定します。

 つまり、Msg引数にマウスの左ボタンが押されたというメッセージを指定してやればいいのです。ではそれは何か。このとき役立つのがWindows APIのヘルプです。ただ、Delphiに付いてくるのは英語で書かれたヘルプファイルなので、ここを読んで日本語で書かれたAPIヘルプを使った方がいいです。
 ヘルプのキーワード検索で「WM_LBUTTONDOWN」を探して見て下さい。以下のように書いてあります。

WM_LBUTTONDOWNメッセージは、 ウィンドウのクライアント領域内にカーソルがある間に、 ユーザーがマウスの左ボタンを押したときにポストされます。

 簡単にいうと、マウスの左ボタンが押されるとWM_LBUTTONDOWMが送られるよ、と書いてあるわけです。SendMessageを使うことでマウスの左ボタンが押されたことにしてしまうわけです。つまり、SendMessageはマウスの左ボタンの代わりというわけですね。次の章では押したいボタンを指定する方法をご紹介します。


第2章:ウィンドウハンドルとは

 Windowsは、全てのウィンドウに対して固有の番号を振り当てます。この固有番号を「ウィンドウハンドル」と呼びます。車のハンドルを思い浮かべて下さい。車のハンドルさえつかめれば、その車を右へ左へと自在に操ることが出来ます。ウィンドウハンドルも同じで、ハンドルさえ知ることが出来ればそのウィンドウを最小化する、大きさを変える、位置を変える、閉じるなど自由自在に操ることが出来ます。
 それではボタンはどうなのでしょうか。「ボタンハンドル」なるものがあるのでしょうか。いいえ、違います。これにも「ウィンドウハンドル」があるのです。それどころかチェックボックス・リストボックス・コンボボックス・ツールバー・ダイアログなどにウィンドウハンドルは割り当てられているのです。Delphiでもいろいろなコンポーネントを使っていますが、ウィンドウハンドルを持つコンポーネントには Handleプロパティがあります(派生元がTWinControlなもの)。
 なお、ハンドルはウィンドウハンドルだけではなく、「メニューハンドル」「デバイスコンテキストハンドル」などいくつかの種類があります。

 それでは具体的に、ハンドルの取得方法をご紹介しましょう。ハンドルを取得するにはWindows APIの「FindWindow」「FindWindowEx」を使います。

 FindWindowの文法と説明は以下のようになっています。

function FindWindow(lpClassName, lpWindowName: PChar): HWND;

FindWindow関数は、 クラス名とウィンドウ名が指定された文字列と一致するトップ レベル ウィンドウのハンドルを取得します。子ウィンドウは探しません。

 FindWindowExの文法と説明は以下のようになっています。

function FindWindowEx(Parent, Child: HWND; ClassName, WindowName: PChar): HWND;

The FindWindowEx function retrieves the handle of a window whose class name and window name match the specified strings. The function searches child windows, beginning with the one following the given child window.

両者の違いを赤で表しました。FindWindowはトップレベルウィンドウ、FindWindowExは子ウィンドウのハンドルを取得するのに使うのです。それではトップレベルウィンドウとは何でしょうか。簡単に言ってしまえば皆さんが普通に感じている、アプリケーションを立ち上げると最初に出てくる、タイトルバーがあって最小化出来たりする普通のウィンドウだと思っていいでしょう。それでは子ウィンドウとは何でしょうか。例えばトップレベルウィンドウの中にボタンがおいてあったとします。先にも述べたように、このボタンもウィンドウハンドルを持っています。しかし独立した存在ではなく、トップレベルウインドウが閉じてなくなってしまうとこのボタンも一緒になくなる運命にあります。このようにトップレベルウィンドウに従属した関係にあるウィンドウを「子ウィンドウ」と呼ぶわけです。MDIにおける子ウィンドウとは概念が違うので注意して下さい。以下に例を示します。

エクスプローラ
エクスプローラ

エクスプローラの子ウィンドウ
WinSightで見たエクスプローラの子ウィンドウ

エクスプローラのウィンドウの下にボタンの子ウィンドウ(Child)が従属していることが分かります。

すなわち、アプリケーションなどのトップレベルウィンドウを操作したければ「FindWindow」を使ってハンドルを取得し、ボタンなどの子ウィンドウを操作したければ「FindWindowEx」を使ってハンドルを取得することになります。

ここで両関数の返値を見て下さい。両者とも「HWND」と定義されています。HWND は「Window Handle」の意味です。「H」が頭に着いている場合は大抵、それはハンドルを表しています。そのほかにもメニューハンドルは「HMENU」、デバイスコンテキストハンドルは「HDC」、ビットマップハンドルは「HBITMAP」など、いろいろあります。ただ、ここでは「操る」点を重視しているのでウィンドウハンドルにだけ言及します。


第3章:アプリケーションを遠隔操作する

 さあ、いよいよ実践です。その前におさらいをしておきましょう。あるアプリケーションを遠隔操作する場合、SendMessageを使って「ウィンドウメッセージ」という命令を送ればいいことが分かりました。そしてそのメッセージを送る相手を特定するためには、FindWindow/FindWindowExを使って相手の「ウィンドウハンドル」を取得してやればいいことも分かりました。それでは以下に具体的な方法を見ていきましょう。

例1:Window標準の電卓ウィンドウを操る

まずは最小化してみましょう。

procedure TForm1.Button1Click(Sender: TObject);
var
 hCalc:HWND; {ハンドル}
begin
 hCalc:=FindWindow(nil,'電卓'); {電卓のハンドルを取得}
 SendMessage(hCalc,WM_SYSCOMMAND,SC_MINIMIZE,0); {最小化}
end;

  1. ウィンドウハンドルは、HWND という変数名です。varで宣言します。
  2. FindWindowで電卓のハンドルを取得します。第二引数にはウィンドウタイトルである「電卓」を指定します。Delphiでいえば Caption プロパティに相当します。第一引数は今のところ nil でいいです。
  3. SendMessageでメッセージを送ります。WM_SYSCOMMANDメッセージは、システムに関するメッセージです。第三引数のwParamにはSC_MINIMIZEを指定して最小化するようにします。

どうですか?いとも簡単に最小化できました。第三引数に SC_MAXIMIZEを指定してみて下さい。本来なら最大化できないはずの電卓が最大化します。電卓は最大化できないのではなくて最大化させないようにしているだけなのです。

次に電卓の位置や大きさを変えてみましょう。

procedure TForm1.Button1Click(Sender: TObject);
var
 hCalc:HWND; {ハンドル}
 Rect:TRect;
begin
 hCalc:=FindWindow(nil,'電卓'); {電卓のハンドルを取得}
 GetWindowRect(hCalc,Rect);  {電卓の位置と大きさを取得}
 MoveWindow(hCalc,0,0,Rect.Right-Rect.Left,Rect.Bottom-Rect.Top,True);
end;

  1. まずは電卓のハンドルを取得します。
  2. GetWindowRectは、与えられたハンドルを持つウィンドウの位置と大きさを取得するWindows APIです。ハンドルはこういうところにも使われます。
  3. MoveWindow APIはウィンドウの位置と大きさを決定します。このようにSendMessage以外にもハンドルを利用してウィンドウを操作できるAPIがいくつかあります。

実行すると電卓の位置が画面の一番左隅へ移動します。また、MoveWindowの所を

MoveWindow(hCalc,0,0,200,200,True);

と変えてみて下さい。電卓の大きさが200x200に小さくなりました。

小さい電卓


例2:スキャンディスクを操る

さあ、いよいよ核心に迫りましょう。ここではスキャンディスクのボタンを押す、文字列を変える、といったことをやってみます。

<重要> WinSightについて
ここから以下の作業では、WinSightはなくてはならないものになります。WinSightはウィンドウハンドルをすべて列挙してくれる上に、子ウィンドウの構造をツリー形式で表示してくれます。また以下に登場する「クラス」を調べるにも大変役立ちます。WinSightは Professional 以上でないと付いてきません。

その1:ボタンを押そう!

スキャンディスクを起動した後、WinSightを起動してください。ツリーに

Popup 00000C24 {ScanDskWDkgClass} SCANDISKW.EXE (0,0)-(200,300) "スキャンディスク-(C:)"

のように表示されます(見つけられない場合は、メニューから [Spy - Follow Focus] を選択して、スキャンディスクのウィンドウをクリックして下さい)。

スキャンディスクのウィンドウ構造
WinSightで見たスキャンディスクのウィンドウ構造

  1. 「Popup」は親ウィンドウであることを示します。最小化すると「Icon」、子ウィンドウだと「Child」と表示されます。
  2. 「00000C24」はウィンドウハンドルを表します。
  3. 「{ScanDskWDkgClass}」はクラス名です。Delphiで作ったアプリケーションだと、Nameプロパティの値に「T」が付いた文字列(例:TForm1)が表示されます。
  4. 「SCANDISKW.EXE」は実行ファイル名です。
  5. 「(0,0)-(200,300)」はウィンドウの位置です。左上のX/Y軸と右下のX/Y軸を表します。
  6. 「"スキャンディスク-(C:)"」はウィンドウタイトルです。Delphiでいえば、Captionプロパティの値になります。

そして今度はそのツリーを開いてみてください。「Child」と書かれた子ウィンドウがずらりと並ぶはずです。その中でクラス名が「Button」と表示されているものがボタンです。この子ウィンドウのハンドルを取得するために使うのが「FindWindowEx」です。それでは、「開始」ボタンを押してみましょう。

procedure TForm1.Button1Click(Sender: TObject);
var
 hScanDisk,
 hStartBtn: HWND;
begin
 //スキャンディスクのウィンドウハンドルを取得
 hScanDisk:=FindWindow('ScanDskWDlgClass',nil);
 //開始ボタンのウィンドウハンドルを取得
 hStartBtn:=FindWindowEx(hScanDisk,0,'Button','開始(&S)');
 //スキャンディスクを前に持ってくる
 SetForeGroundWindow(hScanDisk);
 //ボタンを押すタイミングを計るため、200ミリ秒停止
 Sleep(200);
 //マウスの左ボタンを押す
 SendMessage(hStartBtn,WM_LBUTTONDOWN,0,0);
 //マウスの左ボタンを離す
 SendMessage(hStartBtn,WM_LBUTTONUP,0,0);
end;

  1. FindWindowでスキャンディスクのハンドルを取得します。今回はウィンドウタイトルではなく、クラス名を使って取得しました。なぜなら、スキャンディスクのウィンドウタイトルには「C:」というドライブ名が入っています。もしこれがNECのPC-9800シリーズだったらここは変わってしまうでしょう。また、英語環境だったらまるきりタイトルは変わってしまいます。だから、親ウィンドウを取得する場合はクラス名を使ったほうが確実です。
  2. 次に「開始」ボタンのハンドルを取得します。第一引数に親ウィンドウのハンドルを指定します。第二引数は0で構いません。第三引数にクラス名を、第四引数にボタンの名前、つまりCaptionです。
  3. SetForeGroundWindow は指定したウィンドウハンドルを最前面に持ってくるためのAPIです。
  4. Sleepは、処理を指定した時間(ミリ秒)だけ一時停止します。ここではボタンを押すタイミングを計るため、200ミリ秒停止させています。
  5. SendMessageでマウスの左ボタンを押す(WM_LBUTTONDOWN)・離す(WM_LBUTTONUP)ウィンドウメッセージを送っています。

その2:文字列を変えちゃおう!

「終了」ボタンの文字を変えちゃいましょう。

procedure TForm1.Button1Click(Sender: TObject);
var
 hScanDisk,
 hCloseBtn: HWND;
begin
 //スキャンディスクのウィンドウハンドルを取得
 hScanDisk:=FindWindow('ScanDskWDlgClass',nil);
 //終了ボタンのウィンドウハンドルを取得
 hCloseBtn:=FindWindowEx(hScanDisk,0,'Button','終了(&C)');
 //スキャンディスクを前に持ってくる
 SetForeGroundWindow(hScanDisk);
 //タイミングを計るため、200ミリ秒停止
 Sleep(200);
 //終了ボタンの文字列を変える
 SetWindowText(hCloseBtn,'押さないでくれ');
 //ボタンを再描画して文字列の変更を反映させる
 InvalidateRect(hCloseBtn,nil,True);
end;


例2:スタートボタンにも手を加えちゃおう!

ついにスタートボタンさへも我が手中に!ふぉっふぉっふぉ。<- 大げさ
procedure TForm1.Button1Click(Sender: TObject);
var
 hTrayWnd,
 hStartBtn: HWND;
begin
 hTrayWnd:=FindWindow('Shell_TrayWnd',nil);
 hStartBtn:=FindWindowEx(hTrayWnd,0,'Button',nil);
 EnableWindow(hStartBtn,False);
end;
早速実行してみましょう。スタートボタンはどうなりましたか?(Windowsキーを押されると厳しいモノがありますが.....)

EnableWindowは、指定したハンドルを持つウィンドウを淡色表示するかしないかを決定するAPIです。DelphiだったらEnabledプロパティみたいなモノですね。


例3:デスクトップのアイコンを消す

以前頼まれて作ったやつです。プロジェクトファイルに直接書いてます。
//Desktop Hider 0.1
//制作日:98/11/05

program Hidedsk;

uses
  Windows,
  CommCtrl;

{$R *.RES}

var
 hProgman,
 hDefView,
 hSysList: HWND;
begin
 hProgman:=FindWindow('Progman',nil);
 hDefView:=FindWindowEx(hProgman,0,'SHELLDLL_DefView',nil);
 hSysList:=FindWindowEx(hDefView,0,'SysListView32',nil);
 ListView_DeleteAllItems(hSysList);
end.
デスクトップのアイコン表示部分はリストビューで出来ているのですが、そこに「ListView_DeleteAllItems」を送ることでアイコンを消してます。
実は同じ様な機能がWindowsにあって、レジストリをいじれば消えてくれるのですが、こちらはリストビュー自体を非表示にしているのでデスクトップのコンテキストメニューが出ないんですね(で、まあ、メニューが出るようにならないか、と頼まれたわけです)。しかし、上記の方法だとアイコンだけ消しているのでコンテキストメニューも出るというわけです。ただし副作用として、再起動するとせっかくきれいに配置したアイコンが全て初期状態の位置に戻ってしまう現象があります。

デスクトップの構造
WinSightで見たデスクトップ構造

一番上の「Desktop」は気にしなくていいです。本体は「Progman」以下です。Windows95になってもなぜか Program Manager が生きてますねぇ。一番下に「SysHeader32が」がありますが、これはエクスプローラなどでリストビューを詳細表示にしたとき上に出てくるヘッダーです。普段は「hidden」で隠れてます。デスクトップのアイコン状態をエクスプローラのように詳細表示にするソフトがありますが、それはここらへんをいじっているんですね。

うーんと、あとまだいろいろあるんですけど、基本は同じです。
また暇になったら書き足したいと思います。

一番上へ

Delphi壁の穴 Delphiを覗く システムを覗く レジストリを覗く

リンクは日本Delphi振興会から張ってください。