メッセージフックを使う

←NetScape3.xx以降をお使いの方は目次でジャンプできます。

Windowsならではの多くのアプリケーションの連携。CallBackの章でも書きましたが他のアプリケーションを操作したい。と思う事は良くあります。ここでは、アプリケーションに送られるメッセージを横取りするための手順、フックについて書きます。Gen's Low Techにしてはちょっと難しい内容かも知れませんが、DLLの作成、使用、ポインタの扱い、ファイルを扱う時の手順、ウィンドウプロシージャのいじり方などがある程度分かっていれば何とかなります(多いって...(^^))。コールバック関数も出てきますね。あと、Windows95APIバイブル1はとりあえず必須の参考書として指定しておきます。英語がなんぼのもんじゃいという方はDelphiのヘルプで十分(^^)。

フック 〜 フックで何が出来るか?

Windows95になってタスクトレイと言うものが出来、いわゆる常駐物のアプリケーションなどはここにアイコンが表示される事が多くなりました。時計、IMEをはじめ、私の所では秀丸やAltIME等が入っています。このうち、AltIMEはCHOMBO氏の作られたツールでキーストロークを監視して、右altキーでIMEをオンにしたり、Caps Lock と Ctrlキーを入れ替えたりするプログラムです(秀Capsなんかもそうでしたっけ?)。こういったタイプのプログラムではWindowsが発行するキー操作メッセージを常に監視してすりかえてしまう。というような動作をしていると考えられます。通常は自分がアクティブでない限りメッセージはやってきません。つまり、他のアプリケーションにどんなキー入力があろうと、マウス入力があろうと、自分へのメッセージではない限り知る事は出来ないのです。ではAltIME等のキー入力入れ替えツール等はどうやって他のアプリケーションへのメッセージを知るのでしょうか?というお話です。

fig.1 - フックされたメッセージの流れ
[拡大図表示 | 別ウィンドウで表示[JS]]

自分へのメッセージだけではなく、自分がアクティブでない時に他のアプリケーションに送られるメッセージをも監視出来てしまう。それがフックです(fig.1参照)。通常Windows→TargetApplication(TargetWindow)という流れのメッセージが、一旦、自分の所を経由するわけです。そこで、フックを使うと、監視するだけではなくて、メッセージが目的のアプリケーションに到達する前に、いじる事が出来るというわけ。結局、上記のようなキーボードのリマップを行うようなプログラムを作成する時にはどうしてもフックを避けて通る事は出来ません。また、フックにはユーザのキー、マウス操作を記録、再生すると言う使いこなすと便利なものもあります。結局、メッセージベースで動作しているWindowsにおいて、メッセージをすりかえる事が出来ると言う事は、「よ〜く聞けい、今からはわしが王じゃv(^^)v、フォッフォッフォッ」の下克上状態を作る事が出来てしまうという事です。当然の事ながら、細心の注意を払って作成しないと (@@) 状態 =>しまいに (;_;)です。

フックのインストール、アンインストール

フックを使うには、まずWindowsにフックを処理するための関数を登録(インストール)します。この関数の事を「フィルタ関数」と呼びます。フィルタ関数は前に「コールバック関数を使う」で書きました、CallBack関数になっています。つまり、登録しておけば必要になった時にWindowsが呼び出してくれるわけです。そして、フックする必要がなくなった時点でアンインストールしておきます。フィルタ関数のインストール、アンインストールに使うAPIがSetWindowsHookEx、UnhookWindowsHookExです。

宣言は以下のようになっています。
function SetWindowsHookEx(idHook: Integer; lpfn: TFNHookProc;

hmod: HINST; dwThreadId: DWORD): HHOOK; stdcall;

