ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++实现鼠标控制:封装常见的模拟鼠标、键盘操作函数与TaoToken统一Key接入实践

C++实现鼠标控制:封装常见的模拟鼠标、键盘操作函数与TaoToken统一Key接入实践 1. Windows 下 C 模拟鼠标键盘的真实痛点如果你写过自动化脚本大概率遇到过这样的场景想让程序自动点一下某个按钮、拖拽一个窗口、或者往记事本里敲一串文字结果发现mouse_event在某些窗口上没反应keybd_event发出去的按键被游戏或安全软件拦截甚至同一段代码在 Win10 能用、Win11 就失灵。这不是你代码写错了而是 Windows 输入模拟这套 API 本身经历了从「老接口」到「新接口」的演进选错了工具后面全是坑。C 模拟鼠标键盘操作本质上是往 Windows 的消息队列里注入输入事件。早期大家用mouse_event和keybd_event这两个函数在Winuser.h里声明链接User32.lib就能用写起来确实简单——移动鼠标SetCursorPos(120, 100)单击mouse_event(MOUSEEVENTF_LEFTDOWN|MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)键盘按下 A 键keybd_event(65, 0, 0, 0)再keybd_event(65, 0, KEYEVENTF_KEYUP, 0)。但问题在于mouse_event已经被微软标记为「superseded」也就是被取代的旧接口它在多显示器、高 DPI、以及需要精确控制输入时序的场景下表现不稳定。真正现代的做法是SendInput。它把鼠标和键盘事件统一成INPUT结构体一次调用可以批量注入多个事件而且支持绝对坐标的 65535 归一化映射跨分辨率屏幕也能算准位置。更重要的是SendInput和系统输入栈的结合更紧密很多对mouse_event不友好的程序换成SendInput就能正常工作。这篇文章要解决的就是把散落在各个 API 里的鼠标键盘操作封装成一个可复用的MouseKeyboard类头文件和实现都给全你复制过去改改就能用。同时自动化脚本光有「手」还不够还得有「脑子」——比如根据屏幕内容决定点哪里、根据一段自然语言生成操作序列。这部分我会演示怎么通过 TaoToken 的统一 Key 和 API 通道给 C 自动化程序接上 AI 能力让脚本从「死记坐标」升级成「看懂再操作」。最后用记事本和画图工具做真实验证点击轨迹和按键序列都能肉眼确认。适合谁看写过一点 C、想在 Windows 上做自动化工具或辅助脚本的开发者被mouse_event坑过、想换SendInput但不知道从哪下手的同学以及想把 AI 决策能力塞进本地自动化程序的工程实践者。2. TaoToken 统一 Key 接入给自动化脚本装上 AI 决策层纯坐标驱动的自动化脚本有个天花板目标窗口一挪位置、分辨率一变、按钮样式一改脚本就废了。更聪明的做法是让程序先「看一眼」当前屏幕或一段文本由 AI 判断下一步该点哪里、该输入什么再调用我们封装好的鼠标键盘函数去执行。这就需要一个稳定的模型调用通道。TaoToken 在这里扮演的角色是「统一入口」你不需要分别去对接多家模型的 API也不用为每个模型维护不同的 Key 和请求格式用一套 Key、一个 Base URL就能在 C 程序里通过 HTTP 请求调用对话模型。对于自动化脚本来说这意味着你可以把「生成操作序列」这件事交给模型脚本本身只负责解析返回结果并执行鼠标键盘动作。接入前你需要准备三样东西我把它叫做「三件套」缺一不可配置项说明获取位置Base URLAPI 请求的基础地址https://taotoken.net/apiAPI Key身份凭证放在请求头里控制台 API Keys 页面生成Model ID指定用哪个模型模型列表或文档中查询Base URL 固定用https://taotoken.net/api注意这里不加任何多余路径具体端点拼在它后面。API Key 去控制台创建格式通常是一串以sk-开头的字符串创建后只显示一次记得存好。Model ID 则是你实际要调用的模型标识比如对话场景常用的那些具体以文档里的模型列表为准。在 C 里调用最轻量的方式是直接用 WinHTTP 或 libcurl 发 POST 请求。请求体是标准的 JSON包含model和messages两个核心字段。下面是一个最小可用的请求结构你可以先把它跑通确认 Key 和网络都没问题再往自动化逻辑里嵌{ model: 你的Model ID, messages: [ { role: system, content: 你是一个自动化操作助手根据用户描述输出鼠标键盘操作序列格式为 JSON 数组。 }, { role: user, content: 在画图工具里画一条从左上到右下的斜线 } ] }请求头里带上Authorization: Bearer 你的API Key和Content-Type: application/json发到https://taotoken.net/api对应的对话端点上。返回的 JSON 里choices[0].message.content就是模型生成的文本你把它解析成操作序列再逐条喂给MouseKeyboard类执行。这里有个工程上的建议不要让模型直接输出「坐标数字」因为模型看不到你的屏幕分辨率它给的坐标大概率是错的。更好的分工是——模型负责输出「语义动作」比如{action: click, target: 开始菜单}或{action: type, text: hello}你的 C 程序负责把语义动作翻译成具体坐标和 API 调用。坐标定位可以结合窗口句柄、控件查找或者简单的图像匹配来做模型只做它擅长的「理解意图」和「规划步骤」。如果你打算长期跑自动化任务比如批量处理文档、定时截图分析、或者做一套带 AI 判断的桌面 Agent可以考虑用 Coding Plan 这类长期方案比按次调用更划算适合高频场景。验证模型是否通的时候也可以先用模型对话页面手动发一条消息确认返回正常再写进代码里能省掉不少排查网络的时间。把 AI 决策层和本地输入模拟层分开还有个好处模型换了、Key 换了你的鼠标键盘封装代码一行都不用动。反过来哪天你想把SendInput换成驱动级模拟AI 那层也完全不受影响。这种分层设计是自动化工具能长期维护的关键。3. MouseKeyboard 类完整封装头文件与实现这一节是全文的核心我把鼠标和键盘的常用操作封装成一个类头文件MouseKeyboard.h和实现MouseKeyboard.cpp都给全。设计上统一用SendInput不再混用mouse_event坐标处理、按键映射、组合键、滚轮、拖拽都覆盖到。先看头文件。类的接口设计尽量贴近直觉移动用MoveTo和MoveBy点击用LeftClick、RightClick、MiddleClick按下抬起分开成LeftDown/LeftUp滚轮用Scroll键盘用KeyPress、KeyDown、KeyUp字符串输入用TypeString组合键用Hotkey。// MouseKeyboard.h #pragma once #include windows.h #include string #include vector class MouseKeyboard { public: MouseKeyboard(); ~MouseKeyboard(); // 鼠标移动 void MoveTo(int x, int y); // 绝对坐标移动 void MoveBy(int dx, int dy); // 相对位移 void SmoothMoveTo(int x, int y, int steps 20); // 平滑移动用于轨迹验证 // 鼠标点击 void LeftClick(); void RightClick(); void MiddleClick(); void LeftDoubleClick(); // 鼠标按下/抬起用于拖拽 void LeftDown(); void LeftUp(); void RightDown(); void RightUp(); // 拖拽从起点到终点 void DragTo(int fromX, int fromY, int toX, int toY, int durationMs 300); // 滚轮正数向上负数向下 void Scroll(int amount); // 键盘 void KeyPress(BYTE vk); // 按一下 void KeyDown(BYTE vk); // 按下 void KeyUp(BYTE vk); // 抬起 void TypeString(const std::string text); // 输入字符串 void Hotkey(BYTE modifier, BYTE key); // 组合键如 CtrlC // 辅助 POINT GetCursorPos() const; void SaveCursor(); void RestoreCursor(); private: void SendMouseInput(DWORD flags, LONG dx, LONG dy, DWORD data); void SendKeyInput(WORD vk, DWORD flags); POINT savedPos_; };头文件里几个设计点值得说明。SmoothMoveTo不是简单跳过去而是分步移动每步之间加短暂延时这样在画图工具里能看到一条连续的轨迹方便验证。DragTo封装了「按下—移动—抬起」的完整流程中间加了Sleep因为很多程序在拖拽时如果移动太快会丢事件。Hotkey把修饰键和主键组合起来内部先按下修饰键、再按主键、然后反向抬起顺序不能乱。下面是实现文件。核心是SendMouseInput和SendKeyInput两个私有函数它们构造INPUT结构体并调用SendInput。绝对坐标移动需要把屏幕坐标归一化到 0–65535 范围这一步用GetSystemMetrics拿屏幕宽高来算。// MouseKeyboard.cpp #include MouseKeyboard.h #include thread #include chrono MouseKeyboard::MouseKeyboard() { savedPos_ {0, 0}; } MouseKeyboard::~MouseKeyboard() {} void MouseKeyboard::SendMouseInput(DWORD flags, LONG dx, LONG dy, DWORD data) { INPUT input {0}; input.type INPUT_MOUSE; input.mi.dwFlags flags; input.mi.dx dx; input.mi.dy dy; input.mi.mouseData data; SendInput(1, input, sizeof(INPUT)); } void MouseKeyboard::SendKeyInput(WORD vk, DWORD flags) { INPUT input {0}; input.type INPUT_KEYBOARD; input.ki.wVk vk; input.ki.dwFlags flags; SendInput(1, input, sizeof(INPUT)); } void MouseKeyboard::MoveTo(int x, int y) { double screenW GetSystemMetrics(SM_CXSCREEN) - 1; double screenH GetSystemMetrics(SM_CYSCREEN) - 1; LONG nx static_castLONG(x * (65535.0 / screenW)); LONG ny static_castLONG(y * (65535.0 / screenH)); SendMouseInput(MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE, nx, ny, 0); } void MouseKeyboard::MoveBy(int dx, int dy) { SendMouseInput(MOUSEEVENTF_MOVE, dx, dy, 0); } void MouseKeyboard::SmoothMoveTo(int x, int y, int steps) { POINT cur GetCursorPos(); for (int i 1; i steps; i) { int ix cur.x (x - cur.x) * i / steps; int iy cur.y (y - cur.y) * i / steps; MoveTo(ix, iy); std::this_thread::sleep_for(std::chrono::milliseconds(5)); } } void MouseKeyboard::LeftClick() { SendMouseInput(MOUSEEVENTF_LEFTDOWN, 0, 0, 0); SendMouseInput(MOUSEEVENTF_LEFTUP, 0, 0, 0); } void MouseKeyboard::RightClick() { SendMouseInput(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0); SendMouseInput(MOUSEEVENTF_RIGHTUP, 0, 0, 0); } void MouseKeyboard::MiddleClick() { SendMouseInput(MOUSEEVENTF_MIDDLEDOWN, 0, 0, 0); SendMouseInput(MOUSEEVENTF_MIDDLEUP, 0, 0, 0); } void MouseKeyboard::LeftDoubleClick() { LeftClick(); std::this_thread::sleep_for(std::chrono::milliseconds(50)); LeftClick(); } void MouseKeyboard::LeftDown() { SendMouseInput(MOUSEEVENTF_LEFTDOWN, 0, 0, 0); } void MouseKeyboard::LeftUp() { SendMouseInput(MOUSEEVENTF_LEFTUP, 0, 0, 0); } void MouseKeyboard::RightDown() { SendMouseInput(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0); } void MouseKeyboard::RightUp() { SendMouseInput(MOUSEEVENTF_RIGHTUP, 0, 0, 0); } void MouseKeyboard::DragTo(int fromX, int fromY, int toX, int toY, int durationMs) { MoveTo(fromX, fromY); std::this_thread::sleep_for(std::chrono::milliseconds(50)); LeftDown(); std::this_thread::sleep_for(std::chrono::milliseconds(50)); int steps 20; for (int i 1; i steps; i) { int ix fromX (toX - fromX) * i / steps; int iy fromY (toY - fromY) * i / steps; MoveTo(ix, iy); std::this_thread::sleep_for(std::chrono::milliseconds(durationMs / steps)); } LeftUp(); } void MouseKeyboard::Scroll(int amount) { SendMouseInput(MOUSEEVENTF_WHEEL, 0, 0, static_castDWORD(amount)); } void MouseKeyboard::KeyPress(BYTE vk) { SendKeyInput(vk, 0); SendKeyInput(vk, KEYEVENTF_KEYUP); } void MouseKeyboard::KeyDown(BYTE vk) { SendKeyInput(vk, 0); } void MouseKeyboard::KeyUp(BYTE vk) { SendKeyInput(vk, KEYEVENTF_KEYUP); } void MouseKeyboard::TypeString(const std::string text) { for (char c : text) { SHORT vk VkKeyScanA(c); if (vk -1) continue; BYTE key LOBYTE(vk); BYTE shift HIBYTE(vk); if (shift 1) KeyDown(VK_SHIFT); KeyPress(key); if (shift 1) KeyUp(VK_SHIFT); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } } void MouseKeyboard::Hotkey(BYTE modifier, BYTE key) { KeyDown(modifier); std::this_thread::sleep_for(std::chrono::milliseconds(20)); KeyPress(key); std::this_thread::sleep_for(std::chrono::milliseconds(20)); KeyUp(modifier); } POINT MouseKeyboard::GetCursorPos() const { POINT p; ::GetCursorPos(p); return p; } void MouseKeyboard::SaveCursor() { savedPos_ GetCursorPos(); } void MouseKeyboard::RestoreCursor() { MoveTo(savedPos_.x, savedPos_.y); }编译时记得链接User32.lib在代码里加#pragma comment(lib, User32.lib)或者在项目设置里手动加。TypeString里用了VkKeyScanA把字符转成虚拟键码这样大小写和符号都能处理比硬编码 ASCII 码靠谱。Hotkey里加了 20 毫秒延时是因为有些程序对组合键的时序敏感按太快会识别成单个键。这套封装实测下来在记事本、画图、浏览器这些常规程序上都能稳定工作。如果你要操作的是带反作弊的游戏或者安全级别很高的软件SendInput可能也会被拦那就需要驱动级方案了那属于另一个话题本文不展开。4. 验证请求与成功结果记事本 画图实测代码写完了得验证它真的能跑。我用两个最朴素的工具做测试记事本验证键盘输入和组合键画图工具验证鼠标移动、点击和拖拽轨迹。这两个程序对输入模拟的接受度很高适合做第一轮验证。先写一个测试用的main.cpp把MouseKeyboard类用起来。测试分三段第一段打开记事本输入一段文字再用 CtrlA 全选、CtrlC 复制第二段打开画图用SmoothMoveTo画一条斜线再用DragTo拖一个矩形第三段测试滚轮和右键菜单。// main.cpp #include MouseKeyboard.h #include iostream int main() { MouseKeyboard mk; // 测试1记事本键盘输入 std::cout 3秒后开始记事本测试请确保记事本已打开... std::endl; Sleep(3000); mk.TypeString(Hello TaoToken 2024!); Sleep(500); mk.Hotkey(VK_CONTROL, A); // 全选 Sleep(300); mk.Hotkey(VK_CONTROL, C); // 复制 std::cout 记事本测试完成 std::endl; // 测试2画图工具鼠标轨迹 std::cout 3秒后开始画图测试请确保画图已打开... std::endl; Sleep(3000); mk.SmoothMoveTo(200, 200, 30); mk.LeftDown(); mk.SmoothMoveTo(600, 500, 40); mk.LeftUp(); std::cout 画图轨迹测试完成 std::endl; // 测试3拖拽矩形 Sleep(1000); mk.DragTo(300, 300, 700, 600, 500); std::cout 拖拽测试完成 std::endl; // 测试4滚轮 mk.MoveTo(400, 400); mk.Scroll(120); // 向上滚 Sleep(300); mk.Scroll(-120); // 向下滚 std::cout 滚轮测试完成 std::endl; return 0; }编译命令用 MSVC 的话在开发者命令行里执行cl /EHsc main.cpp MouseKeyboard.cpp /link User32.lib用 MinGW 的话g main.cpp MouseKeyboard.cpp -o mk_test.exe -luser32跑起来后你会看到鼠标自己动起来记事本里出现文字画图里画出线条。这里有个细节SmoothMoveTo的steps参数控制平滑度30 步在 1080P 屏幕上大概每步移动十几像素肉眼能看到连续轨迹。如果步数太少鼠标会「跳」过去画图里就是断线步数太多则太慢。实测 20–40 步是比较舒服的范围。验证键盘时TypeString输入大写字母和符号需要配合 Shift代码里已经处理了。你可以故意输入Hello World! 123这种混合内容看记事本里是否完全一致。如果发现某个符号不对多半是VkKeyScanA对某些键盘布局的映射差异可以针对具体字符做映射表修正。组合键验证用 CtrlA 全选最直观输入文字后按 CtrlA如果记事本里文字被高亮选中说明Hotkey的按下顺序和时序都对。CtrlC 复制后你可以手动 CtrlV 粘贴到别处确认内容。画图工具里DragTo会画出一个矩形框因为按下后移动再抬起画图默认画线。如果你想画真正的矩形得先点画图的矩形工具再拖拽。这正好演示了「先点击工具按钮再执行拖拽」的自动化流程也是后面接 AI 决策层的典型场景——模型判断「需要画矩形」脚本先点矩形工具再拖拽。滚轮测试时把鼠标移到画图的可滚动区域Scroll(120)向上滚一格Scroll(-120)向下滚一格。注意mouseData的单位是 120 的倍数传 120 就是标准的一格。整个验证过程不需要任何额外工具肉眼就能确认成功与否。如果某一步没反应先检查目标窗口是不是在前台、有没有被其他窗口遮挡SendInput是往当前焦点窗口发事件的窗口不在前台自然没效果。5. 常见报错排查401、local proxy failed、reading choices 等接入 AI 通道和跑自动化脚本时报错基本集中在几类。我把踩过的坑整理出来对照着查能省不少时间。401 Unauthorized这是最常见的。原因通常是 API Key 没带、带错、或者格式不对。检查请求头里是不是有Authorization: Bearer sk-xxxx注意Bearer和 Key 之间有一个空格Key 本身不要有多余引号或换行。如果你是从控制台复制的 Key确认复制完整没有漏掉尾部字符。还有一种情况是 Key 被禁用或额度用尽去控制台 API Keys 页面看状态。local proxy failed / connection refused这个报错说明你的程序根本没连上服务器。先确认 Base URL 写的是https://taotoken.net/api没有多写或少写路径。然后检查本机网络能不能正常访问外网可以用浏览器打开文档页面测试。如果你在公司内网可能有防火墙限制需要走正常的网络配置。C 里用 WinHTTP 的话注意设置超时默认超时可能太短导致误报。reading choices 相关错误 / 返回体解析失败这通常不是网络问题而是你解析返回 JSON 的方式不对。模型返回的结构是{choices: [{message: {content: ...}}]}如果你直接按字符串找content可能匹配到别的地方。建议用正经的 JSON 库比如 nlohmann/json解析后按路径取choices[0].message.content。如果返回体里根本没有choices字段那多半是请求体格式错了检查model和messages字段名有没有拼错JSON 有没有语法错误比如多余的逗号。OAuth / 认证方式混淆有些同学把 OAuth 流程和 API Key 认证搞混了。TaoToken 的 API 调用用的是 Bearer Token 方式不需要走 OAuth 授权码那套。如果你在代码里看到grant_type、client_id这些字段那是另一套流程跟这里的 Key 接入不是一回事。确认你用的是 API Key不是其他凭证。SendInput 返回 0 / 鼠标不动这不是 AI 通道的问题是输入模拟本身失败。SendInput返回成功注入的事件数如果返回 0说明被系统拦截了。常见原因目标窗口以管理员权限运行而你的程序不是或者目标程序有输入保护。解决办法是以相同权限运行你的程序或者换用其他输入方式。另外UAC 提示框出现时SendInput是无效的这是 Windows 的安全机制绕不过去。坐标偏移 / 点不准高 DPI 屏幕下GetSystemMetrics返回的是逻辑坐标而SendInput的绝对坐标是基于物理像素归一化的两者不一致就会偏。解决办法是在程序清单里声明 DPI 感知或者用SetProcessDpiAwareness设置。简单验证方法把鼠标移到屏幕四个角看GetCursorPos返回的值是不是接近 (0,0) 和 (屏幕宽,屏幕高)。TypeString 输入乱码或漏字符多半是VkKeyScanA对当前键盘布局不支持某些字符或者输入速度太快目标程序来不及处理。可以在每个字符后加 10–20 毫秒延时实测能解决大部分漏字符问题。中文输入需要走输入法SendInput直接发虚拟键码是打不出中文的得用剪贴板粘贴的方式。排查时建议按「先网络后解析、先权限后坐标」的顺序一层层排除。大部分问题集中在 Key 和请求体格式上把这两块确认好剩下的就是输入模拟本身的细节了。6. 从封装到落地把 AI 决策接进自动化流程把MouseKeyboard类和 TaoToken 通道接起来整个自动化程序的结构就清晰了上层是 AI 决策中间是动作解析底层是输入执行。我建议用一个简单的循环来组织截图或获取当前状态 → 发给模型请求下一步动作 → 解析返回的 JSON → 调用MouseKeyboard执行 → 循环。动作协议可以设计得很简单比如模型返回这样的 JSON 数组[ {action: move, x: 400, y: 300}, {action: click, button: left}, {action: type, text: hello}, {action: hotkey, modifier: ctrl, key: s} ]你的 C 程序解析这个数组逐条映射到MoveTo、LeftClick、TypeString、Hotkey这些方法上。这样模型只负责「决定做什么」坐标和按键的具体执行交给本地代码既发挥了模型的理解能力又保证了执行的精确性。如果你要做的是长期运行的桌面 Agent比如定时整理文件、自动填写表单、批量截图分析建议用 Coding Plan 这类方案调用更稳定也适合高频场景。验证模型返回是否正常时可以先用模型对话页面手动测几条指令确认输出格式符合预期再写进代码。接入文档里有完整的端点和参数说明遇到不确定的字段去那里查最快。最后给一个实用技巧在自动化脚本里加一个「干跑模式」也就是解析出动作后不真正执行只打印日志。这样调试 AI 决策逻辑时不会把鼠标搞得满屏乱飞确认动作序列合理后再开真实执行。这个习惯能帮你省下大量「脚本跑飞了要手动收拾」的时间。
返回列表