当业务系统没有API、命令行接口或可直接集成的数据通道时,桌面自动化往往是打通业务流程的最后一公里。pywinauto库通过Win32 API与Microsoft UI Automation(UIA)访问Windows窗口及控件,使Python脚本能够驱动桌面应用。本文聚焦于pywinauto的基础使用方法,以便快速掌握其核心操作并落地到实际场景中。pywinauto官方仓库见:pywinauto GitHub,官方文档见:pywinauto官方文档

1 基础入门

本章先厘清三个关键问题:pywinauto能做什么、其底层工作机制是什么,以及如何为目标程序选择合适的运行后端。明确这些前提,后续内容的学习将更为顺畅。

1.1 为什么需要桌面自动化?

在自动化控制的版图中,Web端有Selenium、移动端有Appium,而Windows桌面应用长期缺少统一、广泛使用的自动化方案。大部分现代桌面应用可能会提供对应API,但当面对没有API的老旧ERP系统,或者需要批量操作微信、Office、行业客户端软件时,才能真正体现桌面自动化的价值。pywinauto正是为这类场景而生的Python库。pywinauto的核心能力可以概括为模拟真实用户行为,向应用程序窗口和控件发送鼠标点击、键盘输入等操作,同时读取控件的属性与状态。

与那些依赖屏幕坐标盲目定位的库不同,pywinauto通过Windows的可访问性接口识别窗口的控件树结构,再以标题、类名、控件类型等语义化条件精准定位元素。在实际工作中,pywinauto通常出现在以下场景:

  • 桌面应用自动化测试:为没有测试接口的Windows客户端构建UI自动化测试体系,例如银行柜面系统、ERP客户端、行业专用软件;
  • 重复性操作解放:把每天都要手动执行几十次的表单填写、报表导出、数据录入交给脚本,释放人力;
  • 无接口系统的数据采集:从只有界面、没有API的老旧系统中自动提取业务数据,为数据分析提供原料;
  • 辅助工具开发:为特定流程开发桌面辅助操作工具,如自动巡检、自动备份脚本。

目前pywinauto专注于Windows平台,如果自动化的对象运行在非Windows平台,需要另寻方案。

1.2 环境搭建

pywinauto的安装非常轻量:

pip install pywinauto

pywinauto内置pywinauto.keyboard来实现常规按键模拟。如果项目确实需要全局热键、系统级按键监听等能力,再按需安装第三方keyboard库:

pip install keyboard

注意:pywinauto.keyboard与第三方keyboard库是两个不同模块。全局键盘监听可能受到权限、安全软件和运行会话限制。仅发送按键时,优先使用pywinauto自带能力。

安装完成后,用一行代码确认版本,避免后续因环境问题排查半天:

import pywinauto
print(pywinauto.__version__)

1.3 运行后端选择

动手编写脚本之前,必须先理清被测应用程序的界面结构,明确它包含哪些界面元素、每个元素的名称和类型。这一步好比装修前先拿到户型图,是后续顺利实施的基础。

pywinauto作为一个Windows界面自动化工具,其工作原理是通过底层通信技术与目标应用程序进行交互,从而识别和操作界面控件。这种底层通信技术,就是所谓后端。pywinauto提供了两种不同的后端,分别基于两套Windows界面访问机制。项目开始前,需要从中选择一种,这个选择会直接影响脚本能否正确识别控件。选错后端,可能会发现检查工具里明明有的控件,代码却怎么也找不到,这不是pywinauto的bug,而是选择了错误的沟通语言。pywinauto支持的两种后端对比如下:

后端 标识符 适用场景
Win32 API backend="win32" MFC、VB6、VCL、简单WinForms控件、传统老程序(默认后端)
MS UI Automation backend="uia" WinForms、WPF、UWP应用、Qt5、浏览器(需启用辅助功能)

Chrome浏览器默认情况下可能无法完整暴露页面控件信息,需要根据版本情况启用辅助功能支持,例如启动参数--force-renderer-accessibility,否则UIA无法获取其页面中的控件信息。此外,受comtypes库的限制,UIA后端对部分非标准控件及其特有属性的支持可能不完整,但微软官方提供的标准控件通常没有问题。微软关于UIA跨不同Windows控件框架提供统一访问模型的原理说明,可参考UI Automation Overview

后端选定之后,接下来的问题是如何知道目标程序内部有哪些控件?这就需要借助专门的探查工具。以下三款工具分别适用于不同的场景。

  1. Spy++
  • 介绍:随Visual Studio发行版安装,基于Win32 API工作,其显示的控件即为win32后端可操作的控件。
  • 推荐度:适合传统Win32应用及老程序,与win32后端匹配度最高。
  • 下载:随Visual Studio安装,无需单独下载。具体使用见:Spy++
  1. Inspect.exe
  • 介绍:微软官方UI元素检查工具,包含在Windows SDK中。使用时需切换到UI Automation模式,若此时显示的控件层级比Spy++更丰富,则说明应选用uia后端。
  • 推荐度:首选推荐,适用面最广,同时支持UIA和Win32两种模式。
  • 下载:Inspect.exe是Windows SDK自带的工具,推荐通过微软官方下载页面安装。安装后位于C:\Program Files (x86)\Windows Kits\10\bin\<版本号>\<架构>\Inspect.exe,也可搜索文件名定位。网上虽有剥离版本,但建议优先使用官方渠道。
  1. py_inspect
  • 介绍:pywinauto项目提供的多后端检查工具,可在同一界面切换win32与uia后端,直观对比控件层级差异。代码仅约200行,是学习pywinauto架构的绝佳范例。
  • 推荐度:适合需要对比后端差异或学习pywinauto内部原理的场景。
  • 下载:GitHub地址:https://github.com/pywinauto/py_inspect

