Global mouse event handler

64,463

Solution 1

return SetWindowsHookEx(WH_MOUSE_LL, proc, GetModuleHandle(curModule.ModuleName), 0);

This code will fail when you run it on .NET 4 on a Windows version earlier than Windows 8. The CLR no longer simulates unmanaged module handles for managed assemblies. You can't detect this failure in your code because it is missing the required error checking. Both on GetModuleHandle and SetWindowsHookEx. Never skip error checking when you pinvoke, the winapi doesn't throw exceptions. Check if they return IntPtr.Zero and simply throw a Win32Exception when they do.

The fix is simple, SetWindowsHookEx() requires a valid module handle but doesn't actually use it when you set a low-level mouse hook. So any handle will do, you can pass the handle for user32.dll, always loaded in a .NET application. Fix:

IntPtr hook = SetWindowsHookEx(WH_MOUSE_LL, proc, GetModuleHandle("user32"), 0);
if (hook == IntPtr.Zero) 
{
    throw new System.ComponentModel.Win32Exception();
}
return hook;

Solution 2

I just copied your code into a simple windows form and its working as you described it should. How are you using it exactly? Where are you starting and attaching the event?

And for completeness sake this is the code I ended up with - started from a simple C# form template

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

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            MouseHook.Start();
            MouseHook.MouseAction += new EventHandler(Event);
        }

        private void Event(object sender, EventArgs e) { Console.WriteLine("Left mouse click!"); }
    }

    public static class MouseHook
    {
        public static event EventHandler MouseAction = delegate { };

        public static void Start()
        {
            _hookID = SetHook(_proc);


        }
        public static void stop()
        {
            UnhookWindowsHookEx(_hookID);
        }

        private static LowLevelMouseProc _proc = HookCallback;
        private static IntPtr _hookID = IntPtr.Zero;

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

        private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam);

        private static IntPtr HookCallback(
          int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (nCode >= 0 && MouseMessages.WM_LBUTTONDOWN == (MouseMessages)wParam)
            {
                MSLLHOOKSTRUCT hookStruct = (MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT));
                MouseAction(null, new EventArgs());
            }
            return CallNextHookEx(_hookID, nCode, wParam, lParam);
        }

        private const int WH_MOUSE_LL = 14;

        private enum MouseMessages
        {
            WM_LBUTTONDOWN = 0x0201,
            WM_LBUTTONUP = 0x0202,
            WM_MOUSEMOVE = 0x0200,
            WM_MOUSEWHEEL = 0x020A,
            WM_RBUTTONDOWN = 0x0204,
            WM_RBUTTONUP = 0x0205
        }

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

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

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr SetWindowsHookEx(int idHook,
          LowLevelMouseProc lpfn, IntPtr hMod, uint dwThreadId);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool UnhookWindowsHookEx(IntPtr hhk);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,
          IntPtr wParam, IntPtr lParam);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr GetModuleHandle(string lpModuleName);


    }
}

Solution 3

For any future visitor:

I've implemented a thread level mouse hook.

enter image description here

_process = Process.Start(@"c:\windows\notepad.exe");
//_process = Process.Start(@"c:\windows\syswow64\notepad.exe"); // works also with 32-bit

_mouseHook = new MouseHook(_process.Id);
_mouseHook.MessageReceived += MouseHook_MessageReceived;
_mouseHook.Install();

...

private void MouseHook_MessageReceived(object sender, MouseMessageEventArgs e)
{
    Debug.WriteLine($"Mouse Message Code: {e.MessageCode}; X: {e.X}; Y: {e.Y}; Delta: {e.Delta}");
}
Share:
64,463
Jehonathan Thomas
Author by

Jehonathan Thomas

Updated on July 09, 2022

