【GUI 自动化测试】Pywinauto 常见操作

文章目录

  • [1. 打开程序](#1. 打开程序)
  • [2. 定位窗口](#2. 定位窗口)
  • [3. 窗口操作](#3. 窗口操作)
  • [4. 定位控件](#4. 定位控件)
    • [4.1 控件分类](#4.1 控件分类)
    • [4.2 动态解析](#4.2 动态解析)
    • [4.3 child_window](#4.3 child_window)
  • [5. 等待](#5. 等待)
    • [5.1 wait / wait_not](#5.1 wait / wait_not)
    • [5.2 wait_until](#5.2 wait_until)
  • [6. 控件的操作](#6. 控件的操作)
    • [6.1 点击](#6.1 点击)
    • [6.2 文本](#6.2 文本)
  • [7. 鼠标操作](#7. 鼠标操作)
    • [7.1 自动化测试场景示例:抖音点赞](#7.1 自动化测试场景示例:抖音点赞)
  • [8. 键盘操作](#8. 键盘操作)
    • [8.1 输入文本](#8.1 输入文本)
    • [8.2 按键](#8.2 按键)
    • [8.3 自动化测试场景示例:WeChat 发消息](#8.3 自动化测试场景示例:WeChat 发消息)
  • [9. 菜单控件的操作](#9. 菜单控件的操作)
    • [9.1 items](#9.1 items)
    • [9.2 item_by_index](#9.2 item_by_index)
    • [9.3 item_by_path](#9.3 item_by_path)
    • [9.4 menu_select](#9.4 menu_select)
    • [9.5 自动化测试场景示例:批量创建文件](#9.5 自动化测试场景示例:批量创建文件)
  • [10. 列表控件的操作](#10. 列表控件的操作)
    • [10.1 get_items](#10.1 get_items)
    • [10.2 item_count](#10.2 item_count)
    • [10.3 get_item](#10.3 get_item)
    • [10.4 自动化测试场景示例:WeChat 发消息(进阶版)](#10.4 自动化测试场景示例:WeChat 发消息(进阶版))

1. 打开程序

打开应用程序

python 复制代码
start(self, cmd_line......)

部分参数详解:

  • cmd_line:这是启动应用程序的命令行字符串。它必须包含应用程序的路径和名称,还可以包含启动参数。

例如:

  • "notepad.exe":启动记事本程序。
  • "C:\Windows\system32\notepad.exe":使用绝对路径启动记事本程序。
  • "calc.exe":启动计算器程序。

连接已经打开的应用程序:

python 复制代码
connect(self, **kwargs)

部分参数详解:

  • process:目标的进程 ID。
  • handle:目标的窗口句柄。

注意:连接到已在运行的进程,该动作仅根据一个参数执行。

示例:打开记事本程序

python 复制代码
from pywinauto.application import Application

# 打开应用程序
app = Application(backend="uia").start("C:\\Windows\\system32\\notepad.exe")

# 连接已经打开的应用程序
# 通过pid连接
# 获取应用程序对应的pid
# app = Application(backend="uia").connect(process=app.process)
app = Application(backend="uia").connect(process=1704)

# 通过句柄连接
app = Application(backend="uia").connect(handle=65552)

2. 定位窗口

方法一:通过高级 pywinauto API 提供的 window 方法来定位

python 复制代码
app.window(title='',...)

参数说明(参数可以组合使用):

  • title:文本为指定值的元素。
  • title_re:文本匹配指定正则表达式的元素。
  • best_match:标题与指定值相似的元素。
  • class_name:窗口类为指定值的元素。
  • class_name_re:类名匹配指定正则表达式的元素。

我们以 Typora 为例:

示例:定位 Typora 窗口

python 复制代码
# start参数换成Typora的目标路径
#app = Application(backend="uia").start("E:\\Typora195\\Typora\\Typora.exe")
# 通过connect连接已经打开的应用程序
app = Application(backend="uia").connect(process=20184)

# title--精确匹配
win = app.window(title="Untitled• - Typora")

# title_re--正则匹配
win = app.window(title_re=".*po.*")

# class_name--精确匹配
win = app.window(class_name="Chrome_WidgetWin_1")

# class_name--正则匹配
win = app.window(class_name_re=".*Win_1")

# best_match--模糊匹配
win = app.window(best_match="Untitled")

win.wait("visible")
win.print_control_identifiers()

print_control_identifiers() 方法用于打印窗口及其子控件的标识符信息,帮助用户识别控件。

输出内容:

  • 控件的类名、标题、位置(左、上、右、下边界的坐标值)、控制类型等信息。
  • 每个控件的 "best match" 名称列表,这些名称可以用于引用控件。

如下所示:

方法二:通过动态解析对象属性定位

python 复制代码
# 不推荐
app."best_match"名称

示例:定位 typora 窗口

python 复制代码
# 通过动态解析对象属性定位
'''
Pane - 'Untitled• - Typora'    (L1212, T71, R1903, B1011)
['Untitled• - Typora', 'Pane', 'Untitled• - TyporaPane', 'Pane0', 'Pane1']
child_window(title="Untitled• - Typora", control_type="Pane")
'''
app = Application(backend="uia").connect(process=20184)

win = app.Pane

win.wait("visible")

上面写法等价于

python 复制代码
win = app.window(best_match='Pane')

注意:Python 通过动态解析对象属性简化了创建窗口规范。但是属性名与任何变量名都有相同的限制:没有空格、逗号和其他特殊符号,因此不推荐这种写法。

若存在空格或者其他特殊字符等,可采用类似字典的方式访问,如:

python 复制代码
win = app['Untitled• - Typora']
# 上面写法等价于
win = app.window(best_match="Untitled• - Typora")

最简单的定位窗口方法是询问 top_window(),例如:

python 复制代码
win = app.top_window()

需要注意,这是目前相当未经测试的,所以不确定它会返回正确的窗口。它绝对是应用程序的顶级窗口,它可能不是 Z-Order 中最高的窗口,但不一定是我们想要的窗口。

python 复制代码
from pywinauto import Application

app = Application(backend="uia").connect(process=20184)
win = app.top_window()
win.wait('exists')
print(win.print_control_identifiers())

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 表示未处于正常状态。

示例代码如下:

python 复制代码
from pywinauto.application import Application

app = Application(backend='uia').connect(process=20184)
win = app.window(title_re='.*po.*')
# 添加等待
win.wait('exists')

# 最小化
win.minimize()
print("is_minimized:", win.is_minimized())

win.maximize()
print("is_maximized:", win.is_maximized())

win.restore()
print("is_normal", win.is_normal())

print("get_show_state:", win.get_show_state())
print("is_dialog", win.is_dialog())

win.close()

结果如下所示:

4. 定位控件

4.1 控件分类

桌面应用程序客户端的控件和层级结构是 GUI 自动化测试的基础。理解控件的分类、特性和层级关系,能够帮助测试人员更高效地定位和操作控件,实现自动化测试。在实际测试中,结合控件的属性和事件,可以编写灵活且可靠的测试脚本。

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

常见控件示例:

分类 控件名称 说明
窗口与对话框 对话框 (Dialog) 用于与用户交互,如显示警告、确认操作或输入信息。
窗格 (Pane) 通常作为窗口的一部分,用于显示特定内容或功能。
输入控件 按钮 (Button) 用于触发操作,如点击按钮执行某个功能。
编辑栏 (Edit) 用于输入或编辑文本,支持多行或单行输入。
组合框 (ComboBox) 结合文本框和列表框,用户可以选择预定义选项或输入自定义值。
列表框 (ListBox) 用于显示可选择的项目列表,支持单选或多选。
菜单控件 菜单 (Menu) 用于提供功能选项,通常位于窗口顶部。
菜单项 (MenuItem) 菜单中的具体选项,点击后执行对应功能。
弹出菜单 (PopupMenu) 右键单击时弹出的菜单,用于快速访问常用功能。
列表显示控件 列表显示控件 (ListView) 用于以表格形式显示数据,支持多列显示和排序。
容器控件 组框 (GroupBox) 用于对控件进行分组,提高界面的可读性和组织性。
选择控件 复选框 (CheckBox) 用于多选操作,用户可以勾选多个选项。
单选框 (RadioButton) 用于互斥选择,用户只能选择一个选项。
显示控件 状态栏 (StatusBar) 通常位于窗口底部,用于显示应用状态或提示信息。
静态内容 (Static) 用于显示静态文本或图像,通常不可编辑。
导航控件 树状视图 (Tree View) 用于展示分层数据,如文件夹结构或组织结构。
选项卡控件 (TabControl) 用于在有限空间内组织多个页面或选项卡,每个选项卡可以包含不同的内容。
工具控件 工具栏 (Toolbar) 用于放置常用按钮或工具,方便用户快速操作。
工具提示 (ToolTips) 当鼠标悬停在控件上时显示提示信息,帮助用户理解控件功能。
头部内容 头部内容 (Header) 通常用于显示标题或表头信息,如表格的列标题。

通过合理的分类和说明,可以更清晰地理解控件的功能和用途,便于在 GUI 自动化测试中进行操作和定位。

在使用 pywinauto 进行自动化操作时,程序启动后,首先需要定位到目标窗口。窗口是所有控件的容器,只有成功定位到窗口,才能进一步操作窗口内的控件。控件是窗口功能的具体体现,包括按钮、文本框、列表等,对控件的操作是自动化任务的核心。

然而,在对控件进行操作之前,必须先完成控件的定位。定位控件是确保操作准确性的关键步骤,也是自动化流程的基础。接下来,我们将深入学习如何高效定位控件。

定位控件,需要借助前面学到的 print_control_identifiers() 方法,打印窗口及其子控件的标识符信息。

代码如下所示:

python 复制代码
Control Identifiers:

Pane - 'Untitled• - Typora'    (L1204, T207, R1895, B757)
['Untitled• - TyporaPane', 'Pane', 'Untitled• - Typora', 'Pane0', 'Pane1']
child_window(title="Untitled• - Typora", control_type="Pane")
   | 
   | Pane - ''    (L1212, T238, R1887, B749)
   | ['Pane2']
   | 
   | TitleBar - ''    (L1228, T210, R1887, B238)
   | ['TitleBar']
   |    | 
   |    | Menu - '系统'    (L1212, T215, R1234, B237)
   |    | ['系统', '系统Menu', 'Menu', '系统0', '系统1', 'Menu0', 'Menu1']
   |    | child_window(title="系统", auto_id="MenuBar", control_type="MenuBar")
   |    |    | 
   |    |    | MenuItem - '系统'    (L1212, T215, R1234, B237)
   |    |    | ['系统MenuItem', '系统2', 'MenuItem', 'MenuItem0', 'MenuItem1']
   |    |    | child_window(title="系统", control_type="MenuItem")
   |    | 
   |    | Button - '最小化'    (L1748, T208, R1795, B238)
   |    | ['最小化', 'Button', '最小化Button', 'Button0', 'Button1']
   |    | child_window(title="最小化", control_type="Button")
   |    | 
   |    | Button - '最大化'    (L1795, T208, R1841, B238)
   |    | ['最大化', 'Button2', '最大化Button']
   |    | child_window(title="最大化", control_type="Button")
   |    | 
   |    | Button - '关闭'    (L1841, T208, R1888, B238)
   |    | ['关闭', 'Button3', '关闭Button']
   |    | child_window(title="关闭", control_type="Button")
   | 
   | Pane - ''    (L1212, T238, R1887, B749)
   | ['Pane3']
   |    | 
   |    | Pane - ''    (L1212, T238, R1887, B749)
   |    | ['Pane4']
   |    |    | 
   |    |    | Pane - ''    (L1212, T238, R1887, B749)
   |    |    | ['Pane5']
   |    |    |    | 
   |    |    |    | Pane - ''    (L1212, T238, R1887, B749)
   |    |    |    | ['Pane6']
   |    |    |    |    | 
   |    |    |    |    | Menu - ''    (L1212, T238, R1887, B264)
   |    |    |    |    | ['Menu2']
   |    |    |    |    |    | 
   |    |    |    |    |    | MenuItem - '文件(​F)'    (L1212, T238, R1263, B264)
   |    |    |    |    |    | ['文件(\u200bF)MenuItem', '文件(\u200bF)', 'MenuItem2']
   |    |    |    |    |    | child_window(title="文件(​F)", control_type="MenuItem")
   |    |    |    |    |    | 
   |    |    |    |    |    | MenuItem - '编辑(​E)'    (L1263, T238, R1314, B264)
   |    |    |    |    |    | ['编辑(\u200bE)MenuItem', '编辑(\u200bE)', 'MenuItem3']
   |    |    |    |    |    | child_window(title="编辑(​E)", control_type="MenuItem")
   |    |    |    |    |    | 
   |    |    |    |    |    | MenuItem - '段落(​P)'    (L1314, T238, R1366, B264)
   |    |    |    |    |    | ['段落(\u200bP)MenuItem', '段落(\u200bP)', 'MenuItem4']
   |    |    |    |    |    | child_window(title="段落(​P)", control_type="MenuItem")
   |    |    |    |    |    | 
   |    |    |    |    |    | MenuItem - '格式(​O)'    (L1366, T238, R1420, B264)
   |    |    |    |    |    | ['格式(\u200bO)', '格式(\u200bO)MenuItem', 'MenuItem5']
   |    |    |    |    |    | child_window(title="格式(​O)", control_type="MenuItem")
   |    |    |    |    |    | 
   |    |    |    |    |    | MenuItem - '视图(​V)'    (L1420, T238, R1473, B264)
   |    |    |    |    |    | ['MenuItem6', '视图(\u200bV)MenuItem', '视图(\u200bV)']
   |    |    |    |    |    | child_window(title="视图(​V)", control_type="MenuItem")
   |    |    |    |    |    | 
   |    |    |    |    |    | MenuItem - '主题(​T)'    (L1473, T238, R1524, B264)
   |    |    |    |    |    | ['主题(\u200bT)', '主题(\u200bT)MenuItem', 'MenuItem7']
   |    |    |    |    |    | child_window(title="主题(​T)", control_type="MenuItem")
   |    |    |    |    |    | 
   |    |    |    |    |    | MenuItem - '帮助(​H)'    (L1524, T238, R1578, B264)
   |    |    |    |    |    | ['帮助(\u200bH)', '帮助(\u200bH)MenuItem', 'MenuItem8']
   |    |    |    |    |    | child_window(title="帮助(​H)", control_type="MenuItem")
   |    |    |    |    | 
   |    |    |    |    | Pane - ''    (L1212, T264, R1887, B749)
   |    |    |    |    | ['Pane7']
   |    |    |    |    |    | 
   |    |    |    |    |    | Pane - ''    (L1212, T264, R1887, B749)
   |    |    |    |    |    | ['Pane8']
   |    |    |    |    |    | 
   |    |    |    |    |    | Pane - ''    (L1212, T264, R1887, B749)
   |    |    |    |    |    | ['Pane9']
None

4.2 动态解析

基于 best_match 标题来定位

示例一:定位 Typora 工具的菜单栏

python 复制代码
from pywinauto import Application

# 创建Application对象,连接到正在运行的Typora进程
app = Application(backend='uia').connect(process=5092)

# 获取与Typora相关的窗口对象,使用正则表达式匹配窗口标题
win = app.window(best_match="Untitled• - Typora")

# 等待窗口变为可见状态,确保窗口已经加载完成
win.wait("visible")

# 通过窗口的标题获取菜单对象
menu = win['Menu2']

# 打印菜单项的列表,查看菜单中包含的所有选项
print(menu.items())

结果如下所示:

4.3 child_window

child_window() 和定位窗口的方法 window() 参数一样,可以通过标题或者类名进行精确匹配、模糊匹配等,在这里额外拓展几个进行控件定位时需要用到的参数:

属性名称 描述
control_type 具有此控件类型的元素(字符串;用于UIAutomation元素)
auto_id 具有此自动化ID的元素(用于UIAutomation元素)
found_index 返回的已过滤子元素的索引,索引下标从0开始

语法:

python 复制代码
from pywinauto import Application

# 创建Application对象,连接到正在运行的Typora进程
app = Application(backend='uia').connect(process=5092)

# 获取与Typora相关的窗口对象,使用正则表达式匹配窗口标题
win = app.window(best_match="Untitled• - Typora")

# 等待窗口变为可见状态,确保窗口已经加载完成
win.wait("visible")

# 通过控件的属性(标题、自动ID和控件类型)定位菜单栏对象
menu = win.child_window(title="", auto_id="", control_type="MenuBar")

# 打印菜单项的列表,查看菜单中包含的所有选项
print(menu.items())

结果如下所示:

若存在多个控件的 auto_id 相同,可通过添加索引参数来定位:

python 复制代码
from pywinauto import Application

# 创建Application对象,连接到正在运行的Typora进程
app = Application(backend='uia').connect(process=5092)

# 获取与Typora相关的窗口对象,使用正则表达式匹配窗口标题
win = app.window(best_match="Untitled• - Typora")

# 等待窗口变为可见状态,确保窗口已经加载完成
win.wait("visible")

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

# 打印菜单项的列表,查看菜单中包含的所有选项
print(menu.texts())

1 是 文件:

2 是 编辑:

5. 等待

案例:没有添加等待导致引发的错误

python 复制代码
from pywinauto.application import Application

# 创建Application对象,连接到正在运行的Typora进程
app = Application(backend='uia').connect(process=5092)

# 获取与Typora相关的窗口对象,使用正则表达式匹配窗口标题
win = app.window(title_re='.*po.*')

# 添加等待
#win.wait('exists')

# 最小化
win.minimize()
print("is_minimized:", win.is_minimized())

win.maximize()
print("is_maximized:", win.is_maximized())

win.close()

把等待代码注视掉,程序运行报错:

GUI 应用程序行为通常不稳定,脚本需要等待,直到出现新窗口或关闭/隐藏现有窗口。pywinauto 可以隐式地(默认超时)灵活地等待对话框初始化,或者明确地使用专用方法/函数来帮助您使代码更容易和更可靠。

5.1 wait / wait_not

函数如下:

python 复制代码
wait(self, wait_for, timeout=None, retry_interval=None)
wait_not(self, wait_for_not, timeout=None, retry_interval=None)

参数说明:

  • wait_for:表示选择的窗口状态
    • exists:表示窗口是一个有效的句柄
    • visible:表示窗口不隐藏,可以看到
    • enable:表示窗口未被禁用,可操作
    • ready:表示窗口可见且已启用
    • active:表示窗口处于活动状态
  • timeout:表示超时
  • retry_interval:表示重试时间间隔,单位为秒 s

wait_not 与 wait() 类似,wait 是等待处于某种状态,而 wait_not 是等待不处于某种状态,这里以 wait 为例说明。

示例一:"exists" 和 "visible"

python 复制代码
from pywinauto.application import Application
app = Application(backend='uia').connect(process=38544)
win = app.window(title_re='.*Sublime Text.*')
# 检查窗口是有效的句柄
win.wait('exists')
# 检查窗口是否可见
win.wait('visible')
# 检查窗口是否未被禁用
win.wait('enabled')
# 检查窗口是否准备就绪
win.wait('ready')

窗口被最小化之后,在桌面就不可见了,因此最小化时需要将等待状态改为 "exists",应用程序界面在桌面可见时等待状态可以设置为 "visible"。

示例二:"enabled"

计算器中未启用的按钮:

计算器中 启用的按钮:

代码如下所示:

python 复制代码
from pywinauto.application import Application

# 打开计算器
# app = Application(backend="uia").start("calc.exe")
app = Application(backend="uia").connect(process=14752)
win = app.window(title="计算器")
win.wait("visible")

# 启用的按钮
enable_btn = win.child_window(title="记忆加法", auto_id="MemPlus", control_type="Button")

# 未启用的按钮
disabled_btn = win.child_window(title="清除所有记忆", auto_id="ClearMemoryButton", control_type="Button")

enable_btn.wait("enabled")       # 代码执行通过
disabled_btn.wait_not("enabled") # 代码执行通过
#disabled_btn.wait("enabled") # 代码执行不通过 -- 会超时

结果如下所示:

示例三:"ready"

代码如下所示:

python 复制代码
from pywinauto import Application

app = Application(backend="uia").connect(process=17892)
win = app.window(title="计算器")
win.wait("exists")

proc = win.child_window(title="打开导航", auto_id="TogglePaneButton", control_type="Button")
proc.wait("ready") # 等待成功

proc_chid = win.child_window(auto_id="PaneTitleTextBlock", control_type="Text")
proc_chid.wait("ready") # 等待失败

结果如下所示:

注意:is_visible() 用于检查元素是否可见,除此之外,is_enabled() 用于检查元素是否启用。

示例四:"active"

代码如下所示:

python 复制代码
from pywinauto import Application

# 打开计算器
app = Application(backend="uia").connect(process=14752)
win = app.window(title="计算器")

# 把焦点放到计算器上
win.set_focus()

# 先对计算器进行操作: 输入1
num1_btn = win.child_window(title="一", auto_id="num1Button", control_type="Button")

# 点击按钮
num1_btn.click_input()

# 等待成功
win.wait("active")

结果如下所示:

需要注意,'active' 状态指的是窗口是否处于活动状态,需要先操作应用程序使得焦点设置在该窗口上或者配合 set_focus 来使用。

5.2 wait_until

等待满足某个条件。

函数如下:

python 复制代码
wait_until(timeout, retry_interval, func, value=True, op=operator.eq, *args, **kwargs)

部分参数说明:

  • timeout:超时时间
  • retry_interval:重试时间
  • func:执行的函数
  • value:比较的值

示例一:

python 复制代码
from pywinauto.timings import wait_until

i = 0
def work():
    global i
    i += 1
    print("当前i的值为", i)
    return i

# 等待work返回的结果为5,继续往下执行
wait_until(10, 1, work, 5)
print("等待通过")

结果如下所示:

示例二:

python 复制代码
from pywinauto.application import Application
from pywinauto.timings import wait_until

def get_window():
    app = Application(backend="uia").connect(process=14752)
    win = app.window(title="计算器")
    return win.is_visible() # 验证窗口是否为可见状态(必须打开窗口)

def test_wait():
    # 5s钟内等待get_window方法返回结果为True
    wait_until(5, 2, get_window, True)
    print("等待通过")

test_wait()

结果如下所示:

在 pywinauto 中,提供了灵活多样的等待机制,用于处理自动化过程中常见的控件状态变化、窗口加载等问题。这些等待机制能够确保自动化脚本在执行时能够准确地识别目标控件或窗口,并在合适的时间点进行操作。

虽然 pywinauto 提供了多种等待机制,本文重点介绍几种在实际应用中最为常用的等待方法,帮助大家快速掌握核心技巧,确保自动化流程的稳定性和可靠性。

6. 控件的操作

6.1 点击

click_input():模拟鼠标左键单击操作。

使用示例:

python 复制代码
import time

from pywinauto.application import Application

# 打开计算器
app = Application(backend="uia").connect(process=14752)
win = app.window(title="计算器")
win.wait("visible")
time.sleep(2)

# 点击计算器上的按钮, 进行 1 + 2 = 3

# 定位数字按钮1
btn_1 = win.child_window(title="一", auto_id="num1Button", control_type="Button")
# 点击数字按钮1
btn_1.click_input()
time.sleep(2)

# 定位按钮+
add_btn = win.child_window(title="加", auto_id="plusButton", control_type="Button")
# 点击按钮+
add_btn.click_input()
time.sleep(2)

# 定位数字按钮2
btn_2 = win.child_window(title="二", auto_id="num2Button", control_type="Button")
# 点击数字按钮2
btn_2.click_input()
time.sleep(2)

# 定位按钮=
equal_btn = win.child_window(title="等于", auto_id="equalButton", control_type="Button")
# 点击按钮=
equal_btn.click_input()
time.sleep(5)

结果如下所示:

right_click_input():模拟鼠标右键单击操作。

使用示例:

python 复制代码
from pywinauto import Application

# 打开Typora
app = Application(backend='uia').connect(process=8016)
win = app.window(title_re='.*po.*')
win.wait("visible") # 保证窗口是可见的

# 对窗口进行右键操作
win.right_click_input()

结果如下所示:

double_click_input():模拟鼠标左键双击操作。

使用示例:选中

python 复制代码
from pywinauto import Application

# 打开Typora
app = Application(backend='uia').connect(process=8016)
win = app.window(title_re='.*po.*')
win.wait("visible") # 保证窗口是可见的

# 双击标题栏
title_bar = win['TitleBar']
title_bar.double_click_input()

结果如下所示:

6.2 文本

texts():用于获取窗口或控件中的所有文本内容,返回一个列表,其中每个元素是一个字符串,表示窗口或控件中的某个文本片段。

示例一:获取窗口的标题

python 复制代码
from pywinauto import Application

# 打开Typora
app = Application(backend='uia').connect(process=8016)
win = app.window(title_re='.*po.*')
win.wait("visible") # 保证窗口是可见的

# 获取窗口的标题
print(win.texts())

结果如下所示:

示例二:获取控件的标题

python 复制代码
from pywinauto import Application

# app = Application(backend='uia').start("D:\software\Sublime Text 3\sublime_text.exe")
app = Application(backend='uia').connect(process=2392)
win = app.window(title_re='.*Sublime Text.*')
win.wait("visible")
file = win.child_window(title="File",control_type="MenuItem")

print(file.texts())

结果如下所示:

window_text():用于获取窗口或控件的主文本内容,通常是指窗口的标题或主要显示的文本。它返回一个字符串。

示例一:

python 复制代码
from pywinauto import Application

# app = Application(backend='uia').start("D:\software\Sublime Text 3\sublime_text.exe")
app = Application(backend='uia').connect(process=2392)
win = app.window(title_re='.*Sublime Text.*')
win.wait("visible")

print(win.window_text())

结果如下所示:

示例二:获取控件的标题

python 复制代码
from pywinauto import Application

# app = Application(backend='uia').start("D:\software\Sublime Text 3\sublime_text.exe")
app = Application(backend='uia').connect(process=2392)
win = app.window(title_re='.*Sublime Text.*')
win.wait("visible")
file = win.child_window(title="File",control_type="MenuItem")

print(file.window_text())

结果如下所示:

7. 鼠标操作

在使用 pywinauto 进行自动化测试时,我们通常会利用控件的点击方法来实现交互操作。这些方法不仅支持对控件的直接点击,还允许通过指定 coords 参数来实现基于坐标的点击操作。

例如,当我们尝试通过 win.double_click_input(coords=(1043,130)) 来点击 Typora 窗口的最大化按钮时,虽然语法上没有问题,但这种方法可能无法完全满足我们的需求,尤其是在需要更精确或更灵活的鼠标交互时。

示例一:

python 复制代码
from pywinauto.application import Application

# 打开Typora
app = Application(backend='uia').connect(process=8016)
win = app.window(title_re='.*po.*')
win.wait("visible") # 保证窗口是可见的

# 双击标题栏
title_bar = win['TitleBar']
title_bar.double_click_input(coords=(207,21))

为了解决这一问题,pywinauto 提供了一个独立的 mouse 模块,专门用于模拟真实用户的鼠标事件。这个模块的优势在于,它完全独立于控件操作,能够更贴近真实用户的行为模式。通过 mouse 模块,我们可以直接在屏幕上指定坐标进行点击、双击、拖动等操作,而无需依赖控件的层次结构。

示例二:click() 方法操作 Typora 窗口的最大化按钮

python 复制代码
from pywinauto import mouse
from pywinauto.application import Application

# 打开Typora
app = Application(backend='uia').connect(process=8016)
win = app.window(title_re='.*po.*')
win.wait("visible") # 保证窗口是可见的

#通过鼠标点击
mouse.double_click(coords=(207,21))

比如,我们要具体定位到记事本上方中【文件】的坐标。

示例三:

python 复制代码
from pywinauto.application import Application

# 打开记事本
app = Application(backend='uia').connect(process=5044)
win = app.window(title_re='.*记事本.*')
win.wait('visible')

# 双击标题栏
proc = win.child_window(title="文件(F)", control_type="MenuItem")
print(proc.rectangle())

结果如下:

其中,这串数字的含义如下图所示:

显而易见:

  • width = right - left。
  • height = bottom - top。

示例:拖动滚动条

  • rectangle():获取元素对应坐标,返回矩形尺寸,具有 top、left、right、bottom 属性。
  • mid_point():获取元素中间位置坐标,返回类型为元组,元组中两个整数分别是 X、Y 轴的值。

示例四:

python 复制代码
from pywinauto import Application
from pywinauto import mouse

# 打开记事本
app = Application(backend='uia').connect(process=5044)
win = app.window(title_re='.*记事本.*')
win.wait('visible')

#定位滚动条
right_ScrollBar = win.child_window(title="垂直滚动条", auto_id="NonClientVerticalScrollBar", control_type="ScrollBar")
right_ScrollBar.wait('visible')

#获取滚动条中间位置
mid = right_ScrollBar.rectangle().mid_point()
#从中间位置下拉
mouse.scroll(coords=(mid.x, mid.y), wheel_dist=-900)

# 或者直接一步到位(900, 600)是中间的一个大致坐标
#mouse.scroll(coords=(900, 600), wheel_dist=-900)

结果如下所示:可以看到已经滑动到最底部了

mouse 模块常见操作:

方法 功能描述
mouse.click(button='left',coords=(0,0)) 单击指定的坐标
mouse.scroll(coords=(0,0),wheel_dist=1) 滚动鼠标滚轮,wheel_dist表示滚动的距离,大于0是向上滚动,小于0是向下滑动。
mouse.double_click(button='left',coords=(0,0)) 双击指定的坐标
mouse.right_click(coords=(0,0)) 右键单击指定的坐标
mouse.move(coords=(0,0)) 移动鼠标到指定坐标
mouse.wheel_click(coords=(0,0)) 鼠标中键单击指定的坐标
mouse.press(button='left',coords=(0,0)) 按下鼠标按钮
mouse.release(button='left',coords=(0,0)) 释放鼠标按钮

示例五:打开计算器,并把数字键盘上的所有按键挨个点一遍。

python 复制代码
import time

from pywinauto import mouse
from pywinauto.application import Application

# 连接计算器窗口
app = Application(backend="uia").connect(process=14752)
win = app.window(title="计算器")
win.wait("visible") # 这里是为了让程序卡住等一下,直到计算器窗口完全显示在屏幕上,防止后面的操作因为窗口没加载完而报错。

# 找到数字键盘区域
num_pad = win.child_window(title="数字键盘", auto_id="NumberPad", control_type="Group")

# 自动循环点击每一个数字
for num in num_pad.children(): # 遍历数字键盘里的每一个子按钮(比如数字 1、2、3...)。
    point = num.rectangle().mid_point() # 获取当前按钮在屏幕上的中心点坐标(X 轴和 Y 轴)。
    mouse.click(coords=(point.x, point.y)) # 控制你的真实鼠标指针,移动到这个中心点并执行点击。
    time.sleep(2)

结果如下所示:运行这段代码后,你会看到你的鼠标像被 "幽灵" 控制了一样,自己移动到计算器的数字键盘上,每隔 2 秒自动去点一个数字。

7.1 自动化测试场景示例:抖音点赞

代码如下所示:

python 复制代码
import time

from pywinauto import mouse
from pywinauto.application import Application

#连接抖音
app = Application(backend="uia").connect(process=13864)
#定位窗口
win = app.window(title="抖音")
win.wait("visible")

for i in range(0,3):
    #双击视频刷赞
    point = win.rectangle().mid_point()

    time.sleep(2)
    #调用高级api双击
    win.double_click_input()
    # #鼠标操作
    # mouse.double_click(coords=(point.x,point.y))
    #刷到下一个视频
    time.sleep(2)
    mouse.scroll(coords=(point.x,point.y),wheel_dist=-500)

8. 键盘操作

在自动化测试中,pywinauto 提供了强大的键盘操作功能,其中 keyboard 模块是核心组件之一。

keyboard.send_keys() 是一个通用的键盘输入方法,它可以直接将按键序列发送到当前具有焦点的窗口。然而,在实际的自动化场景中,我们通常需要针对特定的控件(如文本框、按钮等)进行精确的输入操作,而不是依赖于窗口的焦点状态。

python 复制代码
from pywinauto.keyboard import send_keys

send_keys("1234567")

结果如下所示:

为了满足这种需求,pywinauto 对键盘输入功能进行了进一步的封装,引入了 type_keys 方法。type_keys 是控件对象(例如 Edit 控件或 Button 控件)的专属方法,可以直接作用于指定的控件,确保输入内容精准无误。这种方法避免了因焦点切换导致的输入错误,特别适合在复杂的用户界面中进行自动化操作。

语法如下:

python 复制代码
type_keys(
    keys,               # 要输入的键序列,可以是普通字符、特殊键或组合键
    pause = None,       # 每次按键后的延迟时间(秒)
    with_spaces = False,# 如果为True,则会在输入的字符串中保留空格。
    with_newlines = False,# 如果为True,则会在输入的字符串中保留换行符。
    ...
)

向当前焦点窗口自动键入键或发送文本。

8.1 输入文本

直接输入文本(支持 Unicode 字符):

python 复制代码
import time

from pywinauto import mouse
from pywinauto.application import Application
from pywinauto.keyboard import send_keys

# 连接记事本窗口
app = Application(backend="uia").connect(process=4884)
win = app.window(title_re='.*记事本.*')
win.wait("visible") # 保证窗口是可见的

# 在记事本中输入内容
#输入文本内容
win.type_keys("---type_keys---")

#保留换行符
win.type_keys("---type_keys---\n",with_newlines=True)

#保留空格
win.type_keys(" ----type keys---- ",with_spaces=True)

#延迟输入,避免输入过快导致内容不完整
win.type_keys("一二三四五六七",with_spaces=True)

结果如下所示:

8.2 按键

使用 {VK_CODE} 格式表示虚拟键码,例如:

按键 代码
Enter {ENTER}
Tab {TAB}
Backspace {BACKSPACE}
Esc {ESC}
方向键 {UP},{DOWN},{LEFT},{RIGHT}
F1-F9 {F1}-{F9}
Shift +
Ctrl ^
Alt %

示例代码:

python 复制代码
from pywinauto import Application

# 连接记事本窗口
app = Application(backend="uia").connect(process=4884)
win = app.window(title_re='.*记事本.*')
win.wait("visible") # 保证窗口是可见的

# 在记事本中输入内容
#发送文本和回车
win.type_keys("Hello World{ENTER}",with_spaces=True)
win.type_keys("Hello edison",with_spaces=True)

结果如下所示:

指定重复次数:可以为特殊键指定重复计数,{ENTER 2} 表示按两次 Enter 键。

代码示例如下:

python 复制代码
#发送文本和回车
win.type_keys("Hello World{ENTER 2}",with_spaces=True)
win.type_keys("Hello bit",with_spaces=True)

结果如下所示:

转义特殊字符:使用 { } 包裹特殊字符(如 {+}{%}{^})以避免被识别为修饰符

python 复制代码
win.type_keys("1+2=3")        # 错误:'+' 会被识别为 Shift
win.type_keys("1{+}2=3")     # 正确

结果如下所示:

8.3 自动化测试场景示例:WeChat 发消息

1)用例设计

GUI 自动化用例的设计主要基于 UI(界面)测试和界面展示的功能来进行用例的设计。

2)代码实现

以用例 "验证用户输入文本后点击发送按钮,消息是否能成功发送并显示在聊天窗口中" 为例。

python 复制代码
import time

from pywinauto.application import Application

# 连接微信应用程序
app = Application(backend="uia").connect(process=12588)
win = app.window(title="Andy")
win.wait("visible") # 这里是为了让程序卡住等一下,直到计算器窗口完全显示在屏幕上,防止后面的操作因为窗口没加载完而报错。

# win.print_control_identifiers()

# 1. 更加精准地定位到微信聊天输入框
edit = app['Andy'].child_window(auto_id="chat_input_field", control_type="Edit")

# 2. 等待并唤起输入框
edit.wait("ready")

# 3. 点击输入框使其获取焦点
edit.click_input()

# 4. 输入文本(建议使用 type_keys)
edit.type_keys("你好 Andy", with_spaces=True)
time.sleep(2)

# 5. 点击发送
send_btn = app['Andy'].child_window(title="发送", control_type="Button")
send_btn.click_input()

# 6. 检查发送结果
# 6.1 先定位到消息列表控件
message_list = win.child_window(auto_id="chat_message_list", control_type="List")

# 6.2 获取列表里所有子控件的文本内容(会返回一个列表)
all_messages = message_list.texts()

# 6.3 打印出来肉眼看一下结构(调试用)
print("当前聊天记录里的文本:", all_messages)

# 6.4 断言检查你发送的话是否在里面
assert any("你好 Andy" in text for text in all_messages), "消息发送失败,未在聊天记录中找到!"

结果如下所示:

9. 菜单控件的操作

9.1 items

返回对话框的菜单项,如果没有菜单项,则返回空列表。

示例:获取菜单

python 复制代码
from pywinauto.application import Application

# 连接记事本窗口
app = Application(backend="uia").connect(process=4884)
win = app.window(title_re='.*记事本.*')
win.wait("visible") # 保证窗口是可见的

# 获取所有的菜单项
menu_bar = win.child_window(title="应用程序", auto_id="MenuBar", control_type="MenuBar")
print(menu_bar.items())

结果如下所示:

9.2 item_by_index

item_by_index(idx):查找索引指定的菜单项,idx 是索引,从 0 开始。

示例

python 复制代码
from pywinauto.application import Application

# 连接记事本窗口
app = Application(backend="uia").connect(process=4884)
win = app.window(title_re='.*记事本.*')
win.wait("visible") # 保证窗口是可见的

# 获取所有的菜单项
menu_bar = win.child_window(title="应用程序", auto_id="MenuBar", control_type="MenuBar")

print(menu_bar.item_by_index(0))
print(menu_bar.item_by_index(1))
print(menu_bar.item_by_index(2))
print(menu_bar.item_by_index(3))
print(menu_bar.item_by_index(4))

结果如下所示:

9.3 item_by_path

语法:item_by_path(path,exact=False)

用于查找路径指定的菜单项:

  • path:用于指定要选择的菜单项路径。
  • exact:设置为True,则要求菜单项名称与路径中的名称完全匹配;如果为False,则允许模糊匹配。

路径可以是 MenuItem->MenuItem->MenuItem ... 形式的字符串,其中每个 MenuItem 是菜单该级别的项目文本。

示例写法:

复制代码
File->Export->ExportAsPNG

空格不影响识别,也可写作:

复制代码
File -> Export -> Export As PNG

示例代码:

python 复制代码
from pywinauto import Application

# 连接记事本窗口
app = Application(backend="uia").connect(process=14940)
win = app.window(title_re='.*记事本.*')
win.wait("visible") # 保证窗口是可见的

#win.print_control_identifiers()

# 获取所有的菜单项
menu_bar = win.child_window(title="应用程序", auto_id="MenuBar", control_type="MenuBar")
#print(menu_bar.items())

# 选择保存文件
menu_bar.item_by_path(path="文件 -> 另存为").click_input()

结果如下所示:

语法:menu_select(path,exact=False)

用于查找路径指定的菜单项:

  • path:用于指定要选择的菜单项路径。
  • exact:设置为 True,则要求菜单项名称与路径中的名称完全匹配;如果为 False,则允许模糊匹配。

注意,menu_select() 的使用和上面有区别,使用 menu_select() 的场景下,通常至少有两个菜单栏:"系统" 和 "应用程序"。系统菜单栏是一个标准的窗口菜单,包含以下项目:"还原"、"移动"、"大小"、"最小化"等。此菜单栏通常有一个 "标题栏" 控件作为父级。应用程序菜单栏通常是我们要找的。在大多数情况下,它的父级是对话框本身,因此可以在对话框的直接子级中找到它。

路径可以是 MenuItem->MenuItem->MenuItem ... 形式的字符串,其中每个 MenuItem 是菜单该级别的项目文本。例如:

复制代码
File->Export->ExportAsPNG

空格并不重要,所以你也可以写:

复制代码
File -> Export -> Export As PNG

示例代码如下:

python 复制代码
from pywinauto import Application

# 连接记事本窗口
app = Application(backend="uia").connect(process=14940)
win = app.window(title_re='.*记事本.*')
win.wait("visible") # 保证窗口是可见的

#win.print_control_identifiers()

# 获取所有的菜单项
menu_bar = win.child_window(title="应用程序", auto_id="MenuBar", control_type="MenuBar")
#print(menu_bar.items())

# 选择保存文件
win.menu_select(path="文件 -> 另存为")

结果如下所示:

9.5 自动化测试场景示例:批量创建文件

使用 记事本 来批量创建文件,针对创建文件的场景我们进行用例的设计,并编写对应的测试脚本

1)用例设计

2)代码实现:验证保存文件后,文件是否正确写入到指定路径。

python 复制代码
from pywinauto.application import Application
import time
import os

# 连接记事本窗口
#app = Application(backend="uia").connect(process=14940)
app = Application(backend='uia').start("C:\\Windows\\system32\\notepad.exe")
win = app.window(title_re='.*记事本.*')
win.wait("visible") # 保证窗口是可见的

# 批量创建五个文件
for i in range(1, 6):
    # 输入内容
    win.type_keys(f"创建第{i}个文件文件")

    # 等待保存文件窗口
    # 1)快捷键
    # win.type_keys(f"创建第{i}个文件文件^s")
    # 2)通过选择菜单选项
    #menu_bar = win.child_window(title="应用程序", auto_id="MenuBar", control_type="MenuBar")
    #menu_bar.item_by_path(path="文件 -> 另存为").click_input()
    # 3)menu_select
    win.menu_select(path="文件 -> 另存为")

    # 切换到保存窗口
    save_win = win.child_window(title="另存为", control_type="Window")

    # 在保存弹窗上输入文件名称
    filename = f"D:\\GitHub_repository\\test_{i}.txt"
    save_win.child_window(title="文件名:", control_type="Edit").type_keys(filename)

    # 在保存弹窗上点击保存按钮
    save_win.child_window(title="保存(S)", control_type="Button").click_input()

    # 验证文件是否成功保存
    time.sleep(1)
    assert os.path.exists(filename)

    # 创建新文件
    win.type_keys("^n")

结果如下所示:

10. 列表控件的操作

10.1 get_items

获取列表视图中的所有项目。

示例:打印文件资源管理列表项

python 复制代码
from pywinauto.application import Application

# 连接记事本窗口
app = Application(backend="uia").connect(process=7348)
win = app.window(title_re='.*文件资源管理器')
win.wait("visible") # 保证窗口是可见的

# 定位列表控件
list_ctrol = win.child_window(title="项目视图", control_type="List")

# 打印子控件信息
print(list_ctrol.get_items())

结果如下所示:

10.2 item_count

获取列表视图中的项数。

示例代码:

python 复制代码
from pywinauto import Application

# 连接记事本窗口
app = Application(backend="uia").connect(process=7348)
win = app.window(title_re='.*文件资源管理器')
win.wait("visible") # 保证窗口是可见的

# 定位列表控件
list_ctrol = win.child_window(title="项目视图", control_type="List")

#打印列表项数量
print("items:",list_ctrol.item_count())

结果如下所示:

10.3 get_item

返回列表视图中的指定项目。

其中,参数 row 可以是行的索引。

示例代码:

python 复制代码
from pywinauto import Application

# 连接记事本窗口
app = Application(backend="uia").connect(process=7348)
win = app.window(title_re='.*文件资源管理器')
win.wait("visible") # 保证窗口是可见的

# 定位列表控件
list_ctrol = win.child_window(title="项目视图", control_type="List")

#获取列表中第一项
print("items:",list_ctrol.get_item(row=0))

结果如下所示:

10.4 自动化测试场景示例:WeChat 发消息(进阶版)

前面我们已经尝试实现了微信发消息,从打开程序、基本操作流程到最后的检查,脚本看似完整,但这里的脚本仍然存在多个问题。若脚本执行了多次:

  • 如何校验本次发送操作一定是成功的呢?
  • 消息列表则存在多条相同的消息,直接将消息拼接在 child_window 参数中,可能存在元素不唯一性报错。

掌握了列表操作后,我们发现聊天记录是一个列表控件,正确的设计和校验思路如下:

  • 为保证每次发送的文本唯一,可动态生成要发送的消息。
  • 发送前后消息列表的数量加 1。
  • 获取消息列表数量后,根据数量定位最后一条消息,检查文本是否为最新的。

示例代码:验证用户输入文本后点击发送按钮,消息是否能成功发送并显示在聊天窗口中。

python 复制代码
from datetime import datetime
import time

from pywinauto.application import Application

# 连接微信应用程序
app = Application(backend="uia").connect(process=15236)
win = app.window(title="Andy")
win.wait("visible") # 这里是为了让程序卡住等一下,直到计算器窗口完全显示在屏幕上,防止后面的操作因为窗口没加载完而报错。

# 0. 获取发送消息之前的消息个数
message_list = win.child_window(auto_id="chat_message_list", control_type="List")
message_count_before = message_list.item_count()

# 1. 更加精准地定位到微信聊天输入框
edit = app['Andy'].child_window(auto_id="chat_input_field", control_type="Edit")

# 2. 等待并唤起输入框
edit.wait("ready")

# 3. 点击输入框使其获取焦点
edit.click_input()

# 4. 输入文本(建议使用 type_keys)
# 生成标准格式:edison 2026-06-22 19:55:30
time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
message = f"edison {time_str}"
# 用 set_edit_text 直接写入,完美支持空格和冒号
edit.set_edit_text(message)
time.sleep(1)

# 5. 点击发送
send_btn = app['Andy'].child_window(title="发送", control_type="Button")
send_btn.click_input()

# 6. 获取发送消息之的消息个数
message_list = win.child_window(auto_id="chat_message_list", control_type="List")
message_count_after = message_list.item_count()

# 6.1 校验消息列表数量增加(1条 or 2条)
assert message_count_after == message_count_before + 1 or message_count_after == message_count_before + 2

# 6.2 校验消息列表最后一条消息对应的文本是否满足message
text = message_list.get_item(row = message_count_after - 1).window_text()
assert text == message
print(text)
print(message)

结果如下所示:

相关推荐
海兰1 小时前
【高速缓存】RedisVL为文本生成嵌入向量实践指南
人工智能·python·机器学习
2601_955760071 小时前
如何用 Claude Opus 5 API 批量扩展长尾关键词和文章选题
前端·python·搜索引擎
艾斯特_2 小时前
从模型调用到可用聊天应用:会话状态与消息协议
python·ai·aigc
Marst Code2 小时前
Python 3.9 已停止维护!从 3.9 到 3.14 全版本深度对比,生产环境该选哪个?
开发语言·python
llwszx2 小时前
【Java/Go后端手撸原生Agent(第九篇):Plan-and-Execute规划模式——从“走一步看一步“到“先谋后动“】
java·python·golang·agent开发·plan模式·规划执行模式
lxw18449125142 小时前
Python uv 完整使用教程
python·conda·pip·uv
hangyuekejiGEO2 小时前
临沂GEO技术解析与行业应用方案
人工智能·python
不做Java程序猿好多年4 小时前
Java中 String、StringBuffer、StringBuilder 的区别详解
开发语言·python
金銀銅鐵4 小时前
[Python] 用 turtle 来绘制国际象棋棋盘(不含棋子)
python·游戏