锁定鼠标光标到指定屏幕-优化

项目需求:

在系统中使用多屏已成为常态,有时候可能会用到触摸屏和非触摸屏多屏混用处理交互动作,这时候来回切换鼠标会成为比较烦恼的问题。为了解决这个问题,最近通过多种方法对鼠标锁定进行设计。但大多都存在些小瑕疵。比如失去焦点、鼠标快速移动、触屏点击等会偶尔造成光标外移。通过比较各种方法,使用系统钩子处理是最理想的方法,记录一下,以备忘记,作为后期开发参考。
在实际应用中发现鼠标光标被锁定后,无法响应另外一块触摸屏弹窗按钮点击事件,在本次优化中解决鼠标光标事件拦截问题,仅拦截处理鼠标移动导致光标移出事件。

代码

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

namespace WpfCursorLock
{
    /// <summary>
    /// 使用低级别鼠标钩子强制限制光标在指定屏幕内
    /// </summary>
    public static class CursorLockerAdvanced
    {
        #region Win32 API 声明

        [StructLayout(LayoutKind.Sequential)]
        public struct POINT
        {
            public int X;
            public int Y;
        }

        [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 bool Contains(int x, int y)
            {
                return x >= Left && x < Right && y >= Top && y < Bottom;
            }
        }

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

        [DllImport("user32.dll")]
        static extern bool ClipCursor(ref RECT lpRect);

        [DllImport("user32.dll")]
        static extern bool ClipCursor(IntPtr lpRect);

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

        [DllImport("user32.dll", SetLastError = true)]
        static extern bool UnhookWindowsHookEx(IntPtr hhk);

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

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

        [DllImport("user32.dll")]
        static extern bool SetCursorPos(int X, int Y);

        [DllImport("user32.dll")]
        static extern bool GetCursorPos(out POINT lpPoint);

        private const int WH_MOUSE_LL = 14;
        private const int WM_MOUSEMOVE = 0x0200;
        private const int WM_LBUTTONDOWN = 0x0201;
        private const int WM_LBUTTONUP = 0x0202;
        private const int WM_RBUTTONDOWN = 0x0204;
        private const int WM_RBUTTONUP = 0x0205;
        private const int WM_MBUTTONDOWN = 0x0207;
        private const int WM_MBUTTONUP = 0x0208;
        private const int WM_MOUSEWHEEL = 0x020A;
        private const int WM_XBUTTONDOWN = 0x020B;
        private const int WM_XBUTTONUP = 0x020C;

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

        #endregion

        private static LowLevelMouseProc _proc = HookCallback;
        private static IntPtr _hookID = IntPtr.Zero;
        private static RECT _lockRect;
        private static bool _isLocked = false;
        private static Screen _targetScreen;

        /// <summary>
        /// 当前锁定的屏幕(null 表示未锁定)
        /// </summary>
        public static Screen TargetScreen => _isLocked ? _targetScreen : null;

        /// <summary>
        /// 是否正在锁定中
        /// </summary>
        public static bool IsLocked => _isLocked;

        /// <summary>
        /// 启动光标锁定到指定屏幕
        /// </summary>
        public static void StartLock(Screen screen)
        {
            if (screen == null)
                throw new ArgumentNullException(nameof(screen));

            _targetScreen = screen;

            // 使用 X, Y, Width, Height 计算边界
            _lockRect = new RECT(
                screen.Bounds.X,
                screen.Bounds.Y,
                screen.Bounds.X + screen.Bounds.Width,
                screen.Bounds.Y + screen.Bounds.Height
            );

            // 先应用 ClipCursor
            ClipCursor(ref _lockRect);
            _isLocked = true;

            // 安装钩子(如果尚未安装)
            if (_hookID == IntPtr.Zero)
            {
                _hookID = SetHook(_proc);
            }
        }

        /// <summary>
        /// 锁定到指定索引的屏幕
        /// </summary>
        public static void StartLock(int screenIndex)
        {
            if (screenIndex < 0 || screenIndex >= Screen.AllScreens.Length)
                throw new ArgumentOutOfRangeException(nameof(screenIndex));

            StartLock(Screen.AllScreens[screenIndex]);
        }

