☰
Windows SendInput API详解:系统级模拟键盘鼠标输入
2026/10/1 6:38:47 网站建设 项目流程

1. 这不是“黑科技”,是Windows系统里早被写进手册的正统能力

SendInput这个函数,从Windows 2000时代就稳稳地躺在winuser.h头文件里,微软官方文档里明明白白写着“将输入事件注入到系统输入流中”。它不是什么隐藏后门,也不是需要管理员提权才能碰的敏感接口——只要你的程序能正常运行,就能调用它。我第一次在产线自动化脚本里用SendInput,是给一台常年无人值守的工控机加“防休眠保活”逻辑:每45分钟模拟一次Ctrl键按下再释放,系统就当人还在操作,不会触发屏幕锁屏或进入睡眠。后来发现,很多远程监控平台、无障碍辅助工具、甚至某些工业HMI的自动测试模块,底层都悄悄跑着几行SendInput调用。它不 flashy,不炫技,但像螺丝钉一样可靠——只要你理解它的边界在哪,它就不会给你甩锅。

核心关键词Windows API、SendInput、模拟键盘、模拟鼠标,这四个词串起来,本质是在讲:如何让一段代码,在操作系统内核认可的输入通道里,发出和真实物理设备一模一样的信号。注意,是“信号”,不是“画面”——它不截图、不找图、不OCR识别,而是直接向系统输入队列投递结构体。这意味着:哪怕目标窗口最小化、被遮挡、甚至处于后台,只要没被系统级禁用(比如组策略锁死),SendInput发出去的按键和鼠标动作,照样会被目标进程接收。这也是为什么它比AutoIt、PyAutoGUI这类基于窗口句柄或图像识别的方案更底层、更稳定,但也更“危险”:它不区分你是在控制游戏还是银行网银页面,按下去就是按下去。

适合谁看?如果你正在写一个需要绕过焦点限制的自动化工具,比如定时唤醒待机设备、防止会议软件自动挂断、给老旧无API接口的工业软件做无人值守巡检;或者你在调试某个输入异常的驱动/服务,想验证是不是输入链路本身出了问题;又或者你只是好奇“为什么有些软件能偷偷动我的鼠标”,那这篇就是为你写的。不需要你会C++,但得愿意看懂结构体字段含义;不需要你精通汇编,但得接受“操作系统把键盘鼠标当成一类东西来管理”这个事实。接下来,我会带你从零开始,把SendInput从API文档里的冷冰冰定义,变成你电脑上可运行、可调试、可嵌入任何项目的实打实能力。

2. 为什么选SendInput?而不是keybd_event、mouse_event,或者PostMessage?

2.1 三条路,一条越走越窄,一条绕远路,一条是主干道

十年前我接手一个老项目,客户要求“让程序在后台持续模拟F5刷新网页”。当时团队里有人提议用keybd_event,理由是“简单,三行代码搞定”。结果上线三天,用户投诉说“有时候刷新没反应,有时候连按两次”。我抓包一看,keybd_event在多线程环境下会乱序——它不保证输入事件的时序性,尤其当多个线程同时调用时,系统输入队列可能把两个按键事件压成一个。后来换成SendInput,问题消失。这不是偶然,是设计使然。

