第4章 メッセージを盗む
: サブクラス化とフック関数

前章で作った TSpeedBtn クラスは、 押しボタンとしての外見は持っていないが、自分自身を描画しマウスクリックには応答することができた。このクラスのオブジェクトは、 Object Pascal という言語上のオブジェクトであってもOSが認識するウィンドウではないので、OSが発行するメッセージを受け取ることができない。前章のテストプログラムでは、親ウィンドウのメッセージハンドラから Dispatch( ) メソッドを使ってメッセージを「分けてもらって」いた。 クラスのこのような仕様は、重大な欠陥であると言わなければならない。 コンストラクタをコードによって呼び出してオブジェクトを作って初期化したあとは、自前でメッセージを取得してイベントを発生し、 最後に WM_DESTROY によって自分を解放する(ようにユーザーには見える)のが望ましい。そうでなければ、 TSpeedBtn クラスのようなオブジェクトを使って、電卓アプリケーションを作ることを想像してほしい。 二十個以上ものボタンにいちいちメッセージを配ったり廃棄したりするのはいかにも煩わしいし、何よりもオブジェクトを使う意味がない。この章では、自前で親ウィンドウからメッセージを「盗む」方法を考察しよう。そして、自動的に オブジェクトにメッセージを配ったり、オブジェクトを廃棄したりするクラスを設計しよう。このクラスを使うユーザーには、オブジェクトは自立しているように見える訳だ。

メッセージを「盗む」多くの方法が知られている。 ここでは、「サブクラス化」とメッセージのフック関数を試してみる。 これらの方法は、プログラミングテクニックとしては一般的で重要な手法であり、どの教科書にも載っている。 多くの場合、メッセージを盗んで独自の処理をすることで、コントロールやシステムデバイスをカスタマイズするために使用される。 この章では、 もっぱらメッセージを盗んで TSpeedBtn に配送するクラスの構築のために使うが、これらの手法の一般的側面もRAD環境でテストしている。

4−0 この章の内容

第4章 メッセージを盗む : サブクラス化とフック関数
4−1 クラス情報とウィンドウ情報
4−2 サブクラス化の簡単な例
4−3 フック関数の簡単な例
4−4 サブクラス化によるメッセージの捕獲
4−5 TMessageDispatcher の仕様とオブジェクト参照リスト
4−6 TMessageDispatcher の実装
4−6−1 とにかく実装する
4−6−2 実装コードを調べる
4−7 まとめ


| arrw18c.gif 前の章へ | 総目次へ | 次の章へ arrw18d.gif

4−1 クラス情報とウィンドウ情報

「サブクラス化」とはもともとOOPの一般的な用語であり、上位クラス(スーパークラス)から、下位クラスが継承のメカニズムによって派生しメソッドやプロパティーを追加してカスタマイズする過程を意味している。 ここではより狭義で使っており、既存クラスの動作処理を変更する事を意味している。 ここで言うサブクラス化とは、簡単に言えば、ウィンドウが持っているウィンドウ関数を任意の関数に入れ替えることによってメッセージの処理をカスタマイズする方法である。 Generic01.dpr ではじめてウィンドウをつくったときのことを思い出してほしい。 TWndClass レコードの各フィールドを定義してから、これを RegisterClass( )で登録し、CreateWindowEx( ) でウィンドウを作る。 どのウィンドウもこのような方法で作られているに違いない。 このとき、ウィンドウ関数は TWndClass レコードの lpfnWndProc フィールドにそのアドレスを格納していた。 つまり、ウィンドウ関数は、ウィンドウの属性ではなく、その雛形となったクラスの属性である。 言い換えれば、 ウィンドウはクラスのインスタンスであるが、ウィンドウ関数はクラスの属性として定義され、 そのインスタンスはすべて同じウィンドウ関数を持つことになる。 Win32 のこのような仕様は、似ているけれども少しだけ違うウィンドウをつくる場合に、同じクラスをつかって CreateWindowEx( ) のパラメータを少し変えることで実現できるようにするために考案されたものだろう。しかし、性質が異なるウィンドウが共通のウィンドウ関数をもつことは、コードを複雑にするだけで合理的とは思えない。 このことを解決する一つの手法がここで言うサブクラス化である。 作成されたあとに、ウィンドウ情報にアクセスして、ウィンドウ関数を入れ替えてしまえばよいのだ。 この節では、特定のウィンドウのクラス情報(TWndClass の各フィールドの値)とウィンドウ情報( CreateWindowEx( ) のパラメータに相当する)にアクセスする方法について考察する。

VCLTForm のインスタンスは、その実装方法がVCLの深い階層によって厚化粧されたものであっても、ハンドルをもつウィンドウであることには違いないだろう。まず、RAD環境でクラス情報とウィンドウ情報にアクセスする関数を作ろう。

  1. エクスプローラで 0401RADClassInfo という名前のフォルダをつくり、0305SpeedBtn03 フォルダから UtilFunc.pas をコピーする。
  2. Delphi をランチャから立ち上げ、デフォルトの Form1 が表示された状態で [ファイル]メニューから[すべて保存]を選んで、0401RADClassInfo フォルダに Unit1.pasMain.pas という名前で、 プロジェクトファイルは RADClassInfo.dpr という名前で保存する。
  3. [プロジェクト]メニューから[プロジェクトに追加]を選び、表示されるダイアログから UtilFunc.pas を選んでOKを押す。
  4. コンポーネントパレットから TLabel を一つと TButton 二つフォームにドロップする。Label1AutoSize プロパティーを falseColorclWhite、 フォントを MS ゴシック の 11 ポイントの大きさにする。 Label1 の大きさを調節する。
  5. Main.pas を以下のようになるように編集する。
  6. f・9を押して、コンパイルして実行する。

以下に、Main.pas の全リストを示す。 実行時に Button1 を押したときの様子をこのコードリストの下の図に示す。

unit Main;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, UtilFunc;

type
  TForm1 = class(TForm)
    Label1: TLabel;
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

function WindowInfo(sobject: string;hWindow: HWND): string;
var
  s: string;
  c: array[0..50] of Char;
begin
  GetWindowText(hWindow,c,50);
  s := 'WindowInfo of '+sobject+#13#13;
  s := s+'TEXT = '+ string(c)+#13;
  s := s+'HANDLE = $'+ AIntToHex(hWindow,8)+#13;
  s := s+'EXSTYLE = $'+ AIntToHex(GetWindowLong(hWindow,GWL_EXSTYLE),8)+#13;
  s := s+'STYLE = $' + AIntToHex(GetWindowLong(hWindow,GWL_STYLE),8)+#13;
  s := s+'WNDPROC = $' + AIntToHex(GetWindowLong(hWindow,GWL_WNDPROC),8)+#13;
  s := s+'HINSTANCE = $'+ AIntToHex(GetWindowLong(hWindow,GWL_HINSTANCE),8)+#13;
  s := s+'HWNDPARENT = $'+ AIntToHex(GetWindowLong(hWindow,GWL_HWNDPARENT),8)+#13;
  s := s+'ID = '+ AIntToStr(GetWindowLong(hWindow,GWL_ID));
  result := s;
end;

function ClassInfo(hWindow:HWND):string;
var
  WC: TWndClass;
  c: array[0..50] of Char;
  s: string;
  i: integer;
  w: word;
begin
  GetClassName(hWindow,c,50);
  GetClassInfo(hInstance,c,WC);
  s := 'ClassInfo of '+ string(c)+#13#13+
       'Style = $'+ AIntToHex(WC.Style,8)+#13+
       'WndProc = $' + AIntToHex(integer(WC.lpfnWndProc),8)+#13+
       'hInstance = $' + AIntToHex(WC.hInstance,8)+#13+
       'ClsExtra = ' + AIntToStr(WC.cbClsExtra)+' byte'#13;
  if WC.cbClsExtra > 0 then begin
    s := s+'  ClsExtraData = ';
    for i := 0 to (WC.cbClsExtra div 2) -1 do begin
      w := GetClassWord(hWindow,i);
      s := s+AIntToHex(LOBYTE(w),1)+AIntToHex(HIBYTE(w),1);
    end;
    s := s+#13;
  end;
  s := s+'WndExtra = ' + AIntToStr(WC.cbWndExtra)+' byte'#13;
  if WC.cbWndExtra > 0 then begin
    s := s+'  WndExtraData = ';
    for i := 0 to (WC.cbWndExtra div 2) -1 do begin
      w := GetWindowWord(hWindow,i);
      s := s+AIntToHex(LOBYTE(w),1)+AIntToHex(HIBYTE(w),1);
    end;
    s := s+#13;
  end;
  s := s+ 'hIcon = $' + AIntToHex(WC.hIcon,8)+#13+
       'hBrush = $' + AIntToHex(WC.hbrBackground,8)+#13+
       'MenuName = ' + string(WC.lpszMenuName);
  result := s;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  s: string;
begin
  s := Main.ClassInfo(Form1.Handle);
  s := s + #13#13+ WindowInfo('TForm1',Form1.Handle);
  Label1.Caption := s;
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  s: string;
begin
  s := Main.ClassInfo(Application.Handle);
  s := s + #13#13+ WindowInfo('TApplication',Application.Handle);
  Label1.Caption := s;
end;

end.
DownloadExe.gifRADClassinfo.zip(162.691kB)

コードの大部分は、WindowInfo( ) 関数と ClassInfo( ) 関数の実装に費やされている。 クラスの情報は、GetClassInfo( ) APIによって得られる。

  function GetClassInfo(hInstance: HINST; 
                      lpClassName: PChar;
                   var lpWndClass: TWndClass): BOOL; stdcall;

第一パラメータはインスタンスハンドル( Delphiではグローバル変数 hInstance として参照可能)、第二パラメータはクラス名、第三パラメータは情報を受け取る TWndClass 型レコードの変数を指定する。 この第二パラメータであるクラス名を取得するために GetClassName( ) APIを使っている。

  function GetClassName(hWnd: HWND; 
                 lpClassName: PChar; 
                   nMaxCount: Integer): Integer; stdcall;

この関数は、ウィンドウハンドルを渡すと lpClassName にクラス名を返してくれる。 nMaxCount はクラス名を受け取るバッファの長さである。 ClassInfo( ) 関数の残りのコードは、 戻り値の文字列に情報を格納するための汚いコードである。 一方、 ウィンドウ情報は、GetWindoLong( ) APIによって得られる。

  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の値
RADClassInfo.gif

