PC 端 UI 自动化:pywinauto 控件树实战笔记

App 自动化做熟之后回头做 PC 端,最大的感受是:Windows 桌面比移动端更不标准——同一个"对话框",Qt、WPF、Win32、Electron 各长各的。pywinauto 是覆盖最广的选择,这篇记录控件树分析和实战中最重要的几手。

一、两种 backend,先分清楚

1
2
3
4
5
6
7
from pywinauto import Application

# UIA backend:支持 Win10+ 的几乎所有框架(WPF/Qt/UWP/Electron 部分)
app = Application(backend="uia").connect(title_re=".*订单管理.*")

# win32 backend:经典 Win32 控件,更快更稳,但只认传统消息循环
app = Application(backend="win32").connect(title="计算器")

选型经验

  • 新框架(Qt5+、WPF)一律先试 uia
  • 老系统工具类程序(控制面板、传统 MDI)用 win32 更快;
  • Electron 应用:UIA 只能看到窗口级结构,内部 DOM 要走 CDP--remote-debugging-port),别硬刚控件树。

二、控件树分析:dump 先行,拒绝盲写选择器

pywinauto 最大的价值是把控件树变成可遍历的 Python 对象:

1
2
3
4
5
6
7
8
9
10
11
# 1) 全树 dump 到文件,先看结构再写脚本
dlg.print_control_identifiers(depth=8, filename="tree.txt")

# 2) 按属性定位(推荐组合:auto_id 优先,text 兜底)
send_btn = dlg.child_window(auto_id="btnSend").wait("visible ready", timeout=10)
send_btn.click_input()

# 3) 表格控件:DataGrid/ListView 的行列取值
grid = dlg.child_window(auto_id="dataGrid").wrapper_object()
grid.select([0, 2]) # 选中第 3 行第 1 列
value = grid.get_item((2, 0)).text()

实战约定:控件定位写选择器前,先 dump 出 auto_id 清单和开发对齐——桌面应用的 auto_id 往往藏在代码的 ObjectName/AutomationProperties.AutomationId 里,和 App 端的 resource-id 是同一个道理:有约定就永远有稳定定位

三、虚拟列表:滚动加载的坑

长列表(万行级别)在 UIA 里是虚拟化的——只渲染可见区域,get_item(5000) 直接报"元素不在可视区"。解法:

1
2
3
4
5
6
7
8
9
10
def find_row(grid, text, max_scrolls=200):
"""二分滚动定位:先粗扫再精扫,比线性滚快一个量级"""
for _ in range(max_scrolls):
visible = grid.visible_items()
if any(text in (it.text() or "") for it in visible):
return _click_match(grid, visible, text)
if _reached_bottom(grid):
return None
grid.scroll_down(30) # 每次滚 30 行
raise TimeoutError(f"未找到包含 {text!r} 的行")

要点:

  • 滚动步长要自适应:一次滚太多会跳过目标(虚拟化渲染有缓冲),30~50 行是安全步长;
  • 找不到先确认"到底了没有"再抛超时,区分"不存在"和"没滚到";
  • 定位到行之后,先滚动到可见再点击click_input() 要求元素真实可见。

四、多窗口/子窗口管理

桌面应用动辄主窗口 + 弹窗 + 子面板:

1
2
3
4
5
6
7
8
# 弹窗出现后接管
with app.window(title_re="确认.*") as confirm:
confirm.child_window(title="确定").click_input()

# 子面板切换:TabControl 要激活页签后控件才可交互
tab = dlg.child_window(auto_id="tabOrders")
tab.select(2) # 切到第 3 个页签
tab.wait("visible ready")

with 上下文管理窗口生命周期是 pywinauto 最省心的设计:弹窗关掉、窗口销毁都自动处理,不会留下野引用。

五、稳定性:桌面版的三板斧

和 App 端一脉相承:

  1. 等待wait("visible ready") 永远带上 timeout,禁止 time.sleep
  2. 焦点click_input() 是真实鼠标点击,受焦点/遮挡影响——自动化期间保证目标窗口在最前(restore() + set_focus()),被其他窗口遮挡时 click_input 会点错地方;
  3. 失败现场:失败时 win32gui 截屏 + print_control_identifiers 导出当前树,两个产物一起归档,定位"控件树变了还是时序问题"一目了然。

六、小结

PC 端自动化的核心是先理解控件树,再写选择器:backend 选对、auto_id 有约定、虚拟列表会滚动、窗口生命周期用 with 管。做到这四点,pywinauto 的脚本稳定性和 App 端可以打平。