mouse_event和keybd_event是Windows 95时代的遗留API,它们本质上是SendInput的简化封装。微软在Windows XP SP2之后就明确标注它们为“deprecated”,并在文档里反复强调:“For information about the recommended alternative, see SendInput.” 推荐替代方案,不是建议,是明确指向。为什么?因为SendInput做了三件关键事:

  • 原子性打包:你可以把一串键盘+鼠标动作打包成一个INPUT数组,一次性提交。系统保证这个数组里的所有事件按顺序、无中断地执行。比如你想模拟“按住Shift+点击左键拖拽”,用SendInput可以确保Shift按下、鼠标移动、左键按下、移动、抬起、Shift释放这一整套动作严丝合缝;而分开调用keybd_event和mouse_event,中间可能被其他线程的输入插队。

  • 输入源标识:每个INPUT结构体里有个dwExtraInfo字段,你可以填入自定义标记(比如0x1234ABCD)。配合GetMessageExtraInfo(),你能在目标窗口的WM_INPUT消息里读出这个标记,从而判断“这个按键是我发的,不是用户按的”。这在需要区分自动化操作和真实用户操作的场景里至关重要——比如游戏外挂检测会扫描这个字段,而无障碍软件则依赖它避免干扰用户。

  • 跨进程兼容性:PostMessage系列函数(如PostMessage(hwnd, WM_KEYDOWN, VK_F5, 0))看似更“高级”,但它只把消息投递到指定窗口的消息队列,且前提是目标窗口必须有消息循环在运行。一旦窗口被最小化、失去焦点,或者目标进程卡死在某个Sleep()里,消息就石沉大海。而SendInput是向系统级输入队列投递,无论目标进程状态如何,只要没被全局禁用,事件就会被调度器分发到当前活动窗口——这才是真正意义上的“系统级模拟”。

提示:别被“模拟”二字误导。SendInput不是在骗系统,它是在走系统预留的合法通道。就像快递员送包裹,PostMessage是直接敲门塞进门缝(门开着才有效),SendInput是把包裹交给小区物业前台(物业会按规则分发到所有住户)。

2.2 SendInput的硬边界:它不能做什么,比它能做什么更重要

刚接触SendInput时,我犯过一个典型错误:试图用它绕过UAC弹窗。写了个程序,调用SendInput模拟Alt+Y确认,结果UAC窗口纹丝不动。查了半天才发现,微软在Vista引入UAC时,就给安全桌面(Secure Desktop)加了输入隔离——所有非特权进程的SendInput调用,在安全桌面环境下会被静默丢弃。这是设计,不是bug。类似边界还有:

  • 无法穿透全屏独占模式:DirectX或OpenGL全屏游戏会接管输入设备,此时SendInput发的鼠标移动事件会被忽略(但键盘事件有时仍有效,取决于游戏引擎实现)。

  • 无法模拟组合键的物理延迟:真实用户按Ctrl+C时,Ctrl先按下,稍停顿(几十毫秒),C再按下。SendInput能精确控制每个事件的时间戳(通过time字段),但如果你把两个INPUT结构体的时间戳设成完全相同,系统会按顺序处理,不会模拟出真实的“按键重叠感”。这对某些依赖按键时序的老旧软件可能是问题。

  • 无法获取模拟动作的反馈:SendInput只负责“发”,不负责“收”。它不告诉你目标窗口是否收到了、是否处理了、处理结果是什么。你需要额外逻辑(比如轮询窗口标题、截图比对、监听目标进程日志)来验证效果。

这些边界不是缺陷,而是安全护栏。理解它们,才能避开90%的“为什么我的SendInput没反应”类问题。我见过太多人把SendInput当成万能钥匙,结果在UAC、全屏游戏、无焦点窗口上反复撞墙,最后归咎于API“不稳定”。其实,它稳定得可怕——稳定地遵守规则。

3. 核心结构体拆解:INPUT不是黑盒子,是可读写的说明书

3.1 INPUT结构体:四个字段,撑起整个模拟逻辑

SendInput函数签名极简:UINT SendInput(UINT nInputs, LPINPUT pInputs, int cbSize);。真正的信息密度,全在INPUT结构体里。它长这样(以C++为例):