idHook: Integer インストールするフィルタ関数のタイプを記述します(後述)。
lpfn: TFNHookProc フィルタ関数のコールバックアドレスを指定します。
hmod: HINST フィルタ関数の入っているDLLのインスタンスハンドルを指定します。Delphiから使う場合、hInstanceと記述しておきます
dwThreadId: DWORD スレッド識別子を記述するのですが、hmodにhInstanceと記述した場合には 0 にしておきます。
通常の(同じDLL内にフィルタ関数がある(仮に名前をHookProc()とする)場合 2番目の引数には@HookProcと関数名の前に"@"(アットマーク)を付けます。コールバック関数の所でやったのと同じですね。これで、関数のアドレスをWindowsに伝える事が出来ます。

function UnhookWindowsHookEx(hhk : HHOOK): BOOL; stdcall;

hhk: HHOOK SetWindowsHookExの戻り値を指定します。そのフックが削除されます。

フィルタ関数の種類

フィルタ関数のインストール、アンインストールが分かった所で、フィルタ関数本体に入ります。但し、フィルタ関数のすべての種類、内容についてはDelphiのヘルプでSetWindowsHookExを引けばそこからたどれますし、参考書だと日本語ですからそちらを参照して下さい。よく使うものとしては、WH_GETMESSAGE(メッセージ)、WH_KEYBOARD(キー操作メッセージ)、WH_MOUSE(マウスメッセージ)等があります。WH_JOURNALPLAYBACK, WH_JOURNALRECORDを使うとキー操作などのメッセージを記録、再生できます。ここで出てきた、WH_XXXXというのはフィルタ関数の種類を表わしているだけで関数の名前ではありません。フィルタ関数自身にはどんな名前を付けても構いません。で、Windowsのコールバック関数と言う事ですから引数の型だけは決まっていまして(前にやりましたね)、これはすべての種類のフィルタ関数で同じ型の引数を取ります。

フィルタ関数のプロトタイプは以下のようになっています。

function HookProc(Code : integer; wParam : WPARAM, lParam : LPARAM): LRESULT; stdcall;

それぞれのパラメータが持つ意味は、フィルタ関数によって違ってきます。

また、フィルタ関数内で次のフック(自分だけがフックをかけているとは限らない(^^)し、自分で複数のフックを仕掛ける時もあります)にメッセージを渡すために以下のAPIを使います。フィルタ関数内でこれを呼んどかないとこのフィルタ関数はメッセージのブラックホールになってしまう可能性があります。こういった事もあり、もう一度言いますが、フィルタ関数をいじる場合、細心の注意を払って下さい。すべてのメッセージを呑み込んだまま、吐き出さないようなコードになったら最後、「ピボッ!」、のお世話になる事になります(^^)。

function CallNextHookEx(hndHook : HHOOK, Code : integer

wParam : integer, lParam : LongInt) : LRESULT ; stdcall;

hhk: HHOOK フィルタ関数をインストールする時に使ったSetWindowsHookExの戻り値 を指定します。下記問題点も参照の事
その他のパラメータは上記フィルタ関数と同じで、HookProc()が受け取った パラメータをそのまま渡しておけばいいです。

以上の事から、フックを行うDLLの雛形として以下のようなものが出来ます。但し、これはまだまだ不完全なソースだ、と言う事を覚えておいて下さい。それを次に説明していきます。

List.1
フックを使う時の雛形(DLL内) 〜 不完全版--------------------
DLLを呼び出す側は、InstallHookProc()を呼び出して、後は寝て待つだけ です(^^)。フックしたメッセージを処理するHookProc()のコールはWindows に任せておきます。もうフックはいいよとなったら、UnHookWindowsHookEx() を呼びます。
変数HostWnd,hndHookをどこに保存するかは下記参照

interface

function HookProc(Code : integer; wParam : integer;  lParam : LongInt) : integer; stdcall;
procedure InstallHookProc(wnd : hWnd); export; stdcall;
procedure UnInstallHookProc; expoert; stdcall;

implementation

//Hook処理本体関数
function HookProc(Code :integer ; wParam :integer ;  lParam : LongInt) : integer;
begin
  ...
  HogeHogeProc(); //フックしたメッセージをここで処理
  PostMessage();  //呼び出し側APPへ通知(必要なら)
  ...
  ...
  Result := CallNextHookEx(hndHook,nCode,wParam,lParam);
end;

//Hookをインストールする手続き
procedure InstallHookProc(wnd : hWnd);
begin
  ...
  HostWnd := Wnd; //呼び出したApplicationのウィンドウハンドルを保存
  hndHook := SetWindowsHookEx(WH_XXXX, @HookProc, hInstance, 0); //フックプロシージャのハンドルを保存
  ...
end;

//Hookをアンインストールする手続き
procedure UnInstallHookProc;
begin
  ...
  //インストールした時のハンドルを使ってフック削除
  UnHookWindowsHookEx(hndHook);
  ...
end;

フックを使う時の注意 〜 Win32でのDLL

いきなりですが(^^)、自プロセス内だけでメッセージフックをする時以外、要するに他のアプリケーションに向けられたメッセージもフックする時は、必ずフィルタ関数はDLL内に置く必要があります。そうでないと自分がアクティブである時しか、Windowsがコールバックできません(コールバック先を見つけられない)。これがまず一つ目。で、DLLの中に入れちゃえばいいんです。ハイ、解決(^^)。

もう一つ問題があります(げっ...!)。単純にフィルタ関数をDLL内に置いてインストールするだけでは、じつは呼び出し側のアプリはフックが起きた事を知る事が出来ません。何らかの形で知ろうとするにはDLL内のHookProc()関数内でメッセージを送ってもらえばいい(PostMessage()する)という事は想像に難くないと思いますが、問題は、メッセージの送り先がちゃんと指定できるかと言う事なのです。ん〜何々?別に知らせてもらわんでも、フィルタ関数の中ですべて事を済ませばいいやんか? ...なるほどォ。...でもね、ダメなんです。fig.1のように(軽い処理だからという理由で)たとえHookProc()内で全て処理してしまうようなスタイルの場合でもCallNextHookEx()に渡す第1パラメータ、hndHook変数(インストールした時に返ってくるフィルタ関数のハンドル)の保存問題(DLL内のどこに変数を宣言して保存するか)(List1参照)があります。これらは、フックにおける注意と言うよりも、Win32でDLLを扱う時の注意とも言うべき「DLL内の共有変数の問題」なのですが、何が問題なのかはひとまず置いといて、Application側へ通知してもらう場合のフックの手順とメッセージの流れについてみておきます。

fig.2 - フィルタ関数のインストールから
WndProcにメッセージが到着するまで
[拡大図表示 | 別ウィンドウで表示[JS]]

フィルタ関数の登録、コールバック発生、Applicationへのメッセージ通知までをイメージにするとfig.2のようになります。この例では、WindowsがコールバックしてきたHookProc()の中からPostMessage()を使ってApplicationへフックした旨通知するようにしています。HookProc()の中でいろんな処理をやってしまうと、重〜い、おも〜い(^^)、Windowsになってしまう可能性がある場合、そういった事態は極力避けるようにして(大体フック自体がイリーガルと言えばイリーガル(^^))、時間のかかる処理は呼び出し側のアプリにやってもらうのがいいでしょう(極端な話、フックメッセージがきたらそれを通知するだけですむ場合もあると思います)。そこで、メッセージを投げて通知してやると言うのが妥当な線になるわけです。とにかく、HookProc()PostMessage()するApplicationのウィンドウをちゃんと分かってないといけません。

フックを使う時の最初の手順として、Application側のSomeProc()の中でDLL内のInstallHookProc()(フィルタ関数をインストールする関数)を呼びます。この時にApplicationのウィンドウハンドルをパラメータとして渡してやります。InstallHookProc()関数は受け取ったウィンドウハンドル(つまり、フック情報を通知すべきウィンドウですね)をしかるべき変数に保存しておいてから、フィルタ関数HookProc()をインストールすれば万事うまく行くはずです。フックが起こった時にコールバックされるHookProc()は、その情報(しかるべき変数)をもとにウィンドウを識別しフック情報を渡す処理(メッセージを送る)を行う事が出来[ます|るはず]。またInstallHookProc()の戻り値がフィルタ関数HookProc()のハンドルになっていますから、これもしかるべき変数にちゃんと保存しておけばHookProc()内でCallNextHookEx()に渡せ[ます|るはず]。でも...!?

DLL内での変数の保持問題 〜 プロセス間での変数共有

ここで、そのしかるべき変数という部分がミソです(^^)。どこに保存しておくかと言う事です。例えばDLLユニット内グローバルな変数を宣言したとします。そこにある変数を代入したとして、そのDLLが別のプロセスからアクセスされた場合、どうなるでしょうか? もうここまでで、問題点が分かってしまった人はこのセクションは飛ばして次に進んで下さい。「そんな事初めて考えた」(^^)という人はまず、Fig.3を参考にしてどうなるかためしてみて下さい。その後、このセクションを読んで下さい。どっちでもない人はもう少しお付き合い下さい(^^)。

Fig.3 複数のプロセスで変数が共有されない事を確かめてみる
ソースには後で使うDLL2のものも入っています。変数が共有できない事を確 かめるのにはDLL1をロードして使います。なお、DLL1,DLL2はサイズが小さ いのでバイナリも含まれますが、テストアプリ(TEST1)はソースのみなので、 各自コンパイルして下さい。
mmf.zipをダウンロード
ソースを見る
別ウィンドウで見る[JS]
TEST1をコンパイルしてから複数(以下AppA,AppBと区別します)起動して、それぞ れDLL1を読込んで下さい。AppAで何か値をセット(SetDataボタンを押す)した後、 GetDataボタンを押し、データを読んで下さい。ちゃんと値がセットされていま す(Memoに表示されます)。

次に、AppBのGetDataボタンを押して下さい。AppAでセットした値とは違いま すね(メモには0が表示されます)。

AppBからさっきとは別の値をセットしてみます。AppBのGetDataボタンを押す と、セットした値が返ってきているはずです。

その後AppAでGetDataボタンを押すと、いま、AppBでセットした値ではなくて 前にAppAでセットした値が返ります。つまり、AppAとAppBではお互いに影響無く 変数にアクセスできているのです。これが本文で書いた ”フツーの”場合です。

DLL内で宣言された変数は、DLLを呼び出したプロセスのアドレス空間にコピーされます。そうする事で、どのプロセスから呼ばれるか分からないDLL内の変数もプロセス毎に安心して使えるのです。フツーの場合(^^)。しか〜し、今の場合逆にそれじゃあ困るのです。自分がアクティブになっていない時も知らせてほしいのですから、一旦知らせた自分のウィンドウハンドル(HostWnd)がプロセス毎に別の値になっては困るわけですね。また、フックをインストールした時に返されるフィルタ関数のハンドル(hndHook)もプロセス毎に変わったのではフックメッセージを正しく次のフィルタ関数に渡す事が出来ません(フィルタ関数をインストールしたのは自分であって、他のプロセス内では単なる0で初期化された変数でしかない)。メッセージを無効にしてしまうようなフィルタ関数だったら最後、ここにめでたくブラックホールの誕生です(^^)

くどいようですが...、今の場合、InstallHookProc()に自分のウィンドウハンドルを渡しました。そして、その関数内でちゃんと変数を保存したんだから、どんな時でもちゃんとその値を使ってくれているつもりになっていると痛い目に会うと言う事です。この場合(ユニット内グローバルな変数としてHostWndhndHookを宣言した場合)どうなるかと言うと、もうお分かりとは思いますが、呼び出し側のアプリがアクティブな時だけ、フックの通知メッセージが飛んできます(T_T)。つまり、前述のように自分がアクティブな時にはDLL内の変数(HostWnd)はこちらが期待する通りの値(自分のウィンドウハンドル)になっています。でも、アクティブでない場合、DLLが呼ばれると変数領域は別のプロセスに割り当てられてそれが使われる事になりますから、ここにはどんな値が入っているかは分かりません(BorlandのPascalコンパイラですから、0で初期化されているはずですが)。そうするとHookProc()内でPostMessage()しても決して自分に向かってはメッセージは飛んでこないと言う事になります。これでは、わざわざDLL内に閉じ込めた甲斐が無いと言うものです。ここまでの話は何だったの?...(^^)。「ようやくDLLをつくって、それを使えるようになったばっかりなのにぃ〜」と言う声が聞こえてきます(^^)が、大丈夫、ここはGen's LowTech。凝った事をやろうとしているのですから、覚えなきゃならん事は多いかもしれませんが(^^;、難しい事はありません、やりません(^^;ゞ。

これでList.1が不完全な雛形だと言う事が分かりました。フックの問題点、と言うよりもWin32のDLLの扱いの注意も分かりました。では、DLL内での変数の保持問題について解決しましょう。Windowsはちゃんとメモリを共有する方法と言うのを提供しています。それが名前付き共有メモリ(メモリマップドファイル)を使うと言う方法です。参考書のDLLの章(第30章)をみると、やはり「DLLを使用するすべてのプロセス間で動的に割り当てられたメモリが共有されるためには名前付き共有メモリを作成します」と書いてあります。また、その次にグローバル、スタティック変数に関しても云々と書いてありますが、Delphiには「モジュール定義ファイル」なるものは存在しませんので無視します(^^)。という事で、名前付き共有メモリ(メモリマップドファイル)を使って、DLL内で変数を共有できるようにします。

メモリマップドファイルの登場

これは、まずシステムの使用しているメモリ空間に領域を確保し、例えばHDD上のファイル名みたいに名前を付けてしまいます。そこに(ディスク上の)ファイルをマッピングしてアクセスしようと言うものです。システムの使用する空間はアプリケーションとは別領域で2GBありますから(全部使えるわけではないけれども)理論上ほぼ無制限に大きなファイルを扱える事になります。それは置いといて...、で、ポイントは実際にディスク上にファイルがある必要はないという点です。どういう事かと言うと、Windowsシステムに「メモリをちょっと貸して」とお願いして、その部分を共有メモリとして利用させてもらうのです。プロセス毎にアドレス空間が別になってしまうのなら、親玉に登場願おうというわけですね。場所を借りる事が出来れば、後は一般的なファイルと同じような感覚でアクセスを行います。イメージとしてはレコード型のバイナリファイルを読み書きする感覚をそのままメモリに対して行うといった感じでしょうか?相手がディスク上のファイルではなくて、名前を付けたメモリ空間だという点だけが異なるのです。作成したファイル(実際にはシステムのメモリ領域)の事をファイルマッピングオブジェクト(以下FMO)と言います。

FMOを作成して読み書き、終われば破棄。既存のFMOに対してはオープンして読み書き、終われば破棄。実際のファイルにアクセスするのと同じ感覚です。最初の作成〜破棄の間に他のプロセスからFMOをオープンしてアクセスする事で、メモリ空間の共有を実現します。この時、FMOを名前によって一意に識別します。これも、ディスク上のファイルと同じですね。FMOの作成はCreateFileMapping()。ここでFMOの名前を設定します。その名前を使って、FMOをオープンして中身にアクセスできるようにするのが、OpenFileMapping()。作成、あるいはオープンしたら、次に実際に読み書きをするためにFMOをそのプロセスのアドレス空間にマッピングします。プロセスのアドレス空間に窓をつくって、システムのアドレス空間にあるFMOを覗けるようにするわけです。この事を「ファイルのビューをマッピングする」と表現します。そのための関数が、MapViewOfFile()。これで、共有変数を読み書きできます。使い終わったら、ファイルアクセスの常、ファイルを閉じなければなりません。UnMapViewOfFile()を呼び出して、マッピングを解除。最後にCloseHandle()FMOのハンドルをクローズします(fig.4参照)。チャンチャン。

MMFについて以下に簡単にまとめておきます(ちょっと長い)。ソースと共に見て下さい。[→飛ばす]
Fig.4 メモリマップドファイルを共有メモリとして使う時の手順
ソースを見る
別ウィンドウで見る[JS]
JavaScriptを解釈できるブラウザをお使いの方は以降(→ソース)の所をクリックしてもらえば該当のソース部分を表示します。
まず、共有したい変数をRecord型の中身(フィールド)として定義しておきます(ここではTShareDataとします)。これは、CreateFileMapping()する時にサイズを指定する必要があるため、Record型にまとめておいて SizeOf(TShareData)とやれば複数の変数があっても一気に領域が確保できるので都合がいいわけです。また、実際にはポインタを操作する事になるので、PShareData = ^TShareData と定義しておきます。ここまでをType節に記述します。(→ソース)

次にファイルの名前を決めます。何でもいいのですが、ユニークになるようにして下さい。この名前をもとにしてファイルを扱います。Const節で宣言しておきます。(→ソース)

  共有メモリにアクセスする時の手順は以下のような感じです
    1.CreateFileMappingでFMOを作成
    2.  OpenFileMappingでFMOをオープン
    3.  MapViewOfFileでFMOのビューをマッピング
    4.    ***変数にアクセス***
    5.  UnmapViewOfFileでFMOのビューを解除
    6.  CloseHandleでFMOをクローズ
    7.CloseHandleでFMOをクローズ
		

1.のCreateFileMappingですが、これは最初に一度だけ作れば後はOpenFileMappingすればいいのですが、じゃあ、いつ作成したらいいのかと言うとDLLがロードされる時初期化が出来る部分、つまりUnitのInitialization部。ここで作ってしまえばいいですね。もし既存のファイルをCreateFileMappingしようとした時にはそのファイルのハンドルが返ってくるだけなので、心配は要りません。同様に、Finalization部で、CloseFileユニット内グローバルで宣言しているhFMOBJを渡してやればOKです。(→ソース)

後は、実際に変数にアクセスする部分毎に2〜6を記述します。サンプルDLL(DLL2)では2〜6をそのまま記述していますが、決まり事なのでOpenFileMapping(), MapViewOfFile()とUnmapViewOfFile(), CloseFile()をまとめて1つの関数にしておけば、ソースの見通しも良くなります。改良したものがこれです(DLL2AMain.pas)。(→ソース)

それぞれのAPIの宣言など詳細についてはヘルプ、参考書を見てもらう事として、今回のようにメモリ共有する時に限ったそれぞれのAPIの使い方をまとめておきます。DLL2Main.pasで使われている形で書きます。

■FMOを作成する
hFMOBJ := CreateFileMapping($FFFFFFFF,nil,
PAGE_READWRITE,0,SizeOf(TShareData),UniqueString );
hFMOBJ CreateFileMapping()が返すFMOのハンドルを保存するための変数です。THandle型。Unitグローバルな変数で宣言しておき閉じる時にCloseFile()で使っています。
SizeOf(TShareData) 共有するメモリのサイズを指定します。前述のようにTShareDataのサイズを渡しています。
UniqueString 共有メモリにつける名前を渡します。以降この名前を使って共有メモリにアクセスします。なお、Const節で宣言した文字列は文字列リテラルとなりPCharでキャストしなくてもそのままPChar型の引数に渡せます。もしString型の文字列を渡す場合はPCharでキャストして渡せばOK。便利になったもんです(^^)。
■既存のFMOをオープンする
_hFMOBJ := OpenFileMapping(FILE_MAP_ALL_ACCESS, False, UniqueString);
_hFMOBJ 共有メモリを使う関数ブロック内ローカルで宣言されている変数です。関数内ではOpenFileMapping()の返す、このハンドルでFMOにアクセスします(MapViewOfFile, UnmapViewOfFile()で使います)。
UniqueString CreateFileMapping()で使ったのと同じ名前を指定します。
■FMOのビューの設定
p := MapViewOfFile(_hFMOBJ, FILE_MAP_ALL_ACCESS, 0, 0, 0));
p; MapViewOfFile()が返してくるハンドルを代入します。これが共有変数へのポインタになっていますので、ビューを設定した後は定義したRecord型のポインタにキャストして使います。これも共有メモリを使う関数内ローカルな変数として宣言しています。"覗き窓"の出現(^^)です。
_hFMOBJ; CreateFileMapping()あるいはOpenFileMapping()の戻り値(FMOのハンドル) を指定します。
■FMOのビューの解除
UnmapViewOfFile(p);
p; MapViewOfFile()が返してきたポインタ値です。これで、プロセス内の"覗き窓"が閉じられます(;_;)。
■FMOのハンドルを閉じる
CloseHandle(_hFMOBJ);
_hFMOBJ; OpenFileMapping()が返してきたFMOのハンドルです。「もう要らないよ」とWindowsに伝えるわけです。Windowはどれだけ参照してきているかをカウントしていて、最後の参照が無くなるとシステムからFMOが削除されます。

