文档

环境与版本

控件树被藏起来是这套方案最硬的约束。先判断自己属于哪种情况,再谈别的。

这是所有 UIA 方案的共同前提

微信 4.1 起,主窗口在 GPU 合成渲染下会退化成一个“纯画布”, UIA 读不到任何子控件。这时脚本找不到会话列表、消息列表、输入框。

这与用不用本库无关 —— 任何基于 UIA 的方案都受同一条约束。

实测现象

微信版本 状态 顶层窗口 ClassName UIA 子树
4.1.15.13 已登录、GPU 合成渲染 Qt51514QWindowIcon 只有 2 个节点
4.1.15.13 登录页 mmui::LoginWindow 正常,10 个节点
4.1.8.107 已登录主界面 mmui::MainWindow 完整控件树

最要命的一点:同一个窗口句柄的 ClassName 会在 mmui::MainWindow(有完整子树)和 Qt51514QWindowIcon(0 个子节点)之间来回切换。 也就是说树是会「掉」的。

最常见的元凶:微信自己更新

同一天实测记录:

时间 顶层 UIA ClassName 控件树
20:10 mmui::MainWindow 完整,收发跑通
20:38 自动更新落地 —
21:11 Qt51514QWindowIcon children = 2

⚠ 判断版本别看 mtime 或文件大小:更新器会保留归档时间戳, 两份不同版本的 Weixin.exe 实测大小和时间戳可以完全一样。 要用 win32api.GetFileVersionInfo() 读,或看运行进程的版本。

为什么会这样

微信 4.0 之后换成自绘 UI 框架,两条机制叠加:

  1. 按需暴露 —— 默认只上报顶层窗口,检测到系统里有「无障碍客户端」 (讲述人 / NVDA 这类屏幕阅读器)时才加载完整 UIA Provider。
  2. 渲染后端切换 —— Qt 在显卡驱动、DPI、多屏、远程桌面、是否管理员运行 等条件下会切到 ANGLE/DXGI 合成渲染,一旦进入合成模式主窗口就变成 Qt51514QWindowIcon。

两条都是环境相关的,所以有人机器上一直正常,有人一升级就全废。

判断自己属于哪种情况

python -m wx4auto.selftest              # 卡住就跑下一步
python tools\probe_a11y.py              # 诊断报告

输出里看三件事:

  • 顶层窗口 ClassName 是 mmui::MainWindow 还是 Qt51514QWindowIcon
  • MMUIRenderSubWindowHW 下面有没有子节点
  • 有没有任何 mmui:: 前缀的元素

对策(按推荐顺序试)

1. 确保「无障碍客户端」在场

启动微信之前就让系统知道有屏幕阅读器。 最简单:Win + Ctrl + Enter 打开 Windows「讲述人」,再启动微信; 确认控件树出来后可以把讲述人关掉。

2. 强制 Qt 走软件渲染

给微信加环境变量再启动:

set QT_ANGLE_PLATFORM=software
set QT_OPENGL=software
set QT_QUICK_BACKEND=software
"D:\ruanjian\Weixin_Old\Weixin\Weixin.exe"

仓库里有现成脚本:tools\launch_weixin_uia.cmd(双击即可)。

3. 关掉高 DPI 兼容性缩放

右键 Weixin.exe → 属性 → 兼容性 → 更改高 DPI 设置 → 勾选「替代高 DPI 缩放行为」并选 系统。

4. 避免远程桌面 / 多屏混合 DPI

远程会话是 Qt 切换到合成渲染的高发场景。尽量在本机、单屏、100% 缩放下跑。

5. 以管理员身份运行脚本

UIA 客户端与目标进程权限不一致时,Windows 会隐藏部分控件层。

实测记录:1、2 两项单独试过都还没能让主窗口变回 mmui::MainWindow (登录窗口一直正常),说明还叠加了渲染后端的问题。这部分属于环境调优,与代码无关。

库自身怎么处理

不假设树一直在,每次操作前体检:

wx._main.tree_exposed()          # -> bool,树在不在
wx._main.explain_tree_missing()  # -> 不在时打印可执行的对策

树不在时明确报错,绝不静默失败 —— 否则你会看到“接口不报错但全是空结果”, 那是最难查的一类现象。

另外两种“看不见”,要分开治

状态 判断依据 能不能自动救
最小化 IsIconic(hwnd) == True ✅ 还原即可
被收进托盘(点 × / Esc) IsWindowVisible == 0 且没最小化 ✅ 走微信自己的托盘逻辑(点托盘图标)
窗口可见但树为空 tree_exposed() == False 等 1~2 分钟,Qt 会自己重建 Provider

被收进托盘后,任何 Win32 手段都救不回来。 ShowWindow(SW_SHOW/SW_RESTORE)、SetWindowPos(SWP_SHOWWINDOW)、 SetForegroundWindow、AttachThreadInput 组合、RedrawWindow 全都实测无效。

其中 SetWindowPos(SWP_SHOWWINDOW) 最坑:它能让 IsWindowVisible 变 True, 但 Qt 收不到“窗口该显示了”,结果是白屏 + 控件树永远为空的窗口 —— 比隐藏还糟。 所以库里绝不会这么做。

适配新版本微信

微信更新后如果某些功能失效,九成是控件类名变了:

python -m wx4auto probe --out dump.txt

在 dump 里找到对应控件,把新类名补进 wx4auto/controls.py 的候选列表(新的放前面)。 每一项都是候选列表,按顺序回退,所以补名字不会破坏旧版本兼容。 界面文案在 languages.py,中/繁/英三份。