上手顺序建议从Inspect.exe开始,其UIA模式覆盖面最广,配合后续介绍的print_control_identifiers()方法,能够快速建立与控件树的对应关系。更稳妥的经验法则是传统程序优先尝试win32后端,WPF、UWP、Qt等现代界面优先尝试uia后端,最终以检查工具的显示结果和最小验证脚本的实际输出为准。Inspect.exe的入门参考文档见:inspect-objects

1.4 第一个自动化脚本

理论铺垫到此为止,接下来让代码实际运行起来。下面这个记事本示例涵盖了pywinauto最核心的工作流程:启动、定位窗口、操作控件、结束。即使暂时不清楚每一行的具体作用,也可以先感受一下它的整体节奏。

from pywinauto import Application
import time

# 1. 启动 Notepad
Application(backend="uia").start("notepad.exe")
time.sleep(1)

# 2. 连接窗口:用正则匹配标题,避免标题变化导致定位失败
app = Application(backend="uia").connect(
    title_re=".*Notepad.*"
)

# 3. 获取主窗口
main_window = app.top_window()

# 查看窗口控件结构
main_window.print_control_identifiers()

# 4. 定位文本编辑区域
edit_area = main_window.child_window(
    control_type="Document"
)

# 5. 输入文本
# {ENTER}表示按下Enter,with_spaces=True允许输入空格
edit_area.type_keys("{ENTER}")
edit_area.type_keys("Hello", with_spaces=True)
edit_area.type_keys("{ENTER}")
edit_area.type_keys(
    "Welcome to pywinauto on Windows 11!",
    with_spaces=True
)

# 6. 点击关闭按钮
close_button = main_window.child_window(
    title="关闭",
    control_type="Button"
)
close_button.click_input()

这段代码中出现的Applicationchild_window()type_keys()等,正是pywinauto的核心API。后续章节会逐一说明它们的设计思路。关于pywinauto的更详细介绍,也可参考:基于pywinauto实现PC端自动化

1.5 桌面自动化工具生态对比

pywinauto并非唯一选择,了解同类工具的差异,有助于在合适场景下做出更合理的技术决策。

工具/框架 定位方式 语言支持 适用场景 优缺点
pywinauto 控件树(Win32 / UIA) Python 标准Windows桌面应用 开源、活跃、易上手;不支持跨平台
AutoIt 控件ID、文本、类名等 AutoIt脚本语言 传统Win32应用,安装包制作 轻量、支持编译为exe;语言小众,生态较弱
SikuliX 图像识别 + OCR Python、Java、Ruby 游戏、Flash、任意不可访问的界面 不依赖控件API;速度慢、受分辨率影响
Robot Framework 关键字驱动,可集成pywinauto Python 测试框架集成 团队协作友好;学习曲线较陡
PowerShell + COM COM对象 PowerShell Office自动化、系统管理 系统自带,无需额外安装;仅限COM可访问的对象

2 核心概念与对象模型

使用pywinauto进行GUI自动化时,核心问题是如何精准定位目标窗口和控件。pywinauto通过三个核心类来解决这一问题。

  • Application:管理目标程序进程,可启动新程序或连接已运行程序。
  • Desktop:代表整个桌面环境,是所有窗口的最顶层根节点。
  • WindowSpecification:描述待查找控件的条件,通过标题、类名、控件类型等属性组合成查找说明。

三者的协作逻辑为先通过Application或Desktop锁定目标窗口所在的容器,再由WindowSpecification在容器内部逐层筛选,直至精准命中目标控件。理清从容器到控件的查找链条后,后续操作只需按部就班地调用对应方法。

2.1 Application对象

Application是pywinauto的主入口点,其设计将自动化操作限定在单个进程边界内,因此可同时控制多个应用实例而互不干扰。

创建Application对象

根据程序是否已经运行,创建方式分为启动和连接两大类:

from pywinauto.application import Application

# 启动记事本
app = Application(backend="uia").start("notepad.exe")

# 通过进程ID连接
app = Application(backend="uia").connect(process=8948)

# 通过程序路径连接
app = Application(backend="uia").connect(path=r"C:\Program Files\app.exe")

# 通过窗口标题连接
app = Application(backend="uia").connect(title="微信")

其中,start()适合从头到尾由脚本掌控的流程。connect()则适合接管一个已经打开、甚至已经登录好的程序(比如已扫码登录的微信),这在实际项目中往往更实用,毕竟让脚本替用户完成扫码登录并不现实。

