Hookクラス

とりあえず標準コンポーネントのTimerっぽく

  1. Newでインスタンス作成
  2. +=演算子でユーザーハンドラに紐付け
  3. Start()でフック開始
  4. Stop()でフック終了

というクラスを作ることにした。・・・マウスフックとキーボードフックは微妙に処理も似ているので、似ている部分は基底クラスとしよう。あとは派生してマウスフッククラスかキーボードフッククラスにすればいいかな。

・・・・

数時間考えたり←のブックマークからリンクしている先人の素晴らしいコードを研究したり、あるいはMSDNでWin32Apiとか調べてこういう形におさまった。

using System;
using System.Windows.Forms;             // KeyEventHandler, Keys, ...
using System.Runtime.InteropServices;   // Marshal
using System.ComponentModel;            // Win32Exception

namespace garu.Util
{
    public abstract class HookBase : IDisposable
    {
        private IntPtr hook;
        private GCHandle hookSave;

        protected HookBase(Win32.WH hookType)
        {
            Win32.HookHandler hookDelegate = new Win32.HookHandler(OnHook);
            hookSave = GCHandle.Alloc(hookDelegate);
            hook = Win32.SetWindowsHookEx(hookType, hookDelegate);
        }

        protected virtual int OnHook(int code, Win32.WM message, IntPtr state)
        {
            return Win32.CallNextHookEx(hook, code, message, state);
        }

        public virtual void Dispose()
        {
            if (hookSave.IsAllocated)
            {
                Win32.UnhookWindowsHookEx(hook);
                hook = IntPtr.Zero;
                hookSave.Free();
            }
        }

        ~HookBase()
        {
            Dispose();
        }
    }

    public class KeyboardHook : HookBase
    {
        public event KeyEventHandler KeyDown;

        public KeyboardHook() : base(Win32.WH.KEYBOARD_LL) { }

        protected override int OnHook(int code, Win32.WM message, IntPtr state)
        {
            if (code >= 0 && KeyDown != null &&
                (message == Win32.WM.KEYDOWN ||
                message == Win32.WM.SYSKEYDOWN))
            {
                Win32.KeyboardHookStruct khs;
                khs = (Win32.KeyboardHookStruct)Marshal.PtrToStructure(
                        state, typeof(Win32.KeyboardHookStruct));
                Keys ky = (Keys)khs.vkCode;
                if (Win32.GetAsyncKeyState(Win32.VK_SHIFT) != 0)
                    ky |= Keys.Shift;
                if (Win32.GetAsyncKeyState(Win32.VK_CONTROL) != 0)
                    ky |= Keys.Control;
                if (Win32.GetAsyncKeyState(Win32.VK_MENU) != 0)
                    ky |= Keys.Alt;
                KeyEventArgs e = new KeyEventArgs(ky);
                KeyDown(this, e);
                if (e.Handled) return 1;
            }
            return base.OnHook(code, message, state);
        }
    }

    public class MouseHook : HookBase
    {
        public event MouseEventHandler MouseMove;

        public MouseHook() : base(Win32.WH.MOUSE_LL) { }

        protected override int OnHook(int code, Win32.WM message, IntPtr state)
        {
            if (code >= 0 && MouseMove != null &&
                message == Win32.WM.MOUSEMOVE)
            {
                Win32.MouseHookStruct mhs;
                mhs = (Win32.MouseHookStruct)Marshal.PtrToStructure(
                    state, typeof(Win32.MouseHookStruct));
                MouseEventArgs e = new MouseEventArgs(
                    MouseButtons.None, 0, mhs.pt.X, mhs.pt.Y, 0);
                MouseMove(this, e);
            }
            return base.OnHook(code, message, state);
        }
    }
}

結局Start()、Stop()は辞めた。インスタンス化したら常駐して破棄したら常駐解除で別にいいだろう。マウスはクリックは今回は必要なさそうなのでMoveだけ実装しといた。とりあえず一本アプリを作りたいので、追々はこのあたりはちゃんとClickやDblClickも実装してしっかりとしたクラスにしたいと思う。
上のコードは実はこれだけではコンパイルできない。実はWin32Apiだけさらにまとめて別クラスを用意しようとしているのだ。Win32というスタティックなクラスでDllImport関係をまとめて定義しようとしている。まだ中途半端なので次回アップします。

一つとても大事な注意点があります。上のコードのデリゲードをセーブしているところです。Frameworkはインスタンスの参照をアンマネージドに渡した場合には参照カウントをアップしないのです。まあアンマネージド=管理されていない、訳ですから当然なのですが、Hookしたハンドラーが、まだ常駐解除していないのに勝手に開放されGCされてしまうのです。↓が悪い例です。

Win32.HookHandler hookDelegate = new Win32.HookHandler(OnHook);
hook = Win32.SetWindowsHookEx(hookType, hookDelegate);

このような書き方をしますとhookDelegateはマネージ下に置かれませんので、コールバックハンドラーがなくなってしまい「不正なアドレスを参照しました」という恐ろしいダイアログを見ることにになります。マネージドとアンマネージドとの相互連携はなかなか骨が折れます。