一、GUI 自动化:为什么要做?

1. 核心意义:解决 “重复、低效、易出错” 的测试痛点
  • 提升测试效率:手动测试重复用例(比如反复验证软件基础功能)时,测试人员要重复操作、耗时且易出错;GUI 自动化工具能 “按预设逻辑批量执行”,短时间完成大量重复工作。
  • 降低人力成本:原本多名测试人员花数小时 / 天数的工作,写好自动化脚本后,工具能自动完成,把测试人员从繁琐重复中解放出来。
2. 适用场景:“重复、稳定、批量” 的 GUI 测试场景

GUI 自动化适合需要重复执行、要求操作一致性的场景,核心作用是 “保障软件质量 + 提效”:

  • 覆盖软件开发全阶段:从集成测试到验收测试,能验证 “界面元素显示、交互流畅度、功能是否符合预期”。
  • 支持特殊测试需求:模拟不同用户环境测兼容性 / 稳定性;大规模测试时能快速反馈,支撑持续集成 / 交付、加速开发周期。

⚠️ 注意:依赖界面元素稳定性—— 如果 UI 频繁变动、交互复杂,自动化脚本的维护成本会很高。

二、为什么选 pywinauto?

pywinauto 是基于 Python 的 Windows 桌面 GUI 自动化库,核心是 “模拟用户操作(点击、输入),精准控制 Windows 窗口 / 控件”,适合自动化测试、批量任务、办公流程优化。

1. 底层技术:适配不同 Windows 应用框架

它靠两种 “后端模式” 覆盖不同类型的 Windows 应用:

  • win32模式:适配传统框架(比如 MFC、VB6 开发的旧应用)。
  • uia模式:适配现代框架(比如 WinForms、WPF、QT5,以及部分浏览器)。
2. 优势:专门为 Windows 场景设计的 “易用 + 强适配”
  • Python 生态友好:Python 简洁易学,脚本开发 / 维护成本低。
  • Windows 兼容性好:支持 Win7 及以上版本,能对接原生控件(按钮、文本框)和第三方控件(WPF、QT)。
  • 降低开发成本:自带 “动态等待控件加载” 机制(不用手动写等待代码);有pywinauto.findwindows模块辅助定位控件,调试方便。
  • 社区支撑强:开源项目,持续更新,文档 / 示例丰富。
3. 局限性:只聚焦 Windows,对部分场景 “不适用”
  • 平台限制:仅支持 Windows,不能用于 Mac/Linux。
  • 控件适配有限:非标准自定义控件(比如某些自研控件)可能需要额外处理。
  • 不支持的应用类型
    • 基于 Web 的应用(比如浏览器里的网页 / 企业 Web 系统):pywinauto 是为本地 Windows 应用设计的,Web 自动化得用 Selenium。
    • 自定义渲染引擎的应用(比如 Flutter、React Native 开发的桌面端):这类应用不暴露标准 Windows UI 接口(Win32/UIA),无法被 pywinauto 识别(比如企业微信、部分 Flutter 桌面应用)。

三、安装pywinauto

建议在每个项目的虚拟环境中安装 pywinauto,原因和操作说明如下:

为什么选 “每个项目的虚拟环境”?
  • 避免版本冲突:不同项目可能需要不同版本的 pywinauto(比如有的项目用 0.6.9,有的用更新版本),虚拟环境能隔离各项目的依赖
  • 保持项目独立性:项目迁移 / 分享时,虚拟环境的依赖清单(如requirements.txt)可以精准复现环境,不会受全局 Python 包的影响。
什么情况适合 “全局安装”?

如果你的所有项目都用相同版本的 pywinauto,且不需要隔离依赖,也可以全局安装(但不推荐,容易踩版本冲突的坑)。

步骤 1:打开项目后配置虚拟环境

!!!!!注意:每个项目的虚拟环境不要放一起,单独创建在该项目的目录下,如上图

这个选项的作用是:打开终端时自动激活当前项目目录下的虚拟环境

即便开启了 “Activate virtualenv”,仍加载之前项目的虚拟环境,是因为PyCharm 终端的 “会话缓存” 未清空,具体原因及解决:

1. 终端会话的缓存残留