        /// <summary>
        /// 锁定到主屏幕
        /// </summary>
        public static void StartLockPrimary()
        {
            StartLock(Screen.PrimaryScreen);
        }

        /// <summary>
        /// 停止光标锁定
        /// </summary>
        public static void StopLock()
        {
            _isLocked = false;
            _targetScreen = null;
            ClipCursor(IntPtr.Zero);

            if (_hookID != IntPtr.Zero)
            {
                UnhookWindowsHookEx(_hookID);
                _hookID = IntPtr.Zero;
            }
        }

        /// <summary>
        /// 切换锁定状态
        /// </summary>
        public static void ToggleLock(Screen screen)
        {
            if (_isLocked)
                StopLock();
            else
                StartLock(screen);
        }

        #region 钩子回调

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

        private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (_isLocked && nCode >= 0)
            {
                var hookStruct = Marshal.PtrToStructure<MSLLHOOKSTRUCT>(lParam);
                int x = hookStruct.pt.X;
                int y = hookStruct.pt.Y;

                // 检查是否超出锁定区域
                if (!_lockRect.Contains(x, y))
                {
                    // 将光标限制在边界内
                    int clampedX = Math.Max(_lockRect.Left, Math.Min(x, _lockRect.Right - 1));
                    int clampedY = Math.Max(_lockRect.Top, Math.Min(y, _lockRect.Bottom - 1));

                    SetCursorPos(clampedX, clampedY);

                    // 对于鼠标移动事件,吞掉越界事件
                    // 对于点击事件,修正位置后允许继续传递
                    int msg = wParam.ToInt32();
                    if (msg == WM_MOUSEMOVE)
                    {
                        return (IntPtr)1; // 阻止事件继续传递
                    }
                }

                // 每次鼠标事件后重新应用 ClipCursor(防止被其他应用覆盖)
                if (_isLocked)
                {
                    ClipCursor(ref _lockRect);
                }
            }

            return CallNextHookEx(_hookID, nCode, wParam, lParam);
        }

        #endregion
    }
}

