グローバルフックを利用して常時 マウスポインタのスクリーン座標を取得する - C#

グローバルフックを利用して常時、マウスポインタのスクリーン座標を取得するコードを紹介します。

概要

こちらの記事ではCuursor.Positionプロパティを利用してマウスのスクリーン座標を取得できましたが、マウスの座標を取得するイベントはフォームのMouseMoveイベントを利用しているため、フォームの外にマウスポインタが移動してしまった場合にはMouseMoveイベントが発生せずマウスポインタの座標は更新されません。マウスポインタの座標を常に取得したい場合は、グローバルフックの機能を利用することで実現できます。
タイマー等で指定したインターバルごとに座標を取得する方法もありますが、インターバル秒ごとの座標取得になってしまうため、あまり良い実装ではないかと思います。グローバルフックであればマウスポインタが移動したタイミングでプロシジャーコールされるため、効率も良い実装になります。

プログラム例

Windows Formアプリケーションを作成します。

UI

下図のUIを作成します。フォームにLabelコントロールを一つ配置します。

コード

下記のコードを記述します。
FormMain.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Diagnostics;

namespace MouseHook
{
  public partial class FormMain : Form
  {
    private IntPtr hHook;

    public FormMain()
    {
      InitializeComponent();
    }

    private void FormMain_Load(object sender, EventArgs e)
    {
      SetHook();
    }

    private int SetHook()
    {
      IntPtr hmodule = WindowsAPI.GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName);

      hHook = WindowsAPI.SetWindowsHookEx((int)WindowsAPI.HookType.WH_MOUSE_LL, (WindowsAPI.HOOKPROC)MyHookProc, hmodule, IntPtr.Zero);
      if (hHook == null) {
        MessageBox.Show("SetWindowsHookEx 失敗", "Error");
        return -1;
      }
      else {
        MessageBox.Show("SetWindowsHookEx 成功", "OK");
        return 0;
      }
    }

    private IntPtr MyHookProc(int nCode, IntPtr wParam, IntPtr lParam)
    {
      if (0 == WindowsAPI.HC_ACTION) {
        WindowsAPI.MSLLHOOKSTRUCT MouseHookStruct = (WindowsAPI.MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(WindowsAPI.MSLLHOOKSTRUCT));
        label1.Text = string.Format("Mouse Position : {0:d}, {1:d}",MouseHookStruct.pt.x, MouseHookStruct.pt.y);
      }

      return WindowsAPI.CallNextHookEx(hHook, nCode, wParam, lParam);

    }

