【C#.NET解説】IntPtr構造体を徹底解説 第2回「Win32 APIを使ったグローバルフック」

2024年11月28日木曜日

API プロセス 基本

t f B! P L
名前空間 System.Runtime.InteropServices

自己紹介

こんにちは、私は原初の地母神、2度目の登場です。今日は、C#.NETでIntPtrを使い、WinAPIによるグローバルフックの実装方法をお伝えします。マウスやキーボードのイベントをグローバルにキャプチャするのに役立つ技術を、一緒に学びましょう。

グローバルフックについての説明と注意点

グローバルフックを設定すると、システム全体のイベントを監視できますが、パフォーマンスへの影響や不正使用のリスクも伴います。以下の点を覚えておいてください:

  • フックを設定するには、アンマネージコードを扱う必要があります。
  • 誤った設定やハンドリングの失敗は、システムの応答性を低下させる可能性があります。
  • フックを設定したら、必ず解除することを忘れないでください。

キーボードフックの方法

まずは、グローバルフックを設定してキーボードのイベントを捕捉する方法を見ていきましょう。
Form1にtextBox1とbuttonStart、buttonStopがある前提です。

C#

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    // Windows APIから必要な関数のインポート
    [DllImport("user32.dll")]
    public static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);

    [DllImport("user32.dll")]
    public static extern bool UnhookWindowsHookEx(IntPtr hhk);

    [DllImport("user32.dll")]
    public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

    [DllImport("kernel32.dll")]
    public static extern IntPtr GetModuleHandle(string lpModuleName);

    private const int WH_KEYBOARD_LL = 13;
    private const int WM_KEYDOWN = 0x0100;
    private LowLevelKeyboardProc _proc = HookCallback;
    private static IntPtr _hookID = IntPtr.Zero;

    // デリゲートのアクセシビリティ(publicが必要)
    public delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);

    // フックを開始するメソッド
    private void buttonStart_Click(object sender, EventArgs e)
    {
        _hookID = SetHook(_proc);
    }

    // フックを停止するメソッド
    private void buttonStop_Click(object sender, EventArgs e)
    {
        UnhookWindowsHookEx(_hookID);
    }

    private static IntPtr SetHook(LowLevelKeyboardProc proc)
    {
        using (Process curProcess = Process.GetCurrentProcess())
        using (ProcessModule curModule = curProcess.MainModule)
        {
            return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0);
        }
    }

    private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
        {
            int vkCode = Marshal.ReadInt32(lParam);
            // キー入力をテキストボックスに表示
            // この部分をフォームのインスタンスにアクセスできるように修正してください
            Form1 form = (Form1)Application.OpenForms["Form1"];
            form.textBox1.AppendText(((Keys)vkCode).ToString() + " ");
        }
        return CallNextHookEx(_hookID, nCode, wParam, lParam);
    }
}

このコードはキーボードフックの基本的な実装です。textBox1に何のキーを押したか表示します。

キーボードの特定キーを無効化する方法

次に、特定のキー(例えばEscキー)を無効化する方法を解説します。

C#

    private static IntPtr HookKeyboardCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
        {
            int vkCode = Marshal.ReadInt32(lParam);
            if (vkCode == 27) // Escキー
            {
                Console.WriteLine("Escキーが無効化されました。");
                return (IntPtr)1; // 処理をキャンセル
            }
            Form1 form = (Form1)Application.OpenForms["Form1"];
            form.textBox1.AppendText(((Keys)vkCode).ToString() + " ");
        }
        return CallNextHookEx(_keyboardHookID, nCode, wParam, lParam);
    }

この例では、先ほど書いたHookKeyboardCallbackに追加で、Escキーが押された場合にフック内で処理をキャンセルし、無効化しています。

マウスとキーボードの両方をフックする方法

