简介这是一份面向C#桌面开发初学者与需要重复点击操作的办公人群的Winform实战示例通过调用Windows API中的user32.dll实现鼠标事件的模拟与自动点击。资源围绕非托管代码的引入方式展开使用DllImport特性完成声明并借助user32.dll提供的窗口消息与鼠标信号传递能力实现固定位置按钮的定时点击适合想了解Windows API调用、消息机制或自制按键工具的开发者参考。压缩包共40个文件约6.11MB以cs源码、cache缓存、pdb调试符号、config配置、exe可执行程序、resx资源及csproj工程文件为主另含sln解决方案与suo用户选项构成一套可直接编译运行的完整工程。目前已有674人学习下载。源码结构简单易懂读者可自由修改点击坐标与间隔逻辑并在此基础上扩展热键、循环次数等功能遇到问题也可与作者交流排错思路。1. 从“模拟点击”说起一份能直接跑的 C# Winform 鼠标事件源码包很多人第一次接触桌面自动化都是从一个很朴素的需求开始的某个软件每天要重复点几十次同样的按钮或者游戏里需要定时在固定位置点一下。市面上的自动点击器要么带广告要么功能被锁要么干脆不给你看源码。这份 C# Winform 资源包走的是另一条路——它把user32.dll里那几个最核心的鼠标事件 API 直接摊开给你看配合一个完整的 Winform 界面你能清楚地看到坐标怎么取、消息怎么发、点击怎么触发。它适合两类人一是想搞明白 Windows 消息机制到底怎么落到代码里的 C# 开发者二是需要一个可改可扩的自动点击工具底座的从业者。源码结构不复杂但该有的都有全局热键、坐标拾取、点击频率控制、前后台点击切换。下面我按实际拆包的顺序把这份资源从原理到跑通再到踩坑完整过一遍。2. 先搞懂 user32.dll 里那几个鼠标 API选型与原理2.1 为什么是 user32.dll而不是 SendKeys 或 UI Automation在 Windows 上做鼠标模拟常见做法有三条路。第一条是System.Windows.Forms.SendKeys但它只能发键盘消息鼠标事件基本无能为力。第二条是 UI Automation功能强、能识别控件但依赖目标程序的无障碍接口很多游戏和自绘界面根本不支持而且调用链长、延迟高。第三条就是直接调user32.dll的mouse_event或SendInput绕过控件层直接往系统输入队列里塞鼠标事件。这份资源选的是第三条路原因很实际它要的是“不管目标窗口是什么技术栈只要在屏幕上就能点”。user32.dll是 Windows 用户态输入的核心库mouse_event和SendInput都在里面。前者是老 API兼容性好但微软已经不推荐后者是新一代支持一次发送多个输入事件更可靠。资源包里两个都有封装默认走SendInput保留mouse_event作为兼容回退。注意SendInput在目标程序以管理员权限运行时如果你的程序不是管理员权限事件会被 UIPI 拦截表现为“代码没报错但点不动”。这是后面避坑章节要重点说的。2.2 核心 API 签名与参数含义先看资源包里对SendInput的 P/Invoke 声明这是整个工具的基石using System; using System.Runtime.InteropServices; public class Win32Api { // SendInput 的输入类型0 是鼠标1 是键盘2 是硬件 public const int INPUT_MOUSE 0; // 鼠标事件标志 public const uint MOUSEEVENTF_MOVE 0x0001; // 移动 public const uint MOUSEEVENTF_LEFTDOWN 0x0002; // 左键按下 public const uint MOUSEEVENTF_LEFTUP 0x0004; // 左键抬起 public const uint MOUSEEVENTF_RIGHTDOWN 0x0008; // 右键按下 public const uint MOUSEEVENTF_RIGHTUP 0x0010; // 右键抬起 public const uint MOUSEEVENTF_ABSOLUTE 0x8000; // 使用绝对坐标 [StructLayout(LayoutKind.Sequential)] public struct INPUT { public int type; public MOUSEINPUT mi; } [StructLayout(LayoutKind.Sequential)] public struct MOUSEINPUT { public int dx; // x 坐标 public int dy; // y 坐标 public uint mouseData; // 滚轮数据这里不用 public uint dwFlags; // 事件标志 public uint time; // 时间戳传 0 即可 public IntPtr dwExtraInfo; // 额外信息传 0 } [DllImport(user32.dll, SetLastError true)] public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize); [DllImport(user32.dll)] public static extern bool SetCursorPos(int X, int Y); [DllImport(user32.dll)] public static extern bool GetCursorPos(out System.Drawing.Point lpPoint); }这段代码里几个关键点值得展开。INPUT结构体的type字段决定系统怎么解释后面的联合体这里固定为INPUT_MOUSE。MOUSEINPUT里的dx和dy在相对模式下是偏移量在绝对模式下是归一化到 0~65535 的坐标值——这是最容易翻车的地方后面会细说。dwFlags是位标志组合一次点击需要发两个事件先LEFTDOWN再LEFTUP中间不加延时的话有些程序会识别不到。2.3 绝对坐标与相对坐标的换算逻辑资源包里封装了一个MoveTo方法用的是绝对坐标模式。但SendInput的绝对坐标不是像素值而是把屏幕宽高映射到 0~65535 的整数。换算公式是// 屏幕像素坐标转 SendInput 绝对坐标 int normalizedX (int)(pixelX * 65535.0 / (Screen.PrimaryScreen.Bounds.Width - 1)); int normalizedY (int)(pixelY * 65535.0 / (Screen.PrimaryScreen.Bounds.Height - 1));为什么要减 1因为 65535 对应的是屏幕最右下角那个像素如果直接除以宽度最右边一列会溢出到屏幕外。这个细节很多网上抄来的代码都没写对表现就是“点屏幕最右边按钮时总是差几个像素”。如果你用相对模式dx和dy就是相对于当前光标位置的偏移适合做小幅移动但做绝对定位时容易累积误差。资源包默认用绝对模式只有在“相对移动”功能里才切到相对模式。2.4 前台点击与后台点击的本质区别资源包界面上有个“后台点击”复选框这不是噱头。前台点击走的是SendInput事件进入系统全局输入队列光标会真的移过去目标窗口必须在前台。后台点击走的是另一条路PostMessage往目标窗口句柄发WM_LBUTTONDOWN和WM_LBUTTONUP光标不动窗口可以在后台。但后台点击有个硬限制很多程序尤其是游戏和带 DirectX 渲染的界面不处理PostMessage发来的鼠标消息因为它们直接从输入设备读状态。所以资源包里后台点击是可选功能默认关闭并且注释里写明了“仅对部分标准 Win32 控件有效”。这个边界感很重要不然用户会以为后台点击万能。3. 把源码跑起来从坐标拾取到定时点击的完整链路3.1 全局热键注册与坐标拾取资源包启动后第一步是注册全局热键用来在任意时刻抓取当前鼠标坐标。核心代码在MainForm.cs里// 注册全局热键 F1用于拾取当前鼠标坐标 [DllImport(user32.dll)] private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk); [DllImport(user32.dll)] private static extern bool UnregisterHotKey(IntPtr hWnd, int id); private const int HOTKEY_ID 9000; private const uint MOD_NONE 0x0000; private const uint VK_F1 0x70; protected override void OnHandleCreated(EventArgs e) { base.OnHandleCreated(e); // 注册 F1 为全局热键无修饰键 RegisterHotKey(this.Handle, HOTKEY_ID, MOD_NONE, VK_F1); } protected override void WndProc(ref Message m) { const int WM_HOTKEY 0x0312; if (m.Msg WM_HOTKEY m.WParam.ToInt32() HOTKEY_ID) { // 拾取当前鼠标位置并填入界面 GetCursorPos(out Point p); txtX.Text p.X.ToString(); txtY.Text p.Y.ToString(); } base.WndProc(ref m); }这里RegisterHotKey的第三个参数是修饰键MOD_NONE表示不需要按 Ctrl 或 Alt。第四个参数是虚拟键码VK_F1对应 F1 键。热键消息WM_HOTKEY会发到窗口过程通过WndProc拦截。注意OnHandleCreated里注册是因为热键需要窗口句柄太早注册会失败。坐标拾取的实际操作流程是把鼠标移到目标位置按 F1坐标自动填入输入框。这比手动输坐标靠谱得多尤其是多显示器环境下负坐标也能正确抓到。3.2 点击任务的参数配置与启动逻辑界面上需要配置的参数有五个X 坐标、Y 坐标、点击间隔毫秒、点击次数0 表示无限、鼠标按键左/右/中。启动按钮的点击事件里资源包用了一个Timer来驱动private System.Windows.Forms.Timer clickTimer; private int clickCount 0; private int targetCount 0; private void btnStart_Click(object sender, EventArgs e) { // 读取界面参数 int x int.Parse(txtX.Text); int y int.Parse(txtY.Text); int interval int.Parse(txtInterval.Text); targetCount int.Parse(txtCount.Text); clickCount 0; // 配置定时器 clickTimer new System.Windows.Forms.Timer(); clickTimer.Interval interval; // 单位毫秒 clickTimer.Tick ClickTimer_Tick; clickTimer.Start(); } private void ClickTimer_Tick(object sender, EventArgs e) { // 执行一次点击 DoClick(currentX, currentY, MouseButton.Left); clickCount; if (targetCount 0 clickCount targetCount) { clickTimer.Stop(); clickTimer.Dispose(); } }Timer.Interval的单位是毫秒但 Windows 窗体定时器的精度大约在 15ms 左右设成 1ms 也不会真的 1ms 触发一次。资源包在注释里写了这一点建议最小间隔不要低于 20ms否则实际频率和设定值偏差很大。targetCount为 0 时表示无限循环直到手动停止。3.3 单次点击的完整事件序列DoClick方法是整个工具最核心的部分它负责组装INPUT数组并调用SendInputpublic static void DoClick(int x, int y, MouseButton button) { // 先移动光标到目标位置 SetCursorPos(x, y); System.Threading.Thread.Sleep(10); // 给系统一点时间处理移动 // 组装按下和抬起两个事件 INPUT[] inputs new INPUT[2]; // 按下事件 inputs[0].type INPUT_MOUSE; inputs[0].mi.dx 0; inputs[0].mi.dy 0; inputs[0].mi.mouseData 0; inputs[0].mi.time 0; inputs[0].mi.dwExtraInfo IntPtr.Zero; inputs[0].mi.dwFlags button MouseButton.Left ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_RIGHTDOWN; // 抬起事件 inputs[1].type INPUT_MOUSE; inputs[1].mi.dx 0; inputs[1].mi.dy 0; inputs[1].mi.mouseData 0; inputs[1].mi.time 0; inputs[1].mi.dwExtraInfo IntPtr.Zero; inputs[1].mi.dwFlags button MouseButton.Left ? MOUSEEVENTF_LEFTUP : MOUSEEVENTF_RIGHTUP; // 一次性发送两个事件 uint result SendInput(2, inputs, Marshal.SizeOf(typeof(INPUT))); if (result ! 2) { // 发送失败记录错误码 int err Marshal.GetLastWin32Error(); Console.WriteLine($SendInput 失败错误码{err}); } }这里有几个设计决策。第一先SetCursorPos再SendInput而不是直接用SendInput的绝对坐标移动。原因是SetCursorPos更直观而且移动和点击分开后调试时能清楚看到是哪一步出了问题。第二按下和抬起之间没有加Sleep因为SendInput是原子操作两个事件会按顺序进入队列中间不会被其他输入打断。第三返回值检查很重要如果返回不是 2说明有事件被拦截错误码通常是 5拒绝访问意味着权限不够。3.4 多显示器与 DPI 缩放下的坐标处理资源包在Program.cs里加了一行 DPI 感知声明[STAThread] static void Main() { // 声明 DPI 感知避免高缩放显示器下坐标偏移 SetProcessDPIAware(); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm()); } [DllImport(user32.dll)] private static extern bool SetProcessDPIAware();不加这行的话在 125% 或 150% 缩放的显示器上GetCursorPos返回的是逻辑坐标而SetCursorPos期望的是物理坐标两者对不上表现就是“拾取的坐标和实际点击位置差一截”。多显示器环境下主显示器的左上角是 (0,0)副显示器在主屏左边的话 X 坐标是负数SendInput的绝对坐标模式对负坐标支持不好资源包的做法是副屏也用SetCursorPos移动然后发相对模式的点击事件。4. 避坑与排查那些让你怀疑人生的翻车现场4.1 现象代码不报错但目标程序毫无反应原因最常见的是权限不对等。目标程序以管理员身份运行而你的 Winform 程序是普通权限Windows 的 UIPI 机制会静默丢弃SendInput事件。另一种可能是目标程序用了 Raw Input 或 DirectInput根本不走系统输入队列。解决在app.manifest里把requestedExecutionLevel设为requireAdministrator让工具以管理员权限启动。如果是 Raw Input 类程序SendInput无解只能换驱动级方案或硬件模拟这超出了这份资源的范围。4.2 现象点击位置总是偏一点越往屏幕右下角偏得越多原因绝对坐标归一化时用了屏幕宽度而不是宽度减一或者没有声明 DPI 感知。前者导致最右列溢出后者导致缩放显示器下坐标被系统二次换算。解决检查换算公式里是否减了 1检查Main函数里是否调用了SetProcessDPIAware。如果目标程序自身也做了 DPI 缩放还需要用GetWindowRect获取目标窗口的实际物理坐标而不是依赖屏幕坐标。4.3 现象点击频率设成 10ms实际一秒只点了十几次原因System.Windows.Forms.Timer的精度受消息循环影响最小实际间隔在 15ms 左右而且界面越复杂越不准。另外SetCursorPos和SendInput之间如果加了Sleep也会拖慢节奏。解决对精度要求高的场景换System.Threading.Timer或Stopwatch配合循环。资源包里留了一个HighPrecisionMode的开关打开后用Stopwatch计算时间差但会占用更多 CPU。一般办公自动化场景20~50ms 间隔足够。4.4 现象后台点击对某些窗口有效对另一些完全无效原因PostMessage发送的WM_LBUTTONDOWN需要目标窗口的窗口过程主动处理。标准 Win32 按钮、编辑框会处理但游戏、浏览器、WPF 自绘控件通常不处理它们从输入队列直接读状态。解决后台点击只作为辅助功能不要指望它通吃。资源包在界面上加了提示文字说明后台点击仅对标准控件有效。如果必须后台操作游戏需要更底层的方案比如直接读写目标进程内存或模拟驱动输入那已经不是这份源码的覆盖范围了。4.5 现象程序运行一段时间后热键失效或点击停止原因RegisterHotKey注册的热键在窗口句柄重建时会丢失比如切换用户或某些系统事件后。另外Timer如果被Dispose后没有重新创建再次启动会报空引用。解决在OnHandleCreated里注册热键在OnHandleDestroyed里注销。Timer每次启动时重新new一个实例不要复用已Dispose的对象。资源包在btnStop_Click里做了完整的清理停止时注销热键、释放定时器、重置计数。5. 进阶技巧把点击工具改造成可配置的自动化脚本引擎这份源码最值钱的地方不是“能点击”而是它提供了一个干净的底座你可以往上叠自己的逻辑。我一般会做三件事。第一把坐标和动作序列抽成 JSON 配置。与其在界面上一个个填不如定义一个动作数组每个动作包含坐标、按键、延时、点击次数。这样一套配置可以对应一个完整的操作流程换台机器只要改坐标就行。// 动作序列的 JSON 结构示例 { actions: [ { x: 100, y: 200, button: left, delay: 500, repeat: 1 }, { x: 300, y: 400, button: left, delay: 1000, repeat: 3 }, { x: 500, y: 600, button: right, delay: 200, repeat: 1 } ] }解析这个 JSON 用System.Text.Json或Newtonsoft.Json都行然后在一个循环里依次执行每个动作之间用Thread.Sleep或Task.Delay控制节奏。这样就从“单点点击器”变成了“流程自动化工具”。第二加一个条件判断层。比如点击后截图用Graphics.CopyFromScreen抓取目标区域和预设的模板图做简单比对匹配成功才继续下一步。这比盲点可靠得多尤其是在网络延迟导致界面加载慢的场景下。模板匹配可以用AForge.NET或OpenCvSharp代码量不大但稳定性提升明显。第三把日志写清楚。每次点击记录时间戳、坐标、执行结果输出到ListView或文件。出问题时能回溯是哪一步偏了而不是靠猜。资源包里已经有一个简单的日志框我把它扩展成了带时间戳和错误码的格式。验证改造是否成功的方法很简单录一段手动操作把坐标和间隔抄进 JSON跑一遍看结果是否一致。如果中间有一步失败日志会告诉你卡在哪。从那以后我每次做自动化脚本都强制先跑一遍“单步模式”——每个动作执行前弹确认框确认无误再切自动。这个习惯帮我省了无数次的后悔药。希望帮到你。本文还有配套的精品资源点击获取