简介面向C#开发者的微信自动化示例工程整合Winform界面与FlaUI自动化库针对定时任务、自动回复、群聊机器人三类高频场景提供可直接编译的桌面程序方案。压缩包共365个文件、47.83MB以127个dll、55个cs源码为主辅以18个json配置、resx资源与sln解决方案文件其中dll为FlaUI等依赖库cs为界面与逻辑代码json存储回复规则与定时参数。工程包含主程序、FlaUI封装、消息处理与定时调度等模块并已生成构建缓存和SQLite相关文件能帮助开发者快速还原环境理解元素查找、事件监听、模拟点击输入等自动化关键链路。已有659人学习/下载适合希望用C#实现桌面端微信自动化或想上手FlaUI的初级到中级开发者参考。1. 用WinForm和FlaUI做微信自动化为什么不做协议层而做UI层微信桌面版没有对外开放的通信接口程序要自动发消息、读会话大多数人第一反应是去抓包、Hook或者碰数据库然后就会发现微信在防自动化上做了很多事。后来我把注意力转回UI层用Windows自带的UI Automation框架拿到微信窗口里的元素树把输入框、会话列表、消息文本当成能定位和操作的控件这就是FlaUI的核心玩法。WinForm只负责做外壳显示状态、提供按钮真正干活的是底层那套基于FlaUI的自动化组件。这个方案跨版本也能活因为微信改的是内部渲染但暴露出来的UIA结构变化相对慢。它适合做办公辅助、个人效率工具也适合需要给桌面应用做回归测试的团队不太适合做风控级、批量营销类的场景。整套方案最关键的一句话把UI当API用别去跟内部实现较劲。2. 微信自动化的方案对比与选型为什么我最后选了FlaUI2.1 三类常见方案坐标模拟、Win32消息、原生UIA做微信自动化绕不开坐标点击、Win32消息、UIA三条路。坐标点击最简单用SetCursorPos和mouse_event按写死的坐标点按钮。缺点很致命Windows的DPI缩放、微信窗口的位置变动、分辨率切换都会让脚本突然失灵今天能跑明天就翻车。Win32消息方式比坐标靠谱一点可以给指定句柄发WM_SETTEXT、WM_KEYDOWN之类的消息但微信的输入区、会话列表全是自绘控件Win32消息根本进不到它自己的内部逻辑里最多能操作标准Edit这个我实际试过效果很差。原生System.Windows.Automation是.NET框架自带的UIA封装能在一定程度上读到微信的控件树。但它的API写起来繁琐Condition和Pattern拆得很碎而且没有内置的输入模拟要自己结合SendKeys。FlaUI是在UIA2、UIA3之上的封装提供了更顺畅的元素查找、等待和模式调用还自带Keyboard、Mouse这类基础输入能力。微信这种自绘界面UIA3的兼容性更好所以选FlaUI基本是一步到位的选择。方案稳定性开发速度维护成本适合场景坐标模拟低快高一次性脚本Win32消息中中中标准控件组成的窗口原生UIA中高慢中对依赖库有严格限制的工程FlaUI高快低自绘界面的自动化表格里的“稳定性”指的是在微信版本变化、窗口移动、系统缩放变化后的存活能力。FlaUI不直接依赖具体坐标而是依赖控件树里的属性所以抗干扰能力比前两类强不少。原生UIA理论上也能做到同样的事但写一个查找条件就要好几行还要自己处理等待和重试真做起来性价比不高。2.2 FlaUI的工作模型元素树、条件查找与自动化模式UIA把每个可见界面元素暴露成AutomationElement相互组成树树的根是桌面往下是窗口、面板、编辑框、列表项。FlaUI的核心操作就是三件事在树里找元素、读属性、调用Pattern。查找元素时用ConditionFactory组合条件比如ByControlType(ControlType.Edit).And(cf.ByProcessId(pid))。Pattern是UIA定义的交互能力比如ScrollPattern滚动列表、ValuePattern读输入框内容、InvokePattern点击按钮。微信能自动化的前提是它实现了部分UIA协议。好消息是左侧会话列表、顶部搜索框、聊天输入区、消息列表中的文本通常都会暴露成可识别的元素坏消息是有一批自绘控件的Name属性是空的AutomationId也不完整只能靠控件类型加几何位置来猜。明白了这三件事后面写代码就不会一头雾水。UIA2和UIA3也需要做个选择。UIA2是托管实现兼容老框架但性能和稳定性都一般UIA3走COM速度快、结构更完整是FlaUI官网推荐的默认方案。我们的项目中只需要引用FlaUI.UIA3不需要引UIA2。另一个常见的坑是有人会同时引用FlaUI.UIA2和FlaUI.UIA3编译时出现AutomationElement分不清这个后面避坑章也会讲。2.3 用一段代码确认微信窗口的控件树动手之前先建一个控制台项目装上FlaUI.UIA3把以下代码跑一遍看看微信窗口里到底暴露了什么。using System; using System.Diagnostics; using System.Linq; using FlaUI.Core.AutomationElements; using FlaUI.Core.Definitions; using FlaUI.UIA3; class WeChatInspector { static void Main() { var proc Process.GetProcessesByName(WeChat).FirstOrDefault(); if (proc null) { Console.WriteLine(请先启动微信); return; } using (var automation new UIA3Automation()) { var root automation.GetRootElement(); var cf automation.ConditionFactory; var window root.FindAllChildren( cf.ByProcessId(proc.Id).And(cf.ByControlType(ControlType.Window)) ).FirstOrDefault(); if (window null) { Console.WriteLine(没找到微信主窗口); return; } PrintTree(window, 0); } } static void PrintTree(AutomationElement element, int depth) { if (depth 10) return; var name string.IsNullOrEmpty(element.Name) ? : $ Name{element.Name}; Console.WriteLine(${new string( , depth * 2)}{element.ControlType}{name}); foreach (var child in element.FindAllChildren()) PrintTree(child, depth 1); } }逻辑说明GetRootElement()拿到桌面根元素ByProcessId(proc.Id)把范围锁到微信进程And合并控件类型条件避免抓到同一个进程下的其他小窗口。PrintTree限制深度10层是因为初次遍历全树会很慢甚至让微信界面短暂卡顿限制深度能保护现场。参数说明如果你跑出来ControlType是Document而不是Edit或者聊天项是Pane而不是ListItem都不奇怪微信每个版本都会微调。代码里输出的Name才是主线索尤其是搜索框的Name在不同语言版本里不一样我的机器上是“搜索”繁体机器上可能是“搜尋”。看到这个输出你已经有了下一步定位的钥匙。还想看得更细的话别只盯着控制台打开FlaUI Inspector或者系统的UI自动化辅助工具在微信界面上点哪儿就显示哪儿的元素属性比一次次改代码快得多。3. 环境准备与第一个可运行Demo把微信窗口抓进WinForm3.1 项目配置与NuGet安装我用的是Windows 10WinForm项目如果是.NET Framework 4.8直接装FlaUI.UIA3就可以如果你用的是.NET 6以上的WinForm同样支持。创建好项目后在包管理控制台执行Install-Package FlaUI.UIA3用命令行的话也可以dotnet add package FlaUI.UIA3装完以后项目会自动带来FlaUI.Core和FlaUI.UIA3两个程序集。这里建议只装UIA3不要手贱再把FlaUI.UIA2也装上否则两个库里的AutomationElement会在代码里造成二义性。如果你一开始用的是FlaUI.UIA2请把引用删干净再重新生成解决方案避免出现“AutomationElement类型在FlaUI.Core和FlaUI.UIA3中不明确”的编译错误。3.2 启动微信进程并找到主窗口日常开发中我一般先让微信保持运行再通过进程名附加这样最省事如果微信没开就用Process.Start拉起它。代码里建议做成一个工具方法using System.Diagnostics; using FlaUI.Core; using FlaUI.Core.AutomationElements; using FlaUI.UIA3; static void AttachWeChat() { var proc Process.GetProcessesByName(WeChat).FirstOrDefault(); if (proc null) { var start new ProcessStartInfo(C:\Program Files\Tencent\WeChat\WeChat.exe) { UseShellExecute true }; Process.Start(start); for (int i 0; i 20; i) { Thread.Sleep(500); proc Process.GetProcessesByName(WeChat).FirstOrDefault(); if (proc ! null proc.MainWindowHandle ! IntPtr.Zero) break; } } using (var automation new UIA3Automation()) { var app Application.Attach(proc.Id); var window app.GetMainWindow(automation); Console.WriteLine($主窗口标题{window.Title}); } }逻辑说明先尝试查找现有微信进程避免重复启动。拉起微信后不能立刻拿句柄很多桌面客户端启动要好几秒所以轮询等待主窗口句柄出现。Application.Attach是FlaUI提供的进程附加方式拿到主窗口后会得到一个Window对象这个对象有自己的Title、BoundingRectangle等属性。参数说明ProcessStartInfo里的路径不是固定的有的系统装在C:\Program Files (x86)\Tencent\WeChat\WeChat.exe更稳的写法是把路径放到配置项里启动前先找一次现有进程找到就跳过启动。轮询次数20次、间隔500毫秒只是起步值老机器上可能要调大。3.3 定位搜索框并打开会话找到主窗口后下一步最好做的是用搜索框定位联系人而不是直接去遍历会话列表。搜索框在微信主窗口顶部UIA类型通常是Edit但也有可能被暴露成Document或Custom所以我一般用“控件类型偏移量”双重判断。示例代码using (var automation new UIA3Automation()) { var app Application.Attach(proc.Id); var window app.GetMainWindow(automation); window.SetFocus(); var searchEdit window.FindFirstDescendant( cf cf.ByControlType(ControlType.Edit) ); if (searchEdit null) { Console.WriteLine(没有找到搜索框可能微信版本改了控件类型); return; } searchEdit.Click(); searchEdit.Enter(某联系人); Thread.Sleep(800); searchEdit.SendKeys({Enter}); Thread.Sleep(1500); Console.WriteLine(已经尝试打开会话); }逻辑说明window.SetFocus()把微信窗口拉到前台这是后续键击能生效的前提。FindFirstDescendant在当前窗口里向下找第一层符合条件的后代Enter是FlaUI对输入文本的封装相当于聚焦后逐个字符送进去。发送回车后微信会跳到对应的聊天窗口。参数说明如果微信主窗口里不止一个Edit第一个找到的可能不是搜索框。一个常见修法是用BoundingRectangle.X来过滤因为搜索框在窗口左上角区域。换句话说var searchEdit window.FindAllDescendants(cf cf.ByControlType(ControlType.Edit)) .OrderBy(e e.BoundingRectangle.Y) .ThenBy(e e.BoundingRectangle.X) .First();这个顺序会优先取到最靠左上角的输入框命中率比直接取第一个高很多。需要说明的是不同微信版本里搜索框和输入框的上下顺序不一样这个过滤条件不是永远有效的自己在Inspector里看一眼坐标再写死。3.4 放进WinForm别让UI线程卡死上面的代码在控制台里没问题但如果直接塞到WinForm的按钮里点击按钮后整个界面会卡住因为自动化过程中的查找、等待都是阻塞操作。我一般的做法是用Task.Run把它丢到后台线程在按钮事件里异步等待private async void btnOpenChat_Click(object sender, EventArgs e) { btnOpenChat.Enabled false; statusLabel.Text 正在打开聊天窗口...; try { var contact txtContact.Text; await Task.Run(() OpenWeChatChat(contact)); statusLabel.Text 已打开; } catch (Exception ex) { statusLabel.Text $失败{ex.Message}; } finally { btnOpenChat.Enabled true; } }逻辑说明async void只用来响应UI事件真正耗时的操作放进Task.Run这样WinForm界面能保持响应。catch里把所有异常接住并显示到Label上避免自动化出错导致界面崩溃。参数说明txtContact是联系人输入框statusLabel只是状态显示没有特殊要求。这里强烈建议把自动化逻辑单独放到一个类里WinForm只做调用不要在每个按钮里写一堆FlaUI代码不然后面加功能会越来越难维护。4. 微信自动化核心操作定位输入框、发消息、读回复4.1 定位聊天输入框Edit不叫Edit的坑进入聊天窗口后最核心的目标就是聊天输入框。微信的一部分版本把输入框暴露成Edit另一部分版本暴露成Document甚至同一个版本在会话列表搜索框和聊天输入框上用的控件类型还不一样。如果直接FindFirstDescendant(cf cf.ByControlType(ControlType.Edit))很可能会找到搜索框或者找到藏在界面角落里不可见的编辑控件。我踩过的坑是用这个方法拿到输入框后SendKeys把文字输进去了但微信根本没收到。原因是拿到的是隐藏Edit不是用户正在看的那一个。我的修法是按几何位置过滤static AutomationElement FindChatInput(Window window) { var candidates window.FindAllDescendants( cf cf.ByControlType(ControlType.Edit) ); return candidates .Where(e e.BoundingRectangle.Height 50) .OrderByDescending(e e.BoundingRectangle.Bottom) .First(); }逻辑说明聊天输入框通常占据窗口底部一块横向区域高度明显大于搜索框而且位置靠下。Height 50这一步过滤掉了搜索框、微信号输入框这类小控件OrderByDescending(Bottom)保证取到最靠近屏幕底部的那一个。参数说明如果你用的微信版本把输入框暴露成Document就把ByControlType(ControlType.Edit)换成ControlType.Document条件可以放宽为同时匹配两种类型var cf automation.ConditionFactory; var cond cf.ByControlType(ControlType.Edit).Or(cf.ByControlType(ControlType.Document));用Or组合是稳妥做法但要注意搜索结果里可能同时出现好几个Document几何位置过滤仍然不能省。4.2 发送文本消息Clipboard和键击模拟确定输入框之后发送文本最稳的方式不是用SendKeys逐字输入而是先把文本放到剪贴板再模拟CtrlV粘贴。这样能绕开中文输入法、特殊字符转义、还有FlaUI在非活动窗口下的键击延迟问题。核心代码public static void SendText(string contact, string content, string wechatPath) { var proc WeChatProcessHelper.EnsureRunning(wechatPath); using (var automation new UIA3Automation()) { var app Application.Attach(proc.Id); var window app.GetMainWindow(automation); window.SetFocus(); var searchBox FindSearchBox(window); searchBox.Click(); searchBox.Enter(contact); Thread.Sleep(800); searchBox.SendKeys({Enter}); Thread.Sleep(1200); var inputBox FindChatInput(window); inputBox.Click(); Clipboard.SetText(content); inputBox.SendKeys(^v); Thread.Sleep(300); inputBox.SendKeys({Enter}); Thread.Sleep(500); } }逻辑说明Clipboard.SetText把内容放到系统剪贴板紧接着模拟CtrlV输入框就拿到了文本。最后模拟回车发送。这里Clipboard来自System.Windows.Forms在WinForm项目里没有问题如果你在控制台项目里用需要加上引用和STA线程设置否则剪贴板会报错。参数说明为什么要用剪贴板而不是inputBox.Enter(content)因为Enter本质上是模拟按键遇到中文会受输入法影响。键盘操作里有几个点要注意searchBox.Click()之后不要立刻Enter先给微信一点反应时间Thread.Sleep的数值不是越大越好主要是给自绘控件的内部状态更新留时间800到1500毫秒通常够用。4.3 读取聊天记录用ControlType过滤读取聊天记录比发送消息更依赖版本。聊天区域里文本元素普遍没有Name但是文字内容通常会以Text控件的形式暴露出来。我的一般做法是先抓窗口里所有Text控件再按区域过滤var allTexts window.FindAllDescendants(cf cf.ByControlType(ControlType.Text)); var visibleTexts allTexts .Where(t !string.IsNullOrWhiteSpace(t.Name)) .Select(t t.Name) .ToList(); foreach (var line in visibleTexts.TakeLast(30)) { Console.WriteLine(line); }逻辑说明FindAllDescendants会返回窗口下所有Text类型后代包括界面上的按钮文字、状态文字所以要用Name非空过滤一遍。TakeLast(30)只取最后30条因为聊天界面里最下面的才是新消息。这个方案有个边界如果聊天记录是图片、文件卡片或者某些表情包它们不一定暴露成Text读取就会漏掉。熟手可以直接打印整个控件树把消息列表里的Text和消息本身区分开但工作量会大很多。对于大多数“读回复”需求抓Text已经够了。4.4 在WinForm里组织一个完整的“发送器”到这里把前面几段拼成一个可以日常用的WinForm小工具就很容易了。界面上只需要三个输入框联系人、消息内容、发送按钮后台只调用一个封装好的方法private async void btnSend_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(txtContact.Text) || string.IsNullOrWhiteSpace(txtMessage.Text)) { resultLabel.Text 联系人和消息内容不能为空; return; } btnSend.Enabled false; try { await Task.Run(() WeChatAutomation.SendText( txtContact.Text, txtMessage.Text, config.WeChatPath)); resultLabel.Text 发送完成; } catch (Exception ex) { resultLabel.Text $发送异常{ex.Message}; } finally { btnSend.Enabled true; } }逻辑说明WeChatAutomation.SendText就是4.2里那个方法这里用Task.Run包一层防止查找元素、等待微信响应时阻塞UI。结果统一写到resultLabel上用户一眼能看到状态。整个WinForm界面不需要任何复杂的控件Name都是显而易见的。参数说明config.WeChatPath建议放到配置文件里不要硬编码。因为微信安装路径在不同机器上差异很大写死在代码里换台机器就翻车。这个工具做到这里已经能实际用了但我还是要提醒一句它只应该控制你本人登录的微信窗口做自动化辅助测试或减轻重复操作不要拿来做批量群发和干扰他人的事。5. FlaUI微信自动化避坑最容易翻车的5个场景5.1 元素明明能找到输入却总是不生效现象用FlaUI高亮定位到了搜索框或输入框代码也点了但SendKeys输入的文字没进去或者进去了又丢。原因微信窗口不在前台或者微信自己的输入焦点被抢走。UIA的Click()只负责点击元素不保证窗口一定被激活如果桌面焦点在别的程序上键击模拟会落到当前前台窗口。解决每次操作前先对窗口执行window.SetFocus()再调用window.AsWindow().IsModalfalse之类的状态检查。最直接的办法是把SetFocus()放在所有Click和SendKeys前面然后再加Thread.Sleep(300)等焦点稳定。如果SetFocus也不太可靠可以调用Win32的SetForegroundWindow但那个会有前置条件限制个人工具里够用就行。5.2 微信窗口最小化或切换到其他会话后句柄就失效现象脚本跑着跑着突然找不回之前定位的元素甚至GetMainWindow返回null可你明明看见微信还开着。原因UIA元素对象和窗口句柄挂着一条连接窗口最小化、切换会话、或者微信界面副屏显示的情况下原来的AutomationElement可能已经失效继续引用会抛ElementNotAvailable异常。解决不要长期持有同一个Window或AutomationElement对象每次操作前重新查找一次。我的习惯是写一个GetFreshWeChatWindow()方法内部重新Application.Attach、重新GetMainWindow每次发消息前都调一遍。性能上多花几十毫秒但稳定度远高于长期复用。5.3 会话列表滚动之后抓不全所有聊天项现象用FindAllDescendants去抓左侧会话列表只能抓到当前屏幕上显示的那十几个往下滚动后列表没有变长。原因微信的会话列表是虚拟化列表只渲染当前可视区域里的项。UIA树里看不到未渲染的元素这是自绘控件的常见行为不是FlaUI的问题。解决抓取前先找到列表容器调用ScrollPattern往下滚动滚一屏抓一次直到滚动条到达底部。FlaUI里可以先拿到List或Pane然后调用Scroll()方法滚动后加延时再重新查找。不要让脚本一次性抓所有没这个必要。5.4 中文输入法把SendKeys打得面目全非现象SendKeys输入“你好”微信里变成了“nihao”或者弹出一个输入法候选框把后续回车都吃掉。原因模拟按键走的是底层虚拟键码输入法处于中文模式时会拦截这些键码转换成拼音候选于是英文按键变成了拼音字母。解决能走剪贴板就不要走SendKeys这是最省事的方案。如果非要逐字输入就先切换到英文输入法// 切到英文模式再用输入法无法导出中文的场景下使用 inputBox.SendKeys(Keys.Control);这在代码里是个玄学操作不同输入法的热键定义不一样。我最后采用的是“剪贴板粘贴”方案把4.2的代码作为标准实现没有再纠结过输入法问题。5.5 高分屏和DPI缩放导致定位错位、点击偏移现象在2K屏或缩放比例125%以上的电脑上BoundingRectangle获取到的坐标和鼠标实际点击位置对不上或者元素明明在点击却点到了旁边。原因WinForm进程默认有DPI感知设置但微信进程的DPI感知模式不一定和你的进程一致。当两边的DPI Awareness水平不同时UIA返回的物理坐标和逻辑坐标会混淆最终导致偏移。解决在WinForm入口处显式设置DPI感知。最简单的方式是编辑app.manifest加入PerMonitorV2声明application xmlnsurn:schemas-microsoft-com:asm.v3 windowsSettings dpiAwareness xmlnshttp://schemas.microsoft.com/SMI/2016/WindowsSettings PerMonitorV2 /dpiAwareness /windowsSettings /application改完重启程序再测试。如果还偏移可以检查BoundingRectangle和Windows屏幕缩放参数是否一致问题基本都能定位出来。6. 进阶把脚本整理成可维护的WinForm自动化库6.1 给元素查找加上超时和重试自动化脚本最容易卡死的地方就是FindFirstDescendant找不到元素时。FlaUI有超时机制但我习惯自己再包一层重试。下面是一个典型封装static AutomationElement WaitForElement( AutomationElement parent, FuncConditionFactory, ConditionBase condition, int timeoutMs 5000) { var sw Stopwatch.StartNew(); while (sw.ElapsedMilliseconds timeoutMs) { var element parent.FindFirstDescendant(condition); if (element ! null) return element; Thread.Sleep(200); } return null; }逻辑说明FindFirstDescendant在找不到时返回null所以轮询到timeoutMs为止。200毫秒的间隔不影响性能但能给微信内部渲染留出时间。哪里需要等就传对应的条件代码可读性会好很多。6.2 从“能跑”到“能维护”日志与开关我吃过不少亏脚本只在自动化工具的界面上显示“发送成功”但实际消息没发出去原因可能是微信弹了一个权限提示框把焦点抢走了。后来我在所有关键动作后都加了日志logTextBox.AppendText($[{DateTime.Now:HH:mm:ss}] 已定位搜索框\n);别小看这几行批量操作时它是唯一能告诉你卡在哪的线索。另一个建议是加一个“自动化总开关”让用户主动决定是否开始执行而不是程序一打开就乱动微信窗口。这个开关在调试阶段特别有用。6.3 活用FlaUI的更多自动化模式FlaUI的价值不止于找元素和发消息。微信窗口里的列表容器在FlaUI里可以调用ScrollPattern聊天区域的元素可以调用ExpandCollapsePattern有的按钮可以走InvokePattern。与其双击坐标不如学习把这些Pattern封装成通用方法。比如轮询聊天窗口时用ScrollPattern判断是否到了底部比固定Sleep高效得多。把这些Pattern能力沉淀到自己的封装类里后再做类似自动化会越来越快。我自己的教训是永远不要相信第一次写出的定位条件是永恒真相微信一更新把Inspector拿出来重新走一遍树比拍脑袋改代码实在。希望帮到你。本文还有配套的精品资源点击获取