Cursor.Position 获取屏幕坐标最直接,但需注意线程安全、DPI缩放、多屏偏移及跨进程场景下的P/Invoke正确用法。
用 Cursor.Position 拿屏幕坐标最直接
windows forms 和 wpf 都能用,本质是调用 user32.dll 的 getcursorpos。返回的是相对于屏幕左上角的 point,单位是像素,x/y 值可能为负(多屏时主屏左侧或上方有副屏)。
常见错误:在非 UI 线程里直接读取——会抛 InvalidOperationException,因为 Cursor.Position 依赖 STA 线程的 COM 上下文。
- WinForms 中可放心在
Timer.Tick或按钮事件里用 - WPF 中推荐用
Dispatcher.Invoke包一层,尤其从后台线程访问时 - 如果只要窗口内坐标,别忘了用
this.PointToClient(Cursor.Position)转换
WPF 里监听鼠标移动得靠 MouseMove 事件
和 WinForms 不同,WPF 没有全局“鼠标位置变更”事件。想实时追踪,必须绑定到具体元素(比如 Window 或 Grid),且该元素得能接收输入(IsHitTestVisible=true,不被透明遮罩挡住)。
容易踩的坑:把事件绑在 Canvas 上却没设 Background="Transparent",结果鼠标移进去就收不到事件——因为默认 Background=null 表示“不参与命中测试”。
-
e.GetPosition(this)返回相对于当前控件左上角的坐标 - 若需绝对屏幕坐标,仍得结合
Cursor.Position - 高频移动下(如拖拽),避免在事件里做重操作,否则 UI 线程卡顿明显
跨进程/无窗口场景要用 GetCursorPos P/Invoke
控制台程序、服务、或隐藏窗体后还想读鼠标位置,Cursor.Position 会失效(抛异常或返回 (0,0))。这时必须手写 P/Invoke 调用系统 API。
关键点在于结构体声明和 DLL 导入方式——错一个字段偏移或调用约定,就可能读出乱值或崩溃。
- 必须用
[DllImport("user32.dll", SetLastError = true)],并指定CallingConvention = CallingConvention.StdCall -
POINT结构体字段顺序不能颠倒,且需[StructLayout(LayoutKind.Sequential)] - 调用前检查
GetLastError()是否为 0,失败时返回值不可信
[DllImport("user32.dll")]
public static extern bool GetCursorPos(out POINT lpPoint);
[StructLayout(LayoutKind.Sequential)]
public struct POINT { public int X; public int Y; }
高 DPI 屏幕下坐标可能“跳变”
Windows 10/11 启用缩放(比如 125%、150%)后,Cursor.Position 返回的仍是物理像素坐标,但 UI 元素布局按逻辑像素渲染。如果你拿这个坐标去比对控件边界,或者做截图定位,就会发现偏差。
根本原因不是 .NET 的 bug,而是 Windows 把“DPI 感知模式”交给了应用自己处理。.NET 5+ 默认是 PerMonitorV2,但旧项目或未显式声明的,可能还在 Unaware 模式。
- 查当前 DPI 缩放:用
Graphics.FromHwnd(IntPtr.Zero).DpiX(注意句柄传 0 是安全的) - 转换逻辑坐标 → 物理坐标:乘以缩放比例;反之除以
- WinForms 可在
app.manifest里加highDpiAware配置;WPF 则需在App.xaml.cs构造函数里调用SetProcessDpiAwarenessContext


















