首页 / 资讯中心 / 文章详情

C++实现Windows鼠标锁定整蛊程序:Win32 API与参数调优详解

C++实现Windows鼠标锁定整蛊程序:Win32 API与参数调优详解 ★ FEATURED ARTICLE
1. 项目缘起与整体设计思路1.1 这个程序到底在做什么先把这个项目的核心说清楚它是一个用 C 写的、运行在 Windows 平台上的小型桌面程序功能非常单一——当它启动后会持续监测鼠标指针的位置一旦发现鼠标在设定的时间窗口内没有移动就自动把指针“锁”回原位或者让指针产生一种“粘滞”的错觉让使用者觉得鼠标好像失灵了。说白了就是一个整蛊朋友、同事的小玩具。这类程序在圈子里通常被叫做“恶搞小程序”或者“prank tool”它的技术门槛其实很低但要做好、做得“稳”、做得不容易被一眼识破里面还是有不少门道的。我见过很多人写这类东西代码跑是能跑但要么 CPU 占用飙到 20% 以上要么鼠标抖得像帕金森要么一运行就被安全软件弹窗拦截体验非常差。所以这篇文章不只是给你一段能编译的代码而是把为什么这么写、参数怎么定、坑在哪里都讲透。它适合谁看如果你刚学完 C 基础语法想找一个“能看见效果”的小项目练手这个非常合适如果你想了解 Windows 下怎么调用系统 API 去读取和干预输入设备状态这也是一个很好的入门切口当然如果你只是想做个无害的小玩笑那照着本文的方案做就行。1.2 为什么选择 C 而不是别的语言很多人第一反应是用 Python 写毕竟pyautogui之类的库调一下就行。但我实测下来Python 方案有几个绕不开的问题一是打包成 exe 之后体积动辄几十兆二是需要额外安装解释器或打包工具三是调用底层 API 的延迟相对高一些做“实时锁鼠标”这种对时序敏感的操作时偶尔会出现指针“跳一下”的破绽。C 的优势在这里体现得很直接直接调用 Win32 API没有中间层编译出来就是一个几百 KB 的独立 exe双击即用不需要任何运行环境。而且GetCursorPos和SetCursorPos这两个函数本身就是系统提供的调用开销极低配合一个合理的轮询间隔CPU 占用可以压到几乎为零。这就是我选 C 的核心原因——轻、快、干净。1.3 整体架构一个主循环加两个 API整个程序的骨架简单到可以用一句话概括一个死循环每次循环读取当前鼠标坐标和上一次的坐标比较如果超过阈值时间没变化就执行干预动作。核心用到的 API 只有两个GetCursorPos(LPPOINT)获取当前鼠标指针在屏幕坐标系中的位置。SetCursorPos(int, int)把鼠标指针强制移动到指定坐标。再配合Sleep()控制轮询节奏以及GetTickCount64()或者std::chrono来做时间差计算。整个程序不需要窗口、不需要消息循环、不需要任何第三方库就是一个控制台程序或者用WinMain隐藏窗口编译命令一行就够。提示这里说的“干预动作”有多种玩法后面第 3 章会给出三种不同效果的完整实现包括“锁死不动”“缓慢回拉”“随机抖动”你可以按需选择。2. 核心细节解析与实操要点2.1 鼠标坐标系统与屏幕分辨率的关系在动手写代码之前必须先把 Windows 的坐标体系搞清楚否则你会遇到“明明设了坐标鼠标却跑到另一个地方”的问题。Windows 默认的坐标系叫屏幕坐标系原点(0, 0)在主显示器的左上角x 轴向右递增y 轴向下递增。GetCursorPos返回的就是这个坐标系下的值。这里有个坑如果你用的是多显示器或者系统设置了缩放比如 125%、150%坐标值可能会和你想象的不一样。GetCursorPos返回的是物理像素坐标还是逻辑坐标取决于程序的 DPI 感知设置。默认情况下没有声明 DPI 感知的程序会被系统做虚拟化缩放导致读到的坐标和实际屏幕像素对不上。解决办法是在程序入口处调用一次SetProcessDPIAware();这个函数告诉系统“我这个程序自己处理缩放”这样GetCursorPos和SetCursorPos用的就是同一套真实像素坐标不会出现偏移。我踩过一次坑在一台 150% 缩放的笔记本上测试鼠标锁的位置总是偏右下查了半天才发现是 DPI 虚拟化导致的加上这一行就正常了。2.2 轮询间隔的取舍10ms 还是 50ms主循环里Sleep()的参数直接决定了程序的手感和资源占用。我做过一组对比测试在同一台机器上运行记录 CPU 占用和“锁鼠标”的成功率轮询间隔CPU 占用单核锁鼠标成功率手感评价1 ms约 8%99%指针几乎纹丝不动但风扇会转10 ms约 1.5%97%基本感觉不到延迟推荐50 ms约 0.3%85%偶尔能“挣脱”有轻微跳动200 ms接近 0%60%明显能甩开效果差结论很明确10ms 是甜点值。它对应大约每秒 100 次的检测频率人眼和手速根本反应不过来同时 CPU 占用低到可以忽略。如果你追求极致省电可以放到 20ms成功率依然有 93% 左右。低于 5ms 就没必要了纯属浪费。注意Sleep(0)或者Sleep(1)在 Windows 上实际休眠时间并不精确系统调度粒度通常在 15ms 左右所以写Sleep(1)实际可能是 15ms。想要更精确的节奏可以用timeBeginPeriod(1)提升定时器精度但记得程序退出前调用timeEndPeriod(1)恢复否则会影响系统全局功耗。2.3 判断“鼠标静止”的逻辑设计最朴素的做法是记录上一次的坐标如果这次读到的坐标和上次完全相等就认为静止。但这样有个问题——人手即使“不动”也会有极微小的抖动尤其是一些高 DPI 鼠标静止时坐标也可能在 ±1 像素内跳变。如果严格判断相等程序可能永远不触发。所以更稳妥的方案是引入一个容差阈值当新旧坐标的曼哈顿距离小于某个值比如 2 像素时就视为“没有移动”。代码大概是这样int dx abs(cur.x - last.x); int dy abs(cur.y - last.y); if (dx dy 2) { // 视为静止 }另一个维度是时间。有时候用户只是短暂停顿比如思考时手离开鼠标一秒钟这时候不应该立刻触发否则会显得很“神经质”。我的做法是加一个静止持续时间阈值比如连续 800ms 都处于静止状态才执行干预。这样既不会误触发又能保证效果。2.4 干预动作的三种实现思路“锁鼠标”并不是只有一种做法不同的实现带来的整蛊效果完全不同硬锁定一旦判定静止立刻SetCursorPos回原位每轮都设。效果是鼠标像被钉住怎么推都推不动最直接但也最容易被发现。软回拉允许鼠标移动但每次移动后按一定比例把它拉回原点比如移动 100 像素只让它走 30 像素。效果是鼠标“变重”像在泥里拖比较隐蔽。随机抖动静止时让指针随机偏移几个像素制造“鼠标坏了”的错觉。这个最有迷惑性但实现时要控制幅度否则太夸张。我个人的经验是软回拉最适合整蛊因为对方第一反应是“鼠标是不是没电了”或者“鼠标垫有问题”而不是“电脑中病毒了”。硬锁定太明显容易引起警觉。3. 实操过程与核心环节实现3.1 开发环境准备与编译命令你需要的工具非常少一个 C 编译器即可。推荐用 MinGW-w64 或者 MSVC两者都行。如果你用 Visual Studio直接新建一个空的控制台项目把代码贴进去就能编译。如果用 MinGW命令行一行搞定g -O2 -o mouse_prank.exe mouse_prank.cpp -lgdi32 -luser32注意这里链接了user32库因为GetCursorPos、SetCursorPos、SetProcessDPIAware都在这个库里。-O2是开启优化让循环跑得更紧凑。编译出来的 exe 大概 100 到 200 KB非常小巧。提示如果你想让程序运行时不显示黑色控制台窗口把入口函数从main改成WinMain并在链接时加上-mwindows参数。这样双击运行后任务栏和桌面上看不到任何窗口隐蔽性更好。3.2 完整代码实现与逐段说明下面给出“软回拉”版本的完整代码这是我认为效果最好的一种。代码不长但每一段都有讲究#include windows.h #include cmath int main() { // 声明 DPI 感知避免缩放导致的坐标偏移 SetProcessDPIAware(); POINT origin {0, 0}; GetCursorPos(origin); // 以程序启动时的位置作为“锚点” POINT last origin; DWORD lastMoveTick GetTickCount(); const int TOLERANCE 2; // 静止判定容差像素 const DWORD IDLE_MS 800; // 静止多久后开始干预 const double PULL_RATIO 0.3; // 回拉比例越小越“重” while (true) { POINT cur; GetCursorPos(cur); int dx abs(cur.x - last.x); int dy abs(cur.y - last.y); if (dx dy TOLERANCE) { // 鼠标在动更新锚点和时间 last cur; lastMoveTick GetTickCount(); } else { // 鼠标静止判断是否超过阈值 if (GetTickCount() - lastMoveTick IDLE_MS) { // 计算回拉后的目标位置 int targetX origin.x (int)((cur.x - origin.x) * PULL_RATIO); int targetY origin.y (int)((cur.y - origin.y) * PULL_RATIO); SetCursorPos(targetX, targetY); last {targetX, targetY}; } } Sleep(10); } return 0; }逐段解释一下关键点。origin是程序启动那一刻鼠标所在的位置作为“回拉中心”。last记录上一轮坐标用来判断有没有动。lastMoveTick记录最后一次检测到移动的时间戳。主循环里如果发现移动超过容差就刷新这些状态否则检查静止时长超过 800ms 就执行回拉。回拉公式origin (cur - origin) * ratio的含义是把鼠标从锚点到当前位置的位移按比例缩短。ratio 0.3意味着你移动 100 像素指针只走 30 像素剩下的 70 像素被“吃掉”了。这个比例可以调0.1 会非常粘0.6 则比较轻微。3.3 参数调优的实测记录我在自己的机器上做了一轮参数扫描记录不同PULL_RATIO下的主观感受供你参考回拉比例移动 500 像素后实际到达主观感受0.1约 50 像素几乎推不动像卡死0.3约 150 像素明显变重但能操作0.5约 250 像素有点迟钝像低电量0.7约 350 像素轻微粘滞不易察觉0.9约 450 像素基本正常效果弱如果你想让对方“能用但难受”0.3 到 0.5 之间最合适。如果只是想开个玩笑马上收手0.7 更温和。另外IDLE_MS我建议不要低于 500ms否则对方每次停下来思考都会触发太频繁反而容易暴露。3.4 让程序更隐蔽的几个小技巧第一进程名不要叫 mouse_prank.exe改成svchost.exe或者runtimebroker.exe之类看起来像系统进程的名字放在任务管理器里不容易被一眼认出。第二程序启动后可以延迟几秒再开始工作比如Sleep(5000)这样对方点击运行后不会立刻发现异常等几秒后鼠标才开始“变重”归因会更模糊。第三加一个退出热键比如检测到按下Ctrl Alt Q就退出。这既是给自己留后路也避免对方实在受不了时只能强制关机。实现方式是在循环里用GetAsyncKeyState检测按键状态if (GetAsyncKeyState(VK_CONTROL) 0x8000 GetAsyncKeyState(VK_MENU) 0x8000 GetAsyncKeyState(Q) 0x8000) { break; }注意GetAsyncKeyState的返回值最高位为 1 表示当前按下所以要和0x8000做与运算。这个检测要放在循环里每轮都查一次。4. 常见问题与排查技巧实录4.1 程序运行后鼠标完全没反应这是最常见的问题通常有三个原因。第一没有以管理员权限运行。在某些系统设置下普通权限的进程无法修改其他高权限窗口的鼠标状态尤其是当焦点在任务管理器或者某些系统界面上时。解决办法是右键“以管理员身份运行”。第二DPI 感知没设置导致坐标偏移回拉的目标位置算错了看起来像没效果。加上SetProcessDPIAware()即可。第三循环里Sleep时间太长比如设了 500ms那干预频率太低鼠标早就跑远了。检查一下间隔参数。4.2 鼠标指针抖动或者闪烁如果指针出现明显的抖动说明回拉幅度过大或者频率过高。可以尝试降低PULL_RATIO让每次回拉更温和或者增大Sleep间隔。另一个可能的原因是容差设置太小导致程序把鼠标的微小抖动误判为“移动”反复切换状态。把TOLERANCE从 2 调到 3 或 4 通常能解决。4.3 安全软件弹窗拦截这类程序因为会调用SetCursorPos干预输入某些安全软件会把它标记为“可疑行为”。这不是 bug是正常的风控。如果你只是自己玩可以在安全软件里把这个 exe 加入信任列表。如果是要发给朋友提前说一声让他放行否则他那边一运行就被拦截效果就没了。4.4 多显示器下的坐标异常在多显示器环境下GetCursorPos返回的坐标可能是负数比如副屏在主屏左侧时x 为负。这时候回拉公式依然成立因为它是基于相对位移计算的不受绝对坐标影响。但如果你用的是“硬锁定”方案直接SetCursorPos(origin.x, origin.y)那就要确保origin是在主屏上取的否则可能把鼠标锁到副屏去。我的建议是统一用相对位移方案兼容性最好。4.5 常见问题速查表现象可能原因解决办法完全无效果权限不足 / DPI 未设置管理员运行 SetProcessDPIAware指针抖动回拉比例过大 / 容差过小调低 ratio调高 tolerance被安全软件拦截行为触发风控加入信任列表多屏坐标错乱锚点取在副屏改用相对位移方案CPU 占用高Sleep 间隔过短改为 10ms 以上退出不了没有热键加 CtrlAltQ 退出检测4.6 几个我踩过的坑第一个坑是忘记恢复定时器精度。早期版本我用了timeBeginPeriod(1)来提升Sleep精度结果程序被强制结束后没有调用timeEndPeriod(1)导致系统全局定时器精度一直保持在高位笔记本电池掉电明显变快。后来我改成在退出热键处理里先调用timeEndPeriod再break问题解决。第二个坑是回拉后没有更新last。如果回拉后不把last设成新位置下一轮比较时会把回拉造成的位移误判为“用户移动”从而重置静止计时器导致干预断断续续。这个 bug 很隐蔽表现是“有时候有效有时候没效”查了好久才定位到。第三个坑是在远程桌面或虚拟机里测试。这类环境下鼠标坐标的更新机制和物理机不同GetCursorPos的刷新频率可能很低导致程序判断失准。所以测试一定要在真实物理机上做虚拟机里的结果不能作为依据。5. 扩展玩法与进阶思路5.1 从“锁鼠标”扩展到“锁键盘”同样的思路可以迁移到键盘上。用GetAsyncKeyState轮询按键状态或者用低级键盘钩子SetWindowsHookEx(WH_KEYBOARD_LL, ...)拦截按键就能实现“某些键失灵”的效果。不过键盘钩子的实现复杂度比鼠标高不少而且更容易被安全软件盯上建议先把鼠标版本玩熟了再考虑。5.2 加入随机性和时间窗口单纯的“一直粘”容易被识破高级一点的玩法是间歇性生效比如每工作 30 秒休息 20 秒让对方以为是“偶发故障”。实现方式是在主循环里加一个状态机用GetTickCount判断当前处于“生效期”还是“休眠期”。这种不确定性会大大增加整蛊的迷惑性。5.3 用配置文件控制参数如果想让程序更灵活可以把PULL_RATIO、IDLE_MS、TOLERANCE这些参数写到一个同目录的config.ini里程序启动时读取。这样不用重新编译就能调整效果也方便你根据不同对象“定制”。读取 ini 可以用GetPrivateProfileInt这个 API非常简单。5.4 关于“恶搞”的边界最后说点实在的。这类程序本质上是干扰他人正常使用电脑所以一定要把握分寸。我的原则是只对熟人用用之前确保对方没有正在处理重要工作用完之后主动告知并帮忙退出。如果对方明显着急或者生气立刻停止。技术本身没有对错但使用技术的人要有分寸感。我见过有人把这类程序发到工作群里让同事下载结果对方正在赶报告差点误了事这就过头了。我在实际使用中的体会是这个项目最大的价值不在于“整蛊”本身而在于它逼着你去理解 Windows 的输入模型、坐标系统、定时器精度和进程权限这些底层概念。把这些搞明白了再去看更复杂的桌面自动化、输入模拟、辅助工具开发都会觉得顺理成章。所以别把它只当成一个玩笑当成一块进入 Win32 编程世界的敲门砖收获会大得多。
阅读完成 · 觉得有帮助?
咨询建站