PyCharm 的终端会保留上一次关闭时的会话状态(包括激活的虚拟环境),即使开启了自动激活,若上次会话的环境未关闭,新打开的终端会继承该状态。

2. 解决:重置终端会话
  • 方法 1:关闭当前终端标签页,重新打开一个新的终端标签(新标签会触发自动激活逻辑);
  • 方法 2:在终端中执行deactivate后,重启 PyCharm,再打开终端。

新终端会优先执行 “Activate virtualenv” 的逻辑,自动激活当前项目的.venv,不会再加载之前的环境。

!!!!!!注意:必须选对和当前项目匹配的解释器,否则会出现环境不对应的问题:

步骤 2:在 PyCharm 里安装 pywinauto
pip install pywinauto==0.6.9 -i https://pypi.tuna.tsinghua.edu.cn/simple

四、UISpy 工具的核心作用

UISpy 是微软提供的 UI 自动化测试工具,用于定位 GUI 程序的 UI 元素(类似 Web 自动化中的开发者工具),是编写 GUI 自动化脚本的前提 —— 只有定位到 UI 元素,才能对其执行操作。

1.UISpy 的功能与适用场景
  1. 功能:可视化查看应用的 UI 结构(控件树)、元素属性(如名称、类名、坐标),辅助选择 UI 元素并模拟操作;
  2. 适用范围:仅支持传统 Windows 原生应用框架的程序(如记事本);若工具无法识别元素(如企业微信),则该程序不支持pywinauto自动化。

2.同类工具

除 UISpy 外,还有 Spy++、Inspect.exe 等,功能与操作逻辑基本一致。

GUI⾃动化简单⽰例

⽰例:打开电脑⾃带记事本,输⼊内容,并保存⽂件。

import time
from pywinauto.application import Application
from pywinauto.timings import wait_until_passes

NOTEPAD_PID = 37884  # 替换为实际PID

# 1. 连接进程并获取窗口
app = Application(backend="win32").connect(process=NOTEPAD_PID)
notepad = app.window()
notepad.set_focus()
time.sleep(2)

# 2. 定位新版记事本输入控件(RichEditD2DPT)
# 增加超时等待,避免控件未加载完成
edit_control = wait_until_passes(
    timeout=10,  # 超时10秒
    retry_interval=1,
    func=lambda: notepad.child_window(class_name="RichEditD2DPT")
)

# 3. 输入文本
edit_control.type_keys("这是GUI自动化输入的内容")
time.sleep(1)

# 4. 快捷键保存+关闭
notepad.type_keys("^s")  # Ctrl+S
time.sleep(1)
notepad.type_keys("%{F4}")  # Alt+F4
后端类型适配场景稳定性推荐度
Win32所有系统记事本(Win7/10/11)✅ 99% 稳定⭐⭐⭐⭐⭐(优先选)
UIA现代应用(如 Office、浏览器)❌ 对记事本兼容差⭐⭐(仅特殊场景用)

五、pywinauto常⻅操作

1.打开程序:2 种方式(启动新程序 / 连接已有程序)

1. 启动新应用程序(start()方法)
  • 作用:启动一个全新的 Windows 应用进程。
  • 参数cmd_line是必填项,需要传入应用的 “可执行文件路径”(支持相对 / 绝对路径)。
    • 示例:
      • 简单启动:Application(backend="uia").start("notepad.exe")(系统自带程序可直接写文件名)
      • 绝对路径启动:start("D:\\software\\Sublime Text 3\\sublime_text.exe")(第三方程序需指定完整路径)
2. 连接已有应用程序(connect()方法)
  • 作用:关联已经在运行的应用进程(无需重新启动)。
  • 参数:只需传 1 个标识参数,常用的有:
    • process:应用的进程 ID(PID,可通过任务管理器查看)
    • handle:窗口句柄(系统给每个窗口分配的唯一标识)
  • 示例
    • 通过 PID 连接:app = Application(backend="uia").connect(process=31528)
    • 通过句柄连接:app = Application(backend="uia").connect(handle=65552)

startconnect的核心区别

方法作用返回值适用场景
start()启动全新的应用进程绑定该新进程的 Application 实例需打开一个未运行的新程序(如启动空白记事本)
connect()关联已在运行的应用进程绑定该已有进程的 Application 实例程序已手动 / 自动打开,仅需关联操作(如关联已打开的 “软件测试.txt”)