ここで、fig.3と同じように先ほど試してみたDLLのテストアプリを動かして下さい。今度はDLL2の方を読込んで同じような操作をすれば変数が共有されている事が分かると思います。自分が呼び出したDLLの中で使われている変数が、知らない間に(ってことはないですが)ちゃんと別のプロセスによって、書き換えられていますよね。上に書いた事とつきあわせてMMFの使い方を見て下さい。そして、List.1の不完全版をMMFを使って書き直したものがList.2です。

List.2
フックを使う時の雛形DLL 〜 共有メモリ版
フック関数のアドレス、呼び出し側のウィンドウハンドルHostWnd,hndHookを共有変数内に保存するようにしたものです。MesID(独自登録したメッセージID)は次章で解説します。

interface

Type
  //共有変数を集めた構造体
  PShareData = ^TShareData;
  TShareData = Record
    hndHook : HHook;
    HostWnd : hWnd;
    MesID   : integer;
    ...
  end;

function HookProc(nCode:integer; wParam:integer; 
                     lParam:LongInt):integer;stdcall;
procedure InstallHookProc(wnd:hWnd);export;stdcall;
procedure UnInstallHookProc;expoert;stdcall;

implementation

function GetFMObj(var h:THandle; var p:pointer):integer;
begin
  FMOを開いて、ビューを設定
