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

浙公网安备 33010602011771号