2.定位窗口:2 种核心方法

定位窗口是操作控件的前提,pywinauto 提供了 “显式定位”(推荐)“动态属性定位”(不推荐) 两种方式。

方法 1:通过window()方法显式定位(推荐)
  • 作用:通过窗口的特征参数(标题、类名等)精准定位窗口。

  • 常用参数(可组合使用):

参数作用示例
title精确匹配窗口标题window(title="untitled - Sublime Text")
title_re正则匹配窗口标题(灵活匹配动态标题)window(title_re=".*Sublime.*")(匹配含 “Sublime” 的标题)
best_match模糊匹配标题(允许轻微差异)window(best_match="Sublime Text")
class_name精确匹配窗口类名(需提前获取类名)window(class_name="PX_WINDOW_CLASS")
class_name_re正则匹配窗口类名window(class_name_re=".*WINDOW_CLASS")
  • 辅助工具print_control_identifiers()方法

    • 作用:打印窗口及其子控件的所有标识信息(标题、类名、控件类型等),帮助你获取定位参数。
    • 示例:win.print_control_identifiers()(执行后会输出类似截图中的控件树结构)

方法 2:通过动态解析对象属性定位(不推荐)
  • 作用:利用 Python 动态属性的特性,直接用 “窗口的 best_match 名称” 作为属性调用。
  • 示例
    • win = app.Dialog(等价于app.window(best_match='Dialog')
    • 含特殊字符的标题:用字典式访问app['untitled • - Sublime Text (UNREGISTERED)']
  • 缺点:属性名不能包含空格、特殊符号,易出错且可读性差,仅适用于简单场景。
补充:简化定位 ——top_window()方法
  • 作用:快速获取应用的 “顶级窗口”(应用的主窗口),无需指定参数。
  • 注意:仅能获取应用的一个顶级窗口,若应用有多个窗口可能定位错误,属于 “简易但不精准” 的方式。
  • 示例
win = app.top_window()  # 获取顶级窗口
win.wait('exists')      # 等待窗口加载

3.窗⼝操作

窗口操作方法:功能 + 返回值

表格里的方法分为 “控制类”(改变窗口状态)和“检查类”(查询窗口状态)两类:

方法功能描述返回值
close()关闭窗口。无返回值。
maximize()将窗口最大化。无返回值。
minimize()最小化窗口无返回值。
restore()窗口恢复正常大小无返回值。
get_show_state()获取窗口的显示状态。返回一个整数值,表示窗口的状态:0: 正常1: 最大化2: 最小化
is_dialog()检查控件是否是顶级窗口。返回布尔值:True 表示是顶级窗口,False 表示不是顶级窗口。
is_maximized()检查窗口是否处于最大化状态。返回布尔值:True 表示窗口已最大化,False 表示未最大化。
is_minimized()检查窗口是否处于最小化状态。返回布尔值:True 表示窗口已最小化,False 表示未最小化。
is_normal()检查窗口是否处于正常状态(既未最小化也未最大化)。返回布尔值:True 表示窗口处于正常状态,False 表示未处于正常状态。

4. 定位控件

4.1控件分类:理解桌面应用的控件层级

打开UI对象检查⼯具UISpy,通过ControlType可查看所定位到控件的类型。

桌面应用的控件是分层级的容器结构(窗口→子控件→子子控件),常见控件按功能分为 10 + 类,每类对应不同的交互逻辑:

  • 直接从主窗口定位:简单快捷,适合控件属性唯一的场景;
  • 逐层从父窗口定位:更精准,适合控件属性重复的场景;
  • 层级是 “路径” 不是 “限制”,pywinauto 支持跨层级定位。
分类控件名称说明
窗口与对话框对话框 (Dialog)用于用户交互(如警告、确认、信息输入)
窗口与对话框窗格 (Pane)窗口的组成部分,显示特定内容 / 功能
输入控件按钮 (Button)触发操作(如点击执行功能)
输入控件编辑栏 (Edit)输入 / 编辑文本(支持单行 / 多行)
输入控件组合框 (ComboBox)结合文本框 + 列表框,支持选择 / 输入自定义值
输入控件列表框 (ListBox)显示可选项目列表(支持单选 / 多选)
菜单控件菜单 (Menu)窗口顶部的功能选项集合
菜单控件菜单项 (MenuItem)菜单中的具体选项,点击执行对应功能
菜单控件弹出菜单 (PopupMenu)右键点击弹出的快捷功能菜单
列表显示控件列表显示控件 (ListView)表格形式显示数据(支持多列、排序)
容器控件组框 (GroupBox)对控件分组,提升界面可读性
选择控件复选框 (CheckBox)多选操作(可勾选多个选项)
选择控件单选框 (RadioButton)互斥选择(只能选一个选项)
显示控件状态栏 (StatusBar)窗口底部显示应用状态 / 提示信息
显示控件静态内容 (Static)显示不可编辑的静态文本 / 图像
导航控件树状视图 (TreeView)展示分层数据(如文件结构、组织架构)
导航控件选项卡控件 (TabControl)有限空间内组织多个页面,每个选项卡含不同内容
工具控件工具栏 (Toolbar)放置常用按钮 / 工具,方便快速操作
工具控件工具提示 (ToolTips)鼠标悬停时显示控件功能提示
头部内容头部内容 (Header)显示标题 / 表头信息(如表格列标题)

代码 / 概念

  • app = Application(backend="uia").connect(process=QQ音乐PID)
  • win = app.window()

对应 QQ 音乐的现实场景

  • 你找到并 “绑定” 了 QQ 音乐这个软件进程
  • 获取 QQ 音乐的顶级主窗口

通俗解释

  • 相当于你走到 QQ 音乐的 “软件本体” 面前,确认 “我要操作的是这个软件,不是其他软件”;但此时你还没接触到它的任何界面(窗口)。
  • 相当于你打开了 QQ 音乐的 “主界面”(就是你双击桌面图标后看到的那个大窗口),这个主窗口是所有其他小窗口(比如下载弹窗、设置窗口、歌词窗口)的 “老大”,所有子窗口都归它管。
# 1. 绑定QQ音乐进程(找到“QQ音乐”这个软件)
app = Application(backend="uia").connect(process=QQ音乐的PID)

# 2. 获取QQ音乐的顶级主窗口(打开QQ音乐的主界面)
# 这一步就是拿到“QQ音乐主界面”这个顶级窗口,所有弹窗/子窗口都在它下面
qqmusic_main_win = app.window()  

# 3. 操作主窗口下的子窗口(比如定位“下载弹窗”)
# 下载弹窗是主窗口的子窗口,相当于主界面上弹出来的小窗口
download_dialog = qqmusic_main_win.child_window(name="添加本地下载音乐", control_type="Dialog")

# 4. 操作子窗口的子控件(比如“取消”按钮)
# 取消按钮是下载弹窗的子控件,相当于弹窗里的一个按钮
cancel_btn = download_dialog.child_window(name="取消", control_type="Button")
cancel_btn.click_input()

把 QQ 音乐进程比作 “一栋房子”:

  • app = connect(process=PID):你确认了要进 “QQ 音乐这栋房子”(不是微信、记事本的房子);
  • win = app.window():你走进了房子的 “客厅”(顶级主窗口),客厅是房子的核心区域;
  • child_window():从客厅走到 “卧室”(下载弹窗),再从卧室找到 “床头柜上的按钮”(取消按钮)—— 所有操作都基于 “客厅(顶级窗口)” 这个起点。

定位控件,需要借助前⾯学到的 print_control_identifiers() ⽅法,打印窗⼝及其⼦控件的标识符信息

Dialog - 'untitled • - Sublime Text (UNREGISTERED)' (L806, T81, R1811, 
B1007)
['Dialog', 'untitled • - Sublime Text (UNREGISTERED)Dialog', 'untitled • - 
Sublime Text (UNREGISTERED)']
child_window(title="untitled • - Sublime Text (UNREGISTERED)", 
control_type="Window")
 | 
 | ScrollBar - '' (L815, T44, R915, B54)
 | ['ScrollBar', 'ScrollBar0', 'ScrollBar1']
 | | 
 | | Button - '左移⼀列' (L0, T0, R0, B0)
 | | ['左移⼀列', 'Button', '左移⼀列Button', 'Button0', 'Button1']
 | | child_window(title="左移⼀列", auto_id="UpButton", 
control_type="Button")
 | | 
 | | Thumb - '位置' (L0, T0, R0, B0)
 | | ['Thumb', '位置', '位置Thumb', 'Thumb0', 'Thumb1', '位置0', '位置1', 
'位置Thumb0', '位置Thumb1']
 | | child_window(title="位置", auto_id="ScrollbarThumb", 
control_type="Thumb")
 | | 
 | | Button - '右移⼀列' (L0, T0, R0, B0)
 | | ['右移⼀列Button', '右移⼀列', 'Button2']
 | | child_window(title="右移⼀列", auto_id="DownButton", 
control_type="Button")
 | 
 | ScrollBar - '' (L715, T144, R725, B244)
 | ['ScrollBar2']
 | | 
 | | Button - '上⼀⾏' (L0, T0, R0, B0)
 | | ['上⼀⾏', '上⼀⾏Button', 'Button3']
 | | child_window(title="上⼀⾏", auto_id="UpButton", 
control_type="Button")
 | | 
 | | Thumb - '位置' (L0, T0, R0, B0)
 | | ['Thumb2', '位置2', '位置Thumb2']
 | | child_window(title="位置", auto_id="ScrollbarThumb", 
control_type="Thumb")
 | | 
 | | Button - '下⼀⾏' (L0, T0, R0, B0)
 | | ['下⼀⾏', '下⼀⾏Button', 'Button4']
 | | child_window(title="下⼀⾏", auto_id="DownButton", 
control_type="Button")
 | 
 | TitleBar - '' (L835, T84, R1802, B119)
 | ['TitleBar']
 | | 
 | | Menu - '系统' (L652, T72, R674, B94)
 | | ['Menu', '系统Menu', '系统', '系统0', '系统1', 'Menu0', 'Menu1'] 
 | | child_window(title="系统", auto_id="MenuBar", control_type="MenuBar") 
 | | | 
 | | | MenuItem - '系统' (L652, T72, R674, B94)
 | | | ['系统2', 'MenuItem', '系统MenuItem', 'MenuItem0', 'MenuItem1']
 | | | child_window(title="系统", control_type="MenuItem")
 | | 
 | | Button - '最⼩化' (L1624, T82, R1684, B119)
 | | ['最⼩化Button', '最⼩化', 'Button5']
 | | child_window(title="最⼩化", control_type="Button")
 | | 
 | | Button - '最⼤化' (L1684, T82, R1743, B119)
 | | ['最⼤化', 'Button6', '最⼤化Button']
 | | child_window(title="最⼤化", control_type="Button")
 ......
 ......

注意:

print_control_identifiers() 方法默认只能打印 “当前可见窗口 / 控件” 的标识信息—— 比如 Sublime 的 File 菜单,只有当 File 菜单被展开(可视化显示)时,它的子菜单(New/Open 等)才会被该方法捕获并打印。

如果要获取 File 的子菜单标识,需要先通过代码展开 File 菜单窗口,再对这个展开的子菜单窗口调用print_control_identifiers(),才能打印出子菜单的控件信息。

操作场景操作步骤print_control_identifiers() 打印内容
仅打开Sublime主窗口直接调用主窗口的该方法主窗口+可见的顶层菜单(File/Edit等)
要获取File的子菜单1. 定位并展开File菜单窗口
2. 调用该子窗口的方法
File菜单下的子菜单(New/Open/Save等)

注意:

定位写法类型具体示例特点 & 使用场景
别名/快捷定位win['File'] 或 win['FileMenuItem'] 或 win['MenuItem21']1. 最简单:直接用打印结果里[]中的任意一个字符串就能定位
2. 原理:自动化库会自动匹配控件的“名称/别名”
3. 缺点:如果有重名控件,可能定位错
精准属性定位win.child_window(title="File", control_type="MenuItem")1. 最精准:通过title(标题)+control_type(控件类型)双重条件定位
2. 原理:指定控件的具体属性,缩小匹配范围
3. 优点:即使有重名控件,也能准确定位目标

打印结果里同时给出这两种信息,是为了:

  1. ['File', 'FileMenuItem', 'MenuItem21'] → 给你快捷写法,日常调试 / 简单场景用,省代码;
  2. child_window(...) → 给你精准写法,正式脚本 / 复杂界面用,避免定位错误。
# 方式1:快捷别名定位(任选其一)
file_menu1 = win['File']
file_menu2 = win['FileMenuItem']

# 方式2:精准属性定位(推荐正式脚本用)
file_menu3 = win.child_window(title="File", control_type="MenuItem")

# 三种方式定位的是同一个控件,点击效果完全一样
file_menu3.click()

简单说:简单场景用 [] 里的快捷名,复杂场景用 child_window 的精准属性,两种都能用,精准写法更稳

注意:打印菜单 / 控件标识,核心不是重复 UI 工具的工作,而是:

对比维度UI工具(UIgoon/Inspect.exe)代码打印print_control_identifiers()
信息来源系统层面抓取的控件元数据(偏“理论值”)自动化库实际识别到的控件信息(偏“实际值”)
准确性❌ 可能不准:
1. 部分自定义控件识别不到
2. 属性与自动化库兼容度低
3. 易受版本影响
✅ 最准确:
1. 输出自动化库可识别的属性
2. 反映控件真实加载状态
3. 是代码定位的直接依据
适用场景快速预览控件结构、初步定位(辅助作用)编写自动化代码时,获取可用的控件标识(核心作用)
局限性无法反映“自动化库是否识别控件”,可能出现“工具可见但代码定位不到”的情况仅能打印当前加载的控件,未加载的临时控件无法捕获
  1. 验证兼容性:确认 UI 工具显示的控件信息,是否能被你用的自动化库(如 pywinauto)识别;
  2. 获取精准标识:直接拿到代码能 “直接用” 的控件属性(比如title/control_type),避免因 UI 工具的 “理论值” 导致代码定位失败;
  3. 排查定位问题:如果代码定位不到控件,打印出来的信息是最直接的排查依据(比如能看到控件是否真的存在、属性名是否和预期一致)。

简单说:UI 工具是 “参考地图”,打印的信息是 “实际能走的路”—— 自动化代码要按 “实际的路” 来写,才不会走偏

4.2动态解析

基于best_match标题来定位

⽰例1:定位sublimeText⼯具的菜单栏
from pywinauto import Application

app = Application(backend="uia").connect(process=34408)

win = app.window(title_re=".*Sublime Text.*")

win.wait('exists')
win.restore()

menu = win['应⽤程序']

print(menu.items())

4.3child_window

child_window() 和定位窗⼝的⽅法 window() 参数⼀样,可以通过标题或者类名进⾏精确匹 配、模糊匹配等,在这⾥额外拓展⼏个进⾏控件定位时需要⽤到的参数:

from pywinauto import Application
# 创建Application对象,连接到正在运⾏的Sublime Text进程 
app = Application(backend='uia').connect(process=38544)
# 获取与Sublime Text相关的窗⼝对象,使⽤正则表达式匹配窗⼝标题 
win = app.window(title_re='.*Sublime Text.*')
# 等待窗⼝变为可⻅状态,确保窗⼝已经加载完成 
win.wait("visible")
# 通过控件的属性(标题、⾃动ID和控件类型)定位菜单栏对象 
menu = win.child_window(title="应⽤程序", auto_id="MenuBar", 
control_type="MenuBar")
# 打印菜单项的列表,查看菜单中包含的所有选项 
print(menu.items())
若存在多个控件的 auto_id 相同,可通过添加索引参数来定位:
from pywinauto import Application 

app = Application(backend='uia').connect(process=38544)

win = app.window(title_re='.*Sublime Text.*')

win.wait("visible")

#存在相同的auto_id和control_type,使⽤found_index来定位 
menu = win.child_window(auto_id="MenuBar", 
control_type="MenuBar",found_index=1)
print(menu.texts())

注意,若元素值为空,或不是唯⼀的情况下,都会导致定位失败

⽰例:定位失败
from pywinauto import Application 

app = Application(backend='uia').connect(process=38544)

win = app.window(title_re='.*Sublime Text.*')

win.wait("visible")

#存在相同的auto_id和control_type 
menu = win.child_window(auto_id="MenuBar", control_type="MenuBar")
print(menu.texts())

控件之间存在⽗⼦关系,可通过 children() ⽅法获取控件的⼦类,也可通过 parent() ⽅法获取控件的⽗类

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