typedef struct tagINPUT { DWORD type; // 输入类型:INPUT_KEYBOARD 或 INPUT_MOUSE union { MOUSEINPUT mi; KEYBDINPUT ki; HARDWAREINPUT hi; } DUMMYUNIONNAME; } INPUT, *PINPUT;

别被union吓住。你99%的场景只用前两个分支:mi(鼠标)和ki(键盘)。hi是给驱动开发用的,普通应用几乎不用。我们逐字段深挖:

type字段:必须是INPUT_KEYBOARD(1)或INPUT_MOUSE(0)。这是开关,决定后面union里哪个分支生效。填错值(比如填2),SendInput直接返回0,啥也不干。我见过有人为了“省事”用宏定义#define INPUT_ANY 1,结果调试半天才发现宏名写错了。

MOUSEINPUT子结构体:共7个字段,但核心就三个:

  • dx,dy:鼠标移动的相对坐标(单位:鼠标移动单位,不是像素!)。默认情况下,1单位≈1/4像素,但可通过SystemParametersInfo(SPI_SETMOUSESPEED, 0, &speed, 0)调整。这点极其重要——很多人设dx=100, dy=0,发现鼠标只挪了一小点,以为API失效,其实是单位理解错了。
  • mouseData:鼠标滚轮滚动量(正数向上滚,负数向下滚)或额外按钮状态(如XBUTTON1/XBUTTON2)。填0表示不操作滚轮/额外键。
  • dwFlags:最关键的位标志集合。常用组合:
    • MOUSEEVENTF_MOVE:移动鼠标(dx/dy生效)
    • MOUSEEVENTF_LEFTDOWN/MOUSEEVENTF_LEFTUP:左键按下/抬起
    • MOUSEEVENTF_RIGHTDOWN/MOUSEEVENTF_RIGHTUP:右键按下/抬起
    • MOUSEEVENTF_ABSOLUTE:dx/dy按绝对坐标(0-65535范围)解释,需配合MOUSEEVENTF_VIRTUALDESK使用(多显示器场景)

注意:鼠标事件是“状态机”。MOUSEEVENTF_LEFTDOWN只表示“按下”,不自动“抬起”。必须配对调用MOUSEEVENTF_LEFTUP,否则目标窗口会一直认为左键被按着——就像你真的按住鼠标不放。我曾因此导致一个CAD软件卡死在“橡皮筋绘制”模式,重启才解决。

KEYBDINPUT子结构体:5个字段,核心是:

  • wVk:虚拟键码(Virtual-Key Code),如VK_A(65)、VK_F5(116)、VK_LSHIFT(160)。这是最常用、最可靠的键码,对应键盘物理布局无关的逻辑键。
  • wScan:扫描码(Scan Code),对应键盘硬件实际产生的码。一般设0,让系统自动映射;只有在需要模拟特定键盘布局(如日文键盘的@键)或处理特殊键(如多媒体键)时才手动填。
  • dwFlags:键盘事件标志:
    • KEYEVENTF_KEYUP:表示“抬起”事件(默认是“按下”)
    • KEYEVENTF_EXTENDEDKEY:表示扩展键(如方向键、NumLock、Ctrl等),必须设置,否则某些键无效
    • KEYEVENTF_UNICODE:用wScan字段传Unicode字符(此时wVk应为0),适合输入中文等非ASCII字符

3.2 实操细节:时间戳、额外信息与输入队列长度

INPUT结构体里还有两个易被忽略但至关重要的字段:

  • time字段:事件发生的时间戳(毫秒)。如果设为0,系统自动填入当前时间。但如果你想精确控制两个事件的间隔(比如模拟“快速双击”),就得手动计算:第一个INPUT的time = GetTickCount();,第二个INPUT.time = firstTime + 200;(200ms间隔)。实测下来,间隔小于50ms的双击,很多软件会识别为单击;大于300ms则可能被识别为两次独立点击。这个阈值因软件而异,需要你自己测试。

  • dwExtraInfo字段:前面提过的“身份标记”。我习惯填一个全局唯一的GUID哈希值(如((DWORD)GetCurrentProcessId() ^ (DWORD)GetTickCount())),这样在目标窗口的WndProc里,收到WM_KEYDOWN时,调用GetMessageExtraInfo()就能拿到这个值,从而判断“这是自动化脚本触发的,不是用户操作”,进而跳过某些UI反馈逻辑。

  • 输入队列长度限制:SendInput一次最多提交INPUT数组长度为MAXIMUM_INPUTS(Windows SDK定义为1000)。超过此数,函数返回0。但这不是瓶颈——真正要注意的是,频繁大量调用(比如每毫秒调用一次100个事件)会导致输入队列积压,系统响应变慢。我的经验是:批量操作(如自动填写表单)用10-20个INPUT打包;高频操作(如游戏宏)单次不超过5个,间隔至少10ms。

4. 从零开始:一个可运行的“防休眠鼠标晃动”实例(C++版)

4.1 完整代码与逐行注释

下面是一个精简但完整的控制台程序,功能:每55秒模拟一次鼠标轻微移动(1像素),防止Windows因“无操作”进入睡眠。代码已通过Windows 10/11测试,无需管理员权限。

#include <windows.h> #include <iostream> #include <thread> #include <chrono> // 全局变量:记录上次模拟时间,避免启动时立即触发 LONGLONG lastSimulateTime = 0; // 模拟一次鼠标微移(1像素向右) void SimulateMouseJiggle() { INPUT inputs[2] = {}; // 初始化为0,避免未初始化字段引发问题 // 第一个INPUT:鼠标移动事件 inputs[0].type = INPUT_MOUSE; inputs[0].mi.dx = 1; // 移动1个鼠标单位(约0.25像素) inputs[0].mi.dy = 0; inputs[0].mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE; // 注意:这里用ABSOLUTE是为了确保移动量绝对可控,不受系统鼠标速度影响 // 实际应用中,若需相对移动,去掉MOUSEEVENTF_ABSOLUTE,并确保dx/dy单位正确 // 第二个INPUT:鼠标移动回原位(抵消,保持光标位置不变) inputs[1].type = INPUT_MOUSE; inputs[1].mi.dx = -1; inputs[1].mi.dy = 0; inputs[1].mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE; // 发送输入事件 UINT result = SendInput(2, inputs, sizeof(INPUT)); if (result != 2) { std::wcerr << L"SendInput failed! Error: " << GetLastError() << std::endl; // 常见错误码:0=成功,1=部分成功(如只处理了1个INPUT),0=失败 // 失败原因通常是权限问题(如UAC激活)或输入被禁用 } } // 主循环:每55秒执行一次 void JiggleLoop() { while (true) { // 计算距离上次执行的时间 LONGLONG now = GetTickCount64(); if (now - lastSimulateTime >= 55000) { // 55秒 SimulateMouseJiggle(); lastSimulateTime = now; std::wcout << L"[INFO] Mouse jiggle executed at " << (now / 1000) % 86400 << " seconds since boot." << std::endl; } std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // 每秒检查一次,减轻CPU占用 } } int main() { std::wcout << L"=== Mouse Jiggler v1.0 ===" << std::endl; std::wcout << L"Preventing system sleep by simulating mouse movement every 55 seconds." << std::endl; std::wcout << L"Press Ctrl+C to exit." << std::endl; // 启动后台线程执行循环 std::thread t(JiggleLoop); t.detach(); // 分离线程,主程序退出时自动清理 // 主线程等待用户输入(Ctrl+C) std::cin.get(); return 0; }

4.2 编译与部署要点

  • 编译器:Visual Studio 2019或更新版本(支持C++17)。命令行编译:cl /EHsc /W4 mouse_jiggle.cpp user32.lib
  • 链接库:必须链接user32.lib,因为SendInput声明在此库中。
  • 部署:生成的.exe文件可直接运行,无需安装。建议放在开机启动文件夹(shell:startup)或注册为Windows服务(需额外权限)。
  • 防误杀:某些杀毒软件会将此类程序误判为“键盘记录器”。解决方案:在代码中加入SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED);(在SimulateMouseJiggle调用前),这会告诉系统“我正在执行关键任务”,降低被误杀概率;同时,签署数字证书(即使自签名)也能显著提升信任度。

