简介:一套面向C#开发者的微信自动化桌面工具源码,依托Winform界面与FlaUI库实现对微信客户端UI的自动操控,解决定时发送消息、关键词自动回复及群聊机器人等重复性操作场景,适合有一定C#基础、希望入门Windows UI自动化或构建个人微信助手的开发者。资源包共365个文件,约47.8MB,包含127个dll运行库、55个cs源码、18个json配置及30余个编译缓存类文件,另有少量exe与数据库资源,整体结构覆盖主程序、FlaUI封装、定时任务、消息处理和配置文件等模块。压缩包内除可运行的Winform工程外,还提供项目文件、编译中间产物与数据库相关脚本,便于直接打开、编译和二次开发。已有659人学习下载,适合参考其UI元素定位、消息监听和定时触发等实现思路,也可作为企业内训或个人自动化办公的起步模板。需要特别提醒的是,微信官方不开放自动化接口,使用前应评估账号风险并遵守平台规则。
1. 没有官方接口的微信自动化:为什么用 FlaUI 而不是抓协议
微信桌面端没有开放消息收发接口,要做定时提醒、值班通知、巡检播报这类工具,抓协议有合规风险,第三方库又大多维护停滞。更务实可控的路线,是把微信当成一个普通 Windows 桌面程序,用 UI 自动化去驱动。这套 Winform 加 FlaUI 的方案,就是基于 Windows UI Automation 对微信客户端做界面级的定位、点击和输入,不碰协议、不依赖特定版本。它能解决给指定联系人发消息、读取会话最新消息、批量触发内部提醒这类需求,适合做内部 RPA、自动化测试和巡检脚本的 .NET 开发者参考。全文从环境搭建讲起,中间展开微信元素定位与消息发送,最后落在 Winform 集成和稳定性排坑上。这套思路不只适用于 Winform,迁到 WPF 甚至 .NET MAUI 的 Windows 端,FlaUI 部分几乎不用改。
2. FlaUI 环境搭建与最小闭环:先拿到微信主窗口
2.1 为什么选 FlaUI:从原生 UIA 到封装库的取舍
Windows 自带 UIAutomation 托管接口不是不能用,而是用起来太原始。想查一个文本框,得先构造 Condition 再调 FindFirst,还要手动处理 COM 异常和超时,代码里六成是基础设施、两成才是业务。FlaUI 把这一层封装成了接近直觉的 API:窗口就是 Window、控件就是 AutomationElement、查找就 FindDescendant、点击就 Click。对 Winform 开发者来说,上手成本几乎为零。
FlaUI 同时支持 UIA2 和 UIA3 两种模式。UIA2 走托管实现,兼容老框架,但遇到 Chromium 渲染的界面经常拿不到子节点;UIA3 走微软的 COM 原生实现,性能和子元素覆盖率都更好。微信 Windows 客户端的会话列表和聊天区有相当一部分是自绘加 Chromium 混合渲染,实测 UIA3 能拿到的文本节点明显更多,所以默认选 FlaUI.UIA3。
| 对比项 | UIA2 | UIA3 |
|---|---|---|
| 实现方式 | 托管代码 | COM 原生 |
| 子元素覆盖率 | 老框架控件好 | 新渲染层更好 |
| 查找性能 | 一般 | 更好,延迟更低 |
| 微信场景推荐度 | 低 | 高 |
选型结论:直接引 FlaUI.Core 和 FlaUI.UIA3 两个 NuGet 包,目标框架用 .NET Framework 4.8,Winform 项目模板直接建,不需要额外配置原生依赖。如果你拿 WPF 工程跑,FlaUI 层的代码原样搬,唯一要改的是界面线程的调度方式。
2.2 最小 Demo:附加到微信进程并读取主窗口
NuGet 装完包,第一个要验证的不是发消息,而是能不能稳定拿到微信主窗口。这一步是后面所有操作的地基,拿不到窗口,后面全是空谈。
using FlaUI.Core; using FlaUI.UIA3; using System.Diagnostics; using System.Linq; Process[] procs = Process.GetProcessesByName("WeChat"); if (procs.Length == 0) { Console.WriteLine("微信未启动,先手动登录一次"); return; } using (var automation = new UIA3Automation()) { var app = FlaUI.Core.Application.Attach(procs[0]); Window mainWindow = app.GetMainWindow(automation, TimeSpan.FromSeconds(10)); if (mainWindow == null) { Console.WriteLine("拿不到主窗口,可能还停在登录页"); return; } Console.WriteLine($"主窗口: {mainWindow.Title}"); }代码分三步:先按进程名找微信,没启动就提示;再用 Attach 附加到已存在进程,而不是 Launch 重新拉起;最后 GetMainWindow 带 10 秒超时。三个参数值得单独说:进程名是 WeChat 不是 Weixin,区分大小写,写错直接拿不到;Attach 和 Launch 的差别在于一个操作已有实例、一个自己拉起新实例,工具里我统一用 Attach,把微信的启动交给用户,避免多开时附加到错误进程;超时时间给 10 秒,足够覆盖从启动到登录完成的空窗期。
提示:登录完成后主窗口标题通常包含当前微信昵称,如果停在扫码登录页,标题可能是"微信登录"或"微信"。判断窗口是否就绪,看 Title 比看 MainWindow 是否为 null 更可靠。
在 Winform 里,我一般把这段放在 Form_Load 之后的一个 Task 里,界面先弹出来,后台去探测微信,结果写到状态栏 Label。用户打开工具就能看到"微信已连接"或"微信未就绪",比黑匣子式的静默失败直观得多。同时注意 using 块不要嵌套在每次操作的循环里,一个进程生命周期内 automation 实例保持全局单例,否则句柄会越积越多,跑一晚上内存涨几百 MB 就是这么来的。
3. 微信 UIA 元素定位实战:从搜索框到发送消息的完整链路
3.1 先搞懂微信的 UIA 元素树长什么样
写定位条件之前,必须知道微信主窗口在 UIA 视角下是什么结构。用 FlaUI 自带工具或者自己写一个递归打印控件树的小程序,把微信主窗口 dump 一遍,大致长这样:
Window "微信 (昵称)" ├── Pane │ ├── Edit "搜索" ← 搜索入口 │ ├── List ← 会话列表 │ │ ├── ListItem "文件传输助手" │ │ ├── ListItem "项目群" │ │ └── ListItem "某联系人" │ └── Pane ← 右侧聊天区 │ ├── Text "聊天信息" │ └── Edit ← 消息输入框这个结构并不严谨,不同版本差异会很大,但主脉络稳定:搜索框是 Edit、会话列表是 List、会话项是 ListItem、输入框是 Edit。定位时我优先用 Name 属性,因为它和界面上显示的文字一致,微信换版本后 AutomationId 经常变,Name 反而稳定得多。控制类型加 Name 双条件是最稳的组合,只靠 AutomationId 的代码基本属于给自己埋坑。
3.2 查找与等待:封装带超时重试的定位方法
FlaUI 的 FindDescendant 找不到元素时返回 null,继续往下走就是空引用异常。所以要先封装一个带重试的查找方法,把"找不到就睡 100 毫秒再试"写死在公共代码里。
public static AutomationElement FindOrWait(Window window, Func<AutomationElement, bool> condition, int timeoutMs = 3000) { var sw = Stopwatch.StartNew(); while (sw.ElapsedMilliseconds < timeoutMs) { var el = window.FindDescendant(condition); if (el != null) return el; Thread.Sleep(100); } return null; }方法的两个参数很关键:condition 是委托,回调里写你自定义的匹配规则;timeoutMs 是超时上限,默认 3 秒。这种方式把"轮询等待"变成了公共能力,后面任何一步找不到元素都能直接复用。注意 FindDescendant 是全窗口深度遍历,高频调用会消耗性能,所以我只在每次动作前调用一次,拿到元素立刻操作,不做二次缓存。
我一般还会配套一个等条件成立的 WaitFor 方法,逻辑类似,只是不用等元素,而是等任意布尔条件。这两个方法加起来,后面所有步骤都用"条件等待"代替固定 Thread.Sleep,慢机器上不会超时,快机器上不会空等。
3.3 搜索联系人、进入会话、发送消息的完整链路
给指定联系人发消息,微信没有全局跳转接口,只能模拟人工操作:点搜索框、输入备注名、等结果、点第一个匹配项、然后发消息。
public void SendToContact(string contactName, string message) { // 1. 聚焦搜索框并输入联系人名 var searchBox = FindOrWait(window, el => el.ControlType == ControlType.Edit && el.Name.Contains("搜索")); if (searchBox == null) throw new TimeoutException("搜索框未找到"); searchBox.Click(); Keyboard.Type(contactName); // 2. 等待搜索结果出现,点第一个匹配项 var item = FindOrWait(window, el => el.ControlType == ControlType.ListItem && el.Name.Contains(contactName), 5000); if (item == null) throw new TimeoutException($"联系人 {contactName} 不在结果里"); item.AsListBoxItem().Select(); // 3. 等会话标题变为目标联系人,确认已经进入会话 WaitFor(() => window.Name.Contains(contactName), 3000); // 4. 定位消息输入框,输入内容并回车发送 var input = FindOrWait(window, el => el.ControlType == ControlType.Edit && el.Properties.ClassName.Value.Contains("Edit")); input.Focus(); Keyboard.Type(message); Keyboard.Type(VirtualKeyShort.ENTER); // 5. 校验编辑框已清空,为空说明发送成功 if (!string.IsNullOrEmpty(input.Patterns.Value.Pattern.Value.Value)) throw new Exception("编辑框未清空,发送可能失败"); }五个步骤里藏着几个容易翻车的细节。第一,搜索框输入用 Keyboard.Type 而不是 SetValue,真实键盘序列才能触发微信的搜索逻辑,SetValue 直接赋值经常输入进去了但搜索不触发。第二,搜索结果里可能同时出现联系人、群聊和聊天记录,FindDescendant 返回的是元素树深度优先的第一个匹配项,通常就是"联系人"分类下的目标。第三,进入会话后校验窗口标题包含联系人名,这一步能拦掉"点到了聊天记录结果"的误操作。第四,发送完成的判据是编辑框 Value 被清空,微信发送成功会清空输入区,这个信号比任何日志都可靠。
这段链路跑通,等于拿到了微信自动化的核心能力。后面无论是改成遍历多个联系人,还是做成定时任务,都是在 SendToContact 外面套循环和调度。到这里 Winform 还没真正集成,下一步就是把这段代码塞进界面里,同时保证界面不卡、状态可见。
4. Winform 集成方案:线程、DataGridView 与断线重连
4.1 跨线程调度:自动化放后台,界面用 BeginInvoke 更新
FlaUI 查找元素动辄几百毫秒,从搜索到最后发送整个流程要 2 到 5 秒,放 UI 线程上窗口直接假死,用户第一反应是程序崩了。标准做法是自动化流程跑在 Task 里,界面更新统一走 Invoke 或 BeginInvoke。
private async void btnSend_Click(object sender, EventArgs e) { string contact = txtContact.Text.Trim(); string message = txtMessage.Text.Trim(); if (string.IsNullOrEmpty(contact) || string.IsNullOrEmpty(message)) return; btnSend.Enabled = false; try { await Task.Run(() => SendToContact(contact, message)); AppendLog("发送成功"); } catch (Exception ex) { AppendLog($"发送失败: {ex.Message}"); } finally { btnSend.Enabled = true; } } private void AppendLog(string text) { if (InvokeRequired) { BeginInvoke(new Action(() => listLog.Items.Add($"{DateTime.Now:HH:mm:ss} {text}"))); return; } listLog.Items.Add($"{DateTime.Now:HH:mm:ss} {text}"); }两个写法的细节值得注意。第一,界面值 txtContact.Text 必须在 Task.Run 之前拷贝到局部变量,千万别在后台线程直接读控件属性,InvokeRequired 那一套只解决写回,不解决读取。第二,BeginInvoke 是异步投递,连续打日志时不会排队卡界面;如果用 Invoke 同步更新,日志一多界面反而变慢。AppendLog 里的 InvokeRequired 判断是为了兼容"手动拖日志"和"代码异步打日志"两条路径。
4.2 发送记录展示:DataGridView 把 bool 渲染成复选框
很多人在网上搜"winform datagridview 将 list 的一列 0 和 1 的值显示为 checkbox",其实就是给 DataGridView 绑定一个含 bool 属性的对象列表,它会自动渲染成复选框列,约等于白送的交互体验。
public class SendRecord { public DateTime Time { get; set; } public string Contact { get; set; } public bool Success { get; set; } public string Error { get; set; } } // 界面初始化时设置数据源 var records = new BindingList<SendRecord>(); dataGrid.AutoGenerateColumns = true; dataGrid.DataSource = records; // 每次发送完成后追加记录 records.Add(new SendRecord { Time = DateTime.Now, Contact = "文件传输助手", Success = true, Error = "" });BindingList 在新增元素时会自动通知 DataGridView 刷新,比每次重新设置 DataSource 省事得多,也不会丢失列宽和排序状态。Success 是 bool 类型时,DataGridView 默认生成 CheckBox 列,不需要写任何自定义 CellTemplate。如果业务模型里存的是 int 0 和 1,加一个只读属性public bool SuccessFlag => SuccessValue == 1;绑定这个属性即可,原始列用 Visible = false 藏掉。
整体界面我用最朴素的布局:顶部输入区放联系人、内容、发送按钮,中间 DataGridView 展示发送记录,底部 ListBox 滚动日志。winform 界面美化不是重点,重点是状态可视化。顺手在窗体底部做一个 StatusStrip,显示"已连接微信"或"未连接",排查问题时一眼看出是自动化层挂了还是微信层挂了,省掉一半的定位时间。
4.3 长时间运行的稳定性:心跳循环与引用重建
值班提醒工具要挂一整天,最大的敌人是微信中途退登、窗口最小化恢复、UI 元素引用失效。方案是做一个心跳循环,定时巡检,异常自动重建连接。
var cts = new CancellationTokenSource(); while (!cts.IsCancellationRequested) { try { EnsureWeChatWindow(); // 检查进程存在、主窗口可获取 DoPendingTasks(); // 执行发送、读取等队列任务 } catch (Exception ex) { AppendLog($"心跳异常: {ex.Message}"); ReAcquireWindow(); // 重建 window 引用 } await Task.Delay(2000, cts.Token); }EnsureWeChatWindow 每次循环都重新走一遍进程探测和 GetMainWindow,不缓存 Window 对象。这个设计的原理是:UIA 元素是"活引用",窗口重绘、微信版本升级、最小化再恢复后,缓存的 AutomationElement 可能指向已销毁的旧对象,重新查找的代价很低,收益却很大。DoPendingTasks 里每个动作都走 FindOrWait,进一步保证每次拿到的都是新元素。三段心跳循环配合条件等待,是我在 winform 项目案例里见过的抗长时间运行最有效的组合。
5. 避坑:微信自动化五个高频问题的现象与解法
5.1 微信进程存在,却拿不到主窗口
现象:Process.GetProcessesByName 能找到微信,但 GetMainWindow 返回 null,或者拿到一个标题只有"微信"的空壳窗口。
原因:微信启动到登录完成之间,主窗口还没创建;另外如果机器上有多个微信登录会话,附加到的进程和当前可见的窗口可能不是同一个。
解决:加 5 次重试,每次间隔 1 秒,同时判断窗口 Title 是否包含登录完成后的标志,比如当前微信昵称。不推荐直接自动化登录窗口输入账号密码,账号安全不该交给脚本,工具只做"等待登录完成"的轮询就好。
5.2 元素找到了,点击却没反应
现象:FindDescendant 正常返回元素,调用 Click() 不报异常,但微信界面纹丝不动。
原因:微信部分搜索结果项是自绘控件,没有实现 Invoke 模式。FlaUI 的 Click 走 Invoke,失败后虽然会尝试坐标点击,但坐标点经常落在控件边缘或遮挡层上。
解决:优先用元素内部的文本子元素来点击,取不到就用 BoundingRectangle 中心点做手动坐标点击。再不行就切键盘路径:先 Click 搜索框,再 Keyboard.Type(contactName) 加 Enter,键盘序列是通用后备方案,基本不会失效。
5.3 发送了,但聊天框内容没清空
现象:编辑框里能看到输入的文字,回车也按了,但消息没出现在会话里,编辑框内容还在。
原因:微信输入框对 Value 模式写入的文本监听不完整,Send 按钮没有被激活。直接用 Keyboard.Type 整串输入偶尔也会丢字符,尤其在慢机器上。
解决:统一用 Keyboard.Type 输入,实测逐字符输入比一次性整串输入更稳。发送后读编辑框 Value 做校验,非空说明没发出去,重试一次,两次失败就记录告警而不是无限循环。
5.4 微信一升级,定位条件全部失效
现象:微信自动更新后,之前用的 AutomationId、ClassName 全都对不上,脚本大面积报找不到元素。
原因:微信客户端 UI 结构随版本调整,UIA 属性会跟着变,这是 UI 自动化的宿命,不是代码写错了。
解决:升级后先用元素树 dump 工具把新版本完整跑一遍,和旧版本对比,只改定位条件,不动业务逻辑。把定位条件全部抽到配置文件里,改配置不改代码,一条在 5.4 场景下能省掉半天返工。
5.5 长时间运行内存膨胀、界面响应变慢
现象:工具跑了一天后,进程内存涨到几百 MB,微信窗口和其他程序交互开始卡顿。
原因:每次查找元素都新建 UIA3Automation 实例没释放,或者订阅了事件没有退订,句柄和缓存越积越多。
解决:整个程序只维护一个 automation 单例,Dispose 逻辑只在退出时执行。事件处理器用完后立刻取消订阅,尤其是窗口关闭事件。这个坑在 winform 项目案例里太常见了,FlaUI 的 Automation 实现了 IDisposable,用 using 包裹或者全局单例二选一,别每次操作都 new 一个。
6. 进阶:三段式验证与元素树基线对比
自动化脚本从"能跑"到"可信",差的不是功能,是验证。我把每次发送拆成"定位、点击、验证"三段,每段独立记录耗时和结果:定位超过 1 秒打警告,点击后检查窗口标题是否变成目标联系人,验证读编辑框 Value 是否清空。三段全过才算成功,任何一段失败都能日志定位到具体环节,而不是只看到一句"发送失败"。
真正救过大命的是元素树基线对比。微信每次升级,我先用 dump 工具导出一份新元素树,和上一版配置存到一起做 diff,差异列表就是这次要改的定位条件。这个习惯源于一次翻车:微信大版本更新后,搜索框从 Edit 变成了自定义控件,当时没有基线,硬是排查了一整个下午。现在我把 dump 脚本挂在工具菜单里,升级微信后第一步就是重新 dump、对比、更新配置,整套流程十分钟内完成。
最后补一个验证技巧:不要用固定 Sleep 等待发送结果,封装一个 WaitFor,条件里读编辑框的 Value 属性,配合 3 秒超时和 100 毫秒轮询,发送成功与否在 300 毫秒内就能判定。从那以后我每次升级微信版本,都强制先跑一遍元素树 dump 和基线对比,这个习惯救了我至少三次大版本翻车。希望帮到你。
本文还有配套的精品资源,点击获取