2013年2月13日水曜日

IME のメッセージを Windows Hook で取得する


Delphi-ML で、IME の変換スタートと終了を知りたい、という投稿がありました。
全ての Edit をサブクラス化して実装しようとされていたのですが、それでは非常に大変だと思い、Windows Hook による方法を投稿しました。

それが、以下のコードです。

unit uIMEStartEnd;
 
interface
 
uses
Winapi.Windows;
 
type
// IME の開始と終了を知らせるイベント
TIMEStartEndNotifyEvent =
procedure(const iWnd: HWND; const iStart: Boolean) of object;
 
// イベントを受け取るイベントリスナを設定・削除する
procedure AddIMEEventListener(const iEvent: TIMEStartEndNotifyEvent);
procedure RemoveIMEEventListener(const iEvent: TIMEStartEndNotifyEvent);
 
implementation
 
uses
Winapi.Messages, Vcl.Controls, System.Generics.Collections;
 
var
// WindowsHook のハンドル
GHookHandle: HHOOK;
// イベントリスナのリスト
GHandlers: TList<TIMEStartEndNotifyEvent>;
// イベントリスナを追加
procedure AddIMEEventListener(const iEvent: TIMEStartEndNotifyEvent);
begin
if (GHandlers.IndexOf(iEvent) < 0) then
GHandlers.Add(iEvent);
end;
 
// イベントリスナを削除
procedure RemoveIMEEventListener(const iEvent: TIMEStartEndNotifyEvent);
begin
if (GHandlers.IndexOf(iEvent) > -1) then
GHandlers.Remove(iEvent);
end;
 
// イベントリスナを呼び出す
// iWnd IME メッセージを受け取ったウィンドウ
// iStart 開始の場合は True
procedure CallEventHandlers(const iWnd: HWND; const iStart: Boolean);
var
Handler: TIMEStartEndNotifyEvent;
begin
for Handler in GHandlers do
Handler(iWnd, iStart);
end;
 
// Hook のメイン関数
function CallWndProc(
iNCode: Integer;
iWParam: WPARAM;
iLParam: LPARAM): LRESULT; stdcall;
begin
// 先に、次のフックチェインを呼び出してしまう
Result := CallNextHookEx(GHookHandle, iNCode, iWParam, iLParam);
 
// iNCode が 0 以下ならフックは作業してはならない
if (iNCode < 0) then
Exit;
 
// lParam は CWPStruct 形式で、メッセージが入っている
with PCWPStruct(iLParam)^ do begin
case message of
WM_IME_STARTCOMPOSITION: begin
// IME 変換開始
CallEventHandlers(hwnd, True);
end;
 
WM_IME_ENDCOMPOSITION: begin
// IME 変換終了
CallEventHandlers(hwnd, False);
end;
end;
end;
end;
 
initialization
begin
// イベントハンドラを管理するリストを作成
GHandlers := TList<TIMEStartEndNotifyEvent>.Create;
 
// WindowsHook
GHookHandle :=
SetWindowsHookEx(WH_CALLWNDPROC, CallWndProc, 0, GetCurrentThreadID);
end;
 
finalization
begin
// Hook を解放
UnhookWIndowsHookEx(GHookHandle);
 
// リストを破棄
GHandlers.Free;
end;
 
end.

具体的にはソースコードのコメントを参照してほしいのですが、一点だけ Windows Hook について説明します。
Windows Hook は Windows の作業に割り込む機構です。
例えば、キーボードの入力があった時や、新しいウィンドウが開くときなど本来アプリケーション側からは見えない Windows の作業に割り込むことができます。
Windows Hook を設定するためには SetWindowsHookEx API を使います。
SetWindowsHookEx の説明を見て頂ければわかるように、様々なフックがあります。
今回は、その中で WH_CALLWNDPROC フックを使う事にしました。
このフックは Window Procedure にメッセージが渡されるタイミングで呼び出されます。
そのタイミングとはいつかというと、具体的には SendMessage API が呼ばれた時です。

PostMessage API を使った場合は、WH_GETMESSAGE フックが使えます。

WH_CALLWNDPROC フックを使うのは IME のメッセージは SendMessage で送られるためです。
そして、今回のソースでは SetWindowsHookEx の3番目の引数に 0 を指定しています。
ここに 0 を指定して、次の引数に現在の Thread ID を指定すると、現在のスレッドで処理される SendMessage についてフックされるます。
この Unit の Initialization 節はメインスレッドから呼ばれるので、メインスレッド(GUI の操作・表示をするスレッド)がメッセージを受け取る度に、CallWndProc 関数が呼ばれることになります。
その結果、Edit などで IME の処理が行われると、それを感知してイベントを発行できます。

また、SetWindowsHookEx の3番目の引数に HInstance を指定してフックする DLL を作ると、全プロセスに結びつくフックを作る事ができます。
これによって、他のプログラムのメッセージを見ることもできます。

これをグローバルフック DLL と呼びます。
グローバルフック DLL の作成については、また次回!