1. 程式人生 > >【C#】解決MouseHook捕獲滑鼠動作,在有些電腦上SetWindowsHookEx失敗返回0的問題

【C#】解決MouseHook捕獲滑鼠動作,在有些電腦上SetWindowsHookEx失敗返回0的問題

最近在debug滑鼠位置捕獲的功能時發現在其中的一臺開發電腦上,SetWindowsHookEx一直返回0,導致Hook設定失敗,有時候調成Release模式又是正常的。程式碼如下:

hMouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProcedure,Marshal.GetHINSTANCE(System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0]), 0);

為什麼一直返回0呢?微軟也沒有告訴我們具體原因,只讓我們查詢System Error Code

Type:
Type: HHOOK
If the function succeeds, the return value is the handle to the hook procedure.
If the function fails, the return value is NULL. To get extended error information, call GetLastError.

通過文件裡寫的call GetLastError方法可以獲取到error code。我這裡的error code是126,查詢對應文件發現詳細錯誤是:

ERROR_MOD_NOT_FOUND
126 (0x7E)
The specified module could not be found.

即模組錯誤。

SetWindowHookEx中唯一跟模組有關的引數只有Marshal.GetHINSTANCE(System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0])了。
在debug過程中,發現GetModules()[0]都是不為null的而且GetHINSTANCE也能獲取到正確的值,實在不知道哪裡的問題。不過經過不懈的搜尋,發現StackOverflow裡的大牛解決過這個問題(連結參考底部)。大概意思就是說在.Net4.0和Win8之前的版本中,CLR不再模擬託管程式集中的非託管控制代碼(我是.net4.0+win10不知為何也遇到了這個問題(lll¬ω¬))。建議我們用user32的控制代碼,而這個控制代碼會一直被.net載入。

所以 程式碼改動下就好了:

hMouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProcedure,GetModuleHandle("user32"), 0);

完整程式碼參考:

