1. 缩放比例下 C# 鼠标坐标错位的真实场景你在做 C# 桌面工具时大概率遇到过这种诡异现象Screen.PrimaryScreen.Bounds返回的是 1536×864可微信截图、系统显示设置里明明写着 1920×1080Cursor.Position拿到的坐标去取色颜色跟肉眼看到的对不上偏出去几十甚至上百像素。这不是代码写错了而是 Windows 的 DPI 缩放机制在背后“偷偷”做了换算。核心检索词先摆出来C# 缩放比例下鼠标光标位置与屏幕大小换算本质是逻辑坐标DIP设备无关像素与物理坐标真实像素之间的映射问题。当系统缩放设为 125%、150%、175% 时Windows 会把每个逻辑像素放大对应倍数渲染而System.Windows.Forms里大量 API 默认返回的是逻辑坐标。1536 1920 ÷ 1.25864 1080 ÷ 1.25一眼就能看出这是 125% 缩放导致的。这个问题适合谁做屏幕取色器、自动化点击脚本、截图标注工具、多屏坐标同步、游戏辅助面板的开发者。只要你的程序需要“把鼠标位置和真实屏幕像素对齐”就绕不开它。我试过在一个取色工具里直接拿Cursor.Position去GetPixel结果在 150% 缩放的笔记本上取到的颜色永远差一格排查半天才发现是缩放没换算。要彻底解决需要三件事让进程声明 DPI 感知级别、拿到真实的缩放系数、在逻辑与物理坐标之间做正确转换。下面从环境准备开始一步步给出可复制的配置和代码。2. TaoToken 统一 Key/API 通道前置准备在写坐标换算代码之前先把调用通道准备好。这里说的通道是指当你的 C# 工具需要接入大模型能力比如让 AI 帮你分析截图内容、识别坐标区域、生成自动化脚本时用一套统一的 Key 和 Base URL 去访问而不是每个模型单独配一遍。TaoToken 做的就是这件事一个 Key 打通多家模型Base URL 固定省去反复改配置的麻烦。官网入口在这里https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 这个不加 UTM。注意区分官网带推广参数API 端点保持干净代码里填的是后者。你需要准备的东西不多第一一个 TaoToken 账号登录后在控制台创建 API Key。控制台地址https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。Key 形如sk-开头的一串字符创建后只显示一次记得立刻复制保存。第二确认你要用的模型 ID。不同任务用不同模型比如做代码补全和坐标逻辑推理选推理能力强的做截图文字识别选多模态的。模型列表在文档里能查到https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。第三如果你用的是 Claude Code 这类编码工具接入方式略有不同走的是 Anthropic 兼容通道https://taotoken.net/ClaudeCodeAnthropic?utm_sourcetaotoken_aicg_blog_endutm_contentClaudeCodeAnthropicutm_campaignrewrite 。普通 C# 程序直接调 REST API 即可用HttpClient发 POST 请求。这里要强调一个原则Base URL、API Key、Model ID 三件套必须配套出现。很多接入失败就是因为只改了 Key 没改 Base URL或者 Model ID 写成了别家的名字。后面第 3 节会给完整的 JSON 配置片段路径和字段名都按真实可用的来。准备好 Key 之后先别急着写业务代码用最简请求验证通道是否通。这一步能帮你排除掉 90% 的环境问题避免后面把网络错误误判成坐标换算错误。3. 可复制的 DPI 感知配置与坐标换算代码这一节是重点直接给能跑的配置和代码。先解决 DPI 感知声明再做坐标换算。3.1 声明进程 DPI 感知级别在 C# 项目里最省事的做法是改app.manifest。右键项目 → 添加 → 新建项 → 应用程序清单文件然后在里面加上 DPI 感知声明application xmlnsurn:schemas-microsoft-com:asm.v3 windowsSettings dpiAware xmlnshttp://schemas.microsoft.com/SMI/2005/WindowsSettingstrue/pm/dpiAware dpiAwareness xmlnshttp://schemas.microsoft.com/SMI/2016/WindowsSettingsPerMonitorV2/dpiAwareness /windowsSettings /applicationPerMonitorV2是 Windows 10 1703 之后推荐的级别支持多显示器不同缩放。如果你用的是 .NET 6 的 WinForms还可以在Program.cs里用代码设置Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm());注意SetHighDpiMode必须在创建任何窗口之前调用否则无效。这一步做完Screen.Bounds返回的就是物理像素了1536×864 会变成 1920×1080。3.2 用 GetDeviceCaps 计算缩放系数即使声明了 DPI 感知某些场景下还是需要手动算缩放比比如你要兼容旧系统或者要精确控制取色。核心 API 是GetDeviceCaps用两个常量DESKTOPHORZRES118真实桌面宽度和HORZRES8逻辑宽度。[DllImport(gdi32.dll)] static extern int GetDeviceCaps(IntPtr hdc, int nIndex); [DllImport(user32.dll)] static extern IntPtr GetDC(IntPtr hWnd); [DllImport(user32.dll)] static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC); public const int HORZRES 8; public const int VERTRES 10; public const int DESKTOPHORZRES 118; public const int DESKTOPVERTRES 117; public static (float scaleX, float scaleY) GetScaleFactor() { IntPtr hdc GetDC(IntPtr.Zero); float scaleX (float)GetDeviceCaps(hdc, DESKTOPHORZRES) / GetDeviceCaps(hdc, HORZRES); float scaleY (float)GetDeviceCaps(hdc, DESKTOPVERTRES) / GetDeviceCaps(hdc, VERTRES); ReleaseDC(IntPtr.Zero, hdc); return (scaleX, scaleY); }实测下来125% 缩放时 scaleX 返回 1.25150% 返回 1.5和系统设置完全一致。3.3 逻辑坐标转物理坐标拿到缩放系数后鼠标位置换算就简单了。Cursor.Position在 DPI 感知进程里返回物理坐标但在非感知进程里返回逻辑坐标。为了保险统一做一次转换public static Point LogicalToPhysical(Point logical) { var (scaleX, scaleY) GetScaleFactor(); return new Point( (int)Math.Round(logical.X * scaleX), (int)Math.Round(logical.Y * scaleY) ); } public static Point PhysicalToLogical(Point physical) { var (scaleX, scaleY) GetScaleFactor(); return new Point( (int)Math.Round(physical.X / scaleX), (int)Math.Round(physical.Y / scaleY) ); }取色时这样用IntPtr hdc GetDC(IntPtr.Zero); GetCursorPos(out Point cursor); var (scaleX, scaleY) GetScaleFactor(); int realX (int)(cursor.X * scaleX); int realY (int)(cursor.Y * scaleY); uint pixel GetPixel(hdc, realX, realY); ReleaseDC(IntPtr.Zero, hdc);3.4 接入通道的 JSON 配置片段如果你的工具要调用模型能力配置文件按这个结构写。以appsettings.json为例{ TaoToken: { BaseUrl: https://taotoken.net/api, ApiKey: sk-你的Key, ModelId: 你的模型ID, TimeoutSeconds: 60 } }C# 里读取var config new ConfigurationBuilder() .AddJsonFile(appsettings.json) .Build(); string baseUrl config[TaoToken:BaseUrl]; string apiKey config[TaoToken:ApiKey]; string modelId config[TaoToken:ModelId];三件套齐了Base URL 固定https://taotoken.net/apiKey 从控制台拿Model ID 从文档查。缺一个都会报错后面第 5 节会讲具体报错对照。4. 验证请求与成功结果确认配置写完必须验证。分两步先验证坐标换算对不对再验证 API 通道通不通。4.1 坐标换算验证写一个简单的控制台测试把逻辑坐标、物理坐标、缩放系数都打印出来然后手动对比系统显示设置。static void Main() { var (scaleX, scaleY) GetScaleFactor(); Console.WriteLine($缩放系数: X{scaleX}, Y{scaleY}); Rectangle bounds Screen.PrimaryScreen.Bounds; Console.WriteLine($屏幕物理尺寸: {bounds.Width}x{bounds.Height}); Point cursor Cursor.Position; Console.WriteLine($鼠标逻辑坐标: {cursor.X},{cursor.Y}); Point physical LogicalToPhysical(cursor); Console.WriteLine($鼠标物理坐标: {physical.X},{physical.Y}); IntPtr hdc GetDC(IntPtr.Zero); uint pixel GetPixel(hdc, physical.X, physical.Y); ReleaseDC(IntPtr.Zero, hdc); Console.WriteLine($该点颜色: #{pixel 0xFFFFFF:X6}); }在 125% 缩放的机器上运行预期输出缩放系数 X1.25屏幕物理尺寸 1920×1080鼠标逻辑坐标比如 800,450物理坐标就是 1000,562。把鼠标移到屏幕某个纯色区域打印的颜色应该和取色工具一致。如果屏幕尺寸还是 1536×864说明 DPI 感知没生效回去检查 manifest 或SetHighDpiMode调用时机。如果颜色对不上检查GetPixel用的是不是物理坐标。4.2 API 通道验证用HttpClient发一个最简请求确认 Key 和 Base URL 可用using var client new HttpClient(); client.DefaultRequestHeaders.Add(Authorization, $Bearer {apiKey}); var payload new { model modelId, messages new[] { new { role user, content 回复 OK 两个字母即可 } } }; var json JsonSerializer.Serialize(payload); var content new StringContent(json, Encoding.UTF8, application/json); var response await client.PostAsync(${baseUrl}/v1/chat/completions, content); string result await response.Content.ReadAsStringAsync(); Console.WriteLine(result);成功的话返回 JSON 里会有choices数组message.content是模型回复。如果返回 401是 Key 问题如果返回 404是 Base URL 或路径问题如果卡住不动是网络或超时问题。这些在第 5 节详细对照。验证通过后你就可以把坐标换算和模型调用串起来比如截图后把物理坐标区域发给模型做识别或者让模型根据坐标生成自动化操作序列。需要长期跑编码和 Agent 任务的可以考虑 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 按需选用。5. 本篇常见报错排查这一节按真实报错来遇到问题直接对号入座。报错一401 Unauthorized / invalid api key最常见。原因通常是 Key 复制时带了空格、Key 已过期、或者请求头格式写错。检查Authorization头是不是Bearer sk-xxx中间一个空格。如果用的是 Claude Code 通道认证方式不同走 https://taotoken.net/ClaudeCodeAnthropic?utm_sourcetaotoken_aicg_blog_endutm_contentClaudeCodeAnthropicutm_campaignrewrite 看对应配置。另外确认 Key 是从控制台新建的不是别处复制的。报错二local proxy failed / connection refused这个报错说明请求根本没发出去卡在本地。检查 Base URL 是不是写成了https://taotoken.net/api有没有多写斜杠或漏写。如果你在代码里配了系统代理而代理没开就会报这个。把HttpClient的代理设为 null 或直连即可。注意这里说的是程序内的 HTTP 代理配置不是让你去搞什么网络工具纯粹是代码层面的HttpClientHandler.Proxy设置问题。报错三reading choices / index out of range返回的 JSON 里没有choices字段或者choices是空数组。原因可能是 Model ID 写错了服务端不认识这个模型返回了错误结构。去文档核对模型 ID 拼写大小写敏感。也可能是请求体格式不对messages数组为空。打印完整返回内容看error字段说了什么。报错四OAuth / token expired如果你用的是需要 OAuth 的工具链token 过期会报这个。重新走一遍授权流程或者换成 API Key 方式。普通 REST 调用不会遇到 OAuth只有特定客户端才会。报错五坐标取色偏移但无报错这个不算异常是逻辑错误。表现是程序正常运行但取到的颜色和实际差一格。原因就是缩放系数没乘或者乘了两次。检查GetScaleFactor返回的是不是 1.0如果 DPI 感知已声明Cursor.Position已经是物理坐标再乘一次就错了。判断方法打印缩放系数和坐标手动算一遍。报错六多显示器下坐标错乱主屏和副屏缩放不同时GetDeviceCaps拿到的是主屏的缩放。多屏场景要用MonitorFromPoint和GetDpiForMonitor按显示器分别取缩放。这个属于进阶先把单屏搞定再处理。排查顺序建议先确认 DPI 感知生效看屏幕尺寸再确认缩放系数正确看返回值最后确认 API 通道看返回 JSON。三步分开验证不要混在一起调。6. 统一通道下的坐标换算实践建议把坐标换算和统一 API 通道结合起来实际项目里能省不少事。我的经验是先固定物理坐标体系再让模型处理逻辑。也就是说程序内部统一用物理像素做计算和存储只在展示给用户时转成逻辑坐标。这样取色、点击、截图区域全都对齐不会出现“看着对、点着偏”的情况。具体做法在程序启动时调用一次GetScaleFactor把结果缓存起来不要每次取色都调GetDeviceCaps那个调用有开销。缩放变化时比如用户拖到另一个显示器监听WM_DPICHANGED消息重新计算。调用模型能力时把物理坐标和缩放系数一起传给模型让模型知道当前环境。比如提示词里写“当前屏幕物理尺寸 1920×1080缩放 125%鼠标物理坐标 1000,562”模型返回的操作建议会更准。这比只给一个坐标强得多。需要快速验证模型对坐标的理解能力可以用模型对话页面试https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 把坐标场景描述进去看它能不能正确推理。API Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 定期轮换 Key 是个好习惯。最后给一个实用技巧写单元测试时把缩放系数做成可注入的接口测试里传 1.0、1.25、1.5 三组值验证换算函数输出。这样换机器、换缩放都不用重新调。坐标换算这种纯计算逻辑测试覆盖到位线上就不会出玄学 bug。