C#控制台程序实现全局鼠标钩子(无需WinForms)
在C#控制台应用中实现全局鼠标钩子(不依赖WinForms)
你说得对,控制台程序完全没必要依赖WinForms的Application.Run()——毕竟低级全局鼠标钩子(WH_MOUSE_LL)本质上只需要一个消息循环就能正常工作,我们完全可以用WinAPI自己实现这个循环,不用引入WinForms的依赖。
核心思路
WH_MOUSE_LL钩子的回调函数是在调用线程的消息队列里被触发的,所以只要我们在控制台主线程里手动启动一个消息循环,就能让钩子正常接收鼠标事件。我们可以用GetMessage、TranslateMessage和DispatchMessage这三个WinAPI函数来实现消息循环,替代Application.Run()。
另外,为了优雅地退出程序并卸载钩子,我们可以监听控制台的Ctrl+C退出事件,确保钩子被正确释放,避免资源泄漏。
修改后的完整代码
using System; using System.Diagnostics; using System.Runtime.InteropServices; class GlobalMouseHook { private static LowLevelMouseProc _proc = HookCallback; private static IntPtr _hookID = IntPtr.Zero; // 用于控制消息循环的退出 private static bool _isRunning = true; public static void Main() { // 设置钩子 _hookID = SetHook(_proc); Console.WriteLine("全局鼠标钩子已启动,按Ctrl+C退出..."); // 注册Ctrl+C退出事件 Console.CancelKeyPress += (sender, e) => { e.Cancel = true; _isRunning = false; // 发送退出消息终止消息循环 PostQuitMessage(0); }; // 自定义消息循环,替代Application.Run() MSG msg; while (_isRunning && GetMessage(out msg, IntPtr.Zero, 0, 0) > 0) { TranslateMessage(ref msg); DispatchMessage(ref msg); } // 卸载钩子 UnhookWindowsHookEx(_hookID); Console.WriteLine("钩子已卸载,程序退出"); } 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_MOUSEMOVE == (MouseMessages)wParam) { MSLLHOOKSTRUCT hookStruct = (MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT)); Console.WriteLine($"鼠标坐标:X={hookStruct.pt.x}, Y={hookStruct.pt.y}"); } return CallNextHookEx(_hookID, nCode, wParam, lParam); } #region WinAPI定义 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; } [StructLayout(LayoutKind.Sequential)] private struct MSG { public IntPtr hwnd; public uint message; public IntPtr wParam; public IntPtr lParam; public uint time; public POINT pt; } [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); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern bool TranslateMessage(ref MSG lpMsg); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr DispatchMessage(ref MSG lpMsg); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern void PostQuitMessage(int nExitCode); #endregion }
关键改动说明
- 自定义消息循环:用
GetMessage循环处理消息,直到收到退出信号,完全替代Application.Run(),不需要引用WinForms。 - 优雅退出处理:监听
Console.CancelKeyPress事件,在用户按Ctrl+C时设置退出标记并发送PostQuitMessage终止消息循环,同时卸载钩子。 - 移除WinForms依赖:整个代码只依赖
System、System.Diagnostics和System.Runtime.InteropServices,都是控制台程序默认的引用。
运行这个程序后,你就能在控制台实时看到鼠标移动的坐标,按Ctrl+C可以安全退出并卸载钩子。
内容的提问来源于stack exchange,提问作者Nikita
相关产品推荐
相关产品推荐