class MouseHook
    {
        private const int WM_MOUSEMOVE = 0x200;
        private const int WM_LBUTTONDOWN = 0x201;
        private const int WM_RBUTTONDOWN = 0x204;
        private
const int WM_MBUTTONDOWN = 0x207; private const int WM_LBUTTONUP = 0x202; private const int WM_RBUTTONUP = 0x205; private const int WM_MBUTTONUP = 0x208; private const int WM_LBUTTONDBLCLK = 0x203; private const int WM_RBUTTONDBLCLK = 0x206; private const int WM_MBUTTONDBLCLK = 0x209; public event MouseEventHandler OnMouseActivity; static int hMouseHook = 0; public const int WH_MOUSE_LL = 14;//low level mouse event public const int WH_MOUSE = 7;//normal level mouse event HookProc MouseHookProcedure; Log _log = new Log("MouseHook", true, Log4netWrapper.Default); [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; } [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] public static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId); [DllImport("kernel32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] public static extern int GetLastError(); [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr GetModuleHandle(string lpModuleName); [DllImport("kernel32.dll")] private static extern int GetCurrentThreadId();//獲取在系統中的執行緒ID [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] public static extern bool UnhookWindowsHookEx(int idHook); [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] public static extern int CallNextHookEx(int idHook, int nCode, Int32 wParam, IntPtr lParam); public delegate int HookProc(int nCode, Int32 wParam, IntPtr lParam); public MouseHook() { } ~MouseHook() { Stop(); } public void Start() { if (hMouseHook == 0) { MouseHookProcedure = new HookProc(MouseHookProc); hMouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProcedure, GetModuleHandle("user32"), 0);//第一個引數是WH_MOUSE_LL,表示捕獲所有執行緒的滑鼠訊息,同時最後一個引數必須是0 //hMouseHook = SetWindowsHookEx(WH_MOUSE, MouseHookProcedure, GetModuleHandle("user32"), GetCurrentThreadId());//只捕獲當前應用程式(當前執行緒)的滑鼠訊息,最後一個引數是當前執行緒id,使用GetCurrentThreadId()獲得,一定不要使用託管執行緒id(Thread.CurrentThread.ManagedThreadId)。 if (hMouseHook == 0) { int errorCode = GetLastError(); _log.E("SetWindowsHookEx failed.error code:" + errorCode); Stop(); } } } public void Stop() { bool retMouse = true; if (hMouseHook != 0) { retMouse = UnhookWindowsHookEx(hMouseHook); hMouseHook = 0; } if (!(retMouse)) { _log.E("UnhookWindowsHookEx failed."); } } private int MouseHookProc(int nCode, Int32 wParam, IntPtr lParam) { //只處理滑鼠左鍵按下的情況 if ((wParam == WM_LBUTTONDOWN) && (nCode >= 0) && (OnMouseActivity != null)) { MouseButtons button = MouseButtons.None; int clickCount = 0; switch (wParam) { case WM_LBUTTONDOWN: button = MouseButtons.Left; clickCount = 1; break; case WM_LBUTTONUP: button = MouseButtons.Left; clickCount = 1; break; case WM_LBUTTONDBLCLK: button = MouseButtons.Left; clickCount = 2; break; case WM_RBUTTONDOWN: button = MouseButtons.Right; clickCount = 1; break; case WM_RBUTTONUP: button = MouseButtons.Right; clickCount = 1; break; case WM_RBUTTONDBLCLK: button = MouseButtons.Right; clickCount = 2; break; } MouseHookStruct MyMouseHookStruct = (MouseHookStruct)Marshal.PtrToStructure(lParam, typeof(MouseHookStruct)); MouseEventArgs e = new MouseEventArgs(button, clickCount, MyMouseHookStruct.pt.x, MyMouseHookStruct.pt.y, 0); OnMouseActivity(this, e); } return CallNextHookEx(hMouseHook, nCode, wParam, lParam); } }

使用方法:

MouseHook hook = new MouseHook();
hook.OnMouseActivity += Hook_OnMouseActivity;
hook.Start();

 private void Hook_OnMouseActivity(object sender, System.Windows.Forms.MouseEventArgs e)
{
  //e.X  e.Y   e.Button == System.Windows.Forms.MouseButtons.Left
}

當程式關閉或者使用結束時一定要呼叫,hook.Stop()解除安裝掉鉤子,不然可能會出現藍屏、宕機之類的系統問題。

2017-12-19更新
最後在使用過程中發現,在click操作中偶爾會出現滑鼠指標卡頓的情況,google了一下大概是low level的鉤子是否響應取決於你主執行緒是否響應,在click過程中,我的主執行緒確實會卡一下,所以就滑鼠指標就會有點跳。解決方法就是新起一個執行緒安裝鉤子:

ThreadPool.QueueUserWorkItem(SetHK);
//...
private void SetHK(object state)
        {
            hook = new MouseHook();
            hook.OnMouseActivity += Hook_OnMouseActivity;
            if (StringConstant.Build)
            {
                hook.Start(Thread.CurrentThread.ManagedThreadId);
                tagMSG Msgs;
                while (GetMessage(out Msgs, IntPtr.Zero, 0, 0) > 0)
                {
                    TranslateMessage(ref Msgs);
                    DispatchMessage(ref Msgs);
                }
            }
        }

其中裡面的tagMSG與Translatemessage對應:

#region Hook
        [DllImport("user32", EntryPoint = "GetMessage")]
        public static extern int GetMessage(out tagMSG lpMsg, IntPtr hwnd, int wMsgFilterMin, int wMsgFilterMax
        );
        [DllImport("user32", EntryPoint = "DispatchMessage")]
        public static extern int DispatchMessage(ref tagMSG lpMsg);
        [DllImport("user32", EntryPoint = "TranslateMessage")]
        public static extern int TranslateMessage(ref tagMSG lpMsg);
        [StructLayout(LayoutKind.Sequential)]
        public class POINT
        {
            public int x;
            public int y;
        }
        public struct tagMSG
        {
            public int hwnd;
            public uint message;
            public int wParam;
            public long lParam;
            public uint time;
            public int pt;
        }
        MouseHook hook;
        #endregion

2018-01-19更新
如果在Winform或者WPF程式中使用“執行緒鉤子”,因為當前操作可能不會在安裝的那個執行緒上,所以會引起偶爾失效的問題。在winform中建議使用Application.AddMessageFilter(),在wpf中使用ComponentDispatcher.ThreadFilterMessage

參考連結