惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Hugging Face - Blog
Hugging Face - Blog
F
Fortinet All Blogs
G
Google Developers Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
V2EX
Y
Y Combinator Blog
博客园_首页
Martin Fowler
Martin Fowler
博客园 - 司徒正美
MyScale Blog
MyScale Blog
宝玉的分享
宝玉的分享
B
Blog
有赞技术团队
有赞技术团队
A
About on SuperTechFans
量子位
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
酷 壳 – CoolShell
酷 壳 – CoolShell
Apple Machine Learning Research
Apple Machine Learning Research
M
MIT News - Artificial intelligence
阮一峰的网络日志
阮一峰的网络日志
Jina AI
Jina AI

博客园 - 丹心石

数据字节位改变 WPF 访问WebAPI Avalonia 避坑经验总结 Avalonia 窗口与配置 Avalonia 程序打包成Linux平台的运行程序 数据处理与转换 信号与槽 Qt 串口通信 Avalonia 设计时DataContext数据山下文和WPF设计时DataContext数据上下文 Avalonia Ursa 使用 Avalonia-消息对话框 Avalonia-数据绑定 Avalonia-样式 阿里图标库iconfont 在Avalonia 中的使用 Avalonia MVVM Avlonia 自定义控件 Sqlite EF For ConsoleCore Sqlite EF CodeFirst For WPF win10 环境变量不可编辑 WCF-双工通讯 MahMetro 框架学习 由于缺少调试目标“xxx”Visual Studio 无法开始调试。或者响应地设置outputpath和AssemblyName 属性 WPF 应用程序本地化-资源文件方式 FastReport 行高设置实现行高随内容自动变化
锁定鼠标光标到指定屏幕
丹心石 · 2026-07-23 · via 博客园 - 丹心石

项目需求:

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

代码

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;
        }
        [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;
                }

                // 每次鼠标事件后重新应用 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();
        }
    }
}