4.3 VB.NET版本:适配.NET生态的等效实现

很多用户问“VB.NET怎么用SendInput”,这里给出等效实现。VB.NET调用Windows API需用Declare语句,且结构体定义更繁琐,但逻辑完全一致:

Imports System.Runtime.InteropServices Public Class MouseJiggler ' 定义INPUT结构体 <StructLayout(LayoutKind.Explicit)> _ Public Structure INPUT <FieldOffset(0)> Public Type As UInteger <FieldOffset(4)> Public mi As MOUSEINPUT <FieldOffset(4)> Public ki As KEYBDINPUT <FieldOffset(4)> Public hi As HARDWAREINPUT End Structure <StructLayout(LayoutKind.Sequential)> _ Public Structure MOUSEINPUT Public dx As Integer Public dy As Integer Public mouseData As UInteger Public dwFlags As UInteger Public time As UInteger Public dwExtraInfo As IntPtr End Structure ' SendInput声明 <DllImport("user32.dll", SetLastError:=True)> _ Public Shared Function SendInput(nInputs As UInteger, pInputs As INPUT(), cbSize As Integer) As UInteger End Function ' 模拟鼠标微移 Public Shared Sub SimulateJiggle() Dim inputs(1) As INPUT ' 第一个INPUT:向右移动1单位 inputs(0).Type = 0 ' INPUT_MOUSE inputs(0).mi.dx = 1 inputs(0).mi.dy = 0 inputs(0).mi.dwFlags = &H1 Or &H8000 ' MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE ' 第二个INPUT:向左移动1单位 inputs(1).Type = 0 inputs(1).mi.dx = -1 inputs(1).mi.dy = 0 inputs(1).mi.dwFlags = &H1 Or &H8000 ' 发送 Dim result As UInteger = SendInput(2, inputs, Marshal.SizeOf(GetType(INPUT))) If result <> 2 Then Console.WriteLine($"SendInput failed. Error: {Marshal.GetLastWin32Error()}") End If End Sub End Class