Comments

  • Jehonathan Thomas
    Jehonathan Thomas almost 2 years

    I have the following code which I got from somewhere to capture mouse events. I modified it and made an event handler so that I can subscribe to it. The mouse events are captured correctly. But it never fires the event-handler. Can anybody figure out whats wrong with the code?

    public static class MouseHook
    {
        public static event EventHandler MouseAction = delegate { };
    
        public static void Start() => _hookID = SetHook(_proc);
        public static void stop() => UnhookWindowsHookEx(_hookID);
    
        private static LowLevelMouseProc _proc = HookCallback;
        private static IntPtr _hookID = IntPtr.Zero;
    
        private static IntPtr SetHook(LowLevelMouseProc proc)
        {
            using (Process curProcess = Process.GetCurrentProcess())
            using (ProcessModule curModule = curProcess.MainModule)
            {
                return SetWindowsHookEx(WH_MOUSE_LL, proc,
                  GetModuleHandle(curModule.ModuleName), 0);
            }
        }
    
        private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam);
    
        private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (nCode >= 0 && MouseMessages.WM_LBUTTONDOWN == (MouseMessages)wParam)
            {
               MSLLHOOKSTRUCT hookStruct = (MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT));  
               MouseAction(null,new EventArgs());
            }
            return CallNextHookEx(_hookID, nCode, wParam, lParam);
        }
    
        private const int WH_MOUSE_LL = 14;
    
        private enum MouseMessages
        {
            WM_LBUTTONDOWN = 0x0201,
            WM_LBUTTONUP   = 0x0202,
            WM_MOUSEMOVE   = 0x0200,
            WM_MOUSEWHEEL  = 0x020A,
            WM_RBUTTONDOWN = 0x0204,
            WM_RBUTTONUP   = 0x0205
        }
    
        [StructLayout(LayoutKind.Sequential)]
        private struct POINT
        {
            public int x;
            public int y;
        }
    
        [StructLayout(LayoutKind.Sequential)]
        private struct MSLLHOOKSTRUCT
        {
            public POINT pt;
            public uint mouseData, flags, time;
            public IntPtr dwExtraInfo;
        }
    
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr SetWindowsHookEx(int idHook,
          LowLevelMouseProc lpfn, IntPtr hMod, uint dwThreadId);
    
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool UnhookWindowsHookEx(IntPtr hhk);
    
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,
          IntPtr wParam, IntPtr lParam);
    
        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr GetModuleHandle(string lpModuleName);
    }
    

    I subscribe to it like this.

    MouseHook.Start();  
    MouseHook.MouseAction += new EventHandler(Event);
    

    Function receiving the event.

    private void Event(object sender, EventArgs e) => Console.WriteLine("Left mouse click!"); 
    

    Update: I put together the working code in to a open source nuget package for user action hooks.

    • user1703401
      user1703401 almost 12 years
      This cannot work in a console mode app, your program must pump a message loop. Application.Run() is required.
    • Jehonathan Thomas
      Jehonathan Thomas almost 12 years
      I actually use the above code inside my WPF application. I call the MouseHook class from App.cs' Onstartup method.
    • Jehonathan Thomas
      Jehonathan Thomas over 10 years
      For all feeling that this causes the mouse to drag, run this in a separate elevated process and use separate threads to handle events.
  • Jehonathan Thomas
    Jehonathan Thomas almost 12 years
    Thanks. But I modified the code with the fix. It used to show the co-ordinates when I use Console.WriteLine(hookStruct.pt.x + ", " + hookStruct.pt.y) inside the HookCallback function. Now its not working.
  • user1703401
    user1703401 almost 12 years
    Erm, wait, this started with "it's not working". It actually was working? Can you at least document the version of .NET and Windows you are using? Do you see first-chance exceptions in the Output window?
  • Jehonathan Thomas
    Jehonathan Thomas almost 12 years
    Yes it was giving the mouse co-ordinates when I was working yesterday. Actually I checked if the event is fired using the debug path tracking. Yes it does! But Console.WriteLine("something") does not show any output. I think the console.writeline is not working correctly. Strange
  • user1703401
    user1703401 almost 12 years
    Erm, wait, you are now saying that the event actually gets fired? Sounds like my suggestion fixed your problem. Start another question about Console.WriteLine(), after you first checked that Debug.Print() doesn't work either.
  • Jehonathan Thomas
    Jehonathan Thomas almost 12 years
    Yes! The above suggestion was useful. But I found that the Visual studio settings was not in x86 debug mode. I changed it and now it works! thanks
  • Hakan SONMEZ
    Hakan SONMEZ about 10 years
    If I want to know mouseClicks' coordinates, what should I do?
  • Md. Rashidul Hasan
    Md. Rashidul Hasan over 8 years
    Works for all mouse button events except 5th button. Actually 4th and 5th button return same code.
  • Admin
    Admin about 8 years
    I just did this. public static int x = 0; // goes in Program.cs and MyConsoleApp.Program.x = hookStruct.pt.x; // found in HookCallback
  • JDR
    JDR almost 7 years
    It works, but for me it causes my app to slow down significantly for the first 10 seconds after startup (i.e. the mouse jitters).
  • Venryx
    Venryx almost 7 years
    @Masum You can distinguish between 4th and 5th mouse-keys (for event types 523 [down] and 524 [up]) by using the lParam[10] entry. For the 4th/forward key, the entry = 2; for the 5th/back key, the entry = 1.
  • Bbb
    Bbb over 6 years
    I now this is old, but here is what I had to do since it wasn't clear to me on how to get the mouse coordinates Works with the above code altered as private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam); public static int horizontalPoint; public static int verticalPoint; and add horizontalPoint = hookStruct.pt.x; verticalPoint = hookStruct.pt.y; to the function private static IntPtr HookCallback(