Application常用方法

app = Application(backend="uia").connect(
    title_re=".*Notepad.*"
)

# 返回当前顶部窗口,返回值为WindowSpecification对象
app.top_window()

# 根据筛选条件返回一个窗口,返回值为WindowSpecification对象
app.window(**kwargs)

# 返回所有符合条件的窗口列表,列表项为Wrapper对象
app.windows(**kwargs)

# 返回指定时间间隔内的CPU使用率
app.cpu_usage()

# 等待进程CPU使用率低于指定阈值
app.wait_cpu_usage_lower(threshold=2.5, timeout=None, usage_interval=None)

# 判断目标进程是否为64位
app.is64bit()

# 强制关闭应用程序
app.kill(soft=False)

wait_cpu_usage_lower()用于等待Application对象的目标进程CPU占用率降至指定阈值以下。程序启动或加载数据期间界面尚未就绪,此时操作控件容易失败。time.sleep()采用固定等待时长,过短则控件未就绪,过长则浪费时间。wait_cpu_usage_lower()依据CPU占用回落判断加载完成,比固定等待更节省时间且更稳定,尤其适合启动耗时不确定的胖客户端应用。

2.2 Desktop对象

Application的进程边界在大多数场景下是优点,能够限定操作范围,避免误触其他程序窗口。但当目标程序界面分散在多个进程中时,此边界反而成为障碍。典型如Windows 10和11的计算器,其界面元素绘制在多个进程中,单一Application对象无法跨进程访问控件。

此时可改用Desktop对象。Desktop代表整个桌面环境,不受进程边界限制,可访问桌面上任意窗口和控件。

from subprocess import Popen
from pywinauto import Desktop
import time

Popen("calc.exe")

time.sleep(2)

desktop = Desktop(backend="uia")

calc = desktop.window(title="计算器")
calc.wait("visible", timeout=10)

print("找到计算器:", calc.window_text())

# 激活计算器
calc.set_focus()
# 计算:123 + 456
calc.type_keys("123")
calc.type_keys("{+}")
calc.type_keys("456")
calc.type_keys("{=}")

result = calc.child_window(auto_id="CalculatorResults", control_type="Text").window_text()

print("计算结果:", result)

选择Application还是Desktop,取决于操作范围。操作单个应用程序,或需要启动与连接特定进程时使用Application;操作任意桌面元素,或需要跨多个进程访问控件时使用DesktopApplication的进程隔离是一把双刃剑,既保证了不误触其他程序,也限制了跨进程访问。理解这一边界,便理解了两个对象各自的定位。

2.3 Window Specification窗口规范

ApplicationDesktop是操作的入口,而窗口规范是pywinauto高级API的核心。窗口规范不是真正的窗口,而是一份查找说明书,只记录要查找的窗口特征以及使用的查找算法。真正的查找动作不会立刻执行,而是等到实际使用时才触发。

这种延迟查找机制的好处是,可以在窗口尚未打开时就定义好查找规则。即使窗口关闭,规范依然保留,可随时复用。

main_window = app.top_window()

print(main_window)
# 

wrapper = main_window.wrapper_object()
print(wrapper)
# 

延迟查找与wrapper_object

真正触发查找的是wrapper_object()方法,它返回对应控件的Wrapper对象,用于调用底层操作接口。如果找不到则抛出ElementNotFoundError异常。Python的语法糖允许隐藏这次显式调用,让生产代码更简洁:

# 获取底层包装对象后最小化
main_window.wrapper_object().minimize()

# 直接最小化窗口,更简洁
main_window.minimize()

这种延迟解析机制是pywinauto稳定性的关键,可以提前定义窗口规范,等待界面就绪后再触发查找,天然契合桌面程序异步弹窗的特性。关于WindowSpecification及其延迟解析机制和相关接口,可进一步查阅pywinauto.application module

多层级规范

窗口规范支持层层嵌套,逐级缩小查找范围:

# 按标题和控件类型定位最大化按钮
main_window.child_window(title="最大化", control_type="Button").click()

完整的筛选条件清单可在官方文档pywinauto.findwindows.find_elements()函数说明中查到,具体可参考pywinauto.findwindows module

属性解析魔法

pywinauto借助最佳匹配算法,将属性访问隐式转换为窗口查找操作,支持简写形式,并对拼写差异具备一定容错能力。

main_window.UntitledNotepad
# 等价于使用模糊匹配查找标题
main_window.window(best_match="UntitledNotepad")

简写受限于Python属性名规则,无法处理含空格、连字符或中文等字符的窗口标题。此时可改用字典式访问,支持传入任意字符串作为匹配条件:

# 按唯一标题直接获取最大化控件
main_window["最大化"]
main_window.window(best_match="最大化")

属性解析魔法在交互式探索时确实方便,但在严谨的生产环境中反而容易掩盖错误。默认情况下,访问一个不存在的属性时,pywinauto不会立即报错,而是将其加入查找系统,等到后续真正使用时才失败。错误被延迟,排查起来很痛苦。此时可禁用魔法查找,让pywinauto在属性访问时立即抛错:

