windows UI 自动化测试框架 pywinauto 使用教程
框架简介
pywinauto 是一个用于自动化测试 windows 应用的 python module,通过使用该自动化测试框架,可以模拟用户界面的交互,实现自动化操作指定应用
官方文档:https://pywinauto.readthedocs.io/en/latest/index.html
github 仓库地址:GitHub - pywinauto/pywinauto: Windows GUI Automation with Python (based on text properties)
pywinauto 支持的应用开发技术包括:
-
Win32 API(backend="win32") - 目前是默认的开发框架,包括 MFC、VB6、VCL、简单的 WinForms 控件以及大多数旧版应用程序。
-
MS UI Automation(backend="uia") - 包括 WinForms、WPF、Store 应用、Qt5 以及浏览器等更现代的应用程序。需要注意的是,Chrome 浏览器在启动前需要设置
--force-renderer-accessibility
命令行标志才能通过 UI Automation访问。此外,由于 comtypes Python库的限制,自定义属性和控件可能不受支持。
框架安装
前提条件:电脑已正确配置 python 环境,最好已安装 pycharm
执行 module 安装命令:
pip install pywinauto
运行以下脚本,检查 pywinauto 是否正常安装:
from pywinauto.application import Application
app = Application(backend="uia").start("notepad.exe")
print(app.process)
框架若正常安装后,将会启动 Notepad(记事本)应用。本教程以网易云音乐(windows 版,v3.0.5)为例,说明 pywinauto 如何使用
应用启动
Application()
实例是自动化测试应用程序起点,因此,Application
实例需要与一个进程相连接,通过两种方式实现:
start(self, cmd_line, timeout=app_start_timeout) # instance method:
或
connect(self, **kwargs) # instance method:
当应用程序未运行并且需要启动它时使用 Start(),用法如下:
app = Application(backend='uia').start(r"c:\path\to\your\application -a -n -y --arguments")
timeout 参数是可选的,只有当应用程序需要很长时间启动时才需要使用它。
当要自动化的应用程序已经启动时使用 Connect()。要指定一个已经运行的应用程序,您需要指定以下选项之一:
process 应用程序的进程 id,例如:
app = Application(backend='uia').connect(process=2341)
handle 应用程序窗口的窗口句柄
app = Application(backend='uia').connect(handle=0x010f0c)
path 应用程序的安装路径
app = Application(backend='uia').connect(path=r"c:\windows\system32\notepad.exe")
使用 Connect()方法,要确保应用已启动且应用窗口在前台(应用获取到焦点)
实战环节
下载、安装网易云音乐,安装路径为:D:\soft\CloudMusic\cloudmusic.exe,代码实现:
path=r'D:\soft\CloudMusic\cloudmusic.exe'#应用程序安装的路径
app = Application(backend='uia').start(path)
print(app.process)
运行以上代码后,网易云音乐将自动打开
获取应用窗口
应用程序启动/连接,获取到 app 对象后,接下来获取 window 对象(窗口)
有许多不同的方法可以做到这一点。最常见的是使用项或属性访问来根据对话框的标题选择窗口。如:
dlg = app.Notepad
或
dlg = app['Notepad']
最简单的方法是请求 top_window()
dlg = app.top_window()
这将返回应用程序顶层窗口中 z 轴顺序最高的窗口
如果这还不够精准控制,那么你可以使用与传递给 findwindows.find_windows() 相同的参数。
dlg = app.window(title_re="Page Setup", class_name="#32770")
最后,你可以获取最基本的窗口:
dialogs = app.windows()
这将返回应用程序中所有可见的、已启用的顶级窗口的列表,
如果有了窗口句柄,可以使用:
app.window(handle=win)
注意:如果窗口的标题很长,那么属性访问可能会很长,在这种情况下,以下使用方法更方便
app.window(title_re=".*Part of Title.*")
备注:对于有多个进程的应用,通过
app = Application(backend='uia').start(r"c:\path\to\your\application -a -n -y --arguments")
启动应用,获取到 app 对象后,调用 app.top_window() 可能会报错,这是因为启动应用获取的 app 对象的进程,可能会在启动应用完成后终止,导致对应进程的窗口找不到。由于 pywinauto 对多进程的应用适配不是很好,解决的办法是:强杀应用,确保应用所有进程都终止,然后再重新调用 start 方法冷启动应用
实战环节
启动网易云音乐,获取首页窗口:
#应用程序安装的路径
path=r'D:\soft\CloudMusic\cloudmusic.exe'
app = Application(backend='uia').start(path)
#打印应用进程
print(app.process)
time.sleep(3)
# 应用启动完成后,获取首页窗口,并打印该窗口下的元素
topwd = app.top_window()
topwd.dump_tree()
首页窗口的元素结构如下:
查找元素
获取到窗口后,就可以在该窗口上查找元素,pywinauto 提供以下方法进行查找元素:
# 通过层级查找控件相关方法+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
window(**kwargs) # 用于窗口的查找
child_window(**kwargs) # 可以不管层级的找后代中某个符合条件的元素,最常用
parent() # 返回此元素的父元素,没有参数
children(**kwargs) # 返回符合条件的子元素列表,支持索引,是BaseWrapper对象(或子类)
iter_children(**kwargs) # 返回子元素的迭代器,是BaseWrapper对象(或子类)
descendants(**kwargs) # 返回符合条件的所有后代元素列表,是BaseWrapper对象(或子类)
iter_children(**kwargs) # 符合条件后代元素迭代器,是BaseWrapper对象(或子类)
查找参数 kwargs 说明:
# 这些是常用的
class_name=None, # 类名
class_name_re=None, # 正则匹配类名
title=None, # 控件的标题文字,对应inspect中Name字段
title_re=None, # 正则匹配文字
control_type=None, # 控件类型,inspect界面LocalizedControlType字段的英文名
best_match=None, # 这个有坑,我不喜欢用,下文有讲解
auto_id=None, # 这个也是固定的可以用,inspect界面AutomationId字段,但是很多控件没有这个属性
实战环节
网易云音乐首页如图所示,我们去查找元素“未登录”
如上文所示,通过获取到 top_window 后,通过 dump_tree() 打印窗口的元素结构后,无法找到元素“未登录”,因此我们需要进一步获取到子 window,然后打印子窗口的元素结构,top_window 的的元素结构如下:
仔细观察元素结构,并与上图比较,未登录最可能在子窗口 title=“ 精选****”中,结合上文提供的 API,我们可以使用 topwd.child_window(control_type="Document") 获取子窗口或子元素,完整代码如下所示:
path=r'D:\soft\CloudMusic\cloudmusic.exe'
app = Application(backend='uia').start(path)
#打印应用进程
print(app.process)
time.sleep(3)
# 应用启动完成后,获取首页窗口,并打印该窗口下的元素
topwd = app.top_window()
topwd.dump_tree()
# 查找子元素
chwd=topwd.child_window(control_type="Document")
# 打印子元素页面结构
chwd.dump_tree()
control_type="Document" 作为筛选条件,可唯一查找到对应元素,子元素的页面结构比较多,这里就不完整展示,可自行调用以上代码查看,这里截取元素"未登录"相关的元素结构如下:
dump_tree() 打印窗口的元素结构,其对应的 child_window() 筛选条件可以直接拿过来使用,以元素"未登录"为例,可以使用以下代码查找到该元素
#应用程序安装的路径
path=r'D:\soft\CloudMusic\cloudmusic.exe'
app = Application(backend='uia').start(path)
#打印应用进程
print(app.process)
time.sleep(3)
# 应用启动完成后,获取首页窗口,并打印该窗口下的元素
topwd = app.top_window()
# topwd.dump_tree()
# 查找子元素
chwd=topwd.child_window(control_type="Document")
# 打印子元素页面结构
# chwd.dump_tree()
btn=chwd.child_window(title="未登录", control_type="Group")
print(btn.get_properties())
使用 child_window() 查找筛选条件,务必要保证筛选条件查找的元素是唯一的,否则会提示匹配异常,如下所示:
这就要求我们设计筛选条件的时候,务必要谨慎,通过单个或多个筛选条件组合,确保命中的元素只有一个
查找元素的方式:获取到顶端窗口后,使用 dump_tree() 打印页面结构,获取到子元素后,重复以上步骤,直到页面结构中出现我们想要的元素。除 dump_tree 外,对于一些元素没有直接可用的筛选条件时,例如某个页面元素的结构如下:
我们无法直接使用 child_window() 获取到该 GropBox,我们可以对其父元素进行遍历,并分别打印每个子元素的信息,如下所示:
app = Application(backend='uia').connect(title="网易云音乐")
#打印应用进程
# print(app.process)
# time.sleep(3)
# 应用启动完成后,获取首页窗口,并打印该窗口下的元素
topwd = app.top_window()
topwd.dump_tree()
# 查找子元素
chwd=topwd.child_window(control_type="Document")
# 遍历元素结构
for child in chwd.children():print(child.get_properties())
此时会打印 chwd 直接子元素的信息,隔代子元素不会打印:
此时,在循环中我们可以增加一系列判断条件,来获取到该元素,例如:
tool_tip = None
for child in chwd.children():f_name = child.friendly_class_name()if f_name == 'ToolTips':tool_tip = child
如果我们查找的元素,无法用筛选条件唯一判定,我们可以观察该元素前后的元素是否唯一,如果前面或后面的元素是唯一的,可获取该唯一元素的 index,然后 index+1 或 index-1 即可拿到该元素,如下所示:
chwd = topwd.child_window(class_name="Chrome_RenderWidgetHostHWND")
ele_index = 0
for index, child in enumerate(chwd.children()):text = child.window_text()if text == '添加新成员':ele_index = index
#取添加为新成员前的元素
try:img = chwd.children()[ele_index - 1]
except Exception as e:logger.info(e)
获取到元素后,我们想验证该元素是否符合预期,可调用 draw_outline(colour='red') 方法,在对应的元素上绘制一圈红色的方框,以上文的“未登录”为例:
# 前提:网易云音乐已启动且在前台
app = Application(backend='uia').connect(title="网易云音乐")
#打印应用进程
# print(app.process)
# time.sleep(3)
# 应用启动完成后,获取首页窗口,并打印该窗口下的元素
topwd = app.top_window()
# topwd.dump_tree()
# 查找子元素
chwd=topwd.child_window(control_type="Document")
btn=chwd.child_window(title="未登录", control_type="Group")
btn.draw_outline(colour='red')
操作元素
查到元素后,就可以对相关元素进行操作,pywinauto 提供以下方法进行操作元素:
# 对页面元素进行操作
# control.click_input() # 鼠标左键点击元素
# control.right_click_input() # 鼠标右键点击元素
# control.type_keys(keys, pause = None, with_spaces = False,) # 输入框输入文本
# control.double_click_input(button ="left", coords = (None, None)) # 左键双击
# control.press_mouse_input(coords = (None, None)) # 指定坐标按下左键,不传坐标默认左上角
# control.release_mouse_input(coords = (None, None)) # 指定坐标释放左键,不传坐标默认左上角
# control.move_mouse_input(coords=(0, 0)) # 将鼠标移动到指定坐标,不传坐标默认左上角
# control.drag_mouse_input(dst=(0, 0)) # 拖拽到指定坐标位置
# 鼠标操作
# 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) #滚轮滑动,正数往上,负数往下
# 键盘操作
# keyboard.send_keys('^a^c') # Ctrl + A 和 Ctrl + C
操作简介
举例说, btn 是一个按钮元素,edit 是一个输入框元素
调用 btn.click_input() ,会对该按钮执行鼠标左键点击,调用 btn.right_click_input() ,会对该按钮执行鼠标右键点击
调用 edit.type_keys('abc'),会对该输入框输入字符 abc,为了确保输入框在焦点状态,一般在输入前,最好执行 edit.click_input()或 edit.set_focus()
如果我们不需要对特定元素进行点击,此时可以直接调用 mouse.click(button='left', coords=(40, 40)),传入坐标,完成点击操作
如果我们的应用支持快捷键设置,例如调用 Ctrl +Shift+W 可切换应用前后台,调用 keyboard.send_keys('^+W') #代表 Ctrl +Shift+W,详细的按键配置可见:https://pywinauto.readthedocs.io/en/latest/code/pywinauto.keyboard.html?highlight=keyboard#module-pywinauto.keyboard
实战环节
完成以下场景:打开网易云音乐,搜索“蓝色多瑙河”,回车确认,鼠标向下滑动,右键选中“上海交响乐团”,点击“播放”,输入快捷键 Ctrl+P 暂停音乐,实现代码如下:
# 应用程序安装的路径
path = r'D:\soft\CloudMusic\cloudmusic.exe'
app = Application(backend='uia').start(path)
# 前提:网易云音乐已启动且在前台
# app = Application(backend='uia').connect(title="网易云音乐")
# 打印应用进程
# print(app.process)
time.sleep(3)
# 应用启动完成后,获取首页窗口,并打印该窗口下的元素
topwd = app.top_window()
# 查找子元素
chwd = topwd.child_window(control_type="Document")
edit_search = chwd.child_window(title="search", control_type="Image")
edit_search.click_input()
edit_search.type_keys('蓝色多瑙河')
# 等待一段时间,确保搜索结果正常刷新
time.sleep(3)
# 回车确认
keyboard.send_keys('{ENTER}')
time.sleep(3)
# 向下滑动
mouse.scroll(coords=(1200, 300), wheel_dist=-1)
# chwd.dump_tree()
text_author=chwd.child_window(title="上海交响乐团", control_type="Text")
text_author.right_click_input()
time.sleep(1)
btn_play = chwd.child_window(title="播放", control_type="Text")
btn_play.click_input()
time.sleep(10)
# 快捷键暂停播放
keyboard.send_keys('^P')
附录
python基于pywinauto实现PC端自动化 python操作微信自动化 - www.pu - 博客园
pywinauto 自动化入门 - 心随所遇 - 博客园