    private void FormMain_FormClosing(object sender, FormClosingEventArgs e)
    {
      WindowsAPI.UnhookWindowsHookEx(hHook);
    }
  }
}
WindowsAPI.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace MouseHook
{
  public static class WindowsAPI
  {
    [DllImport("user32.dll")]
    public static extern IntPtr GetWindowLong(IntPtr hWnd, int nIndex);

    [DllImport("kernel32.dll")]
    public static extern IntPtr GetCurrentThreadId();

    [DllImport("user32.dll")]
    public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);

    [DllImport("user32.dll")]
    public static extern bool SetWindowPos(IntPtr hWnd, int hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);

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

    public const int HC_ACTION = 0;
    public delegate IntPtr HOOKPROC(int nCode, IntPtr wParam, IntPtr lParam);

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

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

    [DllImport("kernel32.dll", EntryPoint = "GetModuleHandleW", SetLastError = true)]
    public static extern IntPtr GetModuleHandle(string moduleName);


 

    [StructLayout(LayoutKind.Sequential)]
    public struct RECT
    {
      public int Left, Top, Right, Bottom;

      public RECT(int left, int top, int right, int bottom)
      {
        Left = left;
        Top = top;
        Right = right;
        Bottom = bottom;
      }

      public RECT(System.Drawing.Rectangle r) : this(r.Left, r.Top, r.Right, r.Bottom) { }

      public int X
      {
        get { return Left; }
        set { Right -= (Left - value); Left = value; }
      }

      public int Y
      {
        get { return Top; }
        set { Bottom -= (Top - value); Top = value; }
      }

      public int Height
      {
        get { return Bottom - Top; }
        set { Bottom = value + Top; }
      }

      public int Width
      {
        get { return Right - Left; }
        set { Right = value + Left; }
      }

      public System.Drawing.Point Location
      {
        get { return new System.Drawing.Point(Left, Top); }
        set { X = value.X; Y = value.Y; }
      }

      public System.Drawing.Size Size
      {
        get { return new System.Drawing.Size(Width, Height); }
        set { Width = value.Width; Height = value.Height; }
      }

      public static implicit operator System.Drawing.Rectangle(RECT r)
      {
        return new System.Drawing.Rectangle(r.Left, r.Top, r.Width, r.Height);
      }

      public static implicit operator RECT(System.Drawing.Rectangle r)
      {
        return new RECT(r);
      }

      public static bool operator ==(RECT r1, RECT r2)
      {
        return r1.Equals(r2);
      }

      public static bool operator !=(RECT r1, RECT r2)
      {
        return !r1.Equals(r2);
      }

      public bool Equals(RECT r)
      {
        return r.Left == Left && r.Top == Top && r.Right == Right && r.Bottom == Bottom;
      }

      public override bool Equals(object obj)
      {
        if (obj is RECT)
          return Equals((RECT)obj);
        else if (obj is System.Drawing.Rectangle)
          return Equals(new RECT((System.Drawing.Rectangle)obj));
        return false;
      }

      public override int GetHashCode()
      {
        return ((System.Drawing.Rectangle)this).GetHashCode();
      }

      public override string ToString()
      {
        return string.Format(System.Globalization.CultureInfo.CurrentCulture, "{{Left={0},Top={1},Right={2},Bottom={3}}}", Left, Top, Right, Bottom);
      }
    }


    public enum WindowLongParam
    {
      GWL_WNDPROC = -4,
      GWLP_HINSTANCE = -6,
      GWLP_HWNDPARENT = -8,
      GWL_ID = -12,
      GWL_STYLE = -16,
      GWL_EXSTYLE = -20,
      GWL_USERDATA = -21,
      DWLP_MSGRESULT = 0,
      DWLP_USER = 8,
      DWLP_DLGPROC = 4
    }

    [Flags()]
    public enum SetWindowPosFlags : uint
    {
      SWP_ASYNCWINDOWPOS = 0x4000,
      SWP_DEFERERASE = 0x2000,
      SWP_DRAWFRAME = 0x0020,
      SWP_FRAMECHANGED = 0x0020,
      SWP_HIDEWINDOW = 0x0080,
      SWP_NOACTIVATE = 0x0010,
      SWP_NOCOPYBITS = 0x0100,
      SWP_NOMOVE = 0x0002,
      SWP_NOOWNERZORDER = 0x0200,
      SWP_NOREDRAW = 0x0008,
      SWP_NOREPOSITION = 0x0200,
      SWP_NOSENDCHANGING = 0x0400,
      SWP_NOSIZE = 0x0001,
      SWP_NOZORDER = 0x0004,
      SWP_SHOWWINDOW = 0x0040,
    }
    
    public enum HCBT : int
    {
      HCBT_MOVESIZE = 0,
      HCBT_MINMAX = 1,
      HCBT_QS = 2,
      HCBT_CREATEWND = 3,
      HCBT_DESTROYWND = 4,
      HCBT_ACTIVATE = 5,
      HCBT_CLICKSKIPPED = 6,
      HCBT_KEYSKIPPED = 7,
      HCBT_SYSCOMMAND = 8,
      HCBT_SETFOCUS = 9,
    }


    public enum HookType : int
    {
      WH_MSGFILTER = -1,
      WH_JOURNALRECORD = 0,
      WH_JOURNALPLAYBACK = 1,
      WH_KEYBOARD = 2,
      WH_GETMESSAGE = 3,
      WH_CALLWNDPROC = 4,
      WH_CBT = 5,
      WH_SYSMSGFILTER = 6,
      WH_MOUSE = 7,
      WH_HARDWARE = 8,
      WH_DEBUG = 9,
      WH_SHELL = 10,
      WH_FOREGROUNDIDLE = 11,
      WH_CALLWNDPROCRET = 12,
      WH_KEYBOARD_LL = 13,
      WH_MOUSE_LL = 14,
    }

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

    [StructLayout(LayoutKind.Sequential)]
    public class MOUSEHOOKSTRUCT
    {
      public POINT pt;
      public int hwnd;
      public int wHitTestCode;
      public int dwExtraInfo;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct MOUSEHOOKSTRUCTEX
    {
      public MOUSEHOOKSTRUCT mouseHookStruct;
      public int MouseData;
    }

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

解説

アプリケーション起動後、フォームのロード時にSetHookメソッドが呼び出され下記のコードが実行されます。
GetModuleHandle() WindowsAPIを呼び出しモジュールのハンドルを取得します。その後SetWindowsHookEx() WindowsAPIを呼び出しフックを設定します。今回はマウスポインタの座標を取得するためマウスの移動時のフックを設定するため、第一引数には、マウスの低レベルイベントのフックを示す WH_MOUSE_LL を与えます。第二引数にはフックにより呼び出されるメソッド、第三引数には先に取得したモジュールのハンドルを与えます。第4引数はnullでよいため、IntPtr.Zeroを与えます。
正常に終了した場合、SetWindowsHookEx() の戻り値がnullでない値が返るため、戻り値のnull判定をし、nullでなければフックの登録に成功したと判定します。SetWindowsHookEx() の戻り値はフックのハンドルとなります。フックのハンドルはフック解除時に必要になるためクラスのメンバ変数に格納しておきます。
private int SetHook()
{
  IntPtr hmodule = WindowsAPI.GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName);

  hHook = WindowsAPI.SetWindowsHookEx((int)WindowsAPI.HookType.WH_MOUSE_LL, (WindowsAPI.HOOKPROC)MyHookProc, hmodule, IntPtr.Zero);
  if (hHook == null) {
    MessageBox.Show("SetWindowsHookEx 失敗", "Error");
    return -1;
  }
  else {
    MessageBox.Show("SetWindowsHookEx 成功", "OK");
    return 0;
  }
}

マウスのイベントが発生しフックにより呼び出されるメソッドが下記のメソッドになります。第一引数のnCodeがHC_ACTIONであれば、アクションによるフック呼び出しと判定して処理を実行します。アクションでないフック呼び出しであった場合は処理はせずに、CallNextHookEx() APIを呼び出し次のフックを処理します。

第一引数のnCodeがHC_ACTIONであった場合は、lParamをキャストしてMSLLHOOKSTRUCT 構造体を取得します。MSLLHOOKSTRUCT 構造体のPOINT型のptメンバにマウスポインタの座標が格納されています。pt.x, pt.y でマウスポインタのx座標、y座標を取得できます。取得した座標値をlabel1に表示しています。

処理完了後、CallNextHookEx() APIを呼び出し次のフックを処理します。
private IntPtr MyHookProc(int nCode, IntPtr wParam, IntPtr lParam)
{
  if (nCode == WindowsAPI.HC_ACTION) {
    WindowsAPI.MSLLHOOKSTRUCT MouseHookStruct 
      = (WindowsAPI.MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(WindowsAPI.MSLLHOOKSTRUCT));
    label1.Text = string.Format("Mouse Position : {0:d}, {1:d}",MouseHookStruct.pt.x, MouseHookStruct.pt.y);
  }
  return WindowsAPI.CallNextHookEx(hHook, nCode, wParam, lParam);
}

フォームが閉じられるタイミングで UnhookWindowsHookEx() を呼び出しフックを解除します。
private void FormMain_FormClosing(object sender, FormClosingEventArgs e)
{
  WindowsAPI.UnhookWindowsHookEx(hHook);
}

実行結果

プロジェクトを実行します。起動直後にフックの登録が実行され、下図のメッセージボックスが表示されます。


ウィンドウが表示され、マウスポインタのスクリーン座標値がLabelに表示されます。マウスポインタがフォーム上になくても、マウスポインタを動かすとフォームのLabelの表示内容が変化し、現在のマウスポインタの座標値が表示されます。



フックを利用してリアルタイムで現在のマウスポインタの位置の座標を取得できました。
著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
掲載日: 2018-07-12
iPentec all rights reserverd.