desktop = Desktop(backend="win32", allow_magic_lookup=False)
app = Application(backend="uia", allow_magic_lookup=False)

2.4 打印控件标识符

前面反复提到控件树和最佳匹配名称。那么,如何查看一个窗口里有哪些控件,以及它们各自叫什么?使用print_control_identifiers()即可。

通过ApplicationDesktop获取目标窗口的WindowSpecification后,都可以直接调用该方法。它会打印出当前窗口完整的控件层级结构,以及每个控件的最佳匹配名称。对照这份输出编写定位代码,比盲目猜测高效得多。

# 打印窗口前3层控件结构,用于查看控件名称、类型和层级关系
main_window.print_control_identifiers(depth=3)

# 将窗口控件结构保存到controls.txt文件中
main_window.print_control_identifiers(filename="controls.txt")

输出示例为一棵直观的控件树:

Control Identifiers:

Dialog - 'Windows NT Properties'    (L688, T518, R1065, B1006)
['Windows NT PropertiesDialog', 'Dialog', 'Windows NT Properties']
child_window(title="Windows NT Properties", control_type="Window")
   |
   | Edit - 'Folder name:'    (L790, T596, R1036, B619)
   | ['3', 'Edit', 'Edit1', 'Edit0']
   | child_window(title="Folder name:", auto_id="13156", control_type="Edit")
   |
   | Button - 'OK'    (L814, T968, R889, B991)
   | ['Button2', 'OK', 'OKButton']
   | child_window(title="OK", auto_id="1", control_type="Button")

读懂这棵树,就掌握了定位控件的钥匙:

  • 方括号[...]中列出了该控件所有可用的最佳匹配名称,任选其一即可访问。
  • child_window(...)行给出了精确的窗口规范,可以直接复制到代码中,这是最推荐的定位写法。

实际使用时,Inspect.exe和print_control_identifiers()可以搭配使用。先用Inspect.exe探查控件结构并确认后端,再用print_control_identifiers()在脚本执行时打印控件树。若两者层级一致,说明定位正确,可直接使用输出中的定位写法。若不一致,则需调整筛选条件,直到打印结果与Inspect.exe显示的层级吻合。

3 控件定位、操作与输入控制

第二章解决从哪里找的问题,本章讲解如何稳定找到并操作控件,涵盖定位方法、操作API、鼠标键盘输入三部分,三者结合足以覆盖绝大多数桌面自动化场景。

3.1 控件定位与筛选条件

3.1.1 控件定位方法

操作控件的标准流程可以拆解为清晰的四步:

  1. 实例化进程,得到Application对象。
  2. 选择窗口,app.window(...)得到WindowSpecification对象。
  3. 定位控件,基于WindowSpecification继续向下查找。
  4. 执行操作,调用控件方法完成交互。

前三步解决它在哪,第四步解决怎么动。

定位控件时,常用的查找方法如下:

  • window(**kwargs):按条件定位窗口或控件,是查找的入口方法。
  • child_window(**kwargs):不限层级向下查找,一步直达目标控件,推荐优先使用。
  • descendants(**kwargs):获取所有后代控件,覆盖面广,适合在层级复杂时全面检索。
  • children(**kwargs):仅获取直接子控件,逐层推进,适合结构清晰时精准导航。
  • iter_children(**kwargs)iter_descendants(**kwargs):分别对应children()descendants()的迭代器版本,便于遍历处理。
  • parent():获取当前控件的父级,用于逆向回溯或跨层级跳转。

策略上,优先用child_window()一步到位。若目标控件有重名或动态属性难以区分,再借助children()parent()逐级缩小范围,descendants()作为兜底的广撒网方案。

3.1.2 实际查找示例

以下代码以记事本(Notepad)为例,演示四种典型定位手法:

# 获取当前应用的主窗口
main_window = app.top_window()

# 通过控件属性定位菜单栏
menu_bar = main_window.child_window(control_type="MenuBar")

# 获取菜单栏中的第一个菜单项(如“文件”)
first_menu = menu_bar.children()[0]

# 通过“文件”菜单项定位,并通过父子关系获取相邻控件文本
find_text = (
    main_window
    .child_window(title="文件", control_type="MenuItem")
    .parent()
    .children()[1]
    .window_text()
)

# 获取窗口中的按钮控件,并点击第一个按钮
buttons = main_window.descendants(control_type="Button")

buttons[0].click_input()

当目标控件难以直接定位时,可先定位附近特征明显的元素(如固定的标签文字),再通过parent()children()从邻近元素绕行到目标控件。

3.1.3 筛选条件详解

定位方法的参数本质上是传入一组筛选条件。pywinauto在控件树中自上而下遍历候选节点,返回第一个或所有完全匹配条件的控件。筛选条件设置是否合理,直接影响定位速度与准确性。以下是常用筛选参数:

