简介:以微信自动化为目标的 Winform 桌面工具源码工程,基于 FlaUI 完成定时发送、自动回复和群聊机器人等交互场景。适合具备 C# 与 Winform 基础、希望快速上手 Windows 客户端 UI 自动化,或需要参考任务调度与消息监听思路的开发者。压缩包共 365 个文件、大小约 47.83MB,主要文件类型包括 cs 源码、dll 依赖、so 原生库、json 配置、resources 资源和 png 图片等;cs 可查看程序逻辑,dll 与 so 支撑 FlaUI 与 SQLite 运行,json 与 resources 对应配置和界面资源。项目内完整呈现 WX.AutomationManage 解决方案,覆盖工程入口、工具类、数据访问与自动化处理等模块,便于分块学习定时任务、自动回复和群聊交互流程。当前已有 659 人学习下载。通过学习可掌握 FlaUI 元素查找、窗口交互、定时触发、关键词匹配及数据存储调用方法,并理解一套自动化工具从界面操作到后台逻辑的完整组织方式,便于迁移到其他桌面软件自动化场景。
1. Winform 基于 FlaUI 做微信自动化:先把这件事的边界说清楚
每天早上一开电脑,先打开微信给客户发当日报价、往项目群里贴同步信息、逐条回复相似问题——这些操作重复到让人怀疑人生。Winform 基于 FlaUI 实现微信自动化,就是把这些肉眼可见的界面操作交给程序去点:你写一个 Winform 小工具,通过 FlaUI 这个 UI Automation 封装库去“看”微信窗口里的控件树,定位会话、输入框、发送按钮,然后模拟真实点击和输入,把整个流程跑起来。它解决的痛点是“不碰协议、不读内存、不注入进程”,纯靠 Windows 自带的 UI Automation 接口操作界面,对个人办公场景来说是最稳妥的一条路。适合有 .NET 基础、想把手头重复微信操作脚本化的开发者,也适合需要在 Winform 项目里集成微信自动化能力的项目案例。它做不了外挂级的批量营销,但做“替人点鼠标”的自动化工具,完全够用。
2. FlaUI 为什么能接管微信窗口:三条路线对比与运行环境
2.1 三条自动化路线的取舍
做微信桌面版自动化,业界常见的路线有三条。第一条是 hook 消息,拦截微信窗口的 Win32 消息或内部函数调用,能拿到聊天数据,但涉及注入和逆向,微信一升级就崩,稳定性差且规则上容易踩线。第二条是读内存或抓本地协议包,能拿到结构化数据,但同样面临版本升级失效、定位偏移、维护成本爆炸的问题。第三条就是我这次要讲的 UI Automation 路线:操作系统本身提供 UIA 接口,允许辅助工具读取界面元素树、获取控件属性、调用控件模式,微信窗口再特殊,它仍然是一棵 Windows 窗口树,消息列表、输入框、按钮这些 UI 元素多多少少都暴露在 UIA 下。
FlaUI 是这条路线里最成熟的 .NET 封装库。它没有直接操作微信的能力,它做的事情是“翻译”——把 Windows 的 UIA COM 接口翻译成你熟悉的 AutomationElement、Condition、Pattern 这些对象。你用 FlaUI 写代码,本质上和你用 Inspect 工具看微信窗口里有什么控件是一模一样的,只是把人工查看变成了程序化查找。为什么在 Winform 项目里选它而不是微软官方的 System.Windows.Automation?因为官方库自带的 UIA2 托管实现在处理自绘控件、虚拟列表时性能差、元素丢失多,而 FlaUI 同时给你 UIA2 和 UIA3 两套实现,微信这类基于自绘的客户端只能靠 UIA3 救场。
2.2 UIA2 与 UIA3 的差异,以及 FlaUI 的选择
FlaUI 里有三个命名空间:FlaUI.Core 是抽象层,FlaUI.UIA2 走 .NET Framework 自带的托管 UIA,FlaUI.UIA3 走 Windows 原生 COM 接口。微信这种重度自绘的界面,UIA2 经常拿不到消息列表项,或者拿到之后 Text 属性是空字符串,检查控件树时能看到的元素比 UIA3 少一大截。原因在于 UIA3 直接调用系统级的 UIAutomationCore.dll,对自绘控件的命中测试和属性提取能力更强。
所以我的建议很直接:新项目一律用 UIA3。初始化对象就一行:
using FlaUI.UIA3; var automation = new UIA3Automation();FlaUI.Core 里的 Application、Window 这些对象不区分 UIA2 还是 UIA3,只有这一个入口有区别。UIA3Automation 实现了 IDisposable,整个程序生命周期里保持单例即可,不需要反复创建。UIA3 的代价是偶尔元素引用会陈旧,操作时抛 ElementDisconnectedException,但这个问题在微信自动化里可以通过“用前重查、不用缓存”的编码习惯规避,后面避坑章节我会专门讲。
2.3 运行环境与 Winform 宿主注意事项
跑微信自动化的环境就三件事:操作系统 Windows 10 或 11,.NET 版本选 .NET Framework 4.8 或 .NET 6 以上,NuGet 引用 FlaUI.UIA3 包。Winform 项目里注意一点:FlaUI 的查找操作是同步阻塞的,微信界面如果卡顿,FindFirst 会拖住 UI 线程,整个窗体变成“未响应”。我一般把自动化操作丢到 async Task 或后台线程里执行,UI 线程只负责显示日志和状态。得想清楚一件事:微信窗口的 UIA 元素树是在微信进程里维护的,你从外部 attach 时,如果微信是以管理员权限启动的,你的 Winform 必须是管理员权限才能附加成功,否则进程句柄能拿到,但元素查找全挂。这个坑我放在第 4 章细说。
还有个小细节:FlaUI 操作微信时,微信窗口不能最小化到托盘,窗口必须处于可见状态,否则虚拟列表不会渲染当前屏幕外的元素,查找会扑空。不需要微信获得焦点,但需要窗口可见。这是 UI Automation 路线的物理限制——系统按需渲染界面元素,窗口不可见就直接不渲染。
3. 跑通第一条自动化链路:进程附加、元素定位与发送
3.1 附加到微信进程并拿到主窗口句柄
写 Winform 微信自动化的第一步,是先通过进程名拿到微信主窗口。微信的进程名是 WeChat,不是 WeChatAppEx 或者别的子进程,直接按进程名过滤:
using System.Diagnostics; using FlaUI.Core; using FlaUI.Core.AutomationElements; using FlaUI.UIA3; var automation = new UIA3Automation(); var processes = Process.GetProcessesByName("WeChat"); if (processes.Length == 0) { MessageBox.Show("未检测到微信进程,请先登录微信。"); return; } var app = Application.Attach(processes[0].Id); var window = app.GetMainWindow(new TimeSpan(0, 0, 10)); if (window == null) { MessageBox.Show("获取微信主窗口失败。"); return; }这段代码做了三件事:按进程名查微信、附加到进程、拿主导窗口。Application.Attach 不会启动新进程,只绑定已有进程;GetMainWindow 的 TimeSpan 是等待窗口就绪的超时时间,微信启动后窗口可能还在初始化,给 10 秒足够。窗口句柄拿到之后,所有元素查找都以 window 为根节点往下找,比每次从桌面根节点全盘扫描快得多。
如果拿到的 window 为 null,最常见原因是微信窗口在托盘里。这时候进程存在但主窗口句柄为 0,需要在任务栏点开微信,或者代码里用 Win32 ShowWindow 恢复窗口。我后面会给出一个通用的“激活微信窗口”方法。
3.2 元素定位:从条件构造到命中测试
拿到主窗口后,下一个目标是找到左侧会话列表里的某个联系人。微信的会话列表是一个自绘 ListView,UIA 暴露出来的结构大致是:窗口下面有一个列表容器,每个会话是 ListItem,ListItem 的 Name 就是好友备注或群名称。用 FlaUI 的 FindFirstDescendant 加条件就能定位:
using FlaUI.Core.AutomationElements; using FlaUI.Core.Definitions; // 查找名为“文件传输助手”的会话项 var targetSession = window.FindFirstDescendant(cf => cf .ByName("文件传输助手") .And(cf.ByControlType(ControlType.ListItem)));FlaUI 的条件构建用的是 System.Linq.Expressions 的风格,cf => cf.ByName(...).And(...) 最后转成一个 ConditionTree。FindFirstDescendant 默认做深度优先遍历,从当前节点往下扫整棵子树。注意这里别用 FindFirstChild,它只查直接子节点,而微信窗口的层级很深,会话列表嵌了好几层。
如果要查找的元素不一定存在,得先判断 null 再操作。微信界面渲染有延迟,尤其是消息列表刚打开时,元素树可能还没建完。第一次查不到,不一定是找错名字,也可能是渲染没完成。正确的做法是轮询:
private AutomationElement? WaitForElement(AutomationElement root, Func<ConditionFactory, ConditionBase> condition, int timeoutMs = 5000) { var sw = Stopwatch.StartNew(); while (sw.ElapsedMilliseconds < timeoutMs) { var element = root.FindFirstDescendant(condition); if (element != null) return element; Thread.Sleep(200); } return null; }这个方法按 200 毫秒的间隔轮询查找,最多等 5 秒。微信聊天窗口打开、会话切换、消息刷新都有延迟,无脑等待 1 秒再查效率太低,轮询是这里最实用的技巧。参数 timeoutMs 要根据场景调:首次加载窗口给 5000,切换会话给 2000,发送后确认给 1000,太长反而掩盖问题。
3.3 在输入框里写消息并触发发送
定位到会话项之后,单击它进入聊天窗口,然后找消息输入框。微信输入框在 UIA 树里通常是一个 Edit 控件,AutomationId 或 Name 在不同版本里不稳定,我用 ControlType 加位置来兜底:
var inputBox = WaitForElement(window, cf => cf.ByControlType(ControlType.Edit)); if (inputBox == null) return; // 通过 ValuePattern 写入文本 var valuePattern = inputBox.Patterns.Value.PatternOrDefault; if (valuePattern != null) { valuePattern.SetValue("这是由 Winform + FlaUI 发送的自动化消息。"); } else { inputBox.Focus(); Thread.Sleep(100); SendKeys.SendWait("这是由 Winform + FlaUI 发送的自动化消息。"); }先走 ValuePattern.SetValue,这条路最稳定,直接绕过键盘输入法状态。但微信输入框对 SetValue 的支持在不同版本上有差异,有些版本会写入成功但不触发微信内部的输入事件,导致发送按钮仍然是灰色。如果 SetValue 走不通,退回到 Focus + SendKeys 的键盘模拟方案。注意 SendKeys 会受当前输入法影响,如果系统正处于中文输入法状态,英文字符串会变成拼音上屏,所以优先推荐 ValuePattern。
发送按钮的定位和点击:
var sendButton = WaitForElement(window, cf => cf.ByName("发送")); if (sendButton == null) { // 部分版本不暴露发送按钮,直接回车发送 inputBox.Focus(); SendKeys.SendWait("{ENTER}"); return; } var invokePattern = sendButton.Patterns.Invoke.PatternOrDefault; invokePattern?.Invoke();微信 3.9 的发送按钮在 UIA 树里是一个 Button,Name 叫“发送”。有些版本在输入框为空时按钮不渲染,输入文字后才冒出来,所以这里也走轮询。如果发送按钮根本找不到,直接回车是兜底方案——微信默认回车发送消息,输入框有焦点时 SendKeys 发 Enter 就能触发。
3.4 把完整流程组装成一个 Winform 方法
整个最小链路串起来,就是一个“给指定联系人发消息”的完整方法:
private async Task SendWeChatMessageAsync(string contactName, string message) { await Task.Run(() => { using var automation = new UIA3Automation(); var process = Process.GetProcessesByName("WeChat").FirstOrDefault(); if (process == null) throw new InvalidOperationException("微信未运行"); var app = Application.Attach(process.Id); var window = app.GetMainWindow(TimeSpan.FromSeconds(10)); if (window == null) throw new InvalidOperationException("获取微信主窗口失败"); // 点击会话搜索框并输入联系人名 var searchBox = WaitForElement(window, cf => cf.ByName("会话搜索框") .And(cf.ByControlType(ControlType.Edit))); searchBox?.Click(); Thread.Sleep(300); SendKeys.SendWait(contactName); Thread.Sleep(500); // 点击搜索结果中的会话项 var session = WaitForElement(window, cf => cf.ByName(contactName) .And(cf.ByControlType(ControlType.ListItem))); if (session == null) throw new InvalidOperationException($"找不到会话:{contactName}"); session.Click(); // 定位输入框并输入消息 var inputBox = WaitForElement(window, cf => cf.ByControlType(ControlType.Edit)); if (inputBox == null) throw new InvalidOperationException("找不到消息输入框"); inputBox.Click(); Thread.Sleep(200); var valuePattern = inputBox.Patterns.Value.PatternOrDefault; if (valuePattern != null) { valuePattern.SetValue(message); } else { SendKeys.SendWait(message); } // 点击发送或回车 var sendButton = WaitForElement(window, cf => cf.ByName("发送") .And(cf.ByControlType(ControlType.Button)), 2000); if (sendButton != null) { sendButton.Patterns.Invoke.PatternOrDefault?.Invoke(); } else { inputBox.Focus(); SendKeys.SendWait("{ENTER}"); } }); }这个方法的流程是:先点搜索框输入联系人名,再从结果里点会话,然后找输入框写消息,最后发送。几个参数值得留意:Click 之后的 Thread.Sleep 是为了等界面刷新,搜索框输入后至少要等 300 到 500 毫秒才会出搜索结果;输入框写入后也等 200 毫秒再找发送按钮,因为按钮的渲染有延迟。这些等待值不是玄学,是微信界面渲染的实际响应速度。
4. FlaUI 微信自动化避坑:五个必踩问题与排查顺序
4.1 管理员权限导致附加成功但查找失败
现象:Application.Attach 没有报错,GetMainWindow 返回了窗口对象,但 FindFirstDescendant 一直返回 null,或者抛 AccessDenied 异常。
原因:微信以管理员权限运行时,普通权限进程只能拿到窗口句柄,拿不到 UIA 元素树的访问权。这是 Windows UIA 的安全机制,不是 FlaUI 的 bug。
解决:给 Winform 项目添加应用清单 app.manifest,把 requestedExecutionLevel 改为 requireAdministrator。改完之后整个 Winform 程序以管理员身份启动,就能正常读取微信的 UIA 元素树。注意这会让每次启动都弹 UAC,但微信自动化没有别的办法——除非你把微信降到普通权限运行,而微信自己会申请管理员权限,所以唯一可靠的方案是让工具配合微信的权限级别。
4.2 微信最小化到托盘后元素查不到
现象:代码运行时微信正好在托盘里,窗口不可见,FindFirstDescendant 返回 null,但是任务栏明明有微信图标。
原因:微信最小化到托盘后,主窗口被隐藏,UIA 元素树中该窗口的子树不会渲染,尤其是虚拟列表区域,系统直接裁剪掉不可见内容。
解决:在执行自动化操作前,先强制恢复微信主窗口。用 FlaUI 自带的 Window.Restore 不一定管用,微信的托盘行为会拦截常规的恢复调用,我直接用 Win32 ShowWindow:
using System.Runtime.InteropServices; [DllImport("user32.dll")] private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); const int SW_RESTORE = 9; var process = Process.GetProcessesByName("WeChat").FirstOrDefault(); if (process != null) { ShowWindow(process.MainWindowHandle, SW_RESTORE); Thread.Sleep(500); }这段代码放在 Application.Attach 之前。别用 ShowWindowAsync,微信可能来不及刷新窗口状态;同步调用后等 500 毫秒再 attach。如果窗口恢复后仍然查不到,还有一种情况是窗口被其他程序完全遮挡,UIA 对完全遮挡窗口的元素提取也会降级,先把微信窗口置顶再操作。
4.3 ValuePattern 写入成功但发送按钮是灰色
现象:inputBox.Patterns.Value.PatternOrDefault 不为 null,SetValue 没有抛异常,但消息没有出现在输入框里,发送按钮不可点。
原因:微信的输入框委托给了自绘的富文本编辑控件,UIA 的 ValuePattern 走的是控件内部的值接口,写入的字符串没有触发微信自身的输入事件和内部状态更新。这是自绘控件常见的“看得见值,看不见事件”问题。
解决:不要用 SetValue,改成 Focus 之后再模拟键盘输入。但键盘输入受输入法影响,我的经验是先用 SetValue 清空输入框,再 Focus,然后用 SendKeys 输入。这样就算输入法处于中文状态,SendKeys 也是以 Unicode 字符逐字符上屏,微信能正常收到输入事件。注意 SendKeys 的字符串内容如果有特殊字符,需要用花括号转义,比如大括号本身要写成 {{}。
如果键盘输入仍然无法触发发送按钮,最后的兜底是直接模拟 Return 键。微信默认回车发送,输入框有焦点时回车即可。
4.4 会话列表是虚拟列表,滚动查找会断
现象:用 FindAllDescendant 查找所有 ListItem 时,只能找到当前可见区域的会话,滚动后新出现的会话不在结果里,或者查找某一屏之外的会话永远返回 null。
原因:微信左侧会话列表是典型的虚拟化列表控件,UIA 只暴露当前渲染出的可视元素,滚出屏幕的项直接销毁,滚进来的才创建。这和 Winform ListView 的 VirtualMode 是同一个机制。
解决:不要做全量查找和滚动遍历,改用会话顶部的搜索框。先点击搜索框输入联系人名称关键字,等搜索结果渲染出 ListItem 再点击。这个方案的好处是搜索结果的列表通常很短,一次渲染全部结果,元素树完整;坏处是每次定位都要多花几百毫秒等待搜索结果。但相比滚动列表的不可控,搜索框方案要稳定得多。如果确实需要遍历几十个会话,就按“输入关键字→拿结果→清空→输入下一个关键字”的节奏分批做,每批之间休息 200 毫秒。
4.5 微信 4.0 之后元素树大幅缩水
现象:在微信 4.0 内测版上,UIA 树里只有窗口外壳和少数几个顶层元素,会话列表、输入框全部消失,FlaUI 基本废掉。
原因:微信 4.0 重写了渲染层,自绘程度更高,大量界面元素不再映射到 UIA 控件树,微软的 UI Automation 接口能看到的界面信息大幅减少。
解决:锁微信版本,用 3.9 系列跑自动化。这不是 FlaUI 的问题,是微信主动收紧了 UIA 暴露的边界。如果你必须用 4.0,那就只能走图像识别路线(截图 + OCR + 坐标点击),但那已经超出 FlaUI 的范畴,需要引入 OpenCV 和 OCR 引擎,复杂度上一个量级。我的建议是,微信 4.0 正式普及前,别急着升级;自动化脚本跑在旧版本上,新版本只用来日常聊天。
5. 进阶:给自动化工具加自检日志与元素树快照
做微信自动化最怕两件事:一是元素结构变了导致查找静默失败,二是界面响应慢导致时序错位。为了在出问题时能快速定位,我每次做新功能都会加一个元素树快照工具,把当前窗口前几层的元素 Name、AutomationId、ControlType 打成日志:
private static void DumpElementTree(AutomationElement root, int depth = 0) { if (depth > 3) return; var children = root.FindAllChildren(); foreach (var child in children) { Console.WriteLine($"{new string(' ', depth * 2)}{child.ControlType}: Name={child.Name}, AutomationId={child.AutomationId}"); DumpElementTree(child, depth + 1); } }这段递归把窗口的三层元素树全部打印出来。微信版本升级、聊天窗口布局调整后,先跑一遍快照,用实际的 Name 和 AutomationId 去改条件,而不是靠猜。我自己被“元素 Name 从‘发送’变成‘发送(S)’”这种小改动坑过两次之后,就养成了先快照后写条件的习惯。自动化脚本的维护重心不是代码逻辑,而是元素定位条件的持续校正。
另一个必须养成的习惯是用前重查。FlaUI 的 AutomationElement 对象不是可靠的引用,微信做一次界面刷新后,之前的元素对象可能处于断开状态。我的代码规范是:任何一次点击或输入之前,都重新从 window 节点 FindFirstDescendant 查找一次,不缓存超过 3 秒的元素对象。配合 WaitForElement 轮询方法,实际运行中很少遇到元素断开问题。
验证发送是否成功,可以在发送后等 1 秒,然后拉取当前聊天窗口里的最后一条消息元素,检查它的 Name 是否等于你发送的内容。这个验证不是每次都要做,但在批量发送的场景里,随机抽查哪怕 1% 的成功率,也能帮你早早发现微信风控限流或者账号异常。微信对高频自动化发消息是有风控的,批量发送务必控制频率,两条消息之间至少间隔 2 到 3 秒,别把脚本跑成压测工具。自动化工具是给人省力的,不是用来制造麻烦的——这是我一直以来的习惯,也希望帮到你。
本文还有配套的精品资源,点击获取