ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Winform 集成 FlaUI 实现微信 PC 端 UI 自动化实战

Winform 集成 FlaUI 实现微信 PC 端 UI 自动化实战 简介这是一份面向C#桌面开发者的Winform微信自动化实战项目源码基于FlaUI库实现UI层面的微信操作适合具备一定C#基础、希望提升办公效率或研究UI自动化的中高级开发者。项目围绕定时任务、自动回复与群聊机器人三大场景展开涵盖元素查找、事件监听、模拟交互等核心逻辑并涉及Windows任务计划程序调用与消息匹配算法设计。资源包共365个文件以127个dll依赖库、55个cs源码文件为主辅以json配置、resources资源、resx与csproj工程文件等压缩包约47.83MB目录结构完整便于直接编译运行与二次开发。目前已有659人学习下载。通过该源码可掌握FlaUI在真实项目中的落地方式理解定时触发器、消息监听与响应规则的实现思路并参考其模块划分与配置管理方式快速搭建属于自己的微信自动化工具。1. 从手动点击到代码驱动Winform 里用 FlaUI 接管微信窗口微信 PC 版没有开放个人自动化接口这是做私域运营、消息归档、批量通知的团队都绕不开的一道墙。常见的替代路径有三条一是走 Web 协议但微信网页版早已受限账号风险高二是 Hook 注入稳定性差且容易被判定异常三是 UI 自动化直接操作已经登录的桌面客户端。前两条路我都不推荐在生产环境用第三条才是可控的。FlaUI 是 .NET 生态里对微软 UIAutomation 封装得比较完整的一套库支持 UIA2 和 UIA3 两种底层驱动能在 Winform 宿主里直接引用把「找到微信窗口、定位控件、模拟输入、读取消息」这些动作写成 C# 代码。它不碰微信进程内存不修改客户端文件本质上是替人点鼠标敲键盘所以账号层面的风险相对可控。这篇笔记面向的是已经会用 C# 写 Winform、想把手动操作变成可复用脚本的开发者从环境搭建一路讲到消息收发的边界处理中间踩过的坑我都会标出来。2. 环境搭建与 FlaUI 选型UIA2 还是 UIA32.1 为什么在 Winform 里选 FlaUI 而不是原生 UIAutomation原生System.Windows.Automation能用但写起来啰嗦查找控件要自己拼PropertyCondition等待逻辑要手写轮询跨进程拿元素还经常遇到超时。FlaUI 在这几件事上做了封装AutomationElement.FindFirstDescendant支持链式条件Retry.WhileNull内置重试Application.Launch直接接管进程生命周期。对 Winform 项目来说引用方式也简单NuGet 装完就能用不需要额外的 COM 注册。选型上真正要纠结的是 UIA2 和 UIA3。UIA3 是微软后来主推的接口对 WPF 和 UWP 支持更好UIA2 走的是老的无障碍接口对 Win32 老程序兼容性更稳。微信 PC 版是典型的 Win32 混合自绘控件实测下来 UIA2 拿到的控件树更完整尤其是聊天列表和消息气泡区域。我的建议是默认用 UIA2遇到 UIA2 找不到的控件再切 UIA3 试。// 在 Winform 项目里初始化 FlaUIUIA2 驱动 using FlaUI.Core; using FlaUI.UIA2; var automation new UIA2Automation(); // 附加到已运行的微信进程而不是重新启动 var wechatProcess System.Diagnostics.Process.GetProcessesByName(WeChat) .FirstOrDefault(); if (wechatProcess null) { throw new Exception(微信未启动请先登录); } var app Application.Attach(wechatProcess); var mainWindow app.GetMainWindow(automation);这段代码的逻辑是先拿到UIA2Automation实例它是所有查找操作的入口然后按进程名找到微信用Application.Attach附加而不是Launch因为微信需要扫码登录脚本重启进程会打断登录态。GetMainWindow拿到主窗口句柄后后续所有控件查找都从这个窗口往下走。参数上唯一要注意的是GetProcessesByName里的进程名微信主进程叫WeChat但有些版本会拆出WeChatAppEx之类的子进程附加时要认准带主窗口的那个。2.2 Winform 宿主项目的引用与初始化Winform 这边不需要特殊配置新建一个 .NET Framework 4.7.2 或 .NET 6 的 Windows 窗体项目都行。NuGet 里搜FlaUI.UIA2和FlaUI.Core两个都装上。如果项目是 .NET 6注意把目标框架写成net6.0-windows否则System.Windows.Automation相关程序集会缺失。!-- csproj 里的关键配置 -- PropertyGroup TargetFrameworknet6.0-windows/TargetFramework UseWindowsFormstrue/UseWindowsForms /PropertyGroup ItemGroup PackageReference IncludeFlaUI.Core Version4.0.0 / PackageReference IncludeFlaUI.UIA2 Version4.0.0 / /ItemGroup版本号这里写的是我本地验证过的 4.0.0实际以 NuGet 最新稳定版为准。UseWindowsForms必须为 true否则 Winform 的设计器会报错。装完之后在窗体代码里using FlaUI.Core就能开始写查找逻辑了。有一点要提醒FlaUI 的操作默认跑在调用线程上如果你在 Winform 的 UI 线程里直接做大量查找界面会卡死常见做法是把自动化逻辑放到Task.Run里通过Invoke回主线程更新状态栏。3. 定位微信控件从窗口树到消息列表的实战路径3.1 用 Inspect 工具摸清控件树写代码之前先用工具看清楚微信的控件结构。微软官方有个 Accessibility Insights或者老牌的 Inspect.exe都能把当前窗口的 UIA 树展开。打开微信主窗口用 Inspect 悬停到左侧会话列表、中间消息区、底部输入框记下每个元素的AutomationId、Name、ControlType。微信的控件命名不太规范很多元素AutomationId是空的只能靠Name和层级关系定位。我一般会先抓三个关键节点会话列表容器、消息列表容器、输入框。会话列表通常是一个List控件子项是ListItem每个 ListItem 的Name就是联系人昵称或群名。消息区是一个List或Pane里面的消息气泡按时间排列。输入框是一个Edit控件ControlType是Edit。把这些记下来代码里就能按图索骥。3.2 查找会话与切换聊天窗口// 找到左侧会话列表里的指定联系人并点击 var sessionList mainWindow.FindFirstDescendant( cf cf.ByControlType(ControlType.List).And(cf.ByName(会话))); if (sessionList null) { // 有些版本没有 Name退化为按层级找第一个 List sessionList mainWindow.FindFirstDescendant( cf cf.ByControlType(ControlType.List)); } var targetSession sessionList.FindAllChildren( cf cf.ByControlType(ControlType.ListItem)) .FirstOrDefault(item item.Name.Contains(文件传输助手)); if (targetSession null) { throw new Exception(未找到目标会话可能被折叠或需要滚动); } targetSession.Click();这段逻辑分三步先定位会话列表容器优先按Name找找不到就退化为找第一个 List然后在子项里按Name模糊匹配联系人最后调用Click切换会话。参数上Contains比Equals更实用因为微信的 ListItem Name 有时会带上未读数比如「文件传输助手 2」。这里有个坑如果目标会话在列表可视区域之外FindAllChildren拿不到它需要先滚动列表。滚动可以用Mouse.Scroll或者对列表控件发ScrollPattern我一般用targetSession.Patterns.ScrollItem.Pattern.ScrollIntoView()但微信对 ScrollItem 模式支持不稳定更稳的做法是模拟鼠标滚轮。3.3 读取消息与发送文本// 读取当前聊天窗口的消息列表 var messageList mainWindow.FindFirstDescendant( cf cf.ByControlType(ControlType.List).And(cf.ByName(消息))); var messages messageList.FindAllChildren( cf cf.ByControlType(ControlType.ListItem)); foreach (var msg in messages) { // 消息文本通常在 Name 属性里部分版本在 Text 模式里 var text msg.Name; if (string.IsNullOrEmpty(text)) { var textPattern msg.Patterns.Text.PatternOrDefault; text textPattern?.DocumentRange.GetText(-1) ?? ; } Console.WriteLine($[{DateTime.Now:HH:mm:ss}] {text}); } // 发送消息定位输入框输入文本回车 var inputBox mainWindow.FindFirstDescendant( cf cf.ByControlType(ControlType.Edit)); inputBox.Focus(); inputBox.AsTextBox().Enter(这是一条自动化测试消息); inputBox.AsTextBox().Keyboard.Press(VirtualKeyShort.ENTER);读取消息时msg.Name在多数微信版本里能直接拿到文本但图片、文件、链接卡片这类消息 Name 可能是空的或者只有类型描述需要结合ControlType和子元素判断。发送消息用AsTextBox().Enter比Keyboard.Type更稳因为前者会先聚焦再输入减少丢字。回车用VirtualKeyShort.ENTER不要用\n微信输入框不认换行符。这里有个血泪经验输入法状态会影响输入如果当前是中文输入法Enter可能触发候选词上屏而不是发送脚本运行前最好把输入法切到英文或者用Keyboard.TypeSimultaneously配合剪贴板粘贴。4. 避坑与常见问题排查4.1 控件找不到报 NullReferenceException现象FindFirstDescendant返回 null后面调用直接崩。原因微信窗口没激活、控件树还没加载完、或者 UIA 驱动选错了。解决先调mainWindow.Focus()激活窗口再用Retry.WhileNull包一层重试超时设 5 秒。如果还不行换 UIA3 驱动再试一次两个驱动拿到的树不一样。4.2 消息读取重复或漏读现象轮询消息列表时同一条消息被读多次或者新消息没读到。原因微信消息列表是虚拟化的滚动时会复用控件FindAllChildren拿到的可能是当前可视区域的子集。解决不要依赖列表索引改用消息的RuntimeId去重维护一个已处理 ID 集合。漏读的话把轮询间隔调到 500ms 以内并且每次读取前先滚动到底部。4.3 输入框输入丢字或触发搜索现象Enter输入长文本时前面几个字丢了或者微信弹出了搜索框。原因输入框没真正获得焦点或者焦点跑到了搜索框。解决输入前先inputBox.Focus()然后Thread.Sleep(100)等焦点稳定。如果还是丢字改用剪贴板方案Clipboard.SetText然后Keyboard.Press(VirtualKeyShort.CTRL, VirtualKeyShort.KEY_V)这个方案对长文本最稳。4.4 微信版本更新后脚本全挂现象昨天还能跑今天微信自动更新后所有查找都失败。原因微信更新会调整控件树结构AutomationId和层级都可能变。解决不要把查找条件写死尽量用Name模糊匹配加ControlType组合并且把关键查找逻辑抽成配置版本更新后只改配置不改代码。我一般会在脚本启动时先做一次「自检」把关键控件找一遍找不到就弹窗提示而不是直接崩。4.5 多开微信时附加到错误进程现象电脑上开了两个微信脚本操作的是小号而不是主号。原因GetProcessesByName返回多个进程FirstOrDefault拿到的顺序不固定。解决按主窗口标题或登录账号筛选遍历所有微信进程找到MainWindowTitle包含目标账号的那个再附加。5. 进阶技巧把自动化脚本做成可维护的 Winform 工具5.1 用状态栏和进度条反馈执行状态Winform 里跑自动化最怕界面假死。我的做法是把 FlaUI 操作全部放到后台线程主线程只负责 UI。状态栏用StatusStrip加ToolStripStatusLabel进度条用ToolStripProgressBar通过Invoke更新。// 后台执行自动化主线程更新状态栏 private async void btnStart_Click(object sender, EventArgs e) { btnStart.Enabled false; statusLabel.Text 正在连接微信...; progressBar.Style ProgressBarStyle.Marquee; await Task.Run(() { var automation new UIA2Automation(); var proc Process.GetProcessesByName(WeChat).First(); var app Application.Attach(proc); var win app.GetMainWindow(automation); this.Invoke(new Action(() { statusLabel.Text 已连接开始读取消息; })); // 后续自动化逻辑... }); progressBar.Style ProgressBarStyle.Blocks; statusLabel.Text 执行完成; btnStart.Enabled true; }这段代码的关键点是Task.Run把耗时操作挪出 UI 线程Invoke把状态更新拉回主线程。ProgressBarStyle.Marquee是滚动条样式适合不确定时长的操作确定进度时可以设Value和Maximum。注意Invoke里不要做耗时操作否则还是会卡。5.2 消息去重与持久化轮询消息时去重是必须的。我用RuntimeId做键存到一个HashSetint[]里但数组做键比较麻烦实际用string.Join(,, runtimeId)转成字符串。持久化用 SQLite 或 LiteDB 都行Dapper 配合 SQLite 写起来很快。// 消息去重与入库 private readonly HashSetstring _processedIds new(); void ProcessMessage(AutomationElement msg) { var id string.Join(,, msg.Properties.RuntimeId.Value); if (_processedIds.Contains(id)) return; _processedIds.Add(id); var text msg.Name; using var conn new SQLiteConnection(Data Sourcewechat.db); conn.Execute( INSERT INTO Messages (RuntimeId, Content, CreatedAt) VALUES (id, text, time), new { id, text, time DateTime.Now }); }RuntimeId是 UIA 给每个元素的唯一标识同一元素多次查找值不变适合做去重键。HashSet放内存里程序重启会丢所以入库时用INSERT OR IGNORE配合唯一索引更稳。Dapper 的Execute直接传匿名对象做参数不用手写SqlParameter。5.3 一个我踩过的坑窗口最小化后控件树失效微信窗口最小化时UIA 拿到的控件树会变成空或者只剩标题栏所有查找都会失败。我一开始没注意脚本在后台跑着跑着就崩了。后来每次操作前先检查窗口状态如果是最小化就mainWindow.SetForeground()恢复。但SetForeground会抢焦点影响用户正常用电脑所以更稳妥的做法是让微信窗口保持可见但可以被其他窗口遮挡不要最小化。如果必须后台运行可以考虑用虚拟桌面或者单独的 Windows 会话但这超出 FlaUI 的能力范围了。从那以后我每次写微信自动化脚本都会在启动时强制检查窗口状态并且把「窗口必须可见」写进使用说明里。希望帮到你。本文还有配套的精品资源点击获取
返回列表