Windows というOSにおけるウィンドウは、特別なオブジェクトである。デスクトップスクリーンに自己を描画する矩形領域を独自にもち、メッセージを受け取ることができるのはウィンドウだけである。VCLでは、ウィンドウを作ってその機能をラップしているのは TWinControl の派生クラスである。VCLのクラスは Delphi の Object Pascal という言語上のクラスであり、そのインスタンスはOSからは見えない。見えているのは、VCLのインスタンスが作るウィンドウだけである。TLabel や TImage 、TPaintBox 、 TSpeedButton などの TGraphicControl の派生クラスは、ウィンドウの特定のクライアント領域の描画機能をカプセル化したものであり、ウィンドウではないのでOSからは見えない。またメッセージを独自に受け取ることができないので、Parent の TWinControl から WM_PAINT などのメッセージを転送してもらって、描画のタイミングを取得している。この章では、OSから見たウィンドウ間の相互関係について調べることにしよう。
右図は、ウィンドウ間の親子関係と所有関係をツリー状に表示した例である。ここでツリーが展開されているのは、筆者の Delphi5 の IDE のウィンドウである。表示部分はそのほんの一部である。この章の目的の一つは、このように、ウィンドウ間の相互関係を調べ、それに基づいてウィンドウの階層を調べて図のように表示し、各ウィンドウの情報を取得するためには、どのようなことをしなければならないかを明らかにすることである。
第6章 ウィンドウの階層について |
6−1 ウィンドウの親子関係と所有関係 |
6−2 クラス情報とウィンドウ情報 |
6−3 トップレベルウィンドウの列挙 |
6−4 トップレベルウィンドウの探索 |
6−5 子ウィンドウの探索 |
6−6 ウィンドウの階層 |
6−7 ウィンドウの階層のツリー表示 |
6−8 まとめ |
VCLの TControl の派生クラスには TWinControl 型の Parent プロパティーがある。これは、自分を描画する対象をどのウィンドウにするのかを決める重要なプロパティーである。TWinControl は TControl の派生クラスであり、OSから見た TWinControl の派生クラスの作るウィンドウの親子関係は、VCLの Parent プロパティーによって決定されるものとほぼ同じである。ちょっと試してみよう。右図のような配置でそれぞれの Parent プロパティーを参照して親を確認してみる。
List 1
procedure TForm1.Button1Click(Sender: TObject); begin Memo1.Clear; Memo1.Lines.Add('Button1.Parent = '+ Button1.Parent.Name); Memo1.Lines.Add('Panel2.Parent = '+ Panel2.Parent.Name); Memo1.Lines.Add('Panel1.Parent = '+ Panel1.Parent.Name); end;
結果は図の通りである。一方、OSから見た親子関係を確認するには GetParent( ) API を使う。
function GetParent(hWnd: HWND): HWND; stdcall;
それでは早速やってみよう。
List 2
function GetParentName(Handle:HWND):string; begin result := FindControl(GetParent(Handle)).Name; end; procedure TForm1.Button1Click(Sender: TObject); begin Memo1.Clear; Memo1.Lines.Add('Button1.Parent = '+ GetParentName(Button1.Handle)); Memo1.Lines.Add('Panel2.Parent = '+ GetParentName(Panel2.Handle)); Memo1.Lines.Add('Panel1.Parent = '+ GetParentName(Panel1.Handle)); end;
結果は前の図と同じになる。GetParent( ) でOSから親のウィンドウハンドルを教えてもらい、それを使って FindControl 関数でVCLのコントロールのインスタンス (TWinControl) を取得してその Name プロパティーを使用している。このほかに
function IsChild(hWndParent, hWnd: HWND): BOOL; stdcall;
という API があって、親子関係をハンドルの値から調べることができる。これは、直接の子ウィンドウだけでなく、孫ウィンドウやひ孫ウィンドウについても true を返す。
親子関係については、TWinControl の派生クラスでは、OS と VCLのそれは同じであることが分かった。ちなみに TWinControl には TControl 型の Controls プロパティーと ControlCount プロパティーがあって、子ウィンドウというか、正確には子コントロールの配列にアクセスできる。
List 3
procedure TForm1.Button1Click(Sender: TObject); var i:integer; begin Memo1.Clear; for i := 0 to Panel1.ControlCount-1 do Memo1.Lines.Add(Panel1.Controls[i].Name); end;
結果は右図のようになる。Controls[] は TControl 型なので Lable1 も入っていることに注意する。
以上のように、親子関係は主に自分をウィンドウとしてどこに描画するかを決めており、VCLとOSではほぼ同じ定義であると言える。しかし、重大な相違がある。CreateWindow(Ex) などで作られたウィンドウは、いずれ DestroyWindow( ) で破棄しなければならないが、OSではこの親子関係を使ってこの操作を容易にしている。つまり、親ウィンドウが破棄されるときは、それに先立って子ウィンドウが自動的に破棄される仕組みになっている。これに対して、Delphi の VCL が作るクラスのオブジェクトの破棄に関係しているのは、親子関係ではなく、以下に示す所有関係である。
VCLの TComponent の派生クラスは、そのコンストラクタ実行時にパラメータとして、オーナーを指定する。
constructor Create(AOwner: TComponent);
ここで指定したオーナーは、読み出し専用の Owner プロパティーに反映される。つまり、VCLのオーナーは、インスタンスを作成時に指定してその後変更できない。Owner の役割はなんだろうか。ヘルプによれば、「Owner プロパティは,コンポーネントを解放する権限を持つコンポーネントを示します。」というように、Owner - Owned の所有関係を利用して、インスタンス破棄時の操作を簡単化しているわけだ。VCLでは TComponent 派生クラスには ComponentCount と Components[] というプロパティーで、自分が所有しているコンポーネントの数とその配列を保持している。通常は、フォームの上に配置されたコンポーネントは、すべてそのフォームに所有される。フォームはデフォルトでは Application オブジェクトに所有される。以下で確かめてみよう。
List 4
procedure TForm1.Button1Click(Sender: TObject); var i:integer; begin Memo1.Clear; for i := 0 to Form1.ComponentCount-1 do Memo1.Lines.Add(Form1.Components[i].Name); end;
結果は右図のようになる。
VCLにおける所有関係とOSから見たウィンドウ間の所有関係とはまったく異なっている。子ウィンドウは、そのウィンドウスタイルに WS_CHILD スタイルを含んでいる。このスタイルを含んでいないウィンドウをトップレベルウィンドウという。OSから見た所有関係は、このトップレベルウィンドウ間の相互関係に他ならない。Delphi でデフォルトで作られる Form1 は子ウィンドウではないので、オーナーを持っているかもしれない。オーナーのウィンドウハンドルは、ウィンドウスタイルに WS_CHILD が含まれていないことを確認して、GetWindowLong() で GWL_HWNDPARENT フィールドを取得する。ちょっとやってみよう。
List 5
function IsChildWindow(Handle:HWND):Boolean; begin result := (GetWindowLong(Handle,GWL_STYLE) and WS_CHILD) <> 0; end; function GetClassNameStr(Handle:HWND):string; var Len:integer; PC:PChar; begin GetMem(PC,100); Len := GetClassName(Handle,PC,100); SetString(result,PC,Len); FreeMem(PC); end; procedure TForm1.Button1Click(Sender: TObject); var Owner:LongInt; begin Memo1.Clear; if not IsChildWindow(Form1.Handle) then begin Owner := GetWindowLong(Form1.Handle,GWL_HWNDPARENT); Memo1.Lines.Add(GetClassNameStr(Owner)); end; end;
結果は右図のように TApplication になる。
別の方法では GetWindow() を使う。
LIst 6
procedure TForm1.Button1Click(Sender: TObject); var Owner:LongInt; begin Memo1.Clear; Owner := GetWindow(Form1.Handle,GW_OWNER); if Owner <> 0 then Memo1.Lines.Add(GetClassNameStr(Owner)); end;
前と同じ結果になる。GetWindow() についての詳細は後述する。以上のように、VCLにおける Form1 と Application ウィンドウの所有関係については、OSのそれとまったく同じである。デフォルトでは、すべてのフォームは Application に所有されている。しかし、フォーム内のコンポーネントの所有関係については、OSとはまったく無関係である。OSにおけるトップレベルウィンドウ間の所有関係は、
1)Owned ウィンドウは必ず Owner ウィンドウの手前に表示される。 2)Owner ウィンドウが最小化されると Owned ウィンドウも最小化される。
という関係である。VCLでは、すべてフォームは Application ウィンドウに所有されているので、Application ウィンドウが最小化されるとすべてのフォームが最小化する。MainWindow である Form1 の最小化ボタンを押すと、これが Application ウィンドウを最小化させ、その結果、すべてのフォームが最小化されるのである。
トップレベルウィンドウの Owner の指定は、ウィンドウ作成時に WS_CHILD スタイルを含まず、CreateWindow(Ex) の hWndParent パラメータにウィンドウハンドルを設定する。作成後の変更はできない。Form2 を Form1 の Owned ウィンドウにしてみよう。そのためには CreateParams( ) を override して Params の WndParent フィールドに Form1.Handle を設定する。
List 7
type TForm2 = class(TForm) private { Private 宣言 } public procedure CreateParams(var Params: TCreateParams); override; end; var Form2: TForm2; implementation {$R *.DFM} uses Unit1; procedure TForm2.CreateParams(var Params: TCreateParams); begin inherited; Params.WndParent := Form1.Handle; end;
結果は右図のようになり、Form2 は常に Form1 の前面に表示される。面白いのは、Form1 を最小化しても Form2 が最小化されず、タスクバーのボタンを押しても Form2 が表示されていると、Form1 が復帰しないなど、まったく標準のウィンドウの動作と一致しなくなる。最小化については、Application ウィンドウ経由なので、どうも直接の所有関係があるウィンドウだけしか追随しないようになっているようだ。また、Form2 が表示されているときにタスクバーのボタンを押しても Form1 が復帰しないで Form2 がアクティブになるのは、OSから見ると、ユーザは該当アプリケーションの一部のウィンドウ(ダイアログかもしれない)だけが最小化されているとみなしているからに違いない。以上のように、Application ウィンドウがすべてのフォームを所有している、というVCLのトップレベルウィンドウの構造は、VCLを構築するうえで好都合であるかもしれないが、VCLユーザにとっては必ずしもありがたいことではない。
Win32 API の第一章で調べたように、ウィンドウを作るには、ウィンドウクラスを定義してOSに登録し、それを基にして CreateWindow(Ex) で作成する。逆に、ウィンドウハンドルを取得できれば、そのウィンドウを作った際の、クラスに関する情報やウィンドウに関する情報を取得できる。Win32 API の第4−1節で作った関数を改造して Form1 について調べてみよう。
List 8
unit Main; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ComCtrls, StdCtrls; type TForm1 = class(TForm) ListView1: TListView; Button1: TButton; Label1: TLabel; ListView2: TListView; Label2: TLabel; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); procedure ListView1DblClick(Sender: TObject); private { Private 宣言 } public { Public 宣言 } end; var Form1: TForm1; implementation {$R *.DFM} procedure ListViewColumnAdd(LV: TListView; ACaption: string; AWidth: integer); begin with LV.Columns.Add do begin Caption := ACaption; Width := AWidth; end; end; procedure ListViewItemAdd(LV: TListView; ACaption,SubItem: string); begin with LV.Items.Add do begin Caption := ACaption; SubItems.Text := SubItem; end; end; function GetClassNameStr(hWindow:HWND):string; var p:PChar; ret:integer; begin GetMem(p,100); ret := GetClassName(hWindow,p,100); SetString(result,p,ret); FreeMem(p); end; function GetWindowTextStr(hWindow:HWND):string; var p:PChar; ret:integer; begin ret := GetWindowTextLength(hWindow); GetMem(p,ret+1); ret := GetWindowText(hWindow,p,ret+1); SetString(result,p,ret); FreeMem(p); end; procedure ClassInfos(LV:TListView;hWindow:HWND); var WC: TWndClass; s:string; begin s := GetClassNameStr(hWindow); ListViewItemAdd(LV,'ClassName',s); GetClassInfo(hInstance,PChar(s),WC); ListViewItemAdd(LV,'Style',Format('$%.8x',[GetClassLong(hWindow,GCL_STYLE),8])); ListViewItemAdd(LV,'WndProc',Format('$%.8x',[GetClassLong(hWindow,GCL_WNDPROC)])); ListViewItemAdd(LV,'hModule',Format('$%.8x',[GetClassLong(hWindow,GCL_HMODULE)])); ListViewItemAdd(LV,'ClsExtra',Format('$%.8x bytes',[GetClassLong(hWindow,GCL_CBCLSEXTRA)])); ListViewItemAdd(LV,'WndExtra',Format('$%.8x bytes',[GetClassLong(hWindow,GCL_CBWNDEXTRA)])); ListViewItemAdd(LV,'hIcon',Format('$%.8x',[GetClassLong(hWindow,GCL_HICON)])); ListViewItemAdd(LV,'hBrush',Format('$%.8x',[GetClassLong(hWindow,GCL_HBRBACKGROUND)])); ListViewItemAdd(LV,'hCursor',Format('$%.8x',[GetClassLong(hWindow,GCL_HCURSOR)])); end; procedure WindowInfos(LV:TListView;hWindow:HWND); begin ListViewItemAdd(LV,'TEXT',GetWindowTextStr(hWindow)); ListViewItemAdd(LV,'HANDLE',Format('$%.8x (%d)',[hWindow,hWindow])); ListViewItemAdd(LV,'EXSTYLE',Format('$%.8x',[GetWindowLong(hWindow,GWL_EXSTYLE)])); ListViewItemAdd(LV,'STYLE',Format('$%.8x',[GetWindowLong(hWindow,GWL_STYLE)])); ListViewItemAdd(LV,'WNDPROC',Format('$%.8x',[GetWindowLong(hWindow,GWL_WNDPROC)])); ListViewItemAdd(LV,'HINSTANCE',Format('$%.8x',[GetWindowLong(hWindow,GWL_HINSTANCE)])); ListViewItemAdd(LV,'HWNDPARENT',Format('$%.8x (%d)', [GetWindowLong(hWindow,GWL_HWNDPARENT),GetWindowLong(hWindow,GWL_HWNDPARENT)])); ListViewItemAdd(LV,'ID',Format('$%.8x (%d)', [GetWindowLong(hWindow,GWL_ID),GetWindowLong(hWindow,GWL_ID)])); end; procedure TForm1.FormCreate(Sender: TObject); begin ListView1.ViewStyle := vsReport; ListView1.RowSelect := true; ListView2.ViewStyle := vsReport; ListView2.RowSelect := true; ListViewColumnAdd(ListView1,' 項 目',90); ListViewColumnAdd(ListView1,' 内 容',140); ListViewColumnAdd(ListView2,' 項 目',90); ListViewColumnAdd(ListView2,' 内 容',140); end; procedure TForm1.Button1Click(Sender: TObject); begin ListView1.Items.Clear; ClassInfos(ListView1,Handle); ListView2.Items.Clear; WindowInfos(ListView2,Handle); end; end.
実行時の様子を右図に示した。情報の表示にはここでは TListView をつかった。サイズを適当に調整してほしい。また、Form1 のほかに Button1.Handle や Application.Handle を設定して試してみてほしい。
ウィンドウを作る際、RegisterClass(Ex) で登録した登録名であるクラス名を、ウィンドウハンドルから取得するには GetClassName() API を使う。
function GetClassName(hWnd: HWND; lpClassName: PChar; nMaxCount: Integer): Integer; stdcall;
第二パラメータはクラス名を受け取る PChar 型のバッファ、第三パラメータはそのサイズである。ここでは、これをラップして、string 型で返す関数を作成した。
function GetClassNameStr(hWindow:HWND):string; var p:PChar; ret:integer; begin GetMem(p,100); ret := GetClassName(hWindow,p,100); SetString(result,p,ret); FreeMem(p); end;
つぎに、ウィンドウテキストをハンドルから取得するには GetWindowTextLength() で長さを調べ、そのサイズの PChar 型のバッファを用意して、GetWindowText() で受け取る。
function GetWindowText(hWnd: HWND; lpString: PChar; nMaxCount: Integer): Integer; stdcall;
パラメータは上記の GetClassName() と同様である。これを包み込んだ関数を以下のように作った。
function GetWindowTextStr(hWindow:HWND):string; var p:PChar; ret:integer; begin ret := GetWindowTextLength(hWindow); GetMem(p,ret+1); ret := GetWindowText(hWindow,p,ret+1); SetString(result,p,ret); FreeMem(p); end;
ウィンドウテキストは、ウィンドウの属性の一つであり、ウィンドウを作成するときの CreateWindow() の第二パラメータ、または CreateWindowEx() の第三パラメータで設定されたものだ。もちろん、ウィンドウ作成後にも簡単に変更できる。TForm がつくるような汎用的なトップレベルウィンドウの場合は、キャプションのテキストがウィンドウテキストに相当する。TButton では、ボタンの上に表示されるテキストであるし、TEdit や TMemo の作る Win32 の 'EDIT' コントロールの場合は、Text プロパティーに相当している。リストボックスやツリービューなど、ウィンドウテキストを直接目に見えるかたちで表示しないウィンドウも多い。その場合、多くはウィンドウテキストは空文字になっている。
マイクロソフトは、ウィンドウズというOSはオブジェクト指向のOSであると主張していたことがある。それで、ウィンドウを作成するためには、まず、「クラス」を登録しなければならない。この「クラス」は Delphi の言語上のクラスとは何の関係もない。API の第1章で示したとおり、TWndClass 型のレコードの各フィールドを設定して、RegisterClass() でOSに登録するのである。
PWndClassA = ^TWndClassA; PWndClassW = ^TWndClassW; PWndClass = PWndClassA; tagWNDCLASSA = packed record style: UINT; //クラススタイル lpfnWndProc: TFNWndProc; //ウィンドウ関数 cbClsExtra: Integer; //クラスの付加情報 cbWndExtra: Integer; //ウィンドウの付加情報 hInstance: HINST; //このクラスを登録するインスタンスハンドル hIcon: HICON; //アイコン hCursor: HCURSOR; //カーソル hbrBackground: HBRUSH; //背景色のブラシハンドル lpszMenuName: PAnsiChar; //メニューリソース名 lpszClassName: PAnsiChar;//クラス名 end; tagWNDCLASS = tagWNDCLASSA; TWndClassA = tagWNDCLASSA; TWndClass = TWndClassA;
逆に、そのインスタンスであるウィンドウのハンドルからクラス情報を取得するには、GetClassInfo() を使う。または、個別に GetClassLong() を使って取得する。ここでは、GetClassLong() を使っている。
function GetClassLong(hWnd: HWND; nIndex: Integer): DWORD; stdcall;
第二パラメータは、どのフィールドを取得するか指定する以下の定数を設定する。
const GCL_MENUNAME = -8; GCL_HBRBACKGROUND = -10; GCL_HCURSOR = -12; GCL_HICON = -14; GCL_HMODULE = -16; GCL_CBWNDEXTRA = -18; GCL_CBCLSEXTRA = -20; GCL_WNDPROC = -24; GCL_STYLE = -26; GCW_ATOM = -32; GCL_HICONSM = -34;
定数の内容は、上記の TWndClass の各フィールドに対応している。
最後に、CreateWindow(Ex) でつくられるウィンドウの情報であるが、これは GetWindowLong() で各パラメータを個別に取得する。
function GetWindowLong(hWnd: HWND; nIndex: Integer): Longint; stdcall;
第二パラメータでどの情報を取得するか設定する。
const GWL_WNDPROC = -4; //ウィンドウ関数のアドレス GWL_HINSTANCE = -6; //このウィンドウがつくられたインスタンスハンドル GWL_HWNDPARENT = -8; //親またはオーナーのウィンドウハンドル GWL_STYLE = -16; //ウィンドウスタイル GWL_EXSTYLE = -20; //エクストラスタイル GWL_USERDATA = -21; //ユーザデータのサイズ GWL_ID = -12; //コントロールID
以上である。これらの情報が何の役に立つのかだんだん分かってくるだろう。
API の 9−5 ウィンドウの列挙 と重複するが、実行中のすべてのトップレベルウィンドウを列挙してみよう。EnumWindows() は以下のような宣言になっている。
function EnumWindows(lpEnumFunc: TFNWndEnumProc; lParam: LPARAM): BOOL; stdcall;
第一パラメータは、この関数が実行されるとシステムから呼び出されるコールバック関数のアドレスを設定する。第二パラメータは、アプリケーション定義のオプションパラメータであり、コールバック関数が呼ばれたときに渡される。コールバック関数の形は以下のようになっている。
function EnumWinProc(hWindow: HWND; lData: LPARAM):BOOL; stdcall;
第一パラメータが列挙されたウィンドウのハンドルである。列挙を続けるときは result に true を、中断するときは false を設定する。
それではさっそく試してみよう。
List 9
unit Main; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ComCtrls, StdCtrls; type TForm1 = class(TForm) ListView1: TListView; Button1: TButton; Label1: TLabel; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); private { Private 宣言 } public { Public 宣言 } end; var Form1: TForm1; implementation {$R *.DFM} uses WinSearch; procedure TForm1.FormCreate(Sender: TObject); begin ListView1.ViewStyle := vsReport; ListView1.RowSelect := true; ListViewColumnAdd(ListView1,'Handle',150); ListViewColumnAdd(ListView1,'ClassName',180); ListViewColumnAdd(ListView1,'WindowText',220); end; type PHWndArray = ^THWndArray; THWndArray = array[0..500] of HWND; function EnumWndProc(hWindow:HWND;lData:LPARAM):BOOL;stdcall; var pWA:PHWndArray; begin pWA := PHWndArray(lData); pWA^[0] := pWA^[0]+1; pWA^[pWA^[0]] := hWindow; result := true; end; function AEnumWindows(var WA:THWndArray):integer; begin WA[0] := 0; EnumWindows(@EnumWndProc,LPARAM(@WA)); result := WA[0]; end; procedure TForm1.Button1Click(Sender: TObject); var SL:TStringList; WA:THWndArray; i,Num:integer; begin SL := TStringList.Create; try Num := AEnumWindows(WA); Label1.Caption := IntToStr(Num); if Num > 0 then for i := 1 to Num do begin SL.Clear; SL.Add(GetClassNameStr(WA[i])); SL.Add(GetWindowTextStr(WA[i])); ListViewItemAdd(ListView1,Format('$%.8x (%d)',[WA[i],WA[i]]),SL.Text); end; finally SL.Free; end; end; end.
ここでは、EnumWindows() API をラップした AEnumWindows() という関数を作成した。列挙されたウィンドウハンドルとその数を受け取るために新たに以下の型を定義している。
type PHWndArray = ^THWndArray; THWndArray = array[0..500] of HWND;
コールバック関数とのやりとりを複雑にしないために THWndArray 型の最初の要素ゼロには、列挙されたハンドルの数が格納される。要素1以降の500個の配列にハンドルが格納される。
また、ここで uses されている WinSearch.pas は前節まででつくった関数を集めたユニットファイルである。以下に示す。
List 10
unit WinSearch; interface uses Windows, Messages, SysUtils, Classes, ComCtrls; procedure ListViewColumnAdd(LV: TListView; ACaption: string; AWidth: integer); procedure ListViewItemAdd(LV: TListView; ACaption,SubItem: string); function IsChildWindow(Handle:HWND):Boolean; function GetClassNameStr(hWindow:HWND):string; function GetWindowTextStr(hWindow:HWND):string; procedure ClassInfos(LV:TListView;hWindow:HWND); procedure WindowInfos(LV:TListView;hWindow:HWND); implementation procedure ListViewColumnAdd(LV: TListView; ACaption: string; AWidth: integer); begin with LV.Columns.Add do begin Caption := ACaption; Width := AWidth; end; end; procedure ListViewItemAdd(LV: TListView; ACaption,SubItem: string); begin with LV.Items.Add do begin Caption := ACaption; SubItems.Text := SubItem; end; end; function IsChildWindow(Handle:HWND):Boolean; begin result := (GetWindowLong(Handle,GWL_STYLE) and WS_CHILD) <> 0; end; function GetClassNameStr(hWindow:HWND):string; var p:PChar; ret:integer; begin GetMem(p,100); ret := GetClassName(hWindow,p,100); SetString(result,p,ret); FreeMem(p); end; function GetWindowTextStr(hWindow:HWND):string; var p:PChar; ret:integer; begin ret := GetWindowTextLength(hWindow); GetMem(p,ret+1); ret := GetWindowText(hWindow,p,ret+1); SetString(result,p,ret); FreeMem(p); end; procedure ClassInfos(LV:TListView;hWindow:HWND); var WC: TWndClass; s:string; begin s := GetClassNameStr(hWindow); ListViewItemAdd(LV,'ClassName',s); GetClassInfo(hInstance,PChar(s),WC); ListViewItemAdd(LV,'Style',Format('$%.8x',[GetClassLong(hWindow,GCL_STYLE),8])); ListViewItemAdd(LV,'WndProc',Format('$%.8x',[GetClassLong(hWindow,GCL_WNDPROC)])); ListViewItemAdd(LV,'hModule',Format('$%.8x',[GetClassLong(hWindow,GCL_HMODULE)])); ListViewItemAdd(LV,'ClsExtra',Format('$%.8x bytes',[GetClassLong(hWindow,GCL_CBCLSEXTRA)])); ListViewItemAdd(LV,'WndExtra',Format('$%.8x bytes',[GetClassLong(hWindow,GCL_CBWNDEXTRA)])); ListViewItemAdd(LV,'hIcon',Format('$%.8x',[GetClassLong(hWindow,GCL_HICON)])); ListViewItemAdd(LV,'hBrush',Format('$%.8x',[GetClassLong(hWindow,GCL_HBRBACKGROUND)])); ListViewItemAdd(LV,'hCursor',Format('$%.8x',[GetClassLong(hWindow,GCL_HCURSOR)])); end; procedure WindowInfos(LV:TListView;hWindow:HWND); begin ListViewItemAdd(LV,'TEXT',GetWindowTextStr(hWindow)); ListViewItemAdd(LV,'HANDLE',Format('$%.8x (%d)',[hWindow,hWindow])); ListViewItemAdd(LV,'EXSTYLE',Format('$%.8x',[GetWindowLong(hWindow,GWL_EXSTYLE)])); ListViewItemAdd(LV,'STYLE',Format('$%.8x',[GetWindowLong(hWindow,GWL_STYLE)])); ListViewItemAdd(LV,'WNDPROC',Format('$%.8x',[GetWindowLong(hWindow,GWL_WNDPROC)])); ListViewItemAdd(LV,'HINSTANCE',Format('$%.8x',[GetWindowLong(hWindow,GWL_HINSTANCE)])); ListViewItemAdd(LV,'HWNDPARENT',Format('$%.8x (%d)', [GetWindowLong(hWindow,GWL_HWNDPARENT),GetWindowLong(hWindow,GWL_HWNDPARENT)])); ListViewItemAdd(LV,'ID',Format('$%.8x (%d)', [GetWindowLong(hWindow,GWL_ID),GetWindowLong(hWindow,GWL_ID)])); end; end.
実行時の様子を以下に示す。ListView1 のサイズは適当に調整してほしい。
筆者の現環境では図のように223個のウィンドウが列挙された。なんだか、列挙の順番もよく分からないし、IME 関係のウィンドウがたくさん列挙されたりして、なにがなんだかよく分からない。列挙の条件をもう少し厳しくしてみよう。WindSearch.pas に以下の関数を追加する。
List 11
function IsOwnedWindow(hWindow:HWND):Boolean; begin result := GetWindow(hWindow,GW_OWNER) <> 0; end;
これはすでに示したように、hWindow のウィンドウが Owned されているときに true を返す。コールバック関数を以下のようにして、所有されていないウィンドウだけを列挙してみよう。これは、たとえば Delphi でつくられたアプリの場合は、フォームは一切列挙しないで Application ウィンドウだけをリストに加えることに対応する。
List 12
function EnumWndProc(hWindow:HWND;lData:LPARAM):BOOL;stdcall; var pWA:PHWndArray; begin result := true; if IsOwnedWindow(hWindow) then exit; pWA := PHWndArray(lData); pWA^[0] := pWA^[0]+1; pWA^[pWA^[0]] := hWindow; end;
実行時の様子を以下にしめす。
うーむ、かなり数が減ったようだ。IME 関係のウィンドウはなくなった。しかし、まだ、実際に目に見えるウィンドウの数よりはるかに多い。さらに条件を厳しくしてみよう。
function IsWindowVisible(hWnd: HWND): BOOL; stdcall;
この API を使って、非可視なウィンドウを排除してみよう。
List 13
function EnumWndProc(hWindow:HWND;lData:LPARAM):BOOL;stdcall; var pWA:PHWndArray; begin result := true; if IsOwnedWindow(hWindow) then exit; if not IsWindowVisible(hWindow) then exit; pWA := PHWndArray(lData); pWA^[0] := pWA^[0]+1; pWA^[pWA^[0]] := hWindow; end;
結果は以下のようになる。
おお、このくらいの数になると自分が起動しているアプリとの対応が十分つくようになるだろう。上の図では、自分自身と Delphi IDE も含めて Delphi 製のアプリが5つ起動されているのが分かる。
次に、EnumWindows() ではなく、GetWindow() を使って同じことをしてみよう。SDK には、リアルタイム性が劣るのでなるべく、EnumXXXX を使うようにしなさい、と注意書きがある。しかし、GetWindow() のよさは、列挙される順番が z-order の順番であることが保証されていることだ。
function GetWindow(hWnd: HWND; uCmd: UINT): HWND; stdcall;
この第二パラメータには、以下のものを指定して、取得するウィンドウと hWnd パラメータで指定されるウィンドウを関係づける。
const { GetWindow() Constants } GW_HWNDFIRST = 0; // hWnd と同種のウィンドウでもっとも z-order が高いものを取得 GW_HWNDLAST = 1; // hWnd と同種のウィンドウでもっとも z-order が低いものを取得 GW_HWNDNEXT = 2; // hWnd と同種のウィンドウで z-order が次の順番のものを取得 GW_HWNDPREV = 3; // hWnd と同種のウィンドウで z-order が前の順番のものを取得 GW_OWNER = 4; // hWnd のオーナーを取得 GW_CHILD = 5; // hWnd の子ウィンドウのうちもっとも z-order が高いものを取得
それではやってみよう。前回のプログラムに Button2 を追加して、その OnClick イベントハンドラを以下のようにする。
List 14
function EnumVisibleNoOwnedWindowAsZOrder(var WA:THWndArray):integer; var hWindow:HWND; begin WA[0] := 0; hWindow := GetWindow(Application.Handle,GW_HWNDFIRST); while (hWindow <> 0) do begin if (not IsOwnedWindow(hWindow)) and IsWindowVisible(hWindow) then begin WA[0] := WA[0]+1; WA[WA[0]] := hWindow end; hWindow := GetWindow(hWindow,GW_HWNDNEXT); end; result := WA[0]; end; procedure TForm1.Button2Click(Sender: TObject); var SL:TStringList; WA:THWndArray; i,Num:integer; begin SL := TStringList.Create; try Num := EnumVisibleNoOwnedWindowAsZOrder(WA); Label1.Caption := IntToStr(Num); if Num > 0 then for i := 1 to Num do begin SL.Clear; SL.Add(GetClassNameStr(WA[i])); SL.Add(GetWindowTextStr(WA[i])); ListViewItemAdd(ListView1,Format('$%.8x (%d)',[WA[i],WA[i]]),SL.Text); end; finally SL.Free; end; end;
結果を下図に示した。
このようにして、一連のウィンドウのリストを取得できるのは、SDK を読むと書いてあるのだが、どうもウィンドウ自身が自分と同種のウィンドウについてのリンクリストの一つをデータとして持っているようなのである。また、z-order のもっとも高い子ウィンドウとオーナーまたは親ウィンドウのデータも持っているらしい。そういう意味では、GetWindow() はこの種の API のなかでは最も低レベルな API と言えるだろう。
前節では EnumWindows() と GetWindow() を使って、トップレベルウィンドウを列挙した。いろいろな条件をつけてリストに加える数を変えることができた。ここでは、特定のクラス名あるいはウィンドウテキストを持つトップレベルウィンドウのハンドルを取得しよう。また、特別な条件に合致するウィンドウハンドルを取得する方法について調べよう。
まず、予めクラス名が分かっているときは、FindWindow() を使ってハンドルを取得する。
function FindWindow(lpClassName, lpWindowName: PChar): HWND; stdcall;
このパラメータについては説明の必要はないだろう。一般に、アプリケーションのトップレベルウィンドウのキャプション、すなわちウィンドウテキストは保持しているドキュメントのファイルネームなどを表示するため、時々刻々変化する。したがって、そのような場合は lpWindowName を nil にしておいた方が検索漏れがない。それでは具体的にやってみよう。メモ帳を起動しておくと、メモ帳のメインウィンドウのクラス名は、'Notepad' なのでこれを使う。これは前節で作ったプログラムで確認できる。なお、FindWindow() では、パラメータの文字列において、大文字小文字の区別はない。前節までのプログラムのフレームを継続して使用し、以下のようなコードを書く。
List 15
var SL:TStringList; procedure TForm1.Button1Click(Sender: TObject); var hWindow:HWND; begin hWindow := FindWindow('Notepad',nil); if hWindow = 0 then exit; SL := TStringList.Create; try SL.Clear; SL.Add(GetClassNameStr(hWindow)); SL.Add(GetWindowTextStr(hWindow)); ListViewItemAdd(ListView1,Format('$%.8x (%d)',[hWindow,hWindow]),SL.Text); finally SL.Free; end; end;
結果を上図に示す。
たとえば、複数のメモ帳が起動されているとき、FindWindow() はどのメモ帳を見つけてくるのだろうか。lpWindowName が予め分かっていれば、それを指定して FindWindow() で見つけられるだろう。そうでなければ、前節でやったように EnumWindow() で列挙しながらクラス名が一致するかどうかで、複数のインスタンスに相当するウィンドウハンドルを取得できるだろう。でも、この場合、どのハンドルが目的とするものなのか判断できない。前節の最後にやったように、GetWindow() のループを利用すると、z-order の順序でハンドルが取得できる。この場合、たとえばユーザが最後にアクセスしたメモ帳が最も z-order が小さい(手前に表示されている)と期待できるので、これで判断できるだろう。試してみよう。メモ帳を三つ起動して、位置をずらしておく。Button2Click を以下のようにする。
List 16
type PHWndArray = ^THWndArray; THWndArray = array[0..500] of HWND; function EnumWindowsOfClassNameAsZOrder(var WA:THWndArray; ClassName:string):integer; var hWindow:HWND; begin WA[0] := 0; hWindow := GetWindow(Application.Handle,GW_HWNDFIRST); while (hWindow <> 0) do begin if (not IsOwnedWindow(hWindow)) and IsWindowVisible(hWindow) and (UpperCase(GetClassNameStr(hWindow)) = UpperCase(ClassName)) then begin WA[0] := WA[0]+1; WA[WA[0]] := hWindow end; hWindow := GetWindow(hWindow,GW_HWNDNEXT); end; result := WA[0]; end; procedure TForm1.Button2Click(Sender: TObject); var SL:TStringList; WA:THWndArray; i,Num:integer; r:TRect; begin SL := TStringList.Create; try Num := EnumWindowsOfClassNameAsZOrder(WA,'Notepad'); Label1.Caption := IntToStr(Num); if Num > 0 then for i := 1 to Num do begin SL.Clear; SL.Add(GetClassNameStr(WA[i])); GetWindowRect(WA[i],r); SL.Add(Format('Left = %d Top = %d',[r.Left,r.Top])); ListViewItemAdd(ListView1,Format('$%.8x (%d)',[WA[i],WA[i]]),SL.Text); end; finally SL.Free; end; end;
実行結果を上図に示す。予め、左から順にマウスでクリックしておいたので、右側から順番に z-order が高くなっているので期待通りに得られているのが分かる。右から順にクリックして z-order を逆順にすると下図のような結果が得られ、やはり期待通りに動作する。
次に、一つのスレッド内で作られたトップレベルウィンドウを列挙する EnumThreadWindows() を試してみよう。
function EnumThreadWindows(dwThreadId: DWORD; lpfn: TFNWndEnumProc; lParam: LPARAM): BOOL; stdcall;
これは EnumWindows() より第一パラメータが多いだけである。コールバック関数は同じである。第一パラメータには、スレッドIDを設定する。これはウィンドウハンドルから GetWindowThreadProcessId() で取得できる。
function GetWindowThreadProcessId(hWnd: HWND; lpdwProcessId: Pointer): DWORD; stdcall;
プロセスIDが必要なければ、第二パラメータは nil にすることができる。必要なときは DWORD 型の変数を用意してそのアドレスを設定する。この関数の戻り値がスレッドIDである。それでは、筆者の Delphi5 の IDE について試してみよう。前節の結果で見たように Delphi の IDE の最も頂点のトップレベルウィンドウのクラス名は TApplication 、そしてウィンドウテキストは Delphi 5 である。以下のように Button1Click にコードを書く。
List 17
type PHWndArray = ^THWndArray; THWndArray = array[0..500] of HWND; function EnumThreadWndProc(hWindow:HWND;lData:LPARAM):BOOL;stdcall; var pWA:PHWndArray; begin pWA := PHWndArray(lData); pWA^[0] := pWA^[0]+1; pWA^[pWA^[0]] := hWindow; result := true; end; function AEnumThreadWindows(var WA:THWndArray; OneOfHandles:HWND):integer; begin WA[0] := 0; EnumThreadWindows(GetWindowThreadProcessId(OneOfHandles,nil), @EnumThreadWndProc,LPARAM(@WA)); result := WA[0]; end; procedure TForm1.Button1Click(Sender: TObject); var SL:TStringList; hWindow:HWND; WA:THWndArray; i,Num:integer; begin hWindow := FindWindow('TApplication','Delphi 5'); if hWindow = 0 then exit; SL := TStringList.Create; try Num := AEnumThreadWindows(WA,hWindow); Label1.Caption := IntToStr(Num); if Num > 0 then for i := 1 to Num do begin SL.Clear; SL.Add(GetClassNameStr(WA[i])); SL.Add(GetWindowTextStr(WA[i])); ListViewItemAdd(ListView1,Format('$%.8x (%d)',[WA[i],WA[i]]),SL.Text); end; finally SL.Free; end; end;
実行結果を上図に示す。48個も列挙されたが、Tooltip や IME 関連のものも含まれている。同じことを IE6 についてやってみよう。
List 18
procedure TForm1.Button2Click(Sender: TObject); var SL:TStringList; hWindow:HWND; WA:THWndArray; i,Num:integer; begin hWindow := FindWindow('IEFrame',nil); if hWindow = 0 then exit; SL := TStringList.Create; try Num := AEnumThreadWindows(WA,hWindow); Label1.Caption := IntToStr(Num); if Num > 0 then for i := 1 to Num do begin SL.Clear; SL.Add(GetClassNameStr(WA[i])); SL.Add(GetWindowTextStr(WA[i])); ListViewItemAdd(ListView1,Format('$%.8x (%d)',[WA[i],WA[i]]),SL.Text); end; finally SL.Free; end; end;
親ウィンドウのハンドルを指定して、その中の子ウィンドウを探す手続きを試してみよう。まず列挙することから始める。
function EnumChildWindows(hWndParent: HWND; lpEnumFunc: TFNWndEnumProc; lParam: LPARAM): BOOL; stdcall;
第一パラメータには親ウィンドウのハンドルを設定する。第二パラメータは、システムから呼ばれるコールバック関数であり、その形は EnumWindows() のときと同じである。第三パラメータも同じで、アプリケーション定義のオプションパラメータで、コールバック関数にそのまま渡される。前節と同じフレームを使い、いくつかコントロールを置いて以下のようにする。
List 19
type PHWndArray = ^THWndArray; THWndArray = array[0..500] of HWND; function EnumChildWndProc(hWindow:HWND;lData:LPARAM):BOOL;stdcall; var pWA:PHWndArray; begin pWA := PHWndArray(lData); pWA^[0] := pWA^[0]+1; pWA^[pWA^[0]] := hWindow; result := true; end; function AEnumChildWindows(var WA:THWndArray; hParent:HWND):integer; begin WA[0] := 0; EnumChildWindows(hParent,@EnumChildWndProc,LPARAM(@WA)); result := WA[0]; end; procedure TForm1.Button1Click(Sender: TObject); var SL:TStringList; WA:THWndArray; i,Num:integer; begin SL := TStringList.Create; try Num := AEnumChildWindows(WA,Handle); Label1.Caption := IntToStr(Num); if Num > 0 then for i := 1 to Num do begin SL.Clear; SL.Add(GetClassNameStr(WA[i])); SL.Add(GetWindowTextStr(WA[i])); ListViewItemAdd(ListView1,Format('$%.8x (%d)',[WA[i],WA[i]]),SL.Text); end; finally SL.Free; end; end;
結果は上図のようになる。おもしろいことに、ListView のなかのヘッダーコントロールや、ComboBox のなかの Edit も列挙される。このように、EnumChildWindows() は、直接の子ウィンドウだけでなく、子の連鎖をたどって、孫やひ孫ウィンドウまで列挙する。この中から、特定のクラス名の子ウィンドウを取得するには、コールバック関数でフィルタをかければよいだろう。ComboBox のなかの Edit を見つけてみよう。
List 20
function FindChildOfClass(hParent:HWND; ClassName:string):HWND; var WA:THWndArray; i,Num:integer; begin result := 0; Num := AEnumChildWindows(WA,hParent); if Num > 0 then for i := 1 to Num do if UpperCase(GetClassNameStr(WA[i])) = UpperCase(ClassName) then begin result := WA[i]; break; end; end; procedure TForm1.Button2Click(Sender: TObject); var SL:TStringList; hChild:HWND; begin SL := TStringList.Create; try hChild := FindChildOfClass(Handle,'EDIT'); if hChild <> 0 then begin SL.Add(GetClassNameStr(hChild)); SL.Add(GetWindowTextStr(hChild)); ListViewItemAdd(ListView1,Format('$%.8x (%d)',[hChild,hChild]),SL.Text); end; finally SL.Free; end; end;
結果は上図のようにうまくいく。もちろん、クラス名のほか、ウィンドウテキストも指定できるし、両方指定することもできる。このような方法で、一つのトップレベルウィンドウのなかの特定のコントロールのハンドルを取得することができる。
つぎに、直接の子ウィンドウだけを列挙することを考える。それには、前にも使った GetWindow() を使う。ただし、最初のハンドルを取得するときに、GW_CHILD を設定して、もっとも z-order の高い子ウィンドウから始める。
List 21
type PHWndArray = ^THWndArray; THWndArray = array[0..500] of HWND; function EnumDirectChildren(var WA:THWndArray; hParent:HWND):integer; var hWindow:HWND; begin WA[0] := 0; hWindow := GetWindow(hParent,GW_CHILD); while (hWindow <> 0) do begin WA[0] := WA[0]+1; WA[WA[0]] := hWindow; hWindow := GetWindow(hWindow,GW_HWNDNEXT); end; result := WA[0]; end; procedure TForm1.Button1Click(Sender: TObject); var SL:TStringList; WA:THWndArray; i,Num:integer; begin SL := TStringList.Create; try Num := EnumDirectChildren(WA,Handle); Label1.Caption := IntToStr(Num); if Num > 0 then for i := 1 to Num do begin SL.Clear; SL.Add(GetClassNameStr(WA[i])); SL.Add(GetWindowTextStr(WA[i])); ListViewItemAdd(ListView1,Format('$%.8x (%d)',[WA[i],WA[i]]),SL.Text); end; finally SL.Free; end; end;
上図に示したとおり、直接の子ウィンドウだけが列挙される。この中から、特定のクラス名やウィンドウテキストを持つ子ウィンドウを取得するには、FindWindowEx() が使える。
function FindWindowEx(Parent, Child: HWND; ClassName, WindowName: PChar): HWND; stdcall;
Child には、検索を始める最初の z-order の子ウィンドウのハンドルを設定する。ゼロを指定すると、最初から検索する。この関数はみつからなかったらゼロを返す。試してみよう。
List 22
procedure TForm1.Button2Click(Sender: TObject); var hWindow:HWND; SL:TStringList; begin SL := TStringList.Create; try hWindow := FindWindowEx(Handle,0,'TButton','Button1'); if hWindow <> 0 then begin SL.Add(GetClassNameStr(hWindow)); SL.Add(GetWindowTextStr(hWindow)); ListViewItemAdd(ListView1,Format('$%.8x (%d)',[hWindow,hWindow]),SL.Text); end; finally SL.Free; end; end;
ここでは、クラス名を TButton 、WindowName を Button1 としてみた。結果を上図に示す。
以上、この節では、子ウィンドウのハンドルを列挙したり、また、特定の子ウィンドウのハンドルを取得する方法について調べた。
これまで調べてきたように、親子関係と所有関係をもとにして、一つのスレッドのなかで各ウィンドウは階層を成していると見なせる。もっとも上位のウィンドウは、所有されていないトップレベルウィンドウである。Delphi でつくられたアプリケーションでは、これは Application ウィンドウに相当する。このウィンドウを頂点として、所有関係と親子関係を使って、すべてのウィンドウを系統図で表したい。これがこの章の目的である。この節では、まず任意のウィンドウからはじめて、階層を遡る方法を調べよう。つぎに、所有されていないトップレベルウィンドウから階層を系統的に下って行く方法を考えよう。
まず、任意の子ウィンドウからはじめて、それを含むトップレベルウィンドウなどを求めてみよう。これにぴったりな API がある。ただし、Win95 では使えない。
FUNCTION GetAncestor(hwnd: HWND; gaFlags: UINT): HWND; stdcall;
第一パラメータは出発点のウィンドウのハンドルを設定する。第二パラメータは以下の定数のうちの一つである。
const GA_PARENT = 1; // 親ウィンドウを返す GA_ROOT = 2; // 親子関係を遡って、直近上位のトップレベルウィンドウを返す GA_ROOTOWNER = 3; // 親子関係と所有関係を遡って、所有されていないトップレベルウィンドウを返す
しかし、わたしの Win2k の環境では GA_ROOT のときと GA_ROOTOWNER のときはおなじ結果になってヘルプどおりには動作しない。これは、GetParent() がやはりヘルプと違ってオーナーを返さないこととよく対応している。古いヘルプは出発ウィンドウが所有されたトップレベルウィンドウの場合でも GetParent() はオーナーを返すとは書いていないので、これの方が現状では正しいようだ。Win95 で使えないこともあって、GetAncestor() は使用しないことにする。ここでは、GetParent() と GetWindow() を使って、それぞれ親子関係と所有関係を遡って失敗するまで繰り返し、一つ戻ることにより、最上位のトップレベルウィンドウを取得する関数を作った。前節のプログラムのフレームをそのまま使って以下のように試した。
List 23
function GetMostTopLevelWindow(hWindow:HWND):HWND; var hAncestor,hPrev:HWND; begin hPrev := hWindow; hAncestor := GetParent(hPrev); while hAncestor <> 0 do begin hPrev := hAncestor; hAncestor := GetParent(hPrev) end; hAncestor := hPrev; while hAncestor <> 0 do begin hPrev := hAncestor; hAncestor := GetWindow(hPrev,GW_OWNER) end; result := hPrev; end; procedure TForm1.Button1Click(Sender: TObject); var SL:TStringList; hWindow:HWND; begin SL := TStringList.Create; try hWindow := GetMostTopLevelWindow(ComboBox1.Handle); if hWindow <> 0 then begin SL.Clear; SL.Add(GetClassNameStr(hWindow)); SL.Add(GetWindowTextStr(hWindow)); ListViewItemAdd(ListView1,Format('$%.8x (%d)',[hWindow,hWindow]),SL.Text); end; finally SL.Free; end; end;
結果を上図に示した。ここでは、最も階層が低い ComboBox1 から出発している。そして、期待通り Application ウィンドウのハンドルの取得に成功している。Button1、Form1、Application から出発しても同じ結果になることが確かめられる。以上で、任意のウィンドウから出発して、最上位のトップレベルウィンドウを取得することができた。
つぎに、トップレベルウィンドウから出発して、親子関係と所有関係を下っていくことを考える。親子関係については、すでに前節で、自分の直接の子ウィンドウを列挙することができた。これを再帰的に使うとどこまでも下っていくことができるだろう。所有関係については、一層だけ下ることはまだやっていなかった。トップレベルウィンドウのハンドルを与えて、直接の Owned ウィンドウを列挙する関数をつくるとよい。これは EnumThreadWindows() で列挙されたものから、オーナーを調べてフィルタリングするとできるだろう。さっそくやってみよう。Button2Click を以下のようにする。AEnumThreadWindows() までは、前々節でやったのと同じである。新たに作ったのは EnumDirectOwnedWindows( ) だけである。
List 24
type PHWndArray = ^THWndArray; THWndArray = array[0..500] of HWND; function EnumThreadWndProc(hWindow:HWND;lData:LPARAM):BOOL;stdcall; var pWA:PHWndArray; begin pWA := PHWndArray(lData); pWA^[0] := pWA^[0]+1; pWA^[pWA^[0]] := hWindow; result := true; end; function AEnumThreadWindows(var WA:THWndArray; OneOfHandles:HWND):integer; begin WA[0] := 0; EnumThreadWindows(GetWindowThreadProcessId(OneOfHandles,nil), @EnumThreadWndProc,LPARAM(@WA)); result := WA[0]; end; function EnumDirectOwnedWindows(var WA:THWndArray; hOwner:HWND):integer; var WWA:THWndArray; i,Num:integer; begin result := 0; Num := AEnumThreadWindows(WWA,hOwner); if Num > 0 then for i := 1 to Num do if GetWindow(WWA[i],GW_OWNER) = hOwner then begin Inc(result); WA[result] := WWA[i] end; end; procedure TForm1.Button2Click(Sender: TObject); var SL:TStringList; hWindow:HWND; WA:THWndArray; i,Num:integer; begin hWindow := FindWindow('TApplication','Delphi 5'); if hWindow = 0 then exit; SL := TStringList.Create; try Num := EnumDirectOwnedWindows(WA,hWindow); Label1.Caption := IntToStr(Num); if Num > 0 then for i := 1 to Num do begin SL.Clear; SL.Add(GetClassNameStr(WA[i])); SL.Add(GetWindowTextStr(WA[i])); ListViewItemAdd(ListView1,Format('$%.8x (%d)',[WA[i],WA[i]]),SL.Text); end; finally SL.Free; end; end;
ここでは、また Delphi5 の IDE の最上位のトップレベルウィンドウからの直接の owned ウィンドウを列挙している。どうやらうまくいっているようだ。Application.Handle からは、期待通り Form1 のみが得られることも確認した。
さて、前節までで調べたことにより、本章の冒頭で示したようなウィンドウの階層をツリー表示するための準備が整った。前節までで試したコードを関数にして WinSearch.pas に整理したものを下にリンクしておく。これを WinSearch.pas として、以降のプロジェクトで使用する。
それでは、さっそく始めることにしよう。まず、EnumVisbleNoOwnedWindows() で可視で所有されていないトップレベルウィンドウを列挙し、それらを頂点とするウィンドウの階層をツリー表示してみよう。Form1 に TreeView1 と Button1 と Label1 を適当に配置して以下のようにする。
List 25
unit Main; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls; type TForm1 = class(TForm) TreeView1: TTreeView; Button1: TButton; Label1: TLabel; procedure Button1Click(Sender: TObject); procedure TreeView1Change(Sender: TObject; Node: TTreeNode); private { Private 宣言 } public procedure EnumChildren(Node:TTreeNode); end; var Form1: TForm1; implementation {$R *.DFM} uses WinSearch; procedure TForm1.EnumChildren(Node: TTreeNode); var WA:THWndArray; i,Num:integer; TN:TTreeNode; begin if Node.Count > 0 then exit; Num := EnumDirectChildren(WA,HWND(Node.Data)); if Num > 0 then for i := 1 to Num do begin TN := TreeView1.Items.AddChildObject(Node, GetClassNameStr(WA[i]),pointer(WA[i])); EnumChildren(TN); end; Num := EnumDirectOwnedWindows(WA,HWND(Node.Data)); if Num > 0 then for i := 1 to Num do begin TN := TreeView1.Items.AddChildObject(Node, '*'+GetClassNameStr(WA[i]),pointer(WA[i])); EnumChildren(TN); end; end; procedure TForm1.Button1Click(Sender: TObject); var WA:THWndArray; i,Num:integer; s:string; begin TreeView1.Items.Clear; Num := EnumVisbleNoOwnedWindows(WA); Label1.Caption := IntToStr(Num); if Num > 0 then for i := 1 to Num do begin s := Format('%s ...... %s',[GetClassNameStr(WA[i]),GetWindowTextStr(WA[i])]); TreeView1.Items.AddObject(nil,s,pointer(WA[i])); end; end; procedure TForm1.TreeView1Change(Sender: TObject; Node: TTreeNode); begin if (Node.Level = 0) and (Node.Count = 0) then begin EnumChildren(Node); Node.Expand(true); end; end; end.
実行して、Button1 をクリックすると右図のようになる。各ウィンドウのクラス名とウィンドウテキストを表示している。Delphi で作られたアプリの最上位のトップレベルウィンドウのクラス名は必ず TApplication になるので、複数のアプリが起動されているとどれがどれだか分からなくなる。しかし、そのウィンドウテキストは、タスクバーのボタンに表示されるものと同じなので、区別がつくようになる。
列挙された各最上位のトップレベルウィンドウをクリックすると、所有関係と親子関係を使って再帰的にウィンドウハンドルを取得して右図のようにツリー表示をする。これは、IE6 の例である。クラス名の先頭に星印がついているときは、その上の階層との関係が所有関係であることを示している。つまり、Owned ウィンドウである。なにも印がないのは、親子関係である。つまり、上の階層の子ウィンドウである。
コードを見てみよう。Button1 を押すと、
procedure TForm1.Button1Click(Sender: TObject); var WA:THWndArray; i,Num:integer; s:string; begin TreeView1.Items.Clear; Num := EnumVisbleNoOwnedWindows(WA); Label1.Caption := IntToStr(Num); if Num > 0 then for i := 1 to Num do begin s := Format('%s ...... %s',[GetClassNameStr(WA[i]),GetWindowTextStr(WA[i])]); TreeView1.Items.AddObject(nil,s,pointer(WA[i])); end; end;
が実行されて、可視な最上位のトップレベルウィンドウが列挙され、それぞれを TreeView1 の最上位のレベルにクラス名とウィンドウテキストを表示する。それと同時に、各 TreeNode.Data に、ポインタにキャストしてウィンドウハンドルを設定しておく。つぎに、最上位項目をクリックすると、
procedure TForm1.TreeView1Change(Sender: TObject; Node: TTreeNode); begin if (Node.Level = 0) and (Node.Count = 0) then begin EnumChildren(Node); Node.Expand(true); end; end;
が実行される。最上位レベルのノードであり、かつ、子ノードを持たないとき、自分の Node を渡して EnumChildren( ) を呼び出す。
procedure TForm1.EnumChildren(Node: TTreeNode); var WA:THWndArray; i,Num:integer; TN:TTreeNode; begin if Node.Count > 0 then exit; Num := EnumDirectChildren(WA,HWND(Node.Data)); if Num > 0 then for i := 1 to Num do begin TN := TreeView1.Items.AddChildObject(Node, GetClassNameStr(WA[i]),pointer(WA[i])); EnumChildren(TN); end; Num := EnumDirectOwnedWindows(WA,HWND(Node.Data)); if Num > 0 then for i := 1 to Num do begin TN := TreeView1.Items.AddChildObject(Node, '*'+GetClassNameStr(WA[i]),pointer(WA[i])); EnumChildren(TN); end; end;
ここでは、EnumDirectChildren() と EnumDirectOwnedWindows() を使って、親子関係と所有関係のそれぞれ一層下のウィンドウを列挙する。そして、見つかったウィンドウ毎に自分を呼び出して、再帰的に階層を下ってゆく。各枝で再帰呼び出しが途切れるのは、
if Num > 0 then
が成り立たないとき、すなわち、子ウィンドウあるいは owned ウィンドウが一つも見つからないときである。このようにして、自分のノードを渡しながら自動的に末端の階層まで検索が進むのである。
つぎに、6−2節でやったように、各ウィンドウに対してそのクラス情報とウィンドウ情報を表示するようにしてみよう。ちょっと変更するだけでできる。全リストを以下に示す。
List 26
unit Main; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ComCtrls, StdCtrls; type TForm1 = class(TForm) ListView1: TListView; Button1: TButton; Label1: TLabel; ListView2: TListView; Label2: TLabel; Label3: TLabel; TreeView1: TTreeView; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); procedure TreeView1Change(Sender: TObject; Node: TTreeNode); private { Private 宣言 } public procedure EnumChildren(Node:TTreeNode); end; var Form1: TForm1; implementation {$R *.DFM} uses WinSearch; procedure TForm1.FormCreate(Sender: TObject); begin ListView1.ViewStyle := vsReport; ListView1.RowSelect := true; ListView2.ViewStyle := vsReport; ListView2.RowSelect := true; ListViewColumnAdd(ListView1,' 項 目',90); ListViewColumnAdd(ListView1,' 内 容',140); ListViewColumnAdd(ListView2,' 項 目',90); ListViewColumnAdd(ListView2,' 内 容',140); end; procedure TForm1.Button1Click(Sender: TObject); var WA:THWndArray; i,Num:integer; s:string; begin TreeView1.Items.Clear; Num := EnumVisbleNoOwnedWindows(WA); Label3.Caption := IntToStr(Num); if Num > 0 then for i := 1 to Num do begin s := Format('%s ...... %s',[GetClassNameStr(WA[i]),GetWindowTextStr(WA[i])]); TreeView1.Items.AddObject(nil,s,pointer(WA[i])); end; end; procedure TForm1.EnumChildren(Node: TTreeNode); var WA:THWndArray; i,Num:integer; TN:TTreeNode; s:string; begin if Node.Count > 0 then exit; Num := EnumDirectChildren(WA,HWND(Node.Data)); if Num > 0 then for i := 1 to Num do begin if IsWindowVisible(WA[i]) then s := '' else s :='$'; TN := TreeView1.Items.AddChildObject(Node, GetClassNameStr(WA[i])+s,pointer(WA[i])); EnumChildren(TN); end; Num := EnumDirectOwnedWindows(WA,HWND(Node.Data)); if Num > 0 then for i := 1 to Num do begin if IsWindowVisible(WA[i]) then s := '' else s :='$'; TN := TreeView1.Items.AddChildObject(Node, '*'+GetClassNameStr(WA[i])+s,pointer(WA[i])); EnumChildren(TN); end; end; procedure TForm1.TreeView1Change(Sender: TObject; Node: TTreeNode); begin if (Node.Level = 0) and (Node.Count = 0) then begin EnumChildren(Node); Node.Expand(true); end; ListView1.Items.Clear; ClassInfos(ListView1,HWND(Node.Data)); ListView2.Items.Clear; WindowInfos(ListView2,HWND(Node.Data)); end; end.
実行時の様子を上に示した。ここでは、自分自身を見ている。クラス名の後ろに $ がついているものは非可視のウィンドウである。
最後に、可視の任意のウィンドウを調べるプログラムをつくろう。といっても、さっきのプログラムをちょっと変更するだけである。デスクトップの任意の位置にある最も手前のウィンドウハンドルを取得できる API がある。
function WindowFromPoint(Point: TPoint): HWND; stdcall;
パラメータはスクリーン座標で与える。ここでは、マウスカーソルの座標を与えて、マウスの位置のウィンドウの情報を取得してみよう。以下に全リストを示す。
List 27
unit Main; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ComCtrls, StdCtrls; type TForm1 = class(TForm) ListView1: TListView; Button1: TButton; Label1: TLabel; ListView2: TListView; Label2: TLabel; TreeView1: TTreeView; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); procedure TreeView1Change(Sender: TObject; Node: TTreeNode); procedure FormDestroy(Sender: TObject); private { Private 宣言 } public idHotkey:Atom; procedure EnumChildren(Node:TTreeNode); procedure WMHotkey(var Msg:TWMHotkey);message WM_HOTKEY; end; var Form1: TForm1; implementation {$R *.DFM} uses WinSearch; procedure TForm1.FormCreate(Sender: TObject); begin ListView1.ViewStyle := vsReport; ListView1.RowSelect := true; ListView2.ViewStyle := vsReport; ListView2.RowSelect := true; ListViewColumnAdd(ListView1,' 項 目',90); ListViewColumnAdd(ListView1,' 内 容',140); ListViewColumnAdd(ListView2,' 項 目',90); ListViewColumnAdd(ListView2,' 内 容',140); idHotkey := GlobalAddAtom('MySecretHotkeyF11'); end; procedure TForm1.Button1Click(Sender: TObject); begin RegisterHotkey(Handle,idHotkey,0,VK_F11); Application.Minimize; end; procedure TForm1.EnumChildren(Node: TTreeNode); var WA:THWndArray; i,Num:integer; TN:TTreeNode; s:string; begin if Node.Count > 0 then exit; Num := EnumDirectChildren(WA,HWND(Node.Data)); if Num > 0 then for i := 1 to Num do begin if IsWindowVisible(WA[i]) then s := '' else s :='$'; TN := TreeView1.Items.AddChildObject(Node, GetClassNameStr(WA[i])+s,pointer(WA[i])); EnumChildren(TN); end; Num := EnumDirectOwnedWindows(WA,HWND(Node.Data)); if Num > 0 then for i := 1 to Num do begin if IsWindowVisible(WA[i]) then s := '' else s :='$'; TN := TreeView1.Items.AddChildObject(Node, '*'+GetClassNameStr(WA[i])+s,pointer(WA[i])); EnumChildren(TN); end; end; procedure TForm1.TreeView1Change(Sender: TObject; Node: TTreeNode); begin if (Node.Level = 0) and (Node.Count = 0) then begin EnumChildren(Node); Node.Expand(true); end; ListView1.Items.Clear; ClassInfos(ListView1,HWND(Node.Data)); ListView2.Items.Clear; WindowInfos(ListView2,HWND(Node.Data)); end; procedure TForm1.FormDestroy(Sender: TObject); begin GlobalDeleteAtom(idHotkey); UnregisterHotkey(Handle,idHotkey); end; procedure TForm1.WMHotkey(var Msg: TWMHotkey); var hFind,hMostTop:HWND; s:string; Node:TTreeNode; i:integer; begin if Msg.HotKey = idHotkey then begin hFind := WindowFromPoint(Mouse.CursorPos); hMostTop := GetMostTopLevelWindow(hFind); if hMostTop <> 0 then begin TreeView1.Items.Clear; s := Format('%s ...... %s',[GetClassNameStr(hMostTop), GetWindowTextStr(hMostTop)]); Node := TreeView1.Items.AddObject(nil,s,pointer(hMostTop)); TreeView1Change(nil,Node); for i := 0 to TreeView1.Items.Count-1 do if HWND(TreeView1.Items.Item[i].Data) = hFind then begin TreeView1.Items.Item[i].Selected := true; TreeView1.SetFocus; Break; end; Application.Restore; UnregisterHotkey(Handle,idHotkey); end else ShowMessage('Sorry, not Found!'); end; end; end.
実行して、Button1 を押すと最小化する。その状態で、取得したいウィンドウの位置にマウスカーソルをあわせ、F11 キーを押すと、復帰して情報を表示する。上図は、筆者の Win2k のタスクバーのトレイにあわせたときのものだ。タスクトレーはツールバーでできていることが分かる。
コードを見てみよう。
procedure TForm1.Button1Click(Sender: TObject); begin RegisterHotkey(Handle,idHotkey,0,VK_F11); Application.Minimize; end;
Button1 を押すと、ホットキーを登録して、最小化する。このとき、F11 キーを押すと、WM_HOTKEY が Form1 に送られてきて、これをメッセージ指令のプロシージャメソッドで捕まえる。
procedure TForm1.WMHotkey(var Msg: TWMHotkey); var hFind,hMostTop:HWND; s:string; Node:TTreeNode; i:integer; begin if Msg.HotKey = idHotkey then begin hFind := WindowFromPoint(Mouse.CursorPos); hMostTop := GetMostTopLevelWindow(hFind); if hMostTop <> 0 then begin TreeView1.Items.Clear; s := Format('%s ...... %s',[GetClassNameStr(hMostTop), GetWindowTextStr(hMostTop)]); Node := TreeView1.Items.AddObject(nil,s,pointer(hMostTop)); TreeView1Change(nil,Node); for i := 0 to TreeView1.Items.Count-1 do if HWND(TreeView1.Items.Item[i].Data) = hFind then begin TreeView1.Items.Item[i].Selected := true; TreeView1.SetFocus; Break; end; Application.Restore; UnregisterHotkey(Handle,idHotkey); end else ShowMessage('Sorry, not Found!'); end; end;
ここで、マウスカーソルの下のウィンドウハンドルを取得し、前節でやったように階層を GetMostTopLevelWindow() で遡って最上位のトップレベルウィンドウのハンドルを取得し、それから、前と同じようにしてツリーを組み立てる。そのなかから、マウスカーソルの下にあったウィンドウを探し出し、それを選択状態にしてクラス情報とウィンドウ情報を表示する。それから復帰して、ホットキーの登録を解除する。
本章では、ウィンドウズにおけるウィンドウ間の親子関係と所有関係を調べ、これらの相互関係によってウィンドウが階層を成していることを調べた。階層から任意のウィンドウを見つけ出したり、階層を遡ったり、系統的に下ってゆく方法を考えた。それを使って、ツリービューにこれらの階層を表示するプログラムを作った。
このような情報は何の役に立つのだろうか? ここで筆者が指摘するまでもなく、これらの情報の重要性に気づいている読者も多いだろう。ここでは、ただ単純に「おもしろいね。」と思って頂くだけでもよいと思っている。
この章を全部読んで、いちいち試しながらコンパイルするのは面倒だけど、最後のプログラムをユーティリティーとして使いたい、と思う読者もいるかもしれない。最後の二つのプログラムの機能をまとめて一つにして Delphi5 でコンパイルした実行可能ファイル WindowTree.exe を以下にリンクしておきます。
また、WinSearch.pas は、いろいろな目的で使い甲斐のあるユニットであると思うので、再度、リンクを示しておきます。