项目中的应用(部分代码)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Drawing;
namespace CursorLockerAdvanced
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        #region  鼠标光标锁屏方法
        [StructLayout(LayoutKind.Sequential)]
        public struct POINT
        {
            public int X;
            public int Y;
        }
        [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 bool Contains(int x, int y)
             {
                return x >= Left && x < Right && y >= Top && y < Bottom;
             }
             public bool Contains(POINT pt)
             {
                 return Contains(pt.X, pt.Y);
             }
             public override string ToString()
             {
                return $"RECT({Left}, {Top}, {Right}, {Bottom})";
             }  
        }
        [StructLayout(LayoutKind.Sequential)]
        private struct MSLLHOOKSTRUCT
        {
            public POINT pt;
            public uint mouseData;
            public uint flags;
            public uint time;
            public IntPtr dwExtraInfo;
        }
        [DllImport("user32.dll")]
        static extern bool ClipCursor(ref RECT lpRect);

        [DllImport("user32.dll")]
        static extern bool ClipCursor(IntPtr lpRect);

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

        [DllImport("user32.dll", SetLastError = true)]
        static extern bool UnhookWindowsHookEx(IntPtr hhk);

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

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

        [DllImport("user32.dll")]
        static extern bool SetCursorPos(int X, int Y);

        [DllImport("user32.dll")]
        static extern bool GetCursorPos(out POINT lpPoint);
        private const int WH_MOUSE_LL = 14;
        private const int WM_MOUSEMOVE = 0x0200;
        private const int WM_LBUTTONDOWN = 0x0201;
        private const int WM_LBUTTONUP = 0x0202;
        private const int WM_RBUTTONDOWN = 0x0204;
        private const int WM_RBUTTONUP = 0x0205;

        private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam);
        private static LowLevelMouseProc _proc = HookCallback;
        private static IntPtr _hookID = IntPtr.Zero;
        private static RECT _lockRect;
        private static bool _isLocked = false;

        /// <summary>
        /// 启动鼠标钩子,强制限制光标在指定屏幕
        /// </summary>
        public static void StartLock(Screen screen)
        {
            
            _lockRect = new RECT
            {
                Left = screen.Bounds.Left,  //这里使用了System.Drawing  中的边界数据获取
                Top = screen.Bounds.Top,
                Right = screen.Bounds.Right,
                Bottom = screen.Bounds.Bottom
            };

            // 先设置 ClipCursor
            ClipCursor(ref _lockRect);
            _isLocked = true;

            // 安装低级别鼠标钩子
            if (_hookID == IntPtr.Zero)
            {
                _hookID = SetHook(_proc);
            }
        }

        /// <summary>
        /// 停止锁定
        /// </summary>
        public static void StopLock()
        {
            _isLocked = false;
            ClipCursor(IntPtr.Zero);
            if (_hookID != IntPtr.Zero)
            {
                UnhookWindowsHookEx(_hookID);
                _hookID = IntPtr.Zero;
            }
        }

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

        private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (_isLocked && nCode >= 0)
            {
                var hookStruct = Marshal.PtrToStructure<MSLLHOOKSTRUCT>(lParam);
                int x = hookStruct.pt.X;
                int y = hookStruct.pt.Y;

               //注释以下方法说明:由于以下方式处理会锁死鼠标,导致其他窗口无法处理鼠标点击事件,只为了锁定光标,这样做牺牲太大,代码做如下优化
                // 检查光标是否超出限制区域
                //bool needClamp = false;

                //if (x < _lockRect.Left) { x = _lockRect.Left; needClamp = true; }
                //if (x > _lockRect.Right - 1) { x = _lockRect.Right - 1; needClamp = true; }
                //if (y < _lockRect.Top) { y = _lockRect.Top; needClamp = true; }
                //if (y > _lockRect.Bottom - 1) { y = _lockRect.Bottom - 1; needClamp = true; }

                // 如果光标超出范围,强制修正位置
                //if (needClamp)
                //{
                //    SetCursorPos(x, y);
                    // 吞掉这个越界的事件,不让它继续传递
                //    return (IntPtr)1;
               // }

            //代码优化说明:锁定光标处理逻辑应仅限于鼠标移动事件,不拦截鼠标其他事件
            // 点击事件(L/R/M button down/up)不拦截,允许穿透到正确屏幕
           // 但点击后如果光标位置被系统修正到屏幕外,下一帧移动会被拉回来
            if(msg==WM_MOUSEMOVE&&!_lockRect.Contains(x,y))
            {
               int clampedX=Math.Max(_lockRect.Left,Math.Min(x,_lockRect.Right-1));
               int clampedY=Math.Max(_lockRect.Top,Math.Min(y,_lockRect.Bottom-1));
               SetCursorPos(clampedX,clampedY);
               return (IntPtr)1;

             }

                // 每次鼠标事件后重新应用 ClipCursor(防止被其他应用覆盖)
                ClipCursor(ref _lockRect);
            }

            return CallNextHookEx(_hookID, nCode, wParam, lParam);
        }


        #endregion

        public MainWindow()
        {
            InitializeComponent();
            Loaded += MainWindow_Loaded;
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            
        }

        private void BtnPrimaryScreen_Click(object sender, RoutedEventArgs e)
        {
            StartLock(Screen.PrimaryScreen);
        }

        private void BtnSecondScreen_Click(object sender, RoutedEventArgs e)
        {
            StartLock(Screen.AllScreens[1]);
        }

        private void BtnThirdScreen_Click(object sender, RoutedEventArgs e)
        {
            StartLock(Screen.AllScreens[2]);
        }

        private void BtnStopLockScreen_Click(object sender, RoutedEventArgs e)
        {
            StopLock();
        }
    }
}

posted @ 2026-07-23 06:38  丹心石  阅读(6)  评论(0)    收藏  举报