Pywinauto:Windows 桌面应用 Python 自动化教程
本文介绍了 Pywinauto 这一用于 Windows 桌面应用自动化的 Python 库。内容包括安装步骤、基础操作(启动/连接应用、窗口控制、控件定位)、交互操作(鼠标/键盘、菜单/列表处理)及等待机制。文中通过微信发消息的实际案例,演示了如何连接应用、定位窗口、发送消息并校验结果,展示了利用 Pywinauto 实现 GUI 自动化测试的完整流程。

本文介绍了 Pywinauto 这一用于 Windows 桌面应用自动化的 Python 库。内容包括安装步骤、基础操作(启动/连接应用、窗口控制、控件定位)、交互操作(鼠标/键盘、菜单/列表处理)及等待机制。文中通过微信发消息的实际案例,演示了如何连接应用、定位窗口、发送消息并校验结果,展示了利用 Pywinauto 实现 GUI 自动化测试的完整流程。

Pywinauto 是一个用于自动化 Windows 图形用户界面 (GUI) 的 Python 库。文章介绍了 Pywinauto 的核心功能,包括跨 GUI 框架支持、简单易用的 API 和录制回放功能。详细讲解了安装步骤和常见操作,如启动/连接应用程序、定位窗口和控件、窗口操作、等待机制、控件交互、鼠标键盘操作以及菜单和列表控件处理。最后以微信发消息为例,演示了完整的自动化测试场景实现。Pywinauto 通过模拟用户操作实现 Windows 应用程序自动化测试,大幅提升 GUI 测试效率。
Pywinauto 是一个用于自动化 Windows 图形用户界面(GUI)的 Python 库。它允许你通过编程方式控制 Windows 应用程序,模拟用户操作(如点击按钮、输入文本、选择菜单等)。
想象你有一个机器人助手,可以代替你操作电脑上的任何软件——点击按钮、输入文字、选择菜单,就像真人操作一样。Pywinauto 就是这样一个能让 Python 控制 Windows 应用程序的'机器人控制器'。它的核心思想就是用代码代替鼠标和键盘。
1. 跨 GUI 框架支持:
2. 简单易用的 API:
3. 录制和回放功能: 可以使用 inspect.exe 或者 UISpy.exe 辅助识别控件。
使用如下指令在命令行窗口进行 pywinauto 的安装和检查:
# 安装
pip install pywinauto
# 检查是否安装成功
pip list
安装完成后,使用 pip list 指令,打印相关信息,表示安装成功。
语法:
app = Application(backend='uia').start(cmd_line...)参数解释:
示例:
# 启动应用程序
app = Application(backend='uia').start('notepad.exe')
语法:
app = Application(backend='uia').connect(process=12345)app = Application(backend='uia').connect(handle=66666)参数解释:
示例:
# 连接打开的应用程序
app = Application(backend='uia').connect(process=27076)
app = Application(backend='uia').connect(handle=66666)
可以结合 print_control_identifiers() 方法,根据打印的控件信息,协助定位窗口;
语法:
win = app.window(title='...')win = app.best_match (不推荐)window() 方法参数解释:
示例:
# 精确匹配
notepad1 = app.window(title='Hello,Pywinauto! - Notepad')
# 正则匹配
notepad2 = app.window(title_re='.*Notepad')
# bestmatch 匹配
notepad3 = app.window(best_match='Hello,Pywinauto! - Notepad')
# 精确匹配
notepad4 = app.window(class_name='Notepad')
# 正则匹配
notepad5 = app.window(class_name_re='.*Notepad')
注意:通过 best_match 的方法定位窗口是不推荐的,因为控件的名称中间有可能会有空格,逗号或者其它的特殊符号,使用上述语法会出现报错,因此不推荐。
常用的窗口操作的方法如下:
示例:
# 连接打开的应用程序
app = Application(backend='uia').connect(process=22860)
# 定位窗口
win = app.window(title_re='.*Notepad')
# 等待窗口可见
win.wait('visible')
# 窗口最大化
time.sleep(3)
win.maximize()
assert win.is_maximized()
# 窗口最小化
time.sleep(3)
win.minimize()
assert win.is_minimized()
# 窗口恢复正常
time.sleep(3)
win.restore()
assert win.is_normal()
# 关闭窗口
win.close()
控件的定位方法:
常见的控件类型如下:
| 分类 | 控件名称 | 说明 |
|---|---|---|
| 窗口与对话框 | 对话框(Dialog) | 用于与用户交互,如显示警告、确认操作或者输入信息。 |
| 窗口与对话框 | 窗格(Pane) | 通常作为窗口的一部分,用于显示特定内容或功能。 |
| 输入控件 | 按钮(Button) | 用于触发操作,如点击按钮执行某个功能。 |
| 输入控件 | 编辑栏(Edit) | 用于输入或编辑文本,支持多行或单行输入。 |
| 输入控件 | 组合框(ComboBox) | 结合文本框和列表框,用户可以选择预定义选项或输入自定义值。 |
| 输入控件 | 列表框(ListBox) | 用于显示可选择的列表项目,支持单选或多选。 |
| 菜单控件 | 菜单(Menu) | 用于提供功能选项,通常位于窗口顶部。 |
| 菜单控件 | 菜单项(MenuItem) | 菜单中的具体选项,点击后执行对应的功能。 |
| 菜单控件 | 弹出菜单(PopupMenu) | 右键单击时弹出的菜单,用于快速访问常用功能。 |
| 列表显示控件 | 列表显示控件(ListView) | 用于以表格形式显示数据,支持多列显示和排序。 |
| 容器控件 | 组框(GroupBox) | 用于对控件进行分组,提高界面的可读性和组织性。 |
| 选择控件 | 复选框(CheckBox) | 用于多选操作,用户可以勾选多个选项。 |
| 选择控件 | 单选框(RadioButton) | 用于互斥选择,用户只能选择一个选项。 |
| 显示控件 | 状态栏(StatusBar) | 通常位于窗口底部,用于显示应用状态或者提示信息。 |
| 显示控件 | 静态内容(Static) | 用于显示静态文本或者图像,通常不可编辑。 |
| 导航控件 | 树状视图(TreeView) | 用于展示分层数据,如文件夹结构或组织结构。 |
| 导航控件 | 选项卡控件(TabControl) | 用于在有限的空间内组织多个页面或选项卡,每个选项卡可以包含不同的内容。 |
| 工具控件 | 工具栏(ToolBar) | 用于放置常用按钮或工具,方便用户快速操作。 |
| 工具控件 | 工具提示(ToolTips) | 当鼠标悬停在控件上时显示提示信息,帮助用户理解控件功能。 |
| 头部内容 | 头部内容(Header) | 通常用于显示标题或者表头信息,如表格的列标题。 |
打印信息示例:
Dialog - '无标题 - Notepad' (L0, T0, R0, B0)
['无标题 - Notepad', 'Dialog', '无标题 - NotepadDialog', 'Dialog0', 'Dialog1']
child_window(title="无标题 - Notepad", control_type="Window")
|
| Pane - '' (L-31988, T-31852, R-30897, B-30874)
| ['无标题Pane', 'Pane', '无标题Pane0', '无标题Pane1', 'Pane0', 'Pane1']
| |
| | Document - '' (L-31988, T-31852, R-30897, B-30874)
| | ['无标题Document', 'Document']
|
| Pane - '' (L-31908, T-31982, R-31334, B-31918)
| ['无标题Pane2', 'Pane2']
| |
| | Pane - '' (L-31908, T-31982, R-31334, B-31918)
| | ['无标题Pane3', 'Pane3']
| | |
| | | TabControl - '' (L-31908, T-31982, R-31334, B-31918)
| | | ['无标题TabControl', 'TabControl 添加新标签页', 'TabControl']
| | | child_window(auto_id="Tabs", control_type="Tab")
| | | |
| | | | ListBox - '' (L-31904, T-31982, R-31408, B-31918)
| | | | ['ListBox', '无标题ListBox']
| | | | child_window(auto_id="TabListView", control_type="List")
| | | | |
| | | | | TabItem - '无标题。未修改。' (L-31900, T-31982, R-31408, B-31918)
| | | | | ['TabItem', '无标题。未修改。', '无标题。未修改。TabItem']
| | | | | child_window(title="无标题。未修改。", control_type="TabItem")
| | | | | |
| | | | | | Static - '无标题' (L-31874, T-31966, R-31801, B-31935)
| | | | | | ['Static', '无标题Static', '无标题', 'Static0', 'Static1']
| | | | | | child_window(title="无标题", control_type="Text")
| | | | | |
......
打印的控件信息建议保存下来,避免多次打印;
定位控件的语法(以按钮为例):
save_btn = win['保存']save_btn = child_window(auto_id='...', control_type='Button', ...)定位控件常用的属性:
定位记事本的最小化按钮 示例:
# 连接应用程序
app = Application(backend='uia').connect(title_re='.*Notepad')
# 获取窗口
notepad = app.window(title_re='.*Notepad')
# 使用 best_match 定位最小化按钮
minimize_btn_1 = notepad['最小化']
# 使用 child_window() 定位最小化按钮
minimize_btn_2 = notepad.child_window(
auto_id='Minimize', control_type='Button'
)
注意:
语法:
win.wait(wait_for, timeout=None, retry_interval=None)win.wait_not(wait_for, timeout=None, retry_interval=None)wait_until(timeout, retry_interval, func, value=True)参数解释:
wait_for 状态:
示例 1:
# 计算器 # 1. 测试 enable # 连接打开的窗口
app = Application(backend='uia').connect(process=15652)
# 定位窗口
win = app.window(title='计算器')
win.wait('visible')
win.print_control_identifiers()
# 定位控件
mem_btn = win.child_window(title="清除所有记忆", auto_id="ClearMemoryButton", control_type="Button")
mem_btn.wait_not('enabled')
mem_btn.wait('enabled')
# 2. 测试 ready (visible + enabled)
# 定位控件
clr_btn = win.child_window(title="清除条目", auto_id="clearEntryButton", control_type="Button")
clr_btn.wait('ready')
assert clr_btn.is_visible()
assert clr_btn.is_enabled()
# 定位控件
mem_btn = win.child_window(title="清除所有记忆", auto_id="ClearMemoryButton", control_type="Button")
assert mem_btn.is_visible()
assert not mem_btn.is_enabled()
# 3. 测试 active # 先激活窗口 - 通过找到数字控件,并点击的方式
num_btn = win.child_window(title="一", auto_id="num1Button", control_type="Button")
num_btn.wait('ready')
num_btn.click_input()
# 激活窗口 - 通过使用 set_focus()
win.set_focus()
win.wait('active')
示例 2:
# 测试 wait_until()
def get_window():
# 连接打开的窗口
app = Application(backend='uia').connect(process=15652)
# 定位窗口
win = app.window(title='计算器')
return win.is_visible()
wait_until(3, 0.5, get_window, True)
点击操作语法(以按钮为例):
btn.click_input():单击控件btn.right_click_input():右击控件btn.double_click_input():双击控件示例:
# 双击 OneNote 标题栏
title_bar = win.child_window(title="控件信息 - OneNote", control_type="TitleBar")
title_bar.double_click_input()
文本操作语法:
texts():获取窗口或者控件所有文本内容,返回列表,每个元素是一个字符串,表示一个文本片段;window_text():表示窗口或控件的主要显示的文本内容;示例:
# 1. 连接 OneNote
app = Application(backend='uia').connect(process=27944)
# 2. 定位窗口
win = app.window(title_re='.*OneNote.*')
win.wait('visible')
# 打印控件文本
funcs = win.child_window(title="功能区选项卡", control_type="Tab")
print(funcs.texts())
print(funcs.window_text())
当需要灵活的鼠标交互时,控件的点击操作就无法满足需求,因此 Pywinauto 提供了 mouse 模块,用于模拟用户真实的鼠标事件。
语法:
mouse.click(coords=(x, y)):单击指定的坐标;mouse.scroll(coords=(x, y), wheel_dist=1):滚动鼠标滚轮;mouse.double_click(coords=(x, y)):双击指定的坐标;mouse.right_click(coords=(x, y)):右键点击指定的坐标;mouse.move(coords=(x, y)):移动鼠标到指定坐标;mouse.wheel_click(coords=(x, y)):鼠标中键点击指定的坐标;mouse.press(coords=(x, y)):按下鼠标按钮;mouse.release(coords=(x, y)):释放鼠标按钮;参数解释:
示例:
# 使用灵活的鼠标交互方式,实现点击计算器的数字键
# 1. 连接计算器
calc = Application(backend='uia').connect(process=15652)
# 2. 定位顶层窗口
win = calc.window(title='计算器')
# win.print_control_identifiers()
win.wait('visible')
# 3. 定位数字区
nums_pad = win.child_window(title='数字键盘', auto_id='NumberPad')
# 4. 循环遍历数字键,依此点击
for key in nums_pad.children():
mid_point = key.rectangle().mid_point()
mouse.click(coords=(mid_point.x, mid_point.y))
time.sleep(1)
Pywinauto 提供了强大的键盘操作功能,keyboard 模块是核心组件之一;
输入文本语法:
send_keys('...'):在焦点窗口输入文本;type_keys(keys, pause=None, with_spaces=False, with_newlines=False):在控件的编辑区输入文本;参数解释:
send_keys() 可以将案件序列发送到具有焦点的窗口,但是实际应用中往往需要在控件中输入,而不依赖窗口的焦点状态,因此 type_keys() 应用更广泛;
示例:
from pywinauto.keyboard import send_keys
# 在焦点窗口输入文本
time.sleep(1)
send_keys('1234567')
# type_keys() 输入文本
# 1. 连接应用程序
app = Application(backend='uia').connect(process=14356)
# 2. 定位窗口
win = app.window(title_re='.*OneNote.*')
win.wait('visible')
# 3. 输入
win.type_keys(keys='1234567')
# 延迟输入
win.type_keys(keys='好好学习,天天向上', pause=1)
# 空格
win.type_keys(keys='好好学习,天天向上', with_spaces=True)
# 换行
win.type_keys(keys='hello\nworld', with_newlines=True)
常用的虚拟键码:
| 按键 | 代码 |
|---|---|
| Enter | {ENTER} |
| Tab | {TAB} |
| Backspace | {BACKSPACE} |
| Esc | {ESC} |
| 方向键 | {UP},{DOWN},{LEFT},{RIGHT} |
| F1~F9 | {F1}~{F9} |
| Shift | + |
| Ctrl | ^ |
| Alt | % |
指定重复次数:
可为特殊键指定重复次数,如 {ENTER 2} 表示按两次 Enter 键。
转义特殊字符:
使用 {} 包裹特殊字符(如 {+},{%},{^}),以避免被识别为修饰符。
示例:
# 虚拟按键
# Enter - {ENTER}
win.type_keys(keys='abcd{ENTER}efg')
win.type_keys(keys='abcd{ENTER 10}efg')
# Tab - {TAB}
win.type_keys(keys='静夜思{TAB}李白')
win.type_keys(keys='静夜思{TAB 10}李白')
# Backspace - {BACKSPACE}
win.type_keys(keys='{BACKSPACE}')
win.type_keys(keys='{BACKSPACE 100}')
# Shift
win.type_keys(keys='+2')
# Ctrl
win.type_keys(keys='^a')
# 1 + 2 = 3
win.type_keys(keys='1 {+} 2 = 3', with_spaces=True)
语法:
items():返回对话框的菜单项,如果没有,返回空列表;item_by_index():返回指定的菜单项;item_by_path(path, exact=False):查找路径上指定的菜单项;menu_select():用户查找指定路径的菜单项;参数解释:
示例 1 返回所有菜单项:
# 使用 Sublime 练习
# 1. 连接应用程序
app = Application(backend='uia').connect(process=17424)
# 2. 定位窗口
win = app.window(title_re='.*Sublime Text.*')
win.wait('visible')
# 3. 定位菜单栏
menu_bar = win.child_window(title="应用程序", auto_id="MenuBar", control_type="MenuBar")
# items
print(menu_bar.items())
# print('-----------------------------------------------------')
print(menu_bar.children())
示例 2 返回指定菜单项:
# item_by_index
print(menu_bar.item_by_index(0))
for i in range(0, len(menu_bar.items())):
print(menu_bar.item_by_index(i))
示例 3 查找路径上的菜单项:
# item_by_path
# 1) 先定位 File -> Save
save = menu_bar.item_by_path(path="File -> Save", exact=True)
# 2) 再获取坐标
point = save.rectangle().mid_point()
# 3) 把鼠标指向按钮
mouse.move(coords=(point.x, point.y))
# 1) 定位 File -> Reopen with Encoding
reopen = menu_bar.item_by_path(path="File -> Reopen with Encoding", exact=True)
# 2) 再获取坐标
point = reopen.rectangle().mid_point()
# 3) 把鼠标指向按钮
mouse.move(coords=(point.x, point.y))
示例 4 多级菜单的定位,以 Sublime 为例:
# 使用 Sublime 练习
# 1. 连接应用程序
app = Application(backend='uia').connect(process=17424)
# 2. 定位窗口
win = app.window(title_re='.*Sublime Text.*')
win.wait('visible')
# win.print_control_identifiers()
# 3. 定位菜单栏
menu_bar = win.child_window(title="应用程序", auto_id="MenuBar", control_type="MenuBar")
# 4. 定位多层次的路径
menu_bar.item_by_path(path='File -> Open Recent').click_input()
# 定位到 Open Recent 菜单
open_recent_menu = win.child_window(title="Open Recent", control_type="Menu")
# 定位到 Clear Items
clear_items = open_recent_menu.item_by_path(path='Clear Items')
clear_items.click_input()
示例 5 查找指定路径上的菜单项:
# menu_select
# 找到 Open Recent 并点击
win.menu_select(path='File -> Open Recent')
# 找到 Clear Items
clear_items = win.child_window(title="Clear Items", auto_id="23", control_type="MenuItem")
clear_items.click_input()
menu_select() 和 item_by_path() 的区别:
语法:
get_items():获取列表中的所有项目;item_count():获取列表项数;get_item(row=0):获取列表中的指定项目;示例:
# 1. 连接应用程序
app = Application(backend='uia').connect(process=26880)
# 2. 定位窗口
win = app.window(title_re='sublime_text.*', control_type='Window')
win.wait('visible')
# 3. 定位列表控件
list_ctrl = win.child_window(title='项目视图', control_type='List')
# 4. 获取列表项
print(list_ctrl.children())
print('------------------------------------------------------')
print(list_ctrl.get_items())
# 5. 获取项数
print(list_ctrl.item_count())
print('----------------------------------------------------------')
print(len(list_ctrl.get_items()))
# 6. 获取某个列表项并双击
list_ctrl.get_item(row=0).double_click_input()
1. 如果发送的消息内容都是一样的,怎么验证发送的消息是否成功?
消息都是相同的,这样不好验证;为了方便验证,因此需要保证发送的消息的唯一性;
保证发送消息的唯一性,采用时间戳的方式,在要发送的消息后面拼接上当前时间的时间戳,保证消息的唯一性;
在聊天窗口读到这条消息,就表示发送成功,没读到这条消息就表示发送失败;
2. 发送消息成功后,聊天窗口有可能会增加 1 条消息,也有可能会增加两条消息(一条为时间,另一条为消息本身),该怎么校验?
消息数量校验:发送前消息的数量 + 1 == 发送后消息的数量 或 发送前消息的数量 + 2 == 发送后消息的数量;
消息内容校验:判断最新的消息和先前记录的发送内容是否一致;
""" 微信发消息 """
from datetime import datetime
from pywinauto.application import Application
# 1. 连接应用程序
app = Application(backend='uia').connect(process=2832)
# 2. 定位窗口
win = app.window(title='文件传输助手', control_type='Window')
win.wait('visible')
# 3. 发送微信消息
# 发送之前要记录消息条数
message_list_before = win.child_window(title='消息', control_type='List')
message_count_before = message_list_before.item_count()
# 发送之前要记录消息的内容
message = 'hello, world!' + ' - ' + str(datetime.now())
# 编辑消息
message_edit = win.child_window(title='输入', control_type='Edit')
message_edit.click_input()
message_edit.type_keys(keys=message, with_spaces=True)
# 点击发送按钮
send_btn = win.child_window(title='发送 (S)', control_type='Button')
send_btn.click_input()
# 4. 校验发送的消息
# 发送之后要记录消息的条数
message_list_after = win.child_window(title='消息', control_type='List')
message_count_after = message_list_after.item_count()
# 校验发送消息的条数
# print(message_count_after)
# print(message_count_before)
assert message_count_after == message_count_before + 1 or message_count_after == message_count_before + 2
# 发送之后要记录最新消息的内容
message_get = message_list_after.get_item(row=message_count_after - 1).window_text()
# 检验最新消息的内容是否正确
# print(message)
# print(message_get)
assert message_get == message

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online