参数 说明 对应Inspect字段
class_name 类名 ClassName
class_name_re 正则表达式匹配类名
title 控件标题文字 Name
title_re 正则表达式匹配标题
control_type 控件类型 LocalizedControlType
auto_id 自动化ID AutomationId
best_match 最佳匹配名称(模糊匹配)

以下为不常用但特定场景下有效的筛选条件:

parent=None,              # 限定父控件
process=None,             # 进程号(每次启动会变化,不建议使用)
top_level_only=True,      # 仅搜索顶层窗口
visible_only=True,        # 仅搜索可见控件
enabled_only=True,       # 仅搜索启用控件
handle=None,              # 窗口句柄
ctrl_index=None,          # 控件在兄弟节点中的索引
found_index=None,         # 返回第几个匹配结果
framework_id=None,        # 框架标识(如WPF、Win32)
backend=None,             # 指定后端(如win32、uia)

定位控件时,不仅要确保当前可用,还需兼顾应用升级后的可维护性。选择定位策略时,建议按以下优先级依次选用auto_idcontrol_type配合auto_idclass_nametitle配合control_type、父容器定位,索引或坐标仅作兜底方案。理想情况下,定位条件应具备唯一性、稳定性和可读性,且条件数量宜少不宜多。

多个条件同时给出时为逻辑与关系,条件越多匹配越精确,但需避免过拟合。建议保留两到三个稳定条件即可。组合条件示例如下:

main_window.child_window(title="添加新标签页", auto_id="AddButton", control_type="Button")

3.2 控件与窗口常用操作

3.2.1 控件常用操作

定位到控件后,pywinauto的控件包装对象提供三类常用操作,包括点击、输入、属性读取。

点击操作

ctrl.click_input()                          # 左键单击
ctrl.right_click_input()                    # 右键单击
ctrl.double_click_input(button="left", coords=(None, None))  # 双击
ctrl.press_mouse_input(coords=(None, None)) # 按下鼠标
ctrl.release_mouse_input(coords=(None, None)) # 释放鼠标
ctrl.move_mouse_input(coords=(0, 0))        # 移动鼠标
ctrl.drag_mouse_input(dst=(0, 0))           # 拖拽到目标坐标

推荐统一使用click_input()系列,该系列模拟真实鼠标操作,兼容性更好。消息级click()速度更快,但部分自绘控件可能无效。

输入操作

ctrl.type_keys(keys, pause=None, with_spaces=False)
  • keys:要输入的文本
  • pause:每字符间隔秒数
  • with_spaces:是否保留空格
# 全选后替换
ctrl.type_keys("^a").type_keys("新内容", with_spaces=True)  

输入带空格的文本时,必须设置with_spaces=True^a全选后直接替换是清空输入框最稳定的方式,比手动删除或逐字符回退更可靠。

属性获取

ctrl.window_text()                         # 窗口标题或显示文本
ctrl.children_texts()                      # 所有子控件文本
ctrl.class_name()                          # 类名
ctrl.element_info.control_type             # 控件类型(UIA)
ctrl.element_info.name                     # 控件名称(UIA)
ctrl.element_info.class_name               # 类名(UIA)
ctrl.is_child(parent)                      # 是否指定父控件的子级
ctrl.rectangle()                           # 位置和尺寸(left, top, right, bottom)
ctrl.legacy_properties().get("Value")      # LegacyIAccessible值属性

window_text()返回固定文案时,可用legacy_properties().get("Value")获取动态变化的内容。

其他操作

除上述三类常用操作外,还有以下辅助操作:

ctrl.draw_outline(colour="green")          # 高亮边框
ctrl.scroll(direction, amount, count=1)    # 滚动
  • direction:"up"、"down"、"left"、"right"
  • amount:"line"或"page"
  • count:滚动次数

draw_outline()是调试阶段的高亮工具,scroll()用于处理长列表(如好友列表、聊天记录)。

3.2.2 窗口操作

除控件操作外,有时还需直接管理窗口本身,如关闭弹窗、最小化还原、判断窗口状态等。以下方法仅适用于窗口级别的控件:

dlg.close()              # 关闭窗口
dlg.minimize()           # 最小化窗口
dlg.maximize()           # 最大化窗口
dlg.restore()            # 还原窗口(从最小化或最大化恢复为正常状态)
dlg.get_show_state()     # 获取窗口状态,返回0=正常,1=最大化,2=最小化
dlg.is_dialog()          # 判断是否为对话框,返回布尔值

脚本收尾时,建议养成清理现场的习惯:关闭打开的窗口,必要时调用kill()终止进程,避免自动化运行后残留窗口影响后续用例执行。

3.3 等待与状态判断

桌面应用的不确定性是自动化的主要挑战之一。窗口何时加载完成?数据何时刷新结束?使用time.sleep()固定等待,要么耗时过长,要么时间不足导致脚本失败。pywinauto提供的等待机制正是为此而生。

等待机制围绕三个核心方法展开。exists()用于检查窗口是否存在,返回布尔值,适合快速判断。wait()用于等待窗口达到指定状态,是日常使用最频繁的方法。wait_not()则相反,用于等待窗口不再处于某个状态,适合处理弹窗关闭等场景。关于wait()wait_not()以及相关时间控制接口的参数和行为,可参考官方Waiting for Long Operations

