跳到主要内容Pywinauto:Windows 桌面应用 Python 自动化教程 | 极客日志PythonWeChat
Pywinauto:Windows 桌面应用 Python 自动化教程
综述由AI生成Pywinauto 这一用于 Windows 桌面应用自动化的 Python 库。内容包括安装步骤、基础操作(启动/连接应用、窗口控制、控件定位)、交互操作(鼠标/键盘、菜单/列表处理)及等待机制。文中通过微信发消息的实际案例,演示了如何连接应用、定位窗口、发送消息并校验结果,展示了利用 Pywinauto 实现 GUI 自动化测试的完整流程。
日志猎手31 浏览 前言
Pywinauto 是一个用于自动化 Windows 图形用户界面 (GUI) 的 Python 库。文章介绍了 Pywinauto 的核心功能,包括跨 GUI 框架支持、简单易用的 API 和录制回放功能。详细讲解了安装步骤和常见操作,如启动/连接应用程序、定位窗口和控件、窗口操作、等待机制、控件交互、鼠标键盘操作以及菜单和列表控件处理。最后以微信发消息为例,演示了完整的自动化测试场景实现。Pywinauto 通过模拟用户操作实现 Windows 应用程序自动化测试,大幅提升 GUI 测试效率。
一、Pywinauto 介绍
1. Pywinauto 是什么?
Pywinauto 是一个用于自动化 Windows 图形用户界面(GUI)的 Python 库。它允许你通过编程方式控制 Windows 应用程序,模拟用户操作(如点击按钮、输入文本、选择菜单等)。
想象你有一个机器人助手,可以代替你操作电脑上的任何软件——点击按钮、输入文字、选择菜单,就像真人操作一样。Pywinauto 就是这样一个能让 Python 控制 Windows 应用程序的'机器人控制器'。它的核心思想就是用代码代替鼠标和键盘。
2. Pywinauto 的主要特点
1. 跨 GUI 框架支持:
- Win32 API (backend='win32'):适用于 MFC、VB6、WinForms 等。
- MS UI Automation (backend='uia'):适用于 WPF、Qt、Modern UI(UWP)等。
2. 简单易用的 API:
- 支持通过窗口标题、类名、控件类型等属性定位元素。
- 提供类似自然语言的链式调用(如 window.Edit.type_keys('hello, world'))。
3. 录制和回放功能:
可以使用 inspect.exe 或者 UISpy.exe 辅助识别控件。
二、使用步骤
1. 安装 Pywinauto
使用如下指令在命令行窗口进行 pywinauto 的安装和检查:
pip install pywinauto
pip list
安装完成后,使用 pip list 指令,打印相关信息,表示安装成功。
2. Pywinauto 的常见操作
1. 打开应用程序
语法:
app = Application(backend='uia').start(cmd_line...)
参数解释:
- cmd_line:启动应用程序的命令行字符串,必须包含应用程序的路径和名称;路径可以是相对路径,也可以是绝对路径;
- 例如:启动记事本,可以写 "notepad.exe",也可以写 "C:\Windows\notepad.exe";
示例:
app = Application(backend='uia').start('notepad.exe')
2. 连接已打开的应用程序
语法:
app = Application(backend='uia').connect(process=12345)
app = Application(backend='uia').connect(handle=66666)
- process:目标的进程 ID,可通过进程 ID 连接应用程序;
- handle:目标的窗口句柄;
app = Application(backend='uia').connect(process=27076)
app = Application(backend='uia').connect(handle=66666)
3. 定位窗口
可以结合 print_control_identifiers() 方法,根据打印的控件信息,协助定位窗口;
win = app.window(title='...')
win = app.best_match (不推荐)
- title:文本为指定的元素;
- title_re:文本匹配指定正则表达式的元素;
- best_match:标题与指定值相似的元素;
- class_name:窗口类为指定值的元素;
- class_name_re:类名匹配正则表达式的元素;
notepad1 = app.window(title='Hello,Pywinauto! - Notepad')
notepad2 = app.window(title_re='.*Notepad')
notepad3 = app.window(best_match='Hello,Pywinauto! - Notepad')
notepad4 = app.window(class_name='Notepad')
notepad5 = app.window(class_name_re='.*Notepad')
注意:通过 best_match 的方法定位窗口是不推荐的,因为控件的名称中间有可能会有空格,逗号或者其它的特殊符号,使用上述语法会出现报错,因此不推荐。
4. 窗口操作
- close():关闭窗口;
- maximize():将窗口最大化;
- minimize():将窗口最小化;
- restore():窗口恢复正常大小;
- get_show_state():获取窗口的显示状态,返回 0 表示正常,1 表示最大化,2 表示最小化;
- is_dialog():检查控件是否是顶级窗口;
- is_maximized():检查窗口是否处于最大化状态;
- is_minimized():检查窗口是否处于最小化状态;
- is_normal():检查窗口是否处于正常状态;
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()
5. 定位控件
- 可以使用 print_control_identifiers() 方法,打印窗口及其子空间的标识符信息;
- 使用 UISpy 工具,定位控件;
- 结合 UISpy 的控件的标识符信息,在打印的信息中搜索,完成对控件的准确定位;
| 分类 | 控件名称 | 说明 |
|---|
| 窗口与对话框 | 对话框(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', ...)
- title:控件名称;
- auto_id:自动化 id;
- control_type:控件类型;
- found_index:元素索引;
app = Application(backend='uia').connect(title_re='.*Notepad')
notepad = app.window(title_re='.*Notepad')
minimize_btn_1 = notepad['最小化']
minimize_btn_2 = notepad.child_window(
auto_id='Minimize', control_type='Button'
)
- 定位控件可能会定位到多个控件;
- 如果定位到多个控件,可以使用参数 found_index,指本次定位的是第几个控件;
- found_index 从 0 开始计数;
6. 等待
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:表示窗口的状态:
- timeout:表示超时
- retry_interval:表示重试时间间隔,单位为 s;
- func:执行的函数;
- value:比较的值;
- exists:表示窗口是一个有效的句柄;
- visible:表示窗口不隐藏,可以看到;
- enable:表示窗口未被禁用,可以操作;
- ready:表示窗口可见且已启用;
- active:表示窗口处于活动状态;
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')
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()
num_btn = win.child_window(title="一", auto_id="num1Button", control_type="Button")
num_btn.wait('ready')
num_btn.click_input()
win.set_focus()
win.wait('active')
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)
7. 控件的操作
btn.click_input():单击控件
btn.right_click_input():右击控件
btn.double_click_input():双击控件
title_bar = win.child_window(title="控件信息 - OneNote", control_type="TitleBar")
title_bar.double_click_input()
texts():获取窗口或者控件所有文本内容,返回列表,每个元素是一个字符串,表示一个文本片段;
window_text():表示窗口或控件的主要显示的文本内容;
app = Application(backend='uia').connect(process=27944)
win = app.window(title_re='.*OneNote.*')
win.wait('visible')
funcs = win.child_window(title="功能区选项卡", control_type="Tab")
print(funcs.texts())
print(funcs.window_text())
8. 鼠标操作
当需要灵活的鼠标交互时,控件的点击操作就无法满足需求,因此 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)):释放鼠标按钮;
- coords:表示鼠标要操作的坐标;
- wheel_dist:表示滚动的距离,大于 0 是向上滚动,小于 0 是向下滚动;
calc = Application(backend='uia').connect(process=15652)
win = calc.window(title='计算器')
win.wait('visible')
nums_pad = win.child_window(title='数字键盘', auto_id='NumberPad')
for key in nums_pad.children():
mid_point = key.rectangle().mid_point()
mouse.click(coords=(mid_point.x, mid_point.y))
time.sleep(1)
9. 键盘操作
Pywinauto 提供了强大的键盘操作功能,keyboard 模块是核心组件之一;
send_keys('...'):在焦点窗口输入文本;
type_keys(keys, pause=None, with_spaces=False, with_newlines=False):在控件的编辑区输入文本;
- keys:要输入的键序列;
- pause:每次按键后的延迟时间;
- with_spaces:True 表示为在输入的字符串中保留空格;
- with_newlines:True 表示为在输入的字符串中保留换行符;
send_keys() 可以将案件序列发送到具有焦点的窗口,但是实际应用中往往需要在控件中输入,而不依赖窗口的焦点状态,因此 type_keys() 应用更广泛;
from pywinauto.keyboard import send_keys
time.sleep(1)
send_keys('1234567')
app = Application(backend='uia').connect(process=14356)
win = app.window(title_re='.*OneNote.*')
win.wait('visible')
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 键。
使用 {} 包裹特殊字符(如 {+},{%},{^}),以避免被识别为修饰符。
win.type_keys(keys='abcd{ENTER}efg')
win.type_keys(keys='abcd{ENTER 10}efg')
win.type_keys(keys='静夜思{TAB}李白')
win.type_keys(keys='静夜思{TAB 10}李白')
win.type_keys(keys='{BACKSPACE}')
win.type_keys(keys='{BACKSPACE 100}')
win.type_keys(keys='+2')
win.type_keys(keys='^a')
win.type_keys(keys='1 {+} 2 = 3', with_spaces=True)
10. 菜单控件的操作
items():返回对话框的菜单项,如果没有,返回空列表;
item_by_index():返回指定的菜单项;
item_by_path(path, exact=False):查找路径上指定的菜单项;
menu_select():用户查找指定路径的菜单项;
- path:用户指定要选择的菜单项路径;路径可以是 "MenuItem -> MenuItem -> MenuItem..." 形式的字符串,每个 MenuItem 是菜单该级别的项目文本,例如:File->Save,空格不重要,可以写也可以省略;
- exact:设置为 True,表示要求菜单项名称与路径中的名称完全匹配;如果为 False,则允许模糊匹配;
app = Application(backend='uia').connect(process=17424)
win = app.window(title_re='.*Sublime Text.*')
win.wait('visible')
menu_bar = win.child_window(title="应用程序", auto_id="MenuBar", control_type="MenuBar")
print(menu_bar.items())
print(menu_bar.children())
print(menu_bar.item_by_index(0))
for i in range(0, len(menu_bar.items())):
print(menu_bar.item_by_index(i))
save = menu_bar.item_by_path(path="File -> Save", exact=True)
point = save.rectangle().mid_point()
mouse.move(coords=(point.x, point.y))
reopen = menu_bar.item_by_path(path="File -> Reopen with Encoding", exact=True)
point = reopen.rectangle().mid_point()
mouse.move(coords=(point.x, point.y))
示例 4 多级菜单的定位,以 Sublime 为例:
app = Application(backend='uia').connect(process=17424)
win = app.window(title_re='.*Sublime Text.*')
win.wait('visible')
menu_bar = win.child_window(title="应用程序", auto_id="MenuBar", control_type="MenuBar")
menu_bar.item_by_path(path='File -> Open Recent').click_input()
open_recent_menu = win.child_window(title="Open Recent", control_type="Menu")
clear_items = open_recent_menu.item_by_path(path='Clear Items')
clear_items.click_input()
win.menu_select(path='File -> Open Recent')
clear_items = win.child_window(title="Clear Items", auto_id="23", control_type="MenuItem")
clear_items.click_input()
menu_select() 和 item_by_path() 的区别:
- 在使用 menu_select() 的场景下,至少需要两个菜单栏:"系统" 和 "应用程序";
- 系统菜单栏是一个标准的窗口菜单,包含:还原,移动,最大化,最小化等,通常有一个标题栏作为父级;
- 应用程序菜单栏:通常是我们要找的,通常父级是对话框本身,可以在对话框直接子集中找到;
11. 列表控件的操作
get_items():获取列表中的所有项目;
item_count():获取列表项数;
get_item(row=0):获取列表中的指定项目;
app = Application(backend='uia').connect(process=26880)
win = app.window(title_re='sublime_text.*', control_type='Window')
win.wait('visible')
list_ctrl = win.child_window(title='项目视图', control_type='List')
print(list_ctrl.children())
print('------------------------------------------------------')
print(list_ctrl.get_items())
print(list_ctrl.item_count())
print('----------------------------------------------------------')
print(len(list_ctrl.get_items()))
list_ctrl.get_item(row=0).double_click_input()
三、自动化场景示例 - 微信发消息
1. 实现思路
- 连接应用程序,定位聊天窗口;
- 发送消息之前记录当前消息的条数;
- 记录要发送的消息内容,并发送消息;
- 发送消息后记录当前消息的条数;
- 验证最后一条消息的内容是否为之前记录的消息内容;
2. 注意事项
1. 如果发送的消息内容都是一样的,怎么验证发送的消息是否成功?
消息都是相同的,这样不好验证;为了方便验证,因此需要保证发送的消息的唯一性;
保证发送消息的唯一性,采用时间戳的方式,在要发送的消息后面拼接上当前时间的时间戳,保证消息的唯一性;
在聊天窗口读到这条消息,就表示发送成功,没读到这条消息就表示发送失败;
2. 发送消息成功后,聊天窗口有可能会增加 1 条消息,也有可能会增加两条消息(一条为时间,另一条为消息本身),该怎么校验?
消息数量校验:发送前消息的数量 + 1 == 发送后消息的数量 或 发送前消息的数量 + 2 == 发送后消息的数量;
消息内容校验:判断最新的消息和先前记录的发送内容是否一致;
3. 代码实现
""" 微信发消息 """
from datetime import datetime
from pywinauto.application import Application
app = Application(backend='uia').connect(process=2832)
win = app.window(title='文件传输助手', control_type='Window')
win.wait('visible')
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()
message_list_after = win.child_window(title='消息', control_type='List')
message_count_after = message_list_after.item_count()
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()
assert message_get == message
相关免费在线工具
- curl 转代码
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online