
1. 从一次坐标漂移说起Windows C 鼠标控制到底难在哪如果你刚开始接触 Windows 平台的 C 鼠标控制大概率会遇到这样一幕代码里明明写的是SetCursorPos(100, 100)运行后鼠标却跑到了别的位置或者点击事件偶尔生效、偶尔丢失。这不是你的代码写错了而是 Windows 输入模拟本身有一套自己的规则——坐标系、DPI 缩放、消息队列、权限层级任何一环没对齐行为就会漂移。所谓 C 鼠标控制本质上是通过 Win32 API 向系统输入子系统注入鼠标事件让操作系统认为「用户真的动了鼠标」。它能做的事情包括把光标移动到指定屏幕坐标、模拟左键/右键/中键按下与抬起、模拟滚轮滚动、读取当前光标位置。适合谁用自动化测试工程师、做桌面辅助工具的开发者、想给游戏或办公软件写宏的 C 学习者以及需要批量验证 UI 交互的后端同学。我这次的目标很具体在本地 Windows 开发环境里用 C 写一个最小可运行的鼠标控制程序覆盖移动、点击、滚轮三类操作并且把调试期的模型调用统一走 TaoToken 的 Key/API 通道避免在多个工具之间来回切换配置。下面会给出可直接复制的鼠标事件结构体与SendInput调用配置再附三步验证动作编译运行、坐标回读、日志比对。先说一个容易被忽略的前提mouse_event这个老 API 虽然还能用但微软已经把它标记为「superseded」推荐用SendInput。原因很实际——mouse_event在涉及多显示器、高 DPI、UAC 提权窗口时行为不稳定而SendInput把事件打包成一个INPUT数组一次性提交原子性更好也更贴近真实硬件输入。所以本文的主线用SendInputmouse_event只在对照时提一句。另外调试这类程序时你往往需要反复问模型「这个结构体字段为什么这么填」「报错 0x 是什么意思」。如果每个工具都单独配一套 Key切换成本很高。TaoToken 在这里的角色就是统一入口一个 Key 覆盖对话、编码、文档查询调试链路不用断。这不是替代你的编辑器而是把「问模型」这件事收敛到一个通道里。2. TaoToken 前置准备统一 Key 与 API 通道怎么配在写代码之前先把调试期的模型调用通道搭好。这一步不涉及任何网络工具纯粹是在本地配置一个 API 入口。TaoToken 提供统一的 Key 管理和 API 通道官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 。你需要先拿到一个 API Key。进入控制台创建即可地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。创建后复制那串以sk-开头的字符串后面配置里会用到。如果你习惯用命令行工具管理 KeyAPI Keys 页面在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 可以在这里查看、轮换、删除 Key。接下来是模型选择。调试 C 代码时我一般用对话模型来问结构体字段和报错含义入口在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。如果你要长时间做编码或 Agent 类任务可以考虑 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 遇到接入细节可以先翻这里。配置的核心是三件套Base URL、Key、Model ID。无论你用的是哪种客户端这三样必须齐全缺一个就会报 401 或模型不存在。下面给一个通用的 JSON 配置片段路径按你实际使用的工具调整但字段名保持一致{ base_url: https://taotoken.net/api, api_key: sk-你的Key粘贴在这里, model: claude-sonnet-4-20250514, timeout: 60 }如果你用的是 Claude Code 这类工具配置会落在 settings 文件里形如{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的Key粘贴在这里, ANTHROPIC_MODEL: claude-sonnet-4-20250514 } }注意 Base URL 只写到/api不要自己拼/v1/chat/completions之类的后缀客户端会自己补。Key 不要提交到 Git建议放在环境变量或本地未跟踪的配置文件里。Model ID 要和你实际调用的模型一致写错了会返回「model not found」。配好之后你可以先用一次最简单的请求验证通道是否通。比如用 curlcurl https://taotoken.net/api/v1/messages \ -H x-api-key: sk-你的Key \ -H anthropic-version: 2023-06-01 \ -H content-type: application/json \ -d {model:claude-sonnet-4-20250514,max_tokens:64,messages:[{role:user,content:回复 ok 两个字母}]}返回里能看到content字段就说明通道正常。这一步做完后面写鼠标控制代码时遇到问题就能直接在同一套配置下问模型不用再折腾 Key。3. 可复制配置SendInput 鼠标事件结构体与调用现在进入正题。Windows 的SendInput函数签名是UINT SendInput( UINT cInputs, LPINPUT pInputs, int cbSize );它接收一个INPUT数组每个INPUT描述一个事件。鼠标事件的关键结构体是MOUSEINPUT嵌套在INPUT里。先看完整定义typedef struct tagMOUSEINPUT { LONG dx; LONG dy; DWORD mouseData; DWORD dwFlags; DWORD time; ULONG_PTR dwExtraInfo; } MOUSEINPUT; typedef struct tagINPUT { DWORD type; union { MOUSEINPUT mi; KEYBDINPUT ki; HARDWAREINPUT hi; } DUMMYUNIONNAME; } INPUT;字段含义逐个说清楚。dx和dy是相对位移只有在dwFlags含MOUSEEVENTF_MOVE时才生效如果你要绝对定位得用MOUSEEVENTF_ABSOLUTE此时dx/dy会被归一化到 0–65535 的屏幕坐标空间。mouseData对滚轮事件有效正值向上滚负值向下滚单位是WHEEL_DELTA120。dwFlags是事件类型标志位time一般填 0 让系统自己打时间戳dwExtraInfo也填 0。下面是一个封装好的辅助函数把「移动」「左键按下/抬起」「右键按下/抬起」「滚轮」都包进去#include windows.h #include iostream // 发送单个鼠标事件 void SendMouseEvent(DWORD flags, LONG dx, LONG dy, DWORD data) { INPUT input {0}; input.type INPUT_MOUSE; input.mi.dx dx; input.mi.dy dy; input.mi.mouseData data; input.mi.dwFlags flags; input.mi.time 0; input.mi.dwExtraInfo 0; SendInput(1, input, sizeof(INPUT)); } // 绝对移动到屏幕坐标 (x, y) void MoveTo(int x, int y) { int screenW GetSystemMetrics(SM_CXSCREEN); int screenH GetSystemMetrics(SM_CYSCREEN); LONG nx (LONG)((double)x * 65535.0 / (screenW - 1)); LONG ny (LONG)((double)y * 65535.0 / (screenH - 1)); SendMouseEvent(MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE, nx, ny, 0); } // 左键单击 void LeftClick() { SendMouseEvent(MOUSEEVENTF_LEFTDOWN, 0, 0, 0); Sleep(30); SendMouseEvent(MOUSEEVENTF_LEFTUP, 0, 0, 0); } // 右键单击 void RightClick() { SendMouseEvent(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0); Sleep(30); SendMouseEvent(MOUSEEVENTF_RIGHTUP, 0, 0, 0); } // 滚轮delta 正数向上负数向下 void Scroll(int delta) { SendMouseEvent(MOUSEEVENTF_WHEEL, 0, 0, (DWORD)(delta * WHEEL_DELTA)); }这里有几个坑要提前说。第一绝对坐标归一化用的是65535.0 / (screenW - 1)不是除以screenW差一个像素在边界上就会偏。第二Sleep(30)不是随便写的按下和抬起之间留一点间隔某些 UI 框架才认得出这是一次完整点击间隔太短会被合并或丢弃。第三滚轮方向WHEEL_DELTA是 120传120向上-120向下别传成像素值。主函数里把三步验证串起来int main() { POINT p; GetCursorPos(p); std::cout 起始坐标: p.x , p.y std::endl; MoveTo(400, 300); Sleep(100); GetCursorPos(p); std::cout 移动后坐标: p.x , p.y std::endl; LeftClick(); Sleep(100); Scroll(1); Sleep(100); Scroll(-1); std::cout 事件发送完毕 std::endl; return 0; }编译命令用 MSVC 或 MinGW 都行。MSVCcl /EHsc mouse_ctrl.cpp user32.libMinGWg mouse_ctrl.cpp -o mouse_ctrl.exe -luser32注意SendInput和GetCursorPos都在user32.lib里MinGW 下要显式链接-luser32否则会报 undefined reference。4. 验证请求与成功结果编译运行、坐标回读、日志比对代码写完不算完得验证它真的按预期工作。我设计了三步验证动作每一步都有明确的成功判据。第一步编译运行。用上面的命令编译如果出现error C2065: INPUT_MOUSE: undeclared identifier说明你没包含windows.h或者包含顺序有问题。正确顺序是先#include windows.h再包含其他头文件。编译通过后运行控制台应该先打印起始坐标然后鼠标跳到 (400, 300) 附近。第二步坐标回读。程序里用GetCursorPos读回移动后的坐标打印出来。成功判据是回读坐标和MoveTo传入的坐标误差在 1–2 像素以内。如果误差很大比如你传 400 却读到 800那基本是 DPI 缩放问题——你的程序没有声明 DPI 感知系统帮你做了缩放。解决办法是在main开头加SetProcessDPIAware();或者在项目清单里声明dpiAware。加了之后重新编译坐标就会对齐。第三步日志比对。把每次事件的类型、参数、时间戳写进日志文件然后和实际观察到的行为比对。比如你发送了LeftClick日志里应该有LEFTDOWN和LEFTUP两条记录间隔约 30ms。如果只有LEFTDOWN没有LEFTUP说明程序在中途异常退出或者Sleep被优化掉了。日志格式建议用 CSV方便后续用脚本分析void LogEvent(const char* name, LONG x, LONG y, DWORD data) { FILE* f fopen(mouse_log.csv, a); if (f) { fprintf(f, %lu,%s,%ld,%ld,%lu\n, GetTickCount(), name, x, y, data); fclose(f); } }把LogEvent插到每个SendMouseEvent调用前后就能得到完整的事件轨迹。实测下来这套三步验证能覆盖 90% 的入门问题编译错误、坐标偏移、事件丢失。验证通过后你可以在同一个 TaoToken 配置下把日志贴给模型问「为什么这次点击没生效」模型能结合上下文给出针对性建议。这就是统一 Key 的价值——调试链路不断问题不用换工具重问。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth调试过程中报错分两类一类是 C 代码本身的一类是模型调用通道的。分开说。C 侧最常见的报错是SendInput返回 0。SendInput返回实际插入的事件数返回 0 说明失败。原因通常是目标窗口以管理员权限运行而你的程序没有提权UIPI用户界面特权隔离会拦截输入。解决办法是以管理员身份运行你的程序或者在开发阶段避免向提权窗口发送事件。另一个原因是cbSize传错必须是sizeof(INPUT)传成sizeof(MOUSEINPUT)会失败。模型调用侧第一个高频报错是 401 Unauthorized。这几乎总是 Key 问题Key 没填、填错、或者复制时带了空格。检查你的配置文件里api_key字段确保是完整的sk-开头字符串。如果用的是环境变量确认变量名和客户端读取的一致。第二个是local proxy failed。这个报错通常出现在客户端配置了本地代理但代理没启动时。检查你的客户端设置里有没有proxy或http_proxy字段如果有确认代理地址可达或者直接删掉代理配置走直连。注意这里说的是客户端自身的网络配置不是让你去搭什么工具只是排查配置项。第三个是reading choices相关报错一般出现在 OpenAI 兼容格式的响应解析中。如果你用的客户端期望choices数组但服务端返回的是 Anthropic 格式的content数组就会解析失败。解决办法是确认客户端的 API 格式和 Base URL 匹配https://taotoken.net/api同时支持两种格式但客户端要选对模式。文档里有格式对照表遇到时翻一下。第四个是 OAuth 相关报错。如果你用的是 Claude Code 这类支持 OAuth 的工具报错OAuth token expired或invalid_grant说明登录态失效。重新走一次授权流程即可。注意 OAuth 和 API Key 是两套机制不要混用用 API Key 就填ANTHROPIC_API_KEY用 OAuth 就走登录。排查顺序建议先看 HTTP 状态码401/403 查 Key404 查 Base URL 和路径429 查配额5xx 查服务端。再看响应体里的error.message通常写得很清楚。最后对照文档里的错误码表。6. 把调试链路收进一个 Key后续怎么扩展走到这里你已经有了一个能跑通的 C 鼠标控制程序也有了统一的模型调用通道。接下来可以往几个方向扩展。第一把鼠标控制封装成类支持链式调用比如Mouse::Move(400, 300).LeftClick().Scroll(1)。第二加入键盘事件SendInput同样支持KEYBDINPUT结构体字段和鼠标类似。第三做录制回放用SetWindowsHookEx监听真实鼠标事件存成脚本再用SendInput回放。第四把日志接到模型做异常检测比如点击间隔异常时自动告警。这些扩展都会涉及更多模型调用。如果你每个工具都单独配 Key切换成本会随工具数量线性增长。TaoToken 的统一 Key 在这里的优势就体现出来了无论你是问结构体、查报错、还是让模型分析日志都走同一个通道。Coding Plan 适合长期编码任务模型对话适合快速问答按需切换即可。最后留一个实用技巧把SetProcessDPIAware()和日志初始化放在main最前面避免后续加功能时忘记。DPI 问题在 4K 屏上尤其明显早加早省心。日志文件建议按日期命名方便回溯。编译脚本可以写个build.bat把cl /EHsc mouse_ctrl.cpp user32.lib和运行命令串起来改完代码一键验证。如果你在配置通道时遇到接入问题可以对照接入文档排查想先验证模型是否可用去模型对话页面发一条测试消息准备长期做编码或 Agent 任务再看 Coding Plan 的额度方案。三个入口按需选不用一次全配。