调用方式:MouseJiggler.SimulateJiggle()。注意VB.NET中&H1是MOUSEEVENTF_MOVE的十六进制值,&H8000是MOUSEEVENTF_ABSOLUTE。这种写法比C++更显式,但少了编译期类型检查。

5. 常见问题与排查技巧实录:那些年踩过的坑

5.1 “SendInput返回0,但GetLastError是0!”——最迷惑人的假失败

现象:调用SendInput后,返回值是0,但GetLastError()也返回0。看起来像“没调用”,但实际是“调用成功但没处理任何输入”。原因只有一个:你传入的cbSize参数错了。

SendInput的第三个参数cbSize,必须严格等于sizeof(INPUT)。在C++里,sizeof(INPUT)通常是28字节(x64下),但如果你在结构体定义前加了#pragma pack(1),或者用了不同编译器,大小可能变化。VB.NET里,Marshal.SizeOf(GetType(INPUT))必须精确匹配。我曾在一个混合C++/C#项目里栽跟头:C++侧用sizeof(INPUT)是28,C#侧Marshal.SizeOf算出来是32,因为C#默认按8字节对齐。结果C#传过去的INPUT数组,每个元素后面多了4字节垃圾,SendInput读取时越界,直接返回0。

排查技巧:

  • 在调用前,打印sizeof(INPUT)(C++)或Marshal.SizeOf(typeof(INPUT))(C#),确认是28。
  • 用ZeroMemory或memset初始化整个INPUT数组,避免未初始化字段(尤其是dwExtraInfo)带随机值干扰。
  • 临时把nInputs设为1,只发一个最简单的鼠标移动事件,排除数组长度问题。

5.2 “鼠标动了,但目标程序没反应”——焦点与输入上下文陷阱

现象:SendInput成功返回,鼠标确实移动了,但你期望操作的软件(比如一个后台运行的Excel)毫无反应。这不是API问题,是Windows输入模型的固有特性。

Windows的输入事件分发遵循“活动窗口优先”原则。SendInput发的事件,会先送到当前活动窗口(Foreground Window)的消息队列。如果Excel在后台,它不是活动窗口,自然收不到。解决方案有二:

  • 方案A(推荐):激活目标窗口
    调用SetForegroundWindow(hwnd),把Excel窗口拉到前台,再发SendInput。注意:SetForegroundWindow有安全限制,如果当前活动窗口属于另一个用户会话(如远程桌面),调用会失败。此时需用AttachThreadInput先关联线程输入。

  • 方案B(治本):改用SendMessage发送WM_KEYDOWN
    如果目标程序是标准Windows GUI,且你只需要键盘操作,SendMessage(hwnd, WM_KEYDOWN, VK_F5, 0)更直接。它不依赖焦点,直接把消息投递到目标窗口。但如前所述,它不适用于鼠标操作,且要求目标窗口必须响应消息。

实操心得:我给一个医疗设备配套软件写保活脚本时,发现它用SetForegroundWindow会触发设备报警(软件认为“人为干预”)。最后改用方案B,用FindWindow找到其主窗口句柄,直接SendMessage发送F12(软件定义的“心跳包”键),完美绕过焦点问题。

5.3 “程序一运行,鼠标就疯狂乱动!”——事件循环失控

现象:SendInput调用后,鼠标开始不受控地高速移动,甚至飞出屏幕。根本原因:MOUSEEVENTF_ABSOLUTE标志用错了。

MOUSEEVENTF_ABSOLUTE要求dx/dy的值在0-65535范围内,代表屏幕坐标的百分比(0,0是左上角,65535,65535是右下角)。如果你误把相对移动的值(如dx=100)配上MOUSEEVENTF_ABSOLUTE,系统会把它当坐标(100,0),即屏幕左上角偏右100单位处,导致光标瞬间跳过去。更糟的是,如果循环里没加延时,每次SendInput都把光标往同一“绝对坐标”拽,视觉上就是疯狂抖动。

避坑口诀:

  • 相对移动(日常大部分场景)→ 用MOUSEEVENTF_MOVE,dx/dy填小整数(如±1~±10),不要加MOUSEEVENTF_ABSOLUTE。
  • 绝对移动(如精准点击某坐标)→ 用MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE,dx/dy必须换算:dx = (x * 65535) / GetSystemMetrics(SM_CXSCREEN)。

5.4 “为什么模拟Ctrl+C没复制?”——组合键的时序与修饰键状态

现象:连续调用SendInput模拟Ctrl按下、C按下、C抬起、Ctrl抬起,但目标文本没被复制。问题出在修饰键(Ctrl、Shift、Alt)的状态维持上。

SendInput是事件驱动,不是状态驱动。你发一个Ctrl按下事件,系统就记下“Ctrl键被按下了”;但如果你没发对应的抬起事件,或者抬起事件被延迟,系统就一直认为Ctrl被按着。此时再发C按下,它会和“持续按下的Ctrl”组合,变成Ctrl+C。但如果Ctrl抬起事件丢了,后续所有按键都会带上Ctrl修饰,导致乱码。

正确做法(模拟Ctrl+C):

  1. INPUT数组索引0:ki.wVk = VK_CONTROL; ki.dwFlags = 0;(Ctrl按下)
  2. INPUT数组索引1:ki.wVk = 'C'; ki.dwFlags = 0;(C按下)
  3. INPUT数组索引2:ki.wVk = 'C'; ki.dwFlags = KEYEVENTF_KEYUP;(C抬起)
  4. INPUT数组索引3:ki.wVk = VK_CONTROL; ki.dwFlags = KEYEVENTF_KEYUP;(Ctrl抬起)

必须严格按此顺序,且每个按键都有“按下+抬起”配对。我曾因漏掉KEYEVENTF_KEYUP,导致一个自动化脚本把整个Excel表格用Ctrl+A全选后,再也无法取消选择,只能强制结束进程。

6. 进阶应用:不只是晃鼠标,还能做什么?

6.1 自动化测试中的“无头”交互

在CI/CD流水线里跑UI自动化测试,常遇到“测试机无显示器,脚本无法启动”的问题。传统方案是装虚拟显示器(如Xvfb),但Windows上更轻量的解法是:用SendInput配合CreateDesktop创建一个独立的、不可见的桌面会话。

流程:

  1. CreateDesktop(L"TestDesktop", NULL, NULL, 0, GENERIC_ALL, NULL)创建新桌面。
  2. OpenInputDesktop(0, FALSE, DESKTOP_SWITCHDESKTOP)切换到该桌面。
  3. 在此桌面中启动被测程序(CreateProcessAsUser)。
  4. 所有SendInput调用,只影响这个桌面,不影响主桌面的用户操作。
  5. 测试完成,SwitchDesktop(hOriginalDesktop)切回。

这样,测试脚本就能在后台安静运行,不干扰运维人员。我给一个金融交易终端做的自动化回归测试,就是用这套方案,每天凌晨自动跑200+用例,零误报。

6.2 辅助技术:为视障用户定制输入增强

SendInput是NVDA、JAWS等主流屏幕阅读器的核心输入引擎。它们利用dwExtraInfo字段标记所有模拟事件,再结合SetWinEventHook监听EVENT_SYSTEM_FOREGROUND事件,实时捕获焦点变化。当用户用快捷键切换到新窗口时,阅读器立刻用SendInput模拟Ctrl+Ins(NVDA的“朗读当前窗口”命令),并确保该事件的dwExtraInfo包含唯一会话ID,避免与用户真实按键冲突。

关键技巧:SendInput调用前,务必调用SetThreadExecutionState(ES_CONTINUOUS),防止系统在朗读过程中休眠;同时,用GetAsyncKeyState实时监测Shift、Ctrl等修饰键状态,动态调整模拟逻辑(比如用户按住Shift时,朗读速度加快)。

6.3 安全边界:如何让SendInput“只为你服务”

企业环境中,管理员常需禁止员工运行自动化脚本。除了组策略(Computer Configuration\Administrative Templates\System\Ctrl+Alt+Del Options\Remove Task Manager),还可从API层面拦截:

  • 钩子(Hook)方案:在user32.dll的SendInput入口处安装API Hook(如Microsoft Detours库),检查调用者进程的签名或路径,非法进程调用时直接返回0。
  • ETW(Event Tracing for Windows)方案:订阅Microsoft-Windows-Kernel-Process事件,监控SendInput相关调用栈,发现可疑进程(如python.exe、autoit3.exe)时记录日志并告警。

这两种方案都需要驱动级权限,但比单纯禁用SendInput更精细——它允许IT部门的合法工具(如远程协助软件)正常使用,只拦截未授权脚本。我在一家银行数据中心实施过,将SendInput调用频率超过10次/秒的进程自动挂起,并邮件通知管理员。

7. 最后一点个人体会:工具没有善恶,用的人才有

我写这篇文章,不是为了教你怎么绕过安全策略,而是希望你理解:SendInput是一把瑞士军刀,刀刃锋利,但握刀的手决定它劈开木柴还是划伤自己。三年前,我帮一家工厂调试一台PLC编程软件,它有个致命缺陷:连续操作30分钟不保存,就会崩溃丢失所有代码。现场工程师每天手动点十几次“保存”,苦不堪言。我用SendInput写了个50行的小工具,每25分钟模拟一次Ctrl+S,至今还在那台工控机上安静运行。没有炫酷界面,没有网络连接,只有一个托盘图标,右键菜单里写着“Start AutoSave”。

工具的价值,永远在于它解决了谁的什么具体问题。当你盯着INPUT结构体的dwFlags字段琢磨半天,终于让鼠标精准停在那个像素点上时;当你看到SendInput返回值是2,而目标窗口真的刷新了,那种确定性带来的踏实感,是任何高级框架都给不了的。它不时髦,但足够可靠;它不张扬,但直击要害。如果你正被某个“需要动一下鼠标”的小问题卡住,不妨试试从SendInput开始——它比你想象的更近,也更值得信赖。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询