# 检查窗口是否存在,返回布尔值
# timeout:超时秒数,retry_interval:重试间隔
main_window.exists(timeout=None, retry_interval=None)   

# 等待窗口达到指定状态
# wait_for可选值:
#   exists  - 窗口或控件存在且可被解析
#   visible - 窗口未隐藏(常用)
#   enabled - 窗口未被禁用
#   ready   - 窗口可见且已启用(常用)
#   active  - 窗口处于活动状态
main_window.wait(wait_for, timeout=None, retry_interval=None) 

# 等待窗口不再处于指定状态
# wait_for_not可选值同上
main_window.wait_not(wait_for_not, timeout=None, retry_interval=None)   

三种方法常与wait_cpu_usage_lower()组合使用,形成完整的加载等待策略:

main_window.wait('ready')              # 等待窗口可见且启用
app.wait_cpu_usage_lower()     # 等待CPU空闲,确认数据加载完成
main_window.wait_not('visible')        # 等待窗口变为不可见

3.4 鼠标与键盘操作

3.4.1 鼠标操作

控件自带的click_input()已覆盖大部分场景,但涉及拖动滑块、在任意坐标操作、控制滚轮等自由鼠标动作时,则需要pywinauto.mouse模块。

from pywinauto import mouse

mouse.move(coords=(x, y))                  # 移动鼠标到指定坐标
mouse.click(button="left", coords=(40, 40))  # 在指定坐标点击
mouse.double_click(button="left", coords=(140, 40))  # 双击
mouse.press(button="left", coords=(140, 40))  # 按下鼠标
mouse.release(button="left", coords=(300, 40))  # 释放鼠标
mouse.right_click(coords=(400, 400))  # 右键点击
mouse.wheel_click(coords=(400, 400))  # 中键点击
mouse.scroll(coords=(1200, 300), wheel_dist=-3)  # 滚动滚轮

直接使用坐标存在缺陷,当窗口位置变动后脚本即失效。更稳健的做法是先获取控件的坐标范围,再基于其中心点执行鼠标操作:

from pywinauto import mouse

def mouse_scroll(control, distance):
    rect = control.rectangle()
    cx = int((rect.left + rect.right) / 2)
    cy = int((rect.top + rect.bottom) / 2)
    mouse.scroll(coords=(cx, cy), wheel_dist=distance)

chat_list = win_main.child_window(control_type="List", title="联系人")
mouse_scroll(control=chat_list, distance=-5)

3.4.2 键盘操作

键盘操作分为控件级和系统级两条路线。控件内输入优先用type_keys(),焦点自动锁定在目标控件上,支持SendKeys语法,适合表单填写等场景:

main_window.type_keys("Hello World", with_spaces=True)  # 输入文本(保留空格)
main_window.type_keys("^a")          # Ctrl+A 全选
main_window.type_keys("{ENTER}")     # 回车键
main_window.type_keys("{TAB}")       # Tab键

关于按键编码、组合键和特殊键的写法,可参考官方pywinauto.keyboard module。当需要全局热键或跨窗口操作时,可使用pywinauto的keyboard模块发送系统级按键:

from pywinauto import keyboard

keyboard.send_keys("^a")      # Ctrl+A 全选
keyboard.send_keys("{ENTER}") # 回车键

两条路线各有侧重。type_keys()精准可控,焦点明确,日常操作优先使用。keyboard模块作用范围广,适用于全局热键或跨窗口输入,但速度与兼容性受目标控件和输入法影响,仅在type_keys()无法满足需求时使用。

3.5 常见控件类型

除了通用的点击、输入和属性读取操作外,pywinauto还针对不同类型的Windows控件提供了相应的包装对象和专用操作方法。常见控件类型包括:

  • Button:按钮;
  • Edit:文本输入框;
  • ComboBox:下拉框;
  • CheckBox:复选框;
  • RadioButton:单选按钮;
  • ListBox / ListView:列表与列表视图;
  • TreeView:树形控件;
  • TabControl:选项卡控件;
  • Menu / MenuItem:菜单及菜单项;
  • Toolbar:工具栏;
  • Slider:滑块;
  • ProgressBar:进度条。

不同控件可支持选择、勾选、展开、获取文本、获取选中项等专项操作。由于Win32和UIA后端对应的控件包装类型及可用方法存在一定差异,实际使用时应根据控件类型和所选后端查阅对应API文档。

当不确定某个控件对应的包装类型时,可以通过wrapper_object()查看:

ctrl = main_window.child_window(auto_id="some_id")
print(ctrl.wrapper_object())

根据返回的Wrapper类型,再查阅pywinauto官方API文档中对应控件的可用方法:

4 高级特性与调试技巧

第三章让脚本具备了执行动作的能力,但能操作不等于可诊断、可恢复。本章进一步补齐生产级自动化所需的观测与调试能力。

4.1 控件截图

自动化脚本执行后,如何向他人证明操作确实完成?截图是最直观的证据。capture_as_image()方法返回控件的PIL Image对象,可进一步处理或保存。