end;

procedure ReleaseFMObj(h:THandle; p:Pointer);
begin
	ビューを解除して、FMOハンドルを閉じる
end;

//Hook処理本体関数
function HookProc(nCode:integer; wParam:integer; 
                     lParam:LongInt):integer;
var
  _hFMObject : THandle;
  p : pointer;
begin
  ...
  
  GetFMObj(_hFMObject, p);
  
  フックしたメッセージをここで処理
  PostMessage(HostWnd, MesID, wParam, lParam);  //呼び出し側APPへ通知(必要なら)
  ...
  ...
  Result := CallNextHookEx(hndHook,nCode,wParam,lParam);
  ReleaseFMObj(_hFMObject, p);
end;

//Hookをインストールする手続き
procedure InstallHookProc(wnd:hWnd);
var
  _hFMObject : THandle;
  p : pointer;
begin
  ...
  GetFMObj(_hFMObject, p);
  
  with PShareData(p)^ do begin
    HostWnd := Wnd;
    hndHook := SetWindowsHookEx(WH_XXXX, @HookProc, hInstance, 0);
  ...
  end;
  ReleaseFMObj(_hFMObject, p);
  
end;

//Hookをアンインストールする手続き
procedure UnInstallHookProc;
var
  _hFMObject : THandle;
  p : pointer;
