親アプリケーションの作成
それから、親アプリケーションの方を見ていきましょう。新規プロジェクトを作成し、「test2.dpr」の名前で保存しました。こちらは、先ほど作ったものと大して違いがないので、部分的に抜粋して紹介します。
以下は、先ほど作った「mhook.dll」からフックインストール用の関数を静的にインポートする部分です。
const hook_dll = 'mhook.dll'; function HookInstall(Parent:THandle): BOOL; stdcall; external hook_dll; function HookUninstall: BOOL; stdcall; external hook_dll; procedure TfrmTest2.btnInstallClick(Sender: TObject); begin if HookInstall(Self.Handle) = False then ShowMessage('失敗'); end; procedure TfrmTest2.btnUninstallClick(Sender: TObject); begin HookUninstall; end;
ここでは、ボタン[btnInstall]をクリックした時に、DLLのフックインストール関数を呼ぶようにしています。
次に、フック関数から投げられたメッセージを受け取る関数onMouseAction()
です。ここでは、フックされたメッセージや、どの部分をクリックしたという情報をTMemoに表示するだけです。
type // フォームの宣言 TfrmTest2 = class(TForm) Memo1: TMemo; btnInstall: TButton; btnUninstall: TButton; procedure btnInstallClick(Sender: TObject); procedure btnUninstallClick(Sender: TObject); private procedure onMouseAction(var msg: TMessage); message WM_USER_MOUSE_ACTION; end; //...省略... procedure TfrmTest2.onMouseAction(var msg: TMessage); var s, t: string; begin // どのメッセージがフックされたのか? case msg.WParam of WM_NCMOUSEMOVE: s := 'WM_NCMOUSEMOVE'; WM_NCLBUTTONDOWN: s := 'WM_NCLBUTTONDOWN'; WM_NCLBUTTONUP: s := 'WM_NCLBUTTONUP'; WM_MOUSEMOVE: s := 'WM_MOUSEMOVE'; WM_LBUTTONDOWN: s := 'WM_LBUTTONDOWN'; WM_LBUTTONUP: s := 'WM_LBUTTONUP'; else s := 'msg=' + IntToStr(msg.WParam); end; // どこをクリックしたのか? case msg.LParam of HTCLIENT: t := 'HTCLIENT'; HTCAPTION: t := 'HTCAPTION'; HTLEFT: t := 'HTLEFT'; HTRIGHT: t := 'HTRIGHT'; HTTOP: t := 'HTTOP'; HTTOPLEFT: t := 'HTTOPLEFT'; HTTOPRIGHT: t := 'HTTOPRIGHT'; HTBOTTOM: t := 'HTBOTTOM'; HTBOTTOMLEFT: t := 'HTBOTTOMLEFT'; HTBOTTOMRIGHT: t := 'HTBOTTOMRIGHT'; HTBORDER: t := 'HTBORDER'; else t := 'ht=' + IntToStr(msg.LParam); end; // メモに表示 Memo1.Lines.Add(s + #9 + t); end;
プログラムを実行してみると、フックしたメッセージと、どの部分をクリックしたのかが表示されるようになります。また、自身のウィンドウだけでなく、メモ帳など別のウィンドウのイベントをフックできていることを確認してみてください。
ウィンドウ同士をぴったりくっつけるソフト
ここまで分かれば、フックされたメッセージを調べて、親アプリケーションの方でウィンドウのリサイズ処理を行えば、ウィンドウ同士をぴったりくっつけるツールが完成します。
ですが、ここでいくつか問題があります。
まず、先のフック関数では、マウスイベントのすべてを親ウィンドウに投げていたので、あまり効率がよくありません。そこで、必要なイベントだけを選んで投げることにすれば、パフォーマンスが良くなります。
次に、ウィンドウのリサイズの際には、どんなメッセージが送られているのかを調べて、どんな風にリサイズを検出すればよいのかを考えます。
そこで、先ほど作ったマウスイベントを表示するテストアプリケーションを使って、ウィンドウのリサイズの時にどんなメッセージが送られてきているのかを調べてみてください。私が試したところ、次のようなメッセージが得られました。
WM_NCMOUSEMOVE HTBOTTOM WM_NCLBUTTONDOWN HTBOTTOM WM_MOUSEMOVE HTCLIENT WM_MOUSEMOVE HTCLIENT WM_MOUSEMOVE HTCLIENT WM_MOUSEMOVE HTCLIENT WM_LBUTTONUP HTCLIENT WM_NCMOUSEMOVE HTBOTTOM
つまり、ウィンドウのリサイズを行う際には、「WM_NCLBUTTONDOWN
」→「WM_MOUSEMOVE
」→「WM_LBUTTONUP
」という順番でメッセージが送られていることが分かります。また、ヒットテストの結果を見ても、「WM_NCLBUTTONDOWN
」の状態でテストすれば、どこをクリックしたのかが判別できそうです。
そこで、共有変数の構造体にマウスの状態を記憶するメンバ変数「Status」を追加して、マウスの状態によって、ユーザーがウィンドウのリサイズをしたかどうかを判別できるようにしました。
function HookProc(code: Integer; wp:WParam; lp:LParam): LRESULT; stdcall; var pms: PMouseHookStruct; begin // Codeが0以下なら即座に戻らなくてはならない if code < 0 then begin Result := CallNextHookEx(ShareData^.hHook, code, wp, lp); Exit; end; // マウス情報を持つ構造体にキャスト pms := PMouseHookStruct(lp); // 任意のメッセージを拾って処理する case wp of WM_NCLBUTTONDOWN: begin case pms^.wHitTestCode of // どこをクリックしたか確認 HTCAPTION, HTLEFT, HTTOP,HTRIGHT,HTBOTTOM, HTBOTTOMRIGHT, HTBOTTOMLEFT, HTTOPRIGHT, HTTOPLEFT: begin ShareData^.Status := msDown; end; end; end; WM_MOUSEMOVE: begin if ShareData^.Status = msDown then begin ShareData^.Status := msMove; end; end; WM_LBUTTONUP: begin if ShareData^.Status = msMove then begin PostMessage(ShareData^.hParent, WM_USER_MOUSE_ACTION, pms.hwnd, 0); ShareData^.Status := msNone; end; end; end; Result := CallNextHookEx(ShareData^.hHook, code, wp, lp); end;
そして、以下が親ウィンドウ側のソースです。DLL側で投げられたメッセージを受け取り、その情報を元に、ウィンドウをリサイズします。この関数以外は、テストのプログラムと同じです。
procedure TfrmGrid.onMouseAction(var msg: TMessage); var handle: HWND; rect: TRect; size: Integer; begin size := 24; // グリッドのサイズ handle := msg.WParam; // 既存の大きさを取得 GetWindowRect(handle, rect); // グリッド適用後の位置を計算 rect.Left := (rect.Left div size) * size; rect.Top := (rect.Top div size) * size; rect.Right := (rect.Right div size) * size; rect.Bottom := (rect.Bottom div size) * size; // ウィンドウを移動する MoveWindow(handle, rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom- rect.Top, True); end;
ここでは、グリッド適用後のサイズを、切り捨てで計算してますが、もうちょっと工夫すると、ベストな位置にリサイズできると思います。
さらなる改造のヒント
こうして、デスクトップにあるウィンドウをグリッドぴったりに合わせるツールが完成しました。ところが、不満点もたくさんあります。
ここでは、グリッドサイズが24に固定されていますが、設定で変更したいとか、せっかくグリッドに合わせるなら、グリッドの線も画面に描画したいという要望もあるでしょう。
また、まれに移動させたくないウィンドウが移動してしまうことがあります。そこで、ウィンドウのクラス名やウィンドウを指定して、除外リストなるものを用意するとさらに利便性が高まるでしょう。
その他のアイデアとして、冒頭で紹介した「窓グリッド」では、タイトルバーを右クリックした時に、ウィンドウをマック風に折りたたんだり、スクロールボタンクリックで、ウィンドウを透明にするという機能を持たせています。これもすべて、フックDLLのマウスイベント内で処理しています。
注意点として、フックDLLを作る時は用心深くコードを書いてください。すべてのイベントを横取りすることになるので、マウス操作などができなくなり、OSを再起動する事態に陥ってしまいます。
デスクトップユーティリティの作成は、Windowsを裏から操っているような気分を味わうことができ、公開するとユーザーからの反応もそこそこありますので、作り甲斐がある分野のソフトだと思います。
普段、デスクトップの操作感に不満がある方は、ぜひ、本稿を参考にして便利なユーティリティを作成してみてください。