Pywinauto:Windows 桌面应用的 Python 自动化神器
文章目录
前言
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
使用如下指令在 pycharm 的命令行窗口进行 pywinauto 的安装和检查:
# 安装
pip install pywinauto
# 检查是否安装成功
pip list
安装完成后,使用 pip list 指令,打印如下信息,表示安装成功:

2. Pywinauto 的常见操作
1. 打开应用程序
语法:
- app = Application(backend='uia').start(self, 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 = Appllication(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_identifires() 方法,根据打印的控件信息,协助定位窗口;
语法:
- win = app.window(title='...')
- win = app.best_match名称
window() 方法参数解释:
- title:文本为指定的元素;
- title_re:文本匹配指定正则表达式的元素;
- best_match:标题与指定值相似的元素;
- class_name:窗口类为指定值的元素;
- class_name_re:类名匹配正则表达式的元素;
示例:
# 精确匹配 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 的方法定位窗口是不推荐的,因为控件的名称中间有可能会有空格,逗号或者其它的特殊符号,使用上述语法会出现报错,因此不推荐。
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') # 使用 best_match 定位最小化按钮 minimize_btn_1 = notepad['最小化'] # 使用 child_window() 定位最小化按钮 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:比较的值;
wait_for 状态:
- exists:表示窗口是一个有效的句柄;
- visible:表示窗口不隐藏,可以看到;
- enable:表示窗口未被禁用,可以操作;
- ready:表示窗口可见且已启用;
- active:表示窗口处于活动状态;
示例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)7. 控件的操作
点击操作语法(以按钮为例):
- 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())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 是向下滚动;
示例:
# 使用灵活的鼠标交互方式, 实现点击计算器的数字键 # 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)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') # 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)10. 菜单控件的操作
语法:
- items():返回对话框的菜单项,如果没有,返回空列表;
- item_by_index():返回指定的菜单项;
- item_by_path(path, exact=False):查找路径上指定的菜单项;
- menu_select():用户查找指定路径的菜单项;
参数解释:
- path:用户指定要选择的菜单项路径;路径可以是 "MenuItem -> MenuItem -> MenuItem..." 形式的字符串,每个 MenuItem 是菜单该级别的项目文本,例如:File->Save,空格不重要,可以写也可以省略;
- exact:设置为 True,表示要求菜单项名称与路径中的名称完全匹配;如果为 False,则允许模糊匹配;
示例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() 的区别:
- 在使用 menu_select() 的场景下,至少需要两个菜单栏:"系统" 和 "应用程序";
- 系统菜单栏是一个标准的窗口菜单,包含:还原,移动,最大化,最小化等,通常有一个标题栏作为父级;
- 应用程序菜单栏:通常是我们要找的,通常父级是对话框本身,可以在对话框直接子集中找到;
11. 列表控件的操作
语法:
- 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. 实现思路
1. 连接应用程序,定位聊天窗口;
2. 发送消息之前记录当前消息的条数;
3. 记录要发送的消息内容,并发送消息;
4. 发送消息后记录当前消息的条数;
5. 验证最后一条消息的内容是否为之前记录的消息内容;
2. 注意事项
1. 如果发送的消息内容都是一样的,怎么验证发送的消息是否成功?
消息都是相同的,这样不好验证;为了方便验证,因此需要保证发送的消息的唯一性;
保证发送消息的唯一性,采用时间戳的方式,在要发送的消息后面拼接上当前时间的时间戳,保证消息的唯一性;
在聊天窗口读到这条消息,就表示发送成功,没读到这条消息就表示发送失败;
2. 发送消息成功后,聊天窗口有可能会增加1条消息,也有可能会增加两条消息(一条为时间,另一条为消息本身),该怎么校验?
消息数量校验:发送前消息的数量 + 1 == 发送后消息的数量 或 发送前消息的数量 + 2 == 发送后消息的数量;
消息内容校验:判断最新的消息和先前记录的发送内容是否一致;
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