begin
  ...
  GetFMObj(_hFMObject, p);
  
  //インストールした時のハンドルを使ってフック削除
  with PShareData(p)^ do
    UnHookWindowsHookEx(hndHook);
  ...
  ReleaseFMObj(_hFMObject, p);
end;

MMFについてはここではこれ以上詳しくは書きません(他にもだいぶ有効な使い方の出来るものみたいですが、また機会があれば書きたいと思います)。今は、メッセージをフックする目的に向かって、進む事にします。でも、もう7合目まで来ています。頂上はもうすぐですよ(^^)。ここまでで、Hookの掛け方、MMFの使い方を学びました。後は、Applicationへメッセージを通知する手段を学べば今回のLowTechはめでたく終了となります。

呼び出し側アプリへの通知 〜 独自メッセージの登録

Appplicationに「フックしたよ〜」と通知するにはメッセージを送ればいいですね。DLL側はHookProc()内でPostMessage()します。これまでで、もうメッセージの宛先を解決する手段も手に入れました。Application側はWndProcをoverrideしておいてフックメッセージだったら処理をする。こんな流れになります。

独自メッセージを登録するには今までだとWM_USER+100等番号を付けてやっていましたが、Win32になってからこれは推奨されていません(「危険です」と書かれているのですから止めときましょう(^^))。代わりにRegisterWindowMessage()と言うAPIを使います。引数にPChar型の文字列を指定し、その文字列をもってWindowsがメッセージを識別、管理します。Application側で識別するのにはRegisterWindowMessage()の戻り値、つまり、今までと同じようにメッセージのID番号で識別します。ま、ユーザメッセージの領域 0xC000〜0xFFFFの中の一つに名前を付けてしまうと考えてもいいかとおもいます。管理はあくまでWindowsに任せます。楽ちん!(^^)「この関数で独自メッセージを登録すれば、そのメッセージ番号が一意で、それまでに登録されたどのメッセージとも衝突しない事が保証(!)されます」とあるので、信用して(^^)、これを使う事にします。