のひとつを指定すると、対応する情報を戻り値として返してくれる。 ウィンドウ作成のときの CreateWindow( ) APIの第二パラメータによって、各ウィンドウにはテキストが関連づけられている。 このテキストは、タイトルバーがあるウィンドウではタイトルバーに表示され、ボタンなどの子ウィンドウではキャプションとして表示されるが、表示しないウィンドウも多い。 このテキストは、GetWindowText( ) APIで受け取れる。 パラメータは、 GetClassName( ) のときと同じだ。以上の説明で上記のコードを理解できるだろう。 Button1 をクリックすると Label1Form1 の情報が表示される。 Button2 をクリックすると Application オブジェクトの情報が表示される。 表示された内容について調べてみよう。なお、ハンドルの値など幾つかの情報は、 Delphi のバージョンやインスタンスごとに異なるので、右図と必ずしも一致しない事に注意してほしい。 ここでは、右図に示された情報について調べることにする。

まず、この節のメインテーマであるウィンドウ関数のアドレスに注目しよう。 TForm1 クラスの WndProc に表示される値と Form1 のウィンドウ情報の WNDPROC に表示される値が同じでないことは、興味深い。 このことは、VCLのどこかの階層でサブクラス化によってウィンドウ関数が取り替えられていることを意味している。 つぎに、Form1HWNDPARENT の値がゼロではないことに注目しよう。これは、Form1 が所有された(Owned)ウィンドウであることを示している。 Form1 の最小化ボタンを押してもタスクバーに吸い込まれるようなアニメーションが見られないのはトップレベルウィンドウではないからだ。 HWNDPARENT の値を憶えておいて、Button2 を押してみよう。 Apllication オブジェクトのハンドルの値と同じであることが分かるだろう。 さらに、ApplicationHWNDPARENT の値はゼロであることが分かるだろう。つまり Form1Application ウィンドウに所有されたウィンドウであることが分かる。 Application ウィンドウは、RADプログラムのアプリケーションに暗黙のうちに作られるトップレベルウィンドウである。 そのテキストは、プロジェクトの名前と同じである。 Apllication ウィンドウは、ディスプレイの中央に位置し幅と高さがゼロ(したがって目に見えない)のウィンドウである。

TForm1 の他の情報について調べよう。クラスの style の値は $00000008 である。これは、Windows.pas から CS_DBLCLKS = 8 であることがわかり、Form1 はマウスボタンのダブルクリックに応答できることを示している。 ウィンドウの STYLE$16CF0000 である。この値は、

                    $16CF0000

  WS_VISIBLE      = $10000000;
  WS_CLIPSIBLINGS =  $4000000;
  WS_CLIPCHILDREN =  $2000000;
  WS_CAPTION      =   $C00000;
  WS_SYSMENU      =    $80000;
  WS_THICKFRAME   =    $40000;
  WS_MINIMIZEBOX  =    $20000;
  WS_MAXIMIZEBOX =     $10000;
  WS_OVERLAPPED   =         0;

のウィンドウスタイルに相当している。 これは、

  WS_OVERLAPPEDWINDOW = (WS_OVERLAPPED or WS_CAPTION or WS_SYSMENU or
    WS_THICKFRAME or WS_MINIMIZEBOX or WS_MAXIMIZEBOX);

を使うと、WS_VISIBLE or WS_CLIPSIBLINGS or WS_CLIPCHILDREN or WS_OVERLAPPEDWINDOW とも表せる。 また拡張スタイル EXSTYLE$00010100 であるから

  WS_EX_CONTROLPARENT = $10000;
  WS_EX_WINDOWEDGE    =   $100;

となっている。 これらの情報は、Form1 のプロパティーがデフォルトの場合に得られたものであり、オブジェクトインスペクタでプロパティーを変えるとウィンドウスタイルは勿論、他の情報も変化する可能性がある。 試してみてほしい。

最後に、 WindowInfo( ) 関数と ClassInfo( ) 関数は、TForm クラスだけではなく TWinControl の派生クラスである TButtonTMemoTListBox などのクラスとオブジェクトにも適用できることを言っておきたい。 そこで、この二つの関数を UtilFunc.pas に実装しよう。 Interface 部の InitMenuCommand( ) の下に

  function WindowInfo(sobject: string;hWindow: HWND): string;
  function ClassInfo(hWindow:HWND):string;

の宣言を追加し、Main.pas から関数の実装部分を切り取って、UtilFunc.pas の実装部の InitMenuCommand( ) 手続きの下に貼り付ける。以上で完了である。 最終的に Main.pas の実装部は以下のようになる。

implementation

{$R *.DFM}

procedure TForm1.Button1Click(Sender: TObject);
var
  s: string;
begin
  s := UtilFunc.ClassInfo(Form1.Handle);
  s := s + #13#13+ WindowInfo('TForm1',Form1.Handle);
  Label1.Caption := s;
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  s: string;
begin
  s := UtilFunc.ClassInfo(Application.Handle);
  s := s + #13#13+ WindowInfo('TApplication',Application.Handle);
  Label1.Caption := s;
end;

end.

redball.gifこの章の目次へ戻る

4−2 サブクラス化の簡単な例

RADSubClass.gif

前節で GetWindowLong( ) APIを使えば、ウィンドウの様々な情報を知ることができることを示した。 ここでは、RAD環境で Form1 のウィンドウ関数を入れ替えてサブクラス化する例を示すことにしよう。 サブクラス化の手法に対する具体的なイメージをつかむことができるだろう。 Form1 のウィンドウ関数はVCLの階層のどこかに埋もれていて、プログラマは普通にはそのソースコードにアクセスできない。このような状況のときでもサブクラス化するとメッセージを盗む事ができる。ここでは、WM_MOUSEMOVE メッセージだけをトラップして処理する例を示す。

  1. エクスプローラで 0402RADSubClass という名前のフォルダをつくる。
  2. Delphi をランチャから立ち上げ、デフォルトの Form1 が表示された状態で [ファイル]メニューから[すべて保存]を選んで、0402RADSubClass フォルダに Unit1.pasMain.pas という名前で、 プロジェクトファイルは RADSubClass.dpr という名前で保存する。
  3. コンポーネントパレットから TLabel を一つフォームにドロップする。Label1AutoSize プロパティーを falseColorclWhite、 フォントを MS ゴシック の 11 ポイントの大きさにする。 右図のように Label1 の大きさを調節する。
  4. Main.pas を以下のようになるように編集する。
  5. f・9を押して、コンパイルして実行する。



unit Main;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;