main_window.capture_as_image().save("screenshot.png")

4.2 级联菜单操作

桌面应用的大量功能入口藏在菜单栏中,pywinauto通过item_by_path()支持多级菜单操作,也支持menu_select()快捷方法:

main_window = app.top_window()

# 定位菜单栏中的文件菜单项
file_menu = main_window.child_window(
    title="文件",
    auto_id="File",
    control_type="MenuItem"
)
file_menu.click_input()  # 点击展开文件菜单

# 打印文件菜单下的所有子菜单项(调试用)
print(file_menu.items())

# 获取文件菜单的父级容器
parent_menu = file_menu.parent()

# 通过完整路径选择级联菜单并点击,此方式一般适用于MenuBar对象
parent_menu.item_by_path("文件->另存为").click_input()

item_by_path()的路径字符串中,->分隔菜单层级,层级文本需与界面显示完全一致。注意不同语言版本的软件,其菜单文本可能存在差异,脚本需按目标环境适配。关于该类控件的详细操作方法可参考:pywinauto操作MenuItem菜单项

4.3 异常处理与调试技巧

自动化脚本运行在真实桌面环境中,运行条件复杂多变,因此异常处理不是可选项,而是必选项。

常见异常

from pywinauto.findwindows import ElementNotFoundError
from pywinauto.timings import TimeoutError

try:
    main_window = app.window(title="不存在的窗口")
    main_window.wait("visible", timeout=5)
except TimeoutError:
    print("等待超时,窗口未出现")
except ElementNotFoundError:
    print("找不到指定的元素")

ElementNotFoundError表示定位条件错误,TimeoutError表示窗口未在预期时间内出现。区分二者能大幅缩短排查时间。

调试技巧

main_window.draw_outline(colour="red")                # 高亮定位到的控件
main_window.print_control_identifiers()               # 打印控件树结构
rect = main_window.rectangle()                       # 获取控件位置尺寸
wrapper = main_window.wrapper_object()               # 获取底层wrapper类型
for win in app.windows():                    # 遍历所有窗口
    print(win.window_text())

找不到控件时,按以下顺序排查:

  1. 后端选型是否正确,
  2. print_control_identifiers()输出中是否真的存在该控件,
  3. 筛选条件是否过严或过松,
  4. 控件是否可见且启用,
  5. 是否被对话框遮挡。

若以上工具均无法定位控件,可改用mousekeyboard模块模拟输入作为兜底方案。

4.4 日志记录与报告生成

自动化脚本在生产环境中运行时,必须具备良好的可追溯性。日志记录能够完整保留脚本运行过程中的关键操作、执行状态和异常信息,使问题排查从依赖经验判断转变为基于运行记录进行定位。关于该库的详细说明,可参考Python日志记录库logging总结,快速上手示例见:

import logging

# 配置日志系统:同时输出到文件和控制台
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler("automation.log", encoding='utf-8-sig'),
        logging.StreamHandler()
    ]
)

def click_safe(control, description):
    """
    安全点击控件,并记录操作日志

    Args:
        control: 待点击的控件对象
        description: 控件描述(用于日志标识)
    """
    try:
        logging.info(f"正在点击控件: {description}")
        control.click_input()
        logging.info(f"控件点击成功: {description}")
    except Exception as e:
        logging.error(f"控件点击失败: {description},异常信息: {e}")
        raise 

file_menu = main_window.child_window(
    title="文件",
    auto_id="File",
    control_type="MenuItem"
)

click_safe(file_menu, "文件菜单")

5 实战案例与工程实践

前四章分别介绍了后端选择、窗口与控件定位、控件交互,以及自动化过程中的常见问题与故障诊断方法。到这里,已经掌握了pywinauto的核心操作。不过,在实际项目中,单独掌握某一个方法还不够,更重要的是能够将这些方法组织成一套完整、稳定的自动化流程。本章首先通过两个完整案例介绍典型的界面自动化流程,然后总结实际使用中经常遇到的问题及解决方法,最后从控件定位、等待机制和对象复用等方面介绍性能优化思路。

5.1 pywinauto界面自动化操作示例

以下代码演示了pywinauto操作Windows界面的标准流程,包含启动程序、连接窗口、定位控件、执行操作,并结合等待机制确保页面加载完成,清晰展示了窗口自动化的核心方法,可作为后续示例的基础模板。

from pywinauto import Application

# 启动控制面板进程
Application().start('control.exe')

# 使用UI Automation后端连接到控制面板窗口
# 注意control.exe只是一个启动器,启动后实际窗口进程为explorer.exe
app = Application(backend='uia').connect(path='explorer.exe', title='控制面板')

# 在控制面板中点击程序链接
control_panel = app.window(title='控制面板')
control_panel.child_window(title="程序", auto_id="name", control_type="Hyperlink").invoke()

# 等待CPU占用率降至50%以下,确保页面加载完成
app.wait_cpu_usage_lower(threshold=50, timeout=30, usage_interval=1.0)