登録するのはいたって簡単で、定数宣言部で

const
  UniqueMessage = 'hogehoge test application';
とやっておいて
RegisterWindowMessage(UniqueMessage);
するだけです。そのまま、
RegisterWindowMessage('hogehoge test application');
とやってもいいのですが、以下のような目的のために文字列定数を宣言しています。

下の例ではDLL側にあるメッセージ登録用文字列を取得する関数をエクスポートして、呼び出し側Applicationではその関数を通じて文字列を取得するようにしています。そして、WndProc()をoverrideしてメッセージを受信できるようにしておきます。まずは、文字列をエクスポートする関数から

function GetHookMsgString:PChar;
begin
  Result := UniqueMessage;
end;

これだけ(^^)(もちろんエクスポートする手続きは踏んで下さい)やっておけばApplication側はこれを呼び出す事で、登録用の文字列を得られます。先ほども書きましたが、文字列リテラルはPChar型の文字列変数にそのまま渡せます。Application側でのメッセージ文字列取得〜メッセージ登録、登録に失敗すると例外を発生させようとした場合、以下のようになります。

var
  HookMessage : string;
  msgHooKing  : integer;

  HookMessage := GetHookMsgString;                          //メッセージ取得
  msgHooking := RegisterWindowMessage(PChar(HookMessage));  //メッセージ登録
  if msgHooking = 0 then begin
    raise Exception.Create('Can''t Regist Message ');
  end;