type
  TForm1 = class(TForm)
    Label1: TLabel;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormClick(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

var
  OriginalProc: TFNWndProc;

function SubClassProc(hWindow: HWND; Msg: UINT;
                       wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
begin
  result := CallWindowProc(OriginalProc,hWindow,Msg,wParam,lParam);
  case Msg of
    WM_MOUSEMOVE: begin
      Form1.Label1.Caption := ' X : '+IntToStr(LOWORD(lParam))+
                              '  Y : '+IntToStr(HIWORD(lParam));
    end;
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  OriginalProc := Pointer(GetWindowLong(Handle,GWL_WNDPROC));
  SetWindowLong(Handle,GWL_WNDPROC,integer(@SubClassProc));
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  SetWindowLong(Handle,GWL_WNDPROC,integer(OriginalProc));
end;

procedure TForm1.FormClick(Sender: TObject);
begin
  MessageDlg('You Clicked Me!',mtInformation,[mbOK],0);
end;

end.

DownloadExe.gifRADSubClass.zip(168.519kB)

実行時の様子を上図に示した。 Form1 の上でマウスポインタを移動させると、その座標が Label1 に表示されるのが分かるだろう。また、 Form1 をクリックするとメッセージダイアログが表示されることも確かめてほしい。 上のリストを見ればわかるように、マウスの座標はサブクラス化した新しいウィンドウ関数 SubClassProc( ) から表示されている。 またマウスのクリックイベントに対する応答は、Form1 の本来のイベントハンドラから実現している。 コードを見ていこう。 サブクラス化は、TForm1.FormCreate( ) ハンドラで行われる。 まず、GetWindowLong( ) で本来のウィンドウ関数のアドレスを TFNWndProc 型のグローバル変数 OriginalProc に格納する。つぎに、SetWindowLong( ) APIを使って、Form1 のウィンドウ関数を SubClassProc に入れ替える。 これでサブクラス化の完了である。 SetWindowLong( )

  function SetWindowLong(hWnd: HWND; 
                       nIndex: Integer; 
                    dwNewLong: Longint): Longint; stdcall;

と宣言されている。第二パラメータ nIndex には、GetWindowLong( ) の第二パラメータに指定できるものと同じものが指定できる。 戻り値は置き換える前の値である。 したがって、このハンドラのコードは次の一行にまとめることもできる。

   OriginalProc := Pointer(SetWindowLong(Handle,GWL_WNDPROC,integer(@SubClassProc)));

サブクラス化を行ったら終端処理として、TForm1.FormDestroy( ) ハンドラで実行しているように終了時直前に元のウィンドウ関数に戻しておくことがこの手法の作法である。 これを怠るとプログラムが正常に終了しないことが多い。 SubClassProc の形は、 標準のウィンドウ関数と同じにする。 また stdcall 規約を忘れないこと。 この関数は、OSによってコールされるのである。サブクラスのウィンドウ関数はメッセージを処理してもしなくても、 CallWindowProc( ) APIによって元のウィンドウ関数を呼び出さなければならない。 このようにして、何重ものサブクラス化が行われても、ウィンドウ関数の連鎖をたどって最終的にはオリジナルのウィンドウ関数にメッセージが届くことが保証される。 もちろん、プログラマはサブクラスのウィンドウ関数でメッセージを完全に処理する決心をして、メッセージの流れをくい止めて後のウィンドウ関数に渡さずに完全に置き換える事もできる。 しかし、そのような例は稀であり、 ここで示したようにメッセージ伝達機構のある断面を「のぞき見て」から、本来の流れに戻すという手法が一般的であるし「完全犯罪」の可能性も高い。ここでは、メッセージの処理をする前に元のウィンドウ関数を呼び出している。 CallWindowProc( ) は、

  function CallWindowProc(lpPrevWndFunc: TFNWndProc; 
                                   hWnd: HWND; 
                                    Msg: UINT; 
                                 wParam: WPARAM; 
                                 lParam: LPARAM): LRESULT; stdcall;

と宣言されている。 使い方は例を見ると理解できるだろう。 サブクラスのウィンドウ関数でメッセージを処理する場合、 その処理はこの例のように本来のウィンドウ関数での処理の「後に」行われるか、「先だって」行われるかは、目的とする動作に影響を及ばさないこともあるが、及ぼすこともあるのでよく検討することが大切である。

redball.gifこの章の目次へ戻る

4−3 フック関数の簡単な例

メッセージを盗み見るもう一つの方法であるフック関数を使って前節と同じ事をやってみよう。サブクラス化はプログラミングテクニックの一つであったのに対して、フック関数のインストールはAPI関数レベルでサポートされている「正式な」メッセージ処理方法である。 使い方によっては、非常に強力な手法であり、 全てのアプリケーションとシステムの共通リソースであるキーボードやマウスのマッピングを変更することもできるし、一つのプロセス内だけで有効なフック関数をインストールする事もできる。 したがって、フック関数をインストールすることはサブクラス化の手法の補完的なものにとどまらない。 ここでは、RAD環境でどこかにあるはずのメッセージループから Form1 のウィンドウ関数に向けられて送り出されるメッセージを横取りして WM_MOUSEMOVE メッセージを処理するようなフック関数の例を示そう。

  1. エクスプローラで 0403RADHook という名前のフォルダをつくる。
  2. Delphi をランチャから立ち上げ、デフォルトの Form1 が表示された状態で [ファイル]メニューから[すべて保存]を選んで、0403RADHook フォルダに Unit1.pasMain.pas という名前で、 プロジェクトファイルは RADHook.dpr という名前で保存する。
  3. コンポーネントパレットから TLabel を一つフォームにドロップする。Label1AutoSize プロパティーを falseColorclWhite、 フォントを MS ゴシック の 11 ポイントの大きさにする。 Label1 の大きさを調節する。
  4. Main.pas を以下のようになるように編集する。
  5. f・9を押して、コンパイルして実行する。


unit Main;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;

type
  TForm1 = class(TForm)
    Label1: TLabel;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormClick(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

var
  Hook: HHOOK;
  hookParent: HWND;

function HookProc(code: Integer; wparam: WPARAM;
                     lparam: LPARAM): LRESULT; stdcall;
var
  Msg: PMsg;
begin
  result := 0;
  if (code = HC_ACTION) and (wparam = PM_REMOVE) then begin
    Msg := PMsg(lparam);
    if Msg^.hwnd = hookParent then begin

      case Msg^.message of

        WM_MOUSEMOVE:begin
          Form1.Label1.Caption := ' X : '+IntToStr(LOWORD(Msg^.lParam))+
                              '  Y : '+IntToStr(HIWORD(Msg^.lParam));
        end;

      end; // case
    end;
  end;
  CallNextHookEx(Hook,code,wparam,lparam);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Hook := SetWindowsHookEx(WH_GETMESSAGE,HookProc,0,
                               GetCurrentThreadID);
  hookParent := Handle;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  UnHookWindowsHookEx(Hook);
end;

procedure TForm1.FormClick(Sender: TObject);
begin
  MessageDlg('You Clicked Me!',mtInformation,[mbOK],0);
end;

end.

DownloadExe.gifRADHook.zip(168.507kB)

実行してみると前節の RADSubClass.exe と全く同じ動作をする事が分かるだろう。 マウスポインタの座標の表示は、コードから分かるように HookProc( ) 関数で実現されている。 それではコードを見ていこう。

フック関数のインストールは TForm1.FormCreate( ) ハンドラで SetWindowsHookEx( ) APIで行っている。

  function SetWindowsHookEx(idHook: Integer; 
                              lpfn: TFNHookProc; 
                              hmod: HINST; 
                        dwThreadId: DWORD): HHOOK; stdcall;

第一パラメータでインストールするフック関数の種類を指定する。 Windows.pas にはここで指定できるパラメータとして

const
  WH_MSGFILTER = -1;
  WH_JOURNALRECORD = 0;
  WH_JOURNALPLAYBACK = 1;
  WH_KEYBOARD = 2;
  WH_GETMESSAGE = 3;
  WH_CALLWNDPROC = 4;
  WH_CBT = 5;
  WH_SYSMSGFILTER = 6;
  WH_MOUSE = 7;
  WH_HARDWARE = 8;
  WH_DEBUG = 9;
  WH_SHELL = 10;

などが定義されている。 例えば、キーボードのマッピングを変更するなら WH_KEYBOARD を指定する。ここでは、WH_GETMESSAGE を指定する。 これは、メッセージループで GetMessage( ) によりメッセージキューからメッセージを取り出した直後に、システムにそのメッセージをフック関数に送ってもらうよう依頼している。RAD環境のプログラミングでは、プログラマはメッセージループの存在を意識することはないが、それでもどこかにメッセージループがあるに違いない。 SetWindowsHookEx( ) の第二パラメータにはインストールするフック関数を指定する。フック関数の形は、

type
  TFNHookProc = function (code: Integer; 
                        wparam: WPARAM; 
                        lparam: LPARAM): LRESULT stdcall;

となっているが、各パラメータの意味は SetWindowsHookEx( ) の第一パラメータで指定したフック関数の種類により異なる。 この例の WH_GETMESSAGE の場合は、codeHC_ACTION であるときそのメッセージを処理できる事を示している。 wparam はそのメッセージがメッセージキューから GetMessage( ) で取り去られるかどうかを示している。 一般には、 取り去られるのでこのパラメータが PM_REMOVE であるかどうか確認すればよい。lparam はメッセージ本体である TMsg レコードへのポインタを表している。

SetWindowsHookEx( ) の第三パラメータは、フック関数が実装されているモジュール(DLL:ダイナミックリンクライブラリ)のハンドルを指定する。 システムレベルのフック関数は、多くのアプリケーションから参照される必要があるので通常DLLに格納されている。 ただし、最後のパラメータ dwThreadId がこのコードが実行されるプロセスでつくられたスレッドを指定しており、かつ、フック関数がこのプロセスのなかにある場合は第三パラメータ hmod にはゼロを指定しなければならない。今の場合は上の条件に合致するのでゼロを指定する。 最後のパラメータ dwThreadId には、インストールされるフック関数が関連づけられるスレッドの番号を指定する。ゼロを指定すると、今の場合、全てのスレッドのメッセージループにインストールされることになる。スレッドとはマルチタスキングされる最小の実行単位のことである。一つの実行ファイルを実行すると一つのプロセスが生じる。 プロセスでとくにスレッドを意識しないでプログラミングを行ったものは自動的に一つのスレッドを持つことになる。 一つのプロセスは二つ以上のスレッドを持つことができる。 例えば、時間のかかる印刷処理を裏スレッドでしながら、文書入力を表スレッドで行うワードプロセッサなどは多い。 この最後のパラメータには、GetCurrentThreadID APIの戻り値を指定する。 SetWindowsHookEx( ) は戻り値としてフック処理を識別するハンドルを返すので、これをグローバル変数 Hook に格納している。 TForm1.FormCreate( ) ハンドラの最後の一行はフック関数で参照するために、フックしたいウィンドウのハンドルをグローバル変数に格納している。 以上で、フック関数のインストールは完了する。

終端処理として、TForm1.FormDestroy( ) ハンドラでフック関数のアンインストールを行っている。UnHookWindowsHookEx( ) のパラメータには解除したいフックハンドルを指定する。

フック関数を見てみよう。最初の if 文でこの関数で処理すべきメッセージかどうか判断している。 つぎに、lparamPMsg 型のポインタ Msg にキャストしている。 TMsg レコードについては、すでにメッセージループの説明のところで紹介したが、ここでも再掲する。

type
  PMsg = ^TMsg;
  TMsg = packed record
    hwnd: HWND;
    message: UINT;
    wParam: WPARAM;
    lParam: LPARAM;
    time: DWORD;
    pt: TPoint;
  end;

if 文で Msg^.hwnd が目的のウィンドウハンドルであることを確認してから、メッセージの処理を行っている。 フック関数の最後には、 フック関数の連鎖を断ち切らないために CallNextHookEx( ) APIを呼び出して、インストールされている(かもしれない)次のフック関数にメッセージを渡す。

前節と本節では、サブクラス化とフック関数をインストールして、本来、Form1 のウィンドウ関数に送られる WM_MOUSEMOVE というメッセージを「盗み見て」、全く同じ動作をする二つのプログラムをつくった。 一つのウィンドウ関数をサブクラス化の手法によって入れ替えると、そのウィンドウ関数に送られてくるメッセージをすべて見ることができる。しかし、他のウィンドウ関数に送られるメッセージを見ることは出来ない。 これに対して、WH_GETMESSAGESetWindowsHookEx( ) の第一パラメータに指定してインストールしたフック関数では、メッセージループを通るそのスレッドのすべてのメッセージを見ることができるが、メッセージループを通らないメッセージは見ることは出来ない。 後の章で説明するが、ボタンやリストボックス・エディットなどのコントロールは、親ウィンドウとの間で SendMessage( ) APIを使ってメッセージを交換することでイベントの処理を行っている。SendMessage( ) を使って送られたメッセージはメッセージループを通過しないので、本節でインストールしたフック関数では捕まえられない。 そのようなメッセージを捕まえるには、WH_CALLWNDPROC を第一パラメータに指定したフック関数を追加インストールすればよい。 いずれにしても、サブクラス化とフック関数のインストールによって捕まえられるメッセージの種類や範囲は、必ずしも同じではない事に注意してほしい。

redball.gifこの章の目次へ戻る

4−4 サブクラス化によるメッセージの捕獲

前節までに、Win32 のメッセージ伝達機構に割り込んでメッセージを「盗み見る」方法として、サブクラス化とフック関数の例を挙げた。 両方法とも、 一般的で強力なプログラム手法であり、その使用法に難しいところは何もないことが分かって頂けたと思う。 本章の最初に述べたとおり、 このうちのいずれかの手法をつかってオブジェクトが自前でメッセージを取得したり、自分自身を解放したりする(ように見える)事ができるようにしよう。 両方法とも一長一短があるが、ここではサブクラス化を採用しよう。ここでは、上の目的のための第一歩として、前章の最後につくった TSpeedBtn クラスにサブクラス化したウィンドウ関数からメッセージを配送する例を示そう。

  1. エクスプローラを使って、新しいフォルダを 0404SubClass01 という名前でつくる。
  2. 作ったフォルダに、エクスプローラを使って 0305SpeedBtn03 から SpeedBtn03.dprTest.pas を、0401RADClassInfo フォルダから UtilFunc.pas をコピーする。
  3. コピーした SpeedBtn03.dpr をダブルクリックして Delphi を立ち上げる。
  4. コードエディタに SpeedBtn03.dpr のコードがあることを確認してから、[ファイル]メニューから[プロジェクトに名前を付けて保存]を選んで、SubClass01.dpr という名前で保存する。
  5. コードエディタを編集して、以下のリストになるようにする。
  6. コンパイルして実行する。

以下に SubClass01.dpr の全リストを示す。

program SubClass01;

uses
  windows,
  messages,
  UtilFunc in 'UtilFunc.pas',
  Test in 'Test.pas';

//------------------------------------------------------------
//             Global Types, Constants and Variables
//------------------------------------------------------------
var
  Clr: COLORREF;

  SP1,SP2: TSpeedBtn;
  OriginalProc: TFNWndProc;

//------------------------------------------------------------
//             Helper Handlers
//------------------------------------------------------------
function SubClassProc(hWindow: HWND; Msg: UINT;
                       wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var
  AMsg: TMessage;
begin
  result := CallWindowProc(OriginalProc,hWindow,Msg,wParam,lParam);
  case Msg of
    WM_LBUTTONDOWN: begin
      AMsg.Msg := Msg;
      AMsg.WParam := WParam;
      AMsg.LParam := LParam;
      AMsg.Result := 0;
      SP1.Dispatch(AMsg);
      SP2.Dispatch(AMsg);
      end;

    WM_PAINT: begin
      AMsg.Msg := Msg;
      AMsg.WParam := WParam;
      AMsg.LParam := LParam;
      AMsg.Result := 0;
      SP1.Dispatch(AMsg);
      SP2.Dispatch(AMsg);
      end;

    WM_DESTROY: begin
      SP1.Free;
      SP2.Free;
      SetWindowLong(hWindow,GWL_WNDPROC,integer(OriginalProc));
      end;
  end; // case
end;  

//------------------------------------------------------------
//             Event Handlers
//------------------------------------------------------------
procedure OnCreate(var Msg: TMessage);
var
  hWindow: HWND;
begin
  hWindow := Msg.Msg;
  Clr := clrBtnFace;
  MakeInitMenu(hWindow);
  ChangeWindowSize(hWindow,400,300);
  CenterWindow(hWindow);

  SP1 := TSpeedBtn.Create(hWindow);
  SP1.Rect := SetDim(50,50,200,100);

  SP2 := TSpeedBtn.Create(hWindow);
  SP2.Rect := SetDim(50,120,200,170);

  OriginalProc := Pointer(GetWindowLong(hWindow,GWL_WNDPROC));
  SetWindowLong(hWindow,GWL_WNDPROC,integer(@SubClassProc));

end;

procedure OnTest(hWindow: HWND);
begin
  MessageBeep($FFFFFFFF);
end;

procedure OnCommand(var Msg: TMessage);
begin
  InitMenuCommand(Msg,OnTest);
end;

procedure OnEraseBkgnd(var Msg: TMessage);
begin
  Msg.Result := EraseBkGnd(Msg.Msg,Clr,Msg.WParam);
end;

procedure OnDestroy(var Msg: TMessage);
begin
  PostQuitMessage(0);
end;

//------------------------------------------------------------
//             Main Window Procedure
//------------------------------------------------------------
function MainWndProc(hWindow: HWND; Msg: UINT; WParam: WPARAM;
                      LParam: LPARAM): LRESULT; stdcall;
var
  AMsg: TMessage;
begin
  Result := 0;

  AMsg.Msg := hWindow;
  AMsg.WParam := WParam;
  AMsg.LParam := LParam;
  AMsg.Result := 0;

  case Msg of

//-------------  WM_CREATE  -----------------------------------

     WM_CREATE: OnCreate(AMsg);

//-------------  WM_COMMAND  ----------------------------------

     WM_COMMAND: OnCommand(AMsg);

//-------------  WM_ERASEBKGND  -------------------------------

     WM_ERASEBKGND: begin
       OnEraseBkgnd(AMsg);
       result := AMsg.Result;
     end;

//-------------  WM_DESTROY  ----------------------------------

     WM_DESTROY: OnDestroy(AMsg);

//-------- そのほかのメッセージの処理 ------
  else begin
    result := DefWindowProc( hWindow, Msg, wParam, lParam );
    end;

  end; // case
end;

//------------------------------------------------------------
//             Main Procedure
//------------------------------------------------------------
begin

  MakeMainWindow(@MainWndProc);

  Halt(MessageLoopNormal);

end.

DownloadExe.gifSubClass01.zip(12.299kB)

コンパイルして実行すると SpeedBtn03.dpr と全く同じ動作をする。 サブクラス化は OnCreate( ) ハンドラで TSpeedBtn クラスのインスタンスを二つつくった後に行われている。 WM_PAINTWM_LBUTTONDOWN メッセージの捕獲とインスタンスへの配送は、サブクラス化したウィンドウ関数の中で行っている。 したがって、メインウィンドウのウィンドウ関数からのハンドラには、Dispatch メソッドがないことに注目する事が重要である。 インスタンスの破棄とサブクラス化からの回復も WM_DESTROY メッセージを捕獲して、新しいウィンドウ関数で行っている。

redball.gifこの章の目次へ戻る

4−5 TMessageDispatcher の仕様とオブジェクト参照リスト

Dispatcher.gif前節で示したように、サブクラス化によって、特定のメッセージの捕獲とオブジェクトへのメッセージの配送のメカニズムをメインウィンドウのウィンドウ関数から独立させることが出来た。 また、オブジェクトの廃棄とサブクラス化の回復のプロセスも隠すことが出来た。 しかし、これだけでは十分ではない。 オブジェクトをクラスからつくるときに自動的にサブクラス化が行われていないからだ。 さらに、どのメッセージを捕獲し、どのオブジェクトに Dispatch するのかという問題は解決されていない。 この節以降で、この問題を解決しよう。

メッセージを自前で取得するすべてのオブジェクトの上位クラスとして TSubClass というクラスを設計しよう。このクラスからの派生クラスは、コンストラクタが実行されると自動的に親ウィンドウをサブクラス化する。また、オブジェクトは自分のメソッドの中で自分自身を解放することができない。ここでは、TMessageDispatcher というクラスをつくり、そのオブジェクト Dispatcher がサブクラス化したウィンドウ関数からメッセージを送ってもらい、それを TSubClass の派生オブジェクトに伝達し、親の解放時にはその子オブジェクトを解放する、というふうに制御することを考える。 望ましい仕様を以下に列挙する。

[ TMessageDispatcher の仕様 ]

  1. TMessageDispatcher のインスタンス(ここでは、Dispatcher とする)は、TSubClass の派生クラスの最初のオブジェクトが作られるとき、一つだけ作られる。このオブジェクトは、ユーザーには見えない。
  2. TMessageDispatcher は、TSubClass の派生クラスのオブジェクトの参照リストとその親ウィンドウのウィンドウハンドルのリストを保持する。
  3. TSubClass のコンストラクタでは、Dispatcher に自分自身の参照( self )を登録し、参照リストに加えてもらう。 このとき、Dispatcher はその親ウィンドウのハンドルを調べ、親リストに登録されていない新しい親であることが分かったら、そのウィンドウ関数をサブクラス化する。
  4. サブクラス化したウィンドウ関数(ここでは、SubClassProc とする)では、WM_DESTROY メッセージ以外のすべてのメッセージをトラップして DispatcherDispatch する。
  5. DispatcherDefaultHandler( ) では、保持しているオブジェクト参照リストと親ハンドルリストを参照してメッセージを各オブジェクトに Dispatch する。
  6. DispatcherDefaultHandler( )WM_DESTROY メッセージを受け取ったら、ハンドルが示す親のサブクラス化を解除し親ハンドルリストから削除するとともに、その子オブジェクトを解放する。
  7. TSubClass のデストラクタでは、Dispatcher が保持しているオブジェクト参照リストから自分自身を取り除く。
  8. Dispatcher 自身の解放は、ユニットの終了処理部(finalization 部)で行う。

以上の機能の TMessageDispatcher を実装するまえに、仕様2.について考えてみよう。DelphiObject Pascal でのオブジェクト参照は、32ビットのポインタであるから integer にキャストする事ができる。また、親のウィンドウハンドルも HWND = integer である。プログラムで TSubClass の派生オブジェクトがいくつ作られるかあらかじめ予期することはできないので、これらのリストは要素数が動的に変化するものでなくてはならない。VCLには、この仕様にぴったりの TList というポインタのリストを保持するクラスがある。 TListClasses ユニットに実装されているので、ここでは自前の TIntegerList クラスを作ることにしよう。それでは早速 TIntegerList を以下の手順で作ってテストすることにしよう。

  1. エクスプローラで 0405RADIntegerList という名前のフォルダをつくる。
  2. Delphi をランチャから立ち上げ、デフォルトの Form1 が表示された状態で [ファイル]メニューから[すべて保存]を選んで、0405RADIntegerList フォルダに Unit1.pasMain.pas という名前で、 プロジェクトファイルは IntegerList.dpr という名前で保存する。
  3. [ファイル]メニューから[新規作成]を選んで表示されるダイアログからユニットを選んでOKを押す。 新たに作られたユニットファイルを[ファイル]メニューから[名前を付けて保存]を選んで Test.pas という名前で保存する。
  4. コンポーネントパレットから TListBox を一つと TButton を四つフォームにドロップする。
  5. Test.pasMain.pas を以下のようになるように編集する。
  6. f・9を押して、コンパイルして実行する。

Test.pasTIntegerList クラスを実装する。以下に全リストを示す。

unit Test;

interface

uses
  windows,messages;

//-----------------  TIntegerList ------------------------------
const
  MaxItem = 200000;

type
TIntegerArray = array[0..MaxItem] of Integer;
PIntegerArray = ^TIntegerArray;

TIntegerList = class(TObject)
private
  FPointer: PIntegerArray;
  FCount: integer;
  FSize: integer;
  FReAllocSize: integer;
  FInitialSize: integer;
  function GetItem(index: integer):Integer;
  procedure SetItem(index: integer; value: Integer);
  function GetCount: integer;
  function GetRealSize: integer;
protected
public
  constructor Create(InitialSize,ReAllocSize:integer);virtual;
  destructor Destroy; override;
  procedure Clear;
  procedure Add(value:Integer);
  procedure Delete(index: integer);
  procedure Insert(index: integer; value: Integer);
  function Search(value: Integer): integer;
  procedure CopyTo(var IL: TIntegerList);
  property Item[i:integer]:Integer read GetItem write SetItem;default;
  property Count: integer read GetCount;
  property Size: integer read FSize;
  property RealSize: integer read GetRealSize;
end;

implementation

//-----------------  TIntegerList ------------------------------
constructor TIntegerList.Create(InitialSize,ReAllocSize:integer);
begin
  if ReAllocSize = 0 then FReAllocSize := $10 else FReAllocSize := ReAllocSize;
  if InitialSize = 0 then FInitialSize := $10 else FInitialSize := InitialSize;
  FSize := FInitialSize;
  GetMem(FPointer,SizeOf(Integer)*FSize);
  FCount := -1;
end;

destructor TIntegerList.Destroy;
begin
  FreeMem(FPointer);
  inherited Destroy;
end;

procedure TIntegerList.Clear;
begin
  FreeMem(FPointer);
  FSize := FInitialSize;
  GetMem(FPointer,SizeOf(Integer)*FSize);
  FCount := -1;
end;

procedure TIntegerList.Add(value:Integer);
begin
  Inc(FCount);
  if FCount > FSize-1 then begin
    Inc(FSize,FReAllocSize);
    ReAllocMem(FPointer,SizeOf(Integer)*FSize);
  end;
  FPointer^[FCount] := value;
end;

procedure TIntegerList.Delete(index: integer);
begin
  if (index > FCount) or (index < 0) then Exit;
  if index = FCount then begin
    Dec(FCount);
    Exit;
  end;
  Move(FPointer^[index+1],FPointer^[index],
             SizeOf(Integer)*(FCount-index));
  Dec(FCount);
end;

procedure TIntegerList.Insert(index: integer; value: Integer);
begin
  if (index > FCount) or (index < 0) then Exit;
  if FCount+1 > FSize-1 then begin
    Inc(FSize,FReAllocSize);
    ReAllocMem(FPointer,SizeOf(Integer)*FSize);
  end;
  Move(FPointer^[index],FPointer^[index+1],
             SizeOf(Integer)*(FCount-index+1));
  FPointer^[index] := value;
  Inc(FCount);
end;

function TIntegerList.GetItem(index: integer):Integer;
begin
  if (index > FCount) or (index < 0) then
    result := 0
  else
    result := FPointer^[index];
end;

procedure TIntegerList.SetItem(index: integer; value: Integer);
begin
  if index > FSize-1 then begin
    repeat
      Inc(FSize,FReAllocSize);
    until  index < FSize-1;
    ReAllocMem(FPointer,SizeOf(Integer)*FSize);
  end;
  FPointer^[index] := value;
  if FCount < index then FCount := index;
end;

function TIntegerList.GetCount: integer;
begin
  result := FCount+1;
end;

function TIntegerList.GetRealSize: integer;
begin
  result := FSize;
end;

function TIntegerList.Search(value: Integer): integer;
var
  i: integer;
begin
  for i := 0 to FCount do
    if FPointer^[i] = value then begin
      result := i;
      Exit;
    end;
  result := -1;
end;

procedure TIntegerList.CopyTo(var IL: TIntegerList);
var
  i: integer;
begin
  IL.Clear;
  for i := 0 to FCount do
    IL[i] := FPointer^[i];
end;

end.

IntegerList.gifこれらのコードには、 Win32APIを一つも使っていないので詳しい説明は不要だろうが、少しだけ説明する。 整数リストを保持するメモリは、コンストラクタで private 部 の FPointerGetMem( ) で確保する。デストラクタでは、このメモリを解放する。 Clear メソッドでは、一度解放してから、再び新たに確保する。Add メソッドでは、配列の最後尾に数値を追加する。そのときメモリが足りなくなったら ReAllocMem( ) で拡張する。Delete メソッドは、配列の中の一つの要素を取り除く。除かれた要素以降の要素を Move( ) で一つ前方へ移す。 Insert メソッドは、これとは反対に要素を一つ挿入する。 Seach メソッドは、保持している整数配列から value の値と等しい要素のインデックスを返す。見つからなかったら−1を返す。配列のインデックスはから始まる。他のメソッドやプロパティーはコードを見れば容易に理解できるだろう。 Main.pas では、RAD環境で TIntegerList クラスのオブジェクトをテストしている。 右図に、リストボックスとボタンを6個配置した様子を示す。以下に全リストを示す。

unit Main;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls,
  Test;

type
  TForm1 = class(TForm)
    ListBox1: TListBox;
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    Button4: TButton;
    Button5: TButton;
    Button6: TButton;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure Button4Click(Sender: TObject);
    procedure Button5Click(Sender: TObject);
    procedure Button6Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

var
  IL: TIntegerList;
  SI: array[0..300] of integer;

procedure TForm1.FormCreate(Sender: TObject);
begin
  IL := TIntegerList.Create($10,$10);
  Randomize;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  IL.Free;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  i,j: integer;
begin
  IL.Clear;
  ListBox1.Items.Clear;
  for i := 0 to 300 do begin
    j := Random(100000000)-50000000;
    SI[i] := j;
    IL.Add(j);
  end;
  for i := 0 to IL.Count-1 do begin
    ListBox1.Items.Add(IntToStr(i)+' : '+IntToStr(SI[i])+
                       ' -> '+ IntToStr(IL[i]));
  end;
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  i, j: integer;
begin
  IL.Clear;
  ListBox1.Items.Clear;
  for i := 0 to 10 do begin
    j := Random(100000000)-50000000;
    IL.Add(j);
  end;
  for i := 0 to IL.Count-1 do begin
    ListBox1.Items.Add(IntToStr(i)+' : '+ IntToStr(IL[i]));
  end;
  IL.Delete(IL.Search(IL[5]));
  ListBox1.Items.Add(' ');
  for i := 0 to IL.Count-1 do begin
    ListBox1.Items.Add(IntToStr(i)+' : '+ IntToStr(IL[i]));
  end;
end;

procedure TForm1.Button3Click(Sender: TObject);
var
  i, j: integer;
begin
  IL.Clear;
  ListBox1.Items.Clear;
  for i := 0 to 10 do begin
    j := Random(100000000)-50000000;
    IL.Add(j);
  end;
  for i := 0 to IL.Count-1 do begin
    ListBox1.Items.Add(IntToStr(i)+' : '+ IntToStr(IL[i]));
  end;
  IL.Insert(5,555555);
  ListBox1.Items.Add(' ');
  for i := 0 to IL.Count-1 do begin
    ListBox1.Items.Add(IntToStr(i)+' : '+ IntToStr(IL[i]));
  end;
end;

procedure TForm1.Button4Click(Sender: TObject);
var
  i, j: integer;
begin
  IL.Clear;
  ListBox1.Items.Clear;
  for i := 0 to 10 do begin
    j := Random(100000000)-50000000;
    IL.Add(j);
  end;
  for i := 0 to IL.Count-1 do begin
    ListBox1.Items.Add(IntToStr(i)+' : '+ IntToStr(IL[i]));
  end;
  IL[7] := 777777;
  ListBox1.Items.Add(' ');
  for i := 0 to IL.Count-1 do begin
    ListBox1.Items.Add(IntToStr(i)+' : '+ IntToStr(IL[i]));
  end;
end;

procedure TForm1.Button5Click(Sender: TObject);
var
  i: integer;
  ILL: TIntegerList;
begin
  IL.Clear;
  ILL := TIntegerList.Create(0,0);
  ListBox1.Items.Clear;
  for i := 0 to 100 do
    IL[i] := Random(100000000)-50000000;
  IL.CopyTo(ILL);
  for i := 0 to ILL.Count-1 do
    ListBox1.Items.Add(IntToStr(i)+' : '+IntToStr(IL[i])+
                       ' -> '+ IntToStr(ILL[i]));
  ILL.Free;
end;

procedure TForm1.Button6Click(Sender: TObject);
var
  i,j: integer;
begin
  IL.Clear;
  ListBox1.Items.Clear;
  for i := 0 to 20 do begin
    j := Random(100000000)-50000000;
    SI[i] := j;
    IL.Add(j);
  end;
  for i := 0 to IL.Count-1 do begin
    ListBox1.Items.Add(IntToStr(i)+' : '+IntToStr(IL[i]));
  end;
  ListBox1.Items.Add(' ');
  j := IL.Search(SI[17]);
  if j = -1 then
    ListBox1.Items.Add('みつかりません')
  else
    ListBox1.Items.Add(IntToStr(j)+' : '+IntToStr(SI[17]));
end;

end.

DownloadExe.gifIntegerList.zip(165.400kB)

FormCreate( )TIntegerList クラスのオブジェクト IL をつくり、FormDestroy( ) で解放している。 Button1Click( )では、300個のランダムな整数を IL に格納し、整数配列に格納した値と比較している。このときの様子を上図に示した。 Button2Click( ) では五番目の要素の Delete メソッドのテスト、Button3Click( ) では五番目の要素への Insert メソッドのテスト、Button4Click( ) では七番目の要素の変更のテスト、Button5Click( ) では Copy メソッドのテスト、Button6Click( ) では Search メソッドをテストしている。

整数配列を動的に保持するクラスが手に入ったので、次節でいよいよ上記の仕様を持つ TMessageDispatcher を実装しよう。

redball.gifこの章の目次へ戻る

4−6 TMessageDispatcher の実装

本節では前節で示した仕様を満たす TMessageDispatcherTSubClass クラスを実装する。本節は、本稿の中で最もややこしい部分である。 難解なのではなく、ふくざつなのである。TSubClass クラスは、本稿でつくる数多くのクラスのほとんどの上位クラスとなる重要なクラスである。 このクラスは、メッセージの捕獲を自動的に行う。 また、TMessageDispatcher は、TSubClass およびその派生クラスに対して特定のメッセージを配送し、適切なタイミングでオブジェクトを廃棄する。とにかく最初は、まず実装してしまい、それからコードを調べることにしよう。

redball.gifこの章の目次へ戻る

4−6−1 とにかく実装する

TMessageDispatcherTSubClass クラスを実装し、テストするプログラムを作る。

  1. エクスプローラを使って、新しいフォルダを 0406SubClass02 という名前でつくる。
  2. 作ったフォルダに、エクスプローラを使って 0404SubClass01 から SubClass01.dprTest.pasUtilFunc.pas をコピーする。
  3. コピーした SubClass01.dpr をダブルクリックして Delphi を立ち上げる。
  4. コードエディタに SubClass01.dpr のコードがあることを確認してから、[ファイル]メニューから[プロジェクトに名前を付けて保存]を選んで、SubClass02.dpr という名前で保存する。
  5. [ファイル]メニューから[新規作成]を選んで、ユニットを作成する。
  6. 作成したユニット Unit1.pas を[ファイル]メニューから[名前を付けて保存]を選んで UtilClass.pas という名前で 0406SubClass02 フォルダに保存する。
  7. コードエディタにフォーカスがあるようにしてから、[ Ctrl ] + [ f.12 ] を押して表示されるダイアログから Test.pas を選んで OK ボタンを押し、コードエディタにユニットファイルを表示させる。
  8. コードエディタを編集して、以下のリストになるようにする。
  9. コンパイルして実行する。

まず、前節でつくった TIntegerListUtilClass.pas に移そう。メモ帳などのテキストエディタに 0405RADIntegerList フォルダの Test.pas を読み込む。 ここから、コピーと貼り付けを繰り返してコードエディタの UtilClass.pas に移すわけだ。 さらに、TMessageDispatcher クラスと TSubClass クラスも実装する。 UtilClass.pas の全リストを以下に示す。

unit UtilClass;

interface

uses
  windows,
  messages;

//------------------------------------------------------------
//             Global Types, Constants and Variables
//------------------------------------------------------------

//------------------------------------------------------------
//                    Classes
//------------------------------------------------------------

//-----------------  TIntegerList ------------------------------
const
  MaxItem = 200000;

type
TIntegerArray = array[0..MaxItem] of Integer;
PIntegerArray = ^TIntegerArray;

TIntegerList = class(TObject)
private
  FPointer: PIntegerArray;
  FCount: integer;
  FSize: integer;
  FReAllocSize: integer;
  FInitialSize: integer;
  function GetItem(index: integer):Integer;
  procedure SetItem(index: integer; value: Integer);
  function GetCount: integer;
  function GetRealSize: integer;
protected
public
  constructor Create(InitialSize,ReAllocSize:integer);virtual;
  destructor Destroy; override;
  procedure Clear;
  procedure Add(value:Integer);
  procedure Delete(index: integer);
  procedure Insert(index: integer; value: Integer);
  function Search(value: Integer): integer;
  procedure CopyTo(var IL: TIntegerList);
  property Item[i:integer]:Integer read GetItem write SetItem;default;
  property Count: integer read GetCount;
  property Size: integer read FSize;
  property RealSize: integer read GetRealSize;
end;

//------------------ TMessageDispatcher ----------------------

TSubClass = class; // forward

TMessageDispatcher = class(TObject)
private
  FSList: TIntegerList;
  FParentList: TIntegerList;
  FParentProc: TIntegerList;
  FMessageList: TIntegerList;
  FMessageObject: TIntegerList;
  FCopyList: TIntegerList;
  FCopyList2: TIntegerList;
  FNeedCopy: Boolean;
  function GetCount:integer;
protected
  function IsNewParent(hWindow:HWND):Boolean;
  function IsParent(hWindow:HWND):Boolean;
  function GetParentProc(hWindow:HWND):TFNWndProc;
public
  constructor Create;
  destructor Destroy; override;
  procedure DefaultHandler(var Message);override;
  procedure Add(Obj:TSubClass);
  procedure Delete(Obj:TSubClass);
  procedure TrapMessage(Msg: UINT; Obj: TSubClass);
  property Count: integer read GetCount;
end;

//------------------ TSubClass -------------------------------
TSubClass = class(TObject)
private
  FParent: HWND;
  FData: integer;
protected
public
  constructor Create(hParent:HWND); virtual;
  destructor Destroy; override;
  property Parent: HWND read FParent;
  property Data : integer read FData write FData;
end;

var
  Dispatcher: TMessageDispatcher;

implementation

uses
  UtilFunc;

//------------------------------------------------------------
//                    Classes
//------------------------------------------------------------

//-----------------  TIntegerList ------------------------------
constructor TIntegerList.Create(InitialSize,ReAllocSize:integer);
begin
  if ReAllocSize = 0 then FReAllocSize := $10 else FReAllocSize := ReAllocSize;
  if InitialSize = 0 then FInitialSize := $10 else FInitialSize := InitialSize;
  FSize := FInitialSize;
  GetMem(FPointer,SizeOf(Integer)*FSize);
  FCount := -1;
end;

destructor TIntegerList.Destroy;
begin
  FreeMem(FPointer);
  inherited Destroy;
end;

procedure TIntegerList.Clear;
begin
  FreeMem(FPointer);
  FSize := FInitialSize;
  GetMem(FPointer,SizeOf(Integer)*FSize);
  FCount := -1;
end;

procedure TIntegerList.Add(value:Integer);
begin
  Inc(FCount);
  if FCount > FSize-1 then begin
    Inc(FSize,FReAllocSize);
    ReAllocMem(FPointer,SizeOf(Integer)*FSize);
  end;
  FPointer^[FCount] := value;
end;

procedure TIntegerList.Delete(index: integer);
begin
  if (index > FCount) or (index < 0) then Exit;
  if index = FCount then begin
    Dec(FCount);
    Exit;
  end;
  Move(FPointer^[index+1],FPointer^[index],
             SizeOf(Integer)*(FCount-index));
  Dec(FCount);
end;

procedure TIntegerList.Insert(index: integer; value: Integer);
begin
  if (index > FCount) or (index < 0) then Exit;
  if FCount+1 > FSize-1 then begin
    Inc(FSize,FReAllocSize);
    ReAllocMem(FPointer,SizeOf(Integer)*FSize);
  end;
  Move(FPointer^[index],FPointer^[index+1],
             SizeOf(Integer)*(FCount-index+1));
  FPointer^[index] := value;
  Inc(FCount);
end;

function TIntegerList.GetItem(index: integer):Integer;
begin
  if (index > FCount) or (index < 0) then
    result := 0
  else
    result := FPointer^[index];
end;

procedure TIntegerList.SetItem(index: integer; value: Integer);
begin
  if index > FSize-1 then begin
    repeat
      Inc(FSize,FReAllocSize);
    until  index < FSize-1;
    ReAllocMem(FPointer,SizeOf(Integer)*FSize);
  end;
  FPointer^[index] := value;
  if FCount < index then FCount := index;
end;

function TIntegerList.GetCount: integer;
begin
  result := FCount+1;
end;

function TIntegerList.GetRealSize: integer;
begin
  result := FSize;
end;

function TIntegerList.Search(value: Integer): integer;
var
  i: integer;
begin
  for i := 0 to FCount do
    if FPointer^[i] = value then begin
      result := i;
      Exit;
    end;
  result := -1;
end;

procedure TIntegerList.CopyTo(var IL: TIntegerList);
var
  i: integer;
begin
  IL.Clear;
  for i := 0 to FCount do
    IL[i] := FPointer^[i];
end;

//------------------ TMessageDispatcher ----------------------
function SubClassProc(hParent: HWND; Msg: UINT;
                       wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var
  AMsg: TMessage;
  OriginalProc: TFNWndProc;
begin

  result := 0;
  if not Dispatcher.IsParent(hParent) then Exit;

  OriginalProc := Dispatcher.GetParentProc(hParent);

  result := CallWindowProc(OriginalProc,hParent,Msg,wParam,lParam);

  AMsg.Msg := Msg;
  AMsg.WParam := wParam;
  AMsg.LParam := lParam;
  AMsg.Result := hParent;

  case Msg of

    WM_DESTROY: begin
      SetWindowLong(hParent,GWL_WNDPROC,integer(OriginalProc));
      Dispatcher.Dispatch(AMsg);
    end;

  else
    Dispatcher.Dispatch(AMsg);

  end; // case

  if AMsg.Result <> 0 then result := AMsg.Result;

end;

constructor TMessageDispatcher.Create;
begin
  FSList := TIntegerList.Create($10,$10);
  FParentList := TIntegerList.Create($10,$10);
  FParentProc := TIntegerList.Create($10,$10);
  FMessageList := TIntegerList.Create($10,$10);
  FMessageObject := TIntegerList.Create($10,$10);
  FCopyList := TIntegerList.Create($10,$10);
  FCopyList2 := TIntegerList.Create($10,$10);
  FNeedCopy := false;
end;

destructor TMessageDispatcher.Destroy;
begin
  FNeedCopy := false;
  FSList.Free;
  FParentList.Free;
  FParentProc.Free;
  FMessageList.Free;
  FMessageObject.Free;
  FCopyList.Free;
  FCopyList2.Free;
  inherited Destroy;
end;

procedure TMessageDispatcher.DefaultHandler(var Message);
var
  AMsg: TMessage;
  hParent: HWND;
  i: integer;
  SO: TSubClass;
begin
  AMsg := TMessage(Message);

  hParent := AMsg.Result;
  TMessage(Message).result := 0; // default return result
  AMsg.Result := 0;              // default object return

  if AMsg.Msg = WM_DESTROY then begin
    for i := FSList.Count-1 downto 0 do begin
      SO := TSubClass(FSList[i]);
      if Assigned(SO) then
        if (SO.Parent = hParent) then begin
          FSList.Delete(i);
          SO.Free;  //SO := nil;
        end;
    end;
    FParentProc.Delete(FParentList.Search(hParent));    
    FParentList.Delete(FParentList.Search(hParent));
    exit;
  end;

  if FMessageList.Search(AMsg.Msg) = -1 then
    exit
  else begin
    if FNeedCopy then begin
      FMessageList.CopyTo(FCopyList);
      FMessageObject.CopyTo(FCopyList2);
      FNeedCopy := false;
    end;
    for i := 0 to FCopyList.Count-1 do
      if DWORD(FCopyList[i]) = AMsg.Msg then begin
        SO := TSubClass(FCopyList2[i]);
        if Assigned(SO) then
          if (SO.Parent = hParent) then begin
            SO.Dispatch(AMsg);
            if AMsg.Result <> 0 then begin
              TMessage(Message).result := AMsg.Result;
             exit;
            end;
          end;
      end;
  end;
end;

procedure TMessageDispatcher.TrapMessage(Msg: UINT; Obj: TSubClass);
begin
  FMessageList.Add(Msg);
  FMessageObject.Add(integer(Obj));
  FNeedCopy := true;
end;

function TMessageDispatcher.IsNewParent(hWindow:HWND):Boolean;
begin
  result := true;
  if FParentList.Count < 1 then Exit;
  if FParentList.Search(hWindow) > -1 then result := false;
end;

function TMessageDispatcher.IsParent(hWindow:HWND):Boolean;
begin
  result := false;
  if FParentList.Count < 1 then Exit;
  if FParentList.Search(hWindow) = -1 then
    result := false
  else
    result := true;
end;

function TMessageDispatcher.GetParentProc(hWindow:HWND):TFNWndProc;
begin
  result := TFNWndProc(FParentProc[FParentList.Search(hWindow)]);
end;

procedure TMessageDispatcher.Add(Obj:TSubClass);
var
  OriginalProc: integer;
begin
  FSList.Add(Integer(Obj));

  if IsNewParent(Obj.Parent) then begin
    FParentList.Add(Obj.Parent);
    OriginalProc := GetWindowLong(Obj.Parent,GWL_WNDPROC);
    FParentProc.Add(OriginalProc);
    SetWindowLong(Obj.Parent,GWL_WNDPROC,integer(@SubClassProc));
  end;
end;

procedure TMessageDispatcher.Delete(Obj:TSubClass);
var
  i: integer;
begin
  FSList.Delete(FSList.Search(Integer(Obj)));
  for i := FMessageObject.Count-1 downto 0 do
    if FMessageObject[i] = integer(Obj) then begin
      FMessageObject.Delete(i);  // UnTrap Message
      FMessageList.Delete(i);
    end;
  FNeedCopy := true;
end;

function TMessageDispatcher.GetCount:integer;
begin
  result := FSList.Count;
end;

//------------------ TSubClass -------------------------------
constructor TSubClass.Create(hParent:HWND);
begin
  if not Assigned(Dispatcher) then Dispatcher := TMessageDispatcher.Create;
  FParent := hParent;
  Dispatcher.Add(self);
end;

destructor TSubClass.Destroy;
begin
  Dispatcher.Delete(self);
  inherited Destroy;
end;

initialization

finalization
  Dispatcher.Free;

end.

UtilClass.pas のコードを調べるのは後回しにして、TSubClass から派生するように TSpeedBtn クラスを少し変更しよう。 Test.pas の全リストを以下に示す。

unit Test;

interface

uses
  windows,
  messages,
  UtilFunc,
  UtilClass;

type

TSpeedBtn = class(TSubClass)
private
  FColor: COLORREF;
  FRect: TRect;
public
  constructor Create(hParent: HWND); override;
  destructor Destroy; override;
  procedure DefaultHandler(var Message); override;
  property Rect: TRect read FRect write FRect;
end;

implementation

constructor TSpeedBtn.Create(hParent: HWND);
begin
  inherited Create(hParent);
  FColor := clrBlue;
  Dispatcher.TrapMessage(WM_PAINT,self);
  Dispatcher.TrapMessage(WM_LBUTTONDOWN,self);
end;

destructor TSpeedBtn.Destroy;
begin
  InvalidateRect(Parent,@FRect,true);
  inherited Destroy;
end;

procedure TSpeedBtn.DefaultHandler(var Message);
var
  Msg: TMessage;
  hBr: HBRUSH;
  DC: HDC;
  p: TPoint;
begin
  Msg := TMessage(Message);
  case Msg.Msg of
    WM_PAINT:begin
      hBr := CreateSolidBrush(FColor);
      DC := GetDC(Parent);
        FillRect(DC,FRect,hBr);
        DeleteObject(hBr);
        SetBkMode(DC,TRANSPARENT);
        SetTextColor(DC,clrWhite);
        DrawText(DC,'SpeedBtn',8,FRect,DT_CENTER or
                                  DT_SINGLELINE or DT_VCENTER);
      ReleaseDC(Parent,DC);
    end;
    WM_LBUTTONDOWN:begin
      p.x := Msg.LParamLo;
      p.y := Msg.LParamHi;
      if PtInRect(FRect,p) then begin
        if FColor = clrBlue then FColor := clrRed else FColor := clrBlue;
        InvalidateRect(Parent,@FRect,true);
      end;
    end;
  end; // case
end;

end.

以前の TSpeedBtn クラスとの変更点は、

  1. TObject からではなく、TSubClass から派生した。そして、TSubClass と重複する FParent フィールドを削除した。
  2. コンストラクタをオーバーライドした。
  3. デストラクタをオーバーライドした。

である。コンストラクタの実装部は以下のようになっている。

constructor TSpeedBtn.Create(hParent: HWND);
begin
  inherited Create(hParent);
  FColor := clrBlue;
  Dispatcher.TrapMessage(WM_PAINT,self);
  Dispatcher.TrapMessage(WM_LBUTTONDOWN,self);
end;

ここでは、最初に継承メソッドを呼び出して TSubClass のコンストラクタのコードを実行する。 後で説明するように、TSubClass のコンストラクタでは、TMessageDispatcher のただ一つのインスタンス Dispatcher を作成している。 継承メソッドを呼び出した後で、FColor フィールドを初期化し、Dispatcher のメソッド TrapMessage( ) を二度呼び出している。 このメソッドについても後で説明するが、やっていることは、捕捉したいメッセージとともに自分自身のインスタンス参照 selfDispatcher に登録し、 このメッセージを配送してくれるように依頼しているのである。これで、hParent パラメータで示される親ウィンドウを自動的にサブクラス化して、二つのメッセージがこのインスタンスに届けられるというわけである。
この TSpeedBtn のインスタンスを二つつくって、テストする SubClass02.dpr の全リストを以下に示す。

program SubClass02;

uses
  windows,
  messages,
  UtilFunc in 'UtilFunc.pas',
  Test in 'Test.pas',
  UtilClass in 'UtilClass.pas';

//------------------------------------------------------------
//             Global Types, Constants and Variables
//------------------------------------------------------------
var
  Clr: COLORREF;

  SP1,SP2: TSpeedBtn;

//------------------------------------------------------------
//             Event Handlers
//------------------------------------------------------------
procedure OnCreate(var Msg: TMessage);
var
  hWindow: HWND;
begin
  hWindow := Msg.Msg;
  Clr := clrBtnFace;
  MakeInitMenu(hWindow);
  ChangeWindowSize(hWindow,400,300);
  CenterWindow(hWindow);

  SP1 := TSpeedBtn.Create(hWindow);
  SP1.Rect := SetDim(50,50,200,100);

  SP2 := TSpeedBtn.Create(hWindow);
  SP2.Rect := SetDim(50,120,200,170);

end;

procedure OnTest(hWindow: HWND);
begin
  MessageBeep($FFFFFFFF);
end;

procedure OnCommand(var Msg: TMessage);
begin
  InitMenuCommand(Msg,OnTest);
end;

procedure OnEraseBkgnd(var Msg: TMessage);
begin
  Msg.Result := EraseBkGnd(Msg.Msg,Clr,Msg.WParam);
end;

procedure OnDestroy(var Msg: TMessage);
begin
  PostQuitMessage(0);
end;

//------------------------------------------------------------
//             Main Window Procedure
//------------------------------------------------------------
function MainWndProc(hWindow: HWND; Msg: UINT; WParam: WPARAM;
                      LParam: LPARAM): LRESULT; stdcall;
var
  AMsg: TMessage;
begin
  Result := 0;

  AMsg.Msg := hWindow;
  AMsg.WParam := WParam;
  AMsg.LParam := LParam;
  AMsg.Result := 0;

  case Msg of

//-------------  WM_CREATE  -----------------------------------

     WM_CREATE: OnCreate(AMsg);

//-------------  WM_COMMAND  ----------------------------------

     WM_COMMAND: OnCommand(AMsg);

//-------------  WM_ERASEBKGND  -------------------------------

     WM_ERASEBKGND: begin
       OnEraseBkgnd(AMsg);
       result := AMsg.Result;
     end;

//-------------  WM_DESTROY  ----------------------------------

     WM_DESTROY: OnDestroy(AMsg);

//-------- そのほかのメッセージの処理 ------
  else begin
    result := DefWindowProc( hWindow, Msg, wParam, lParam );
    end;

  end; // case
end;

//------------------------------------------------------------
//             Main Procedure
//------------------------------------------------------------
begin

  MakeMainWindow(@MainWndProc);

  Halt(MessageLoopNormal);

end.

DownloadExe.gifSubClass02.zip(13.517kB)

SubClass02.gifSubClass02.dprSkeleton01.dpr と異なる主な点は、 uses 節と、OnCreate( ) ハンドラで TSpeedBtn のインスタンスを二つ作成していることだ。 実行すると右図のように SubClass01.dpr と全く同じ動作をすることが確かめられる。 ご覧のように、プロジェクトファイルのコードでは、インスタンスを生成する以外のメッセージ処理のコードは完全になくなっている。 この時点で TSpeedBtn は、メッセージを自前で捕獲し処理する能力を獲得したと言えるだろう。 また、自身を解放することもできる。これは Dispatcher が適切なタイミングで行ってくれるのだ。

redball.gifこの章の目次へ戻る

4−6−2 実装コードを調べる

UtilClass.pasTMessageDispatcher クラスの宣言を以下に示す。

//------------------ TMessageDispatcher ----------------------

TSubClass = class; // forward

TMessageDispatcher = class(TObject)
private
  FSList: TIntegerList;
  FParentList: TIntegerList;
  FParentProc: TIntegerList;
  FMessageList: TIntegerList;
  FMessageObject: TIntegerList;
  FCopyList: TIntegerList;
  FCopyList2: TIntegerList;
  FNeedCopy: Boolean;
  function GetCount:integer;
protected
  function IsNewParent(hWindow:HWND):Boolean;
  function IsParent(hWindow:HWND):Boolean;
  function GetParentProc(hWindow:HWND):TFNWndProc;
public
  constructor Create;
  destructor Destroy; override;
  procedure DefaultHandler(var Message);override;
  procedure Add(Obj:TSubClass);
  procedure Delete(Obj:TSubClass);
  procedure TrapMessage(Msg: UINT; Obj: TSubClass);
  property Count: integer read GetCount;
end;

private 部の TIntegerList 型のフィールド変数 FSList は、TSubClass 及びその派生クラスのインスタンス参照のリストを保持する。TSubClass のコンストラクタのコードを見てみよう。

constructor TSubClass.Create(hParent:HWND);
begin
  if not Assigned(Dispatcher) then Dispatcher := TMessageDispatcher.Create;
  FParent := hParent;
  Dispatcher.Add(self);
end;

ここでは、UtilClass.pasimplementation 予約語の直ぐ上に宣言されている TMessageDispatcher 型の変数 Dispatcher が既に作成されているかどうか調べ、もし作成されていなければ作成する。 これにより、TSubClass の最初のインスタンスだけが Dispatcher を作成する。 つぎに、親のハンドルを FParent に格納し、そのあと DispatcherAdd メソッドを使って自分自身を登録している。

procedure TMessageDispatcher.Add(Obj:TSubClass);
var
  OriginalProc: integer;
begin
  FSList.Add(Integer(Obj));

  if IsNewParent(Obj.Parent) then begin
    FParentList.Add(Obj.Parent);
    OriginalProc := GetWindowLong(Obj.Parent,GWL_WNDPROC);
    FParentProc.Add(OriginalProc);
    SetWindowLong(Obj.Parent,GWL_WNDPROC,integer(@SubClassProc));
  end;
end;

Add メソッドでは、FSList にオブジェクト参照を追加してから、このオブジェクトの親ウィンドウがすでに登録されている親か、登録されていない新しい親かを IsNewParent( ) メソッドを使って調べ、新しい親ならサブクラス化を実行する。 if....end; の中のコードを見てみよう。 ここは、サブクラス化を行う重要な部分である。

  1. 親のウィンドウハンドルを保持する FParentList にこの新しい親を追加する。
  2. GetWindowLong( ) APIを使って現在設定されているウィンドウ関数を取得し、ローカル変数 OriginalProc に格納する。
  3. 親ウィンドウの OriginalProc の値を FParentProc に追加する。
  4. 親ウィンドウのウィンドウ関数を SubClassProc に置き換える。

の手順でサブクラス化する。GetWindowLong( )SetWindowLong( ) APIはすでに説明した。 3.のコードについて考えよう。SubClass01 では、サブクラス化する以前のもともとのウィンドウ関数をグローバル変数に保持した。 このときは、親がメイウィンドウだけであることがあらかじめ分かっていたからである。 TSubClass クラスの派生クラスは、メインウィンドウの上に作られるかもしれないし、パネルなどのコンテナの上につくられるかもしれないし、ダイアログなどのポップアップウィンドウの上に作られるかもしれない。 要するに、 あらかじめ親の数と種類を予測できないので、グローバル変数にもとのウィンドウ関数のアドレスを保持することができないのである。もとのウィンドウ関数のアドレスが必要なのは、 サブクラス化したウィンドウ関数内で CallWindowProc( ) によりメッセージを渡す場面である。そこでは、メッセージのほかに参照できるものとしては、親ウィンドウのハンドルだけである。したがって、親ウィンドウのハンドルだけを知って、もとのウィンドウ関数のアドレスにアクセスできなければならない。そこで、Add メソッドで親のウィンドウハンドルを FParentList に追加するのと同じタイミングで OriginalProcFParentProc に追加する。 GetParentProc( ) メソッドは、親のハンドルからサブクラス化する前のウィンドウ関数を取り出す。

function TMessageDispatcher.GetParentProc(hWindow:HWND):TFNWndProc;
begin
  result := TFNWndProc(FParentProc[FParentList.Search(hWindow)]);
end;

これによって、ウィンドウハンドルからもともとのウィンドウ関数を取り出すことができた。

以上で、サブクラス化が完了したので、捕捉したいメッセージとその配送先を登録する TrapMessage( ) メソッドを見てみよう。

procedure TMessageDispatcher.TrapMessage(Msg: UINT; Obj: TSubClass);
begin
  FMessageList.Add(Msg);
  FMessageObject.Add(integer(Obj));
  FNeedCopy := true;
end;

ここでは、private 部のフィールド変数に追加しているだけだ。 そして、FNeedCopy フラグを true にして、コピーリストも更新する。

メッセージの流れを追跡しよう。 全てのメッセージは SubClassProc から入ってくる。

function SubClassProc(hParent: HWND; Msg: UINT;
                       wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var
  AMsg: TMessage;
  OriginalProc: TFNWndProc;
begin

  result := 0;
  if not Dispatcher.IsParent(hParent) then Exit;

  OriginalProc := Dispatcher.GetParentProc(hParent);

  result := CallWindowProc(OriginalProc,hParent,Msg,wParam,lParam);

  AMsg.Msg := Msg;
  AMsg.WParam := wParam;
  AMsg.LParam := lParam;
  AMsg.Result := hParent;

  case Msg of

    WM_DESTROY: begin
      SetWindowLong(hParent,GWL_WNDPROC,integer(OriginalProc));
      Dispatcher.Dispatch(AMsg);
    end;

  else
    Dispatcher.Dispatch(AMsg);

  end; // case

  if AMsg.Result <> 0 then result := AMsg.Result;

end;

もとのウィンドウ関数を CallWindowProc( ) をつかって実行した後で、TMessage 型のレコード AMsg を組み立てる。 その際、 Result フィールドにはウィンドウハンドルを格納する。 そして、WM_DESTROY 以外のメッセージはすべて Dispatcher に送り込む。 WM_DESTROY のときは、サブクラス化を解除してから Dispatcher に送る。 Dispatcher は、送られたメッセージを DefaultHandler( ) で処理している。

procedure TMessageDispatcher.DefaultHandler(var Message);
var
  AMsg: TMessage;
  hParent: HWND;
  i: integer;
  SO: TSubClass;
begin
  AMsg := TMessage(Message);

  hParent := AMsg.Result;
  TMessage(Message).result := 0; // default return result
  AMsg.Result := 0;              // default object return

  if AMsg.Msg = WM_DESTROY then begin
    for i := FSList.Count-1 downto 0 do begin
      SO := TSubClass(FSList[i]);
      if Assigned(SO) then
        if (SO.Parent = hParent) then begin
          FSList.Delete(i);
          SO.Free;  //SO := nil;
        end;
    end;
    FParentProc.Delete(FParentList.Search(hParent));
    FParentList.Delete(FParentList.Search(hParent));
    exit;
  end;

  if FMessageList.Search(AMsg.Msg) = -1 then
    exit
  else begin
    if FNeedCopy then begin
      FMessageList.CopyTo(FCopyList);
      FMessageObject.CopyTo(FCopyList2);
      FNeedCopy := false;
    end;
    for i := 0 to FCopyList.Count-1 do
      if DWORD(FCopyList[i]) = AMsg.Msg then begin
        SO := TSubClass(FCopyList2[i]);
        if Assigned(SO) then
          if (SO.Parent = hParent) then begin
            SO.Dispatch(AMsg);
            if AMsg.Result <> 0 then begin
              TMessage(Message).result := AMsg.Result;
             exit;
            end;
          end;
      end;
  end;
end;

ここでは、最初に Result フィールドからウィンドウハンドルを取りだし、 代わりにデフォルトの値ゼロを格納する。 次に、メッセージが WM_DESTROY のときは、オブジェクトリストを逆にたどって、親ハンドルが一致するオブジェクトを探し出して削除している。 そして、親ウィンドウ関数リストや親ハンドルリストからも削除している。 WM_DESTROY メッセージ以外のメッセージは、登録されているメッセージかどうか調べて、登録されていなければ何もしない。 一つでも登録されていれば、 FNeedCopy フラグが true ならば、メッセージリストとオブジェクトリストをコピーする。 そして、そのコピーを使って、 メッセージを要求しているオブジェクトを for ループ により探して、見つかったら Dispatch メソッドを使ってメッセージを送る。 デフォルトのゼロ以外の Result が返ってきたら、SubClassProc に変数渡しで送り返す。 SubClassProc では、この値を戻り値にしてシステムに返す。

オブジェクトの検索の際、なぜコピーリストを使うのか疑問に思われるかもしれない。 これは、オブジェクトがメッセージを受け取ったときに行う処理のなかで、他のオブジェクトや自分自身を削除したり、あるいは、新しいオブジェクトを追加するかもしれないからである。 このとき、メッセージを受け取った時点で検索をはじめたときの FMessageListFMessageObject は、DeleteTrapMessage メソッドにより変化させられる。 これでは、検索のループがめちゃくちゃになるので、メッセージを送るための検索中は変動しないようにコピーリストを使用するのである。 このことは非常に重要であり、 コピーを使うことにより、後につくるスピードボタンなどのクリックイベントで自分自身を消去する事さえできるようになる。 VCLのオブジェクトは、自分のイベントハンドラで自分を解放することができる。 これは、実装するプログラマの見地に立つと、イベントハンドラの処理としては最も厳しい処理の一つである。

TMessageDispatcher の唯一のインスタンス Dispatcher は、UtilClass.pas の終了処理部で削除する。

initialization

finalization
  Dispatcher.Free;

end.

Dispatcher の宣言は interface 部にあるので、このユニットを uses する全てのユニットから参照できる。

以上で、TMessageDispatcherTSubClass の実装コードの説明は終わりである。 TSubClass のインスタンスは Dispatcher をつくり自分自身を登録する。 さらにその派生クラスである TSpeedBtn のコンストラクタに見られるように DispatcherTrapMessage メソッドを使って特定のメッセージを自分に送ってくれるよう依頼する。Dispacther は、TSubClass のインスタンス参照やその親のハンドルやウィンドウ関数のリスト、捕捉するメッセージのリストなどを保持して、SubClassProc から送られたメッセージを適切なインスタンスに配達する、という図式が理解出来ただろう。

redball.gifこの章の目次へ戻る

4−7 まとめ

本章では、Win32 システムにおけるウィンドウの雛形であるウィンドウクラスの情報やウィンドウ自体の情報を取得し、さらに変更する方法について調べた。 また、サブクラス化やフック関数のインストールによってウィンドウ関数に送られてくるメッセージを「盗み見る」方法を、簡単なRADプログラムで試してみた。 スピードボタンのようなウィンドウではないオブジェクトが独自に親ウィンドウのメッセージを処理するために、サブクラス化の手法を使って TMessageDispatcher クラスと TSubClass クラスを実装した。 前章でつくった TSpeedBtnTSubClass の派生クラスとして実装し直し、 メインウィンドウのウィンドウ関数に依存しないでメッセージを取得し、処理する事ができることを確かめた。

TSubClass は、本稿でつくるクラスのうちの大部分の上位クラスとなる重要なクラスである。 TSubClass の派生クラスは、DispatcherTrapMessage メソッドを使って、親ウィンドウから特定のメッセージを独自に取得するメカニズムを持っている。


ここまでのプログラムファイル

  1. Skeleton01.dpr (3 kB)
  2. UtilFunc.pas (8 kB)
  3. UtilClass.pas (11 kB)
  4. Test.pas (2 kB)



redball.gifこの章の目次へ戻る