# 进入程序和功能页面
# 页面标题变为程序,需切换到该窗口
programs_window = app.window(title='程序')
programs_window.child_window(title="程序和功能", auto_id="name", control_type="Hyperlink").invoke()

# 再次等待新页面加载完成
app.wait_cpu_usage_lower(threshold=50, timeout=30, usage_interval=1.0)

# 获取程序和功能窗口并打印控件标识信息
window = app.window(title='程序和功能')
window.print_control_identifiers()

# 获取垂直滚动条并执行点击操作
scrollbar = window.child_window(title="垂直滚动条", auto_id="NonClientVerticalScrollBar", control_type="ScrollBar")
scrollbar.child_window(title="上一行", auto_id="UpButton", control_type="Button").click_input()   # 向上滚动一行
scrollbar.child_window(title="向下翻页", auto_id="DownPageButton", control_type="Button").click_input()  # 向下翻页
scrollbar.child_window(title="下一行", auto_id="DownButton", control_type="Button").click_input()  # 向下滚动一行

5.2 图片尺寸调整自动化示例

以下代码演示了使用pywinauto自动化Windows画图软件,完成打开图片、调整尺寸及关闭程序的完整流程,重点展示了控件定位、状态判断、输入操作和异常处理等常用技术。

import logging
from pywinauto import actionlogger, Application

# 日志配置
actionlogger.enable()
logger = logging.getLogger('pywinauto')
logger.handlers[0] = logging.FileHandler("info.log")

# 启动画图
app = Application(backend='uia').start(r'mspaint.exe')
main = app.window(title_re='.*画图*.')
main.wait('visible')

# 打开图片
main['文件'].invoke()
main.child_window(title_re='打开', control_type='MenuItem', found_index=0).invoke()
main.child_window(title="文件名(N):", auto_id="1148", control_type="Edit").type_keys(
    r'd:\demo.jpg', with_spaces=True
)
main.print_control_identifiers()
main.child_window(title="打开(O)", auto_id="1").click_input()

# 调整大小
main.child_window(title="重设大小和倾斜", control_type="Button").click_input()
dialog = main.child_window(title="重设大小和倾斜", control_type="Window")

# 取消保持纵横比
aspect = dialog.child_window(title="保持纵横比", auto_id="MaintainAspectRatioButton", control_type="Button")
if aspect.get_toggle_state() == 1:
    aspect.toggle()

# 选择像素模式
pixel = dialog.child_window(title="像素", control_type="RadioButton")
if not pixel.is_selected():
    pixel.select()

# 设置宽高
dialog.child_window(auto_id="HorizontalResizeTextBox", control_type="Edit").set_text('640')
dialog.child_window(auto_id="VerticalResizeTextBox", control_type="Edit").set_text('480')
dialog.child_window(title="确定", auto_id="PrimaryButton", control_type="Button").click_input()

# 关闭不保存
main.child_window(title="关闭", control_type="Button").click_input()
unsave = main.child_window(title="不保存", auto_id="SecondaryButton", control_type="Button")
if unsave.exists():
    unsave.click_input()

5.3 常见问题与解决方案

以下是社区中高频出现的pywinauto疑难杂症及对应处理方法。

问题一:找不到控件

# 错误:魔法属性查找
dlg = app.Dialog
# 正确:使用明确筛选条件
dlg = app.window(class_name="WeChatMainWndForPC")

原因:best_match模糊匹配不稳定。建议使用class_nameauto_id精确查找。

问题二:路径中的特殊字符

# 错误:转义问题
app.start("C:\Program Files\app.exe")
# 正确:原始字符串
app.start(r"C:\Program Files\app.exe")

原因:反斜杠被当作转义符,建议使用原始字符串r"..."

问题三:type_keys丢失空格

ctrl.type_keys("Hello World")          # 空格被忽略
ctrl.type_keys("Hello World", with_spaces=True)  # 保留空格

原因:type_keys()默认忽略空格,含空格时应传入with_spaces=True

问题四:魔法属性查找

# 不推荐,依赖模糊匹配且与语言版本耦合
dlg = app.微信
# 推荐,最快最准
dlg = app.window(class_name="WeChatMainWndForPC")

建议优先使用window()class_nameauto_id,避免使用魔法属性。

问题五:后台运行干扰
脚本在其他用户会话或后台服务中运行时,可能无法获取前台控件,应确保脚本与目标程序在同一用户会话下运行,且桌面未锁定。

性能优化建议

  • 定位优先用class_name,速度最快
  • 避免频繁使用best_match
  • child_window()替代逐层遍历
  • wait()替代time.sleep()
  • 复用Application对象,减少重复创建
save_dlg.wait("ready", timeout=10)
app.wait_cpu_usage_lower(threshold=2.5)

优化的核心是减少无效等待,而非让代码跑得更快。多次操作同一控件时,可缓存其包装对象:

edit_box = dlg.child_window(class_name="Edit")
edit_box.set_edit_text("text1")
edit_box.set_edit_text("text2")

6 参考


原文地址: https://www.cveoy.top/t/topic/qHr3 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录