次に、Application側のWndProc()をOverrideしてフックメッセージを受信、識別出来るようにしておきます。

interface部

Type
  TForm1 = Class(TForm)
  private
    procedure WndProc(var Message: TMessage);override;
    .....
  end;

implementation部
procedure TForm1.WndProc;
begin
  //フックDLLからメッセージがやってきた場合の処理をここに書く
  if Message.Msg = msgHooking then begin
    //処理内容
  end;
  //--------------------------------------------------------

  //デフォルトの処理
  inherited WndProc(Message);
end;

これで受信が出来るようになりました。ようやくfig.2にあるようなメッセージの流れを作る事が出来たのです。キーボードのリマップをするだけならfig.1のような形にしてしまって、メッセージ送受信をしなくてもいいかもしれませんが、往々にして、通知してほしい場合が出てきます。フックで何がやりたいのかをまずは明確にして下さい。

再びフィルタ関数〜何をフックしようか

List.3 フック登録関数
function SetHook(Wnd: HWnd): Boolean;
var
  _hFMObject : THandle;
  p : pointer;
  RS : integer;
begin
  
  //MMF使用準備処理
  GetFMObj(_hFMObject, p);
  
  //フック情報構造体初期化
     とフック関数の登録
  with pHookInfo(p)^ do begin
    //呼び出し側のウィンドウハンドルを保存
    HostWnd := Wnd;
    
    //キーボードフックをインストール
    hndHook := SetWindowsHookEx(
                       WH_KEYBOARD, 
                       @HookProc, 
                       HInstance, 0);
    
    //アプリケーションへ通知
       するメッセージを登録
    MsgID := RegisterWindowMessage(
              strRegKeyHook);
  end;
  
  //MMF使用終了処理
  ReleaseFMObj(_hFMObject, p);