それでは最後にグローバルフックを設定してキーボードのイベントを捕捉する方法を見ていきましょう。
Form1にtextBox1にキー、textBox2にマウスのどのボタンをクリックして座標がどこかを書いています。

C#

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    // Windows APIから必要な関数のインポート
    [DllImport("user32.dll")]
    public static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);

    [DllImport("user32.dll")]
    public static extern IntPtr SetWindowsHookEx(int idHook, LowLevelMouseProc lpfn, IntPtr hMod, uint dwThreadId);

    [DllImport("user32.dll")]
    public static extern bool UnhookWindowsHookEx(IntPtr hhk);

    [DllImport("user32.dll")]
    public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

    [DllImport("kernel32.dll")]
    public static extern IntPtr GetModuleHandle(string lpModuleName);

    private const int WH_KEYBOARD_LL = 13;
    private const int WH_MOUSE_LL = 14;
    private const int WM_KEYDOWN = 0x0100;
    private const int WM_LBUTTONDOWN = 0x0201;
    private const int WM_RBUTTONDOWN = 0x0204;
    
    private LowLevelKeyboardProc _keyboardProc = HookKeyboardCallback;
    private LowLevelMouseProc _mouseProc = HookMouseCallback;
    private static IntPtr _keyboardHookID = IntPtr.Zero;
    private static IntPtr _mouseHookID = IntPtr.Zero;

    public delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
    public delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam);

    // フックを開始するメソッド
    private void buttonStart_Click(object sender, EventArgs e)
    {
        _keyboardHookID = SetKeyboardHook(_keyboardProc);
        _mouseHookID = SetMouseHook(_mouseProc);
    }

    // フックを停止するメソッド
    private void buttonStop_Click(object sender, EventArgs e)
    {
        UnhookWindowsHookEx(_keyboardHookID);
        UnhookWindowsHookEx(_mouseHookID);
    }

    private static IntPtr SetKeyboardHook(LowLevelKeyboardProc proc)
    {
        using (Process curProcess = Process.GetCurrentProcess())
        using (ProcessModule curModule = curProcess.MainModule)
        {
            return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0);
        }
    }

    private static IntPtr SetMouseHook(LowLevelMouseProc proc)
    {
        using (Process curProcess = Process.GetCurrentProcess())
        using (ProcessModule curModule = curProcess.MainModule)
        {
            return SetWindowsHookEx(WH_MOUSE_LL, proc, GetModuleHandle(curModule.ModuleName), 0);
        }
    }

    private static IntPtr HookKeyboardCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
        {
            int vkCode = Marshal.ReadInt32(lParam);
            Form1 form = (Form1)Application.OpenForms["Form1"];
            form.textBox1.AppendText(((Keys)vkCode).ToString() + " ");
        }
        return CallNextHookEx(_keyboardHookID, nCode, wParam, lParam);
    }

    private static IntPtr HookMouseCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0)
        {
            MSLLHOOKSTRUCT hookStruct = (MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT));
            Form1 form = (Form1)Application.OpenForms["Form1"];
            string button = "";

            if (wParam == (IntPtr)WM_LBUTTONDOWN)
            {
                button = "Left Click";
            }
            else if (wParam == (IntPtr)WM_RBUTTONDOWN)
            {
                button = "Right Click";
            }

            if (!string.IsNullOrEmpty(button))
            {
                form.textBox2.AppendText($"{button} at ({hookStruct.pt.x}, {hookStruct.pt.y})\n");
            }
        }
        return CallNextHookEx(_mouseHookID, nCode, wParam, lParam);
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct POINT
    {
        public int x;
        public int y;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct MSLLHOOKSTRUCT
    {
        public POINT pt;
        public int mouseData;
        public int flags;
        public int time;
        public IntPtr dwExtraInfo;
    }
}

さあ、これでグローバルフックを使った入力制御の基本がわかりましたね。大地の安定を守るように、システムの安定を保つフックの使い方を習得してください。

このブログを検索

QooQ