end;
List.4 Filter関数
function HookProc(Code: Integer; 
                   wParam: WPARAM; 
                   lParam: LPARAM): 
                   LRESULT; stdcall;
var
  LP        : LongInt;
begin
  ...
  if Code >=0 then begin
    LP := lParam;
    if (LP shr 31) = 1 then begin
      with pHookInfo(p)^ do
        PostMessage(HostWnd, 
          MsgID, wParam, lParam);
    end;
  end;
  ...
end;
さて、ここまでで、既に雛形はほぼ出来あがっています。ここでもう一度フィルタ関数の話題に移ります。フックの心臓部です。まずは、キーボードメッセージを監視するようなルーチンを考えてみます。簡単なサンプルとして、キーが押されたらそのコード(仮想キーコード)をそのまま通知するようなものを考えてみます。Application側では英数文字だったらメモに仮想キーコードと共に表示するようにしています。
[List.2 | 別ウィンドウで見る[JS]]←NotAvailable

キー操作のフックにはWH_KEYBOARDを指定してフックをインストールします。フックのインストール関数はList.3のような形にになります(抜粋)。

フックをインストールしたプロセスのウィンドウハンドル、フィルタ関数のハンドル、独自メッセージのID番号などはすべて共有変数の中に入れておきます。

次にフィルタ関数本体です。WH_KEYBOARDでは仮想キーコードがwParamに、スキャンコードなどの情報がlParamに入っています。このlParamのビット31にキーの遷移状態が入っていますので、ここを見ればキーが押されたのか、放されたのか分かりますね。こんな感じになります(List.4 抜粋)。

当然の事ながらKeyDown,KeyUpの2回HookProc()が呼ばれます(キー操作のたびに送られる)から、単純にPostMessage()してしまうとApplication側へも2回メッセージが行ってしまいます。上記のようにやっておけば1回しかPostMessage()しません(キーが放された時)。ま、このような場合だと、そのまま垂れ流して(^^)、Application側でlParamの値を見て条件分岐しても全然構わないので、上のようなコードはサンプルのためのサンプルだと言う感は否めませんが...(^^;ゞ。というか、こういったフックアプリの場合、たいていは常駐して、ひたすら監視しつづける事が多く、フックメッセージがやってきたら処理、する事が多いですね。キーマップの入れ替えをするだけの場合みたいにHookProc()内で全て処理しちゃわないで、Application側へ通知する必要がある場合の事です。その場合、Application側はユーザ操作をあまり考えなくてもいいので、多少メッセージ処理が重たくなっても構わないという事になりますから、「垂れ流し→Application側で処理」というような形の方がいいと思います。なるだけHookProc()内の処理が軽くなる設計をするように心がけて下さい。「上流の流れはなるべく止めるな」です(^^)。 続く...

前編終わり

→後編