# -*- coding: utf-8 -*-
"""
pywinauto Windows 应用自动化教程
=================================
pywinauto 是一个用于 Windows GUI 自动化的 Python 库,类似于 Selenium,但专门用于桌面应用。
核心概念:
1. Application - 应用程序对象,用于启动或连接到应用
2. Window - 窗口对象,代表一个窗口或对话框
3. Control - 控件对象,代表窗口中的按钮、输入框等元素
4. Backend - 后端引擎,有两种:
- "win32" - 旧的 Win32 API,兼容性好但功能有限
- "uia" - UI Automation,现代化,推荐使用(支持 WPF、Electron 等)
常用方法:
- start() - 启动应用
- connect() - 连接到已运行的应用
- window() - 获取窗口
- child_window() - 查找子控件
- click() - 点击
- type_keys() - 输入文本
- set_focus() - 设置焦点
查找控件的方式:
1. 通过标题: title="登录"
2. 通过类名: class_name="Button"
3. 通过控件类型: control_type="Edit"
4. 通过 AutomationId: auto_id="username"
5. 通过索引: found_index=0
"""
import time
from pathlib import Path
from pywinauto import Application
from pywinauto.keyboard import send_keys # 用于模拟键盘输入
class WindowsAutomator:
"""
Windows 应用自动化类
这个类封装了 pywinauto 的常用操作,提供类似 Selenium 的 API
"""
def __init__(self, app_path):
"""
初始化自动化器
参数:
app_path: 应用程序的完整路径,例如 "D:\\App\\app.exe"
"""
self.app_path = app_path
self.app = None # Application 对象
self.main_window = None # 主窗口对象
self.screenshots_dir = Path("screenshots")
self.screenshots_dir.mkdir(exist_ok=True)
def start_app(self, timeout=10):
"""
启动应用程序
参数:
timeout: 启动超时时间(秒)
说明:
- backend="uia" 使用 UI Automation 后端,适合现代应用(Electron、WPF 等)
- backend="win32" 使用 Win32 API,适合老旧应用
- 如果一个不行,可以尝试另一个
"""
print("启动应用...")
# 创建 Application 对象并启动应用
# backend="uia" 是推荐的后端,支持更多现代应用
self.app = Application(backend="uia").start(self.app_path, timeout=timeout)
time.sleep(3) # 等待应用完全启动
print("应用已启动")
def connect_to_app(self, title=None, timeout=10):
"""
连接到已经运行的应用
参数:
title: 窗口标题(可选)
timeout: 连接超时时间(秒)
使用场景:
- 应用已经在运行
- 需要连接到多个应用实例中的某一个
示例:
automator.connect_to_app(title="我的应用")
"""
print("连接到应用...")
if title:
# 通过窗口标题连接
self.app = Application(backend="uia").connect(title=title, timeout=timeout)
else:
# 通过可执行文件路径连接
self.app = Application(backend="uia").connect(path=self.app_path, timeout=timeout)
print("已连接到应用")
def get_window(self, title_re=None, class_name=None, timeout=10):
"""
获取窗口对象
参数:
title_re: 窗口标题的正则表达式,例如 ".*登录.*" 匹配包含"登录"的标题
class_name: 窗口类名
timeout: 查找超时时间(秒)
返回:
窗口对象
说明:
- 如果不指定参数,返回顶层窗口(通常是主窗口)
- title_re 支持正则表达式,更灵活
- 窗口必须可见才能被找到
示例:
# 查找标题包含"登录"的窗口
window = automator.get_window(title_re=".*登录.*")
# 获取主窗口
window = automator.get_window()
"""
try:
if title_re:
# 通过标题正则表达式查找窗口
self.main_window = self.app.window(title_re=title_re, timeout=timeout)
elif class_name:
# 通过类名查找窗口
self.main_window = self.app.window(class_name=class_name, timeout=timeout)
else:
# 获取顶层窗口(主窗口)
self.main_window = self.app.top_window()
# 等待窗口变为可见状态
self.main_window.wait('visible', timeout=timeout)
# 打印窗口信息(用于调试)
print(f"窗口标题: {self.main_window.window_text()}")
print(f"窗口类名: {self.main_window.class_name()}")
return self.main_window
except Exception as e:
print(f"获取窗口失败: {e}")
# 如果找不到窗口,尝试列出所有窗口帮助调试
print("\n尝试列出所有顶层窗口:")
try:
windows = self.app.windows()
for i, win in enumerate(windows):
print(f" [{i}] {win.window_text()} - {win.class_name()}")
if windows:
# 使用第一个窗口作为备选
self.main_window = windows[0]
print(f"\n使用第一个窗口: {self.main_window.window_text()}")
return self.main_window
except Exception as e2:
print(f"列出窗口也失败: {e2}")
raise Exception("无法获取窗口")
def print_controls(self):
"""
打印窗口的控件树(用于调试)
说明:
这是最重要的调试工具!
会打印出窗口中所有控件的层级结构和属性,包括:
- 控件类型(Button、Edit、Text 等)
- 控件标题/文本
- AutomationId(如果有)
- 类名
使用方法:
1. 运行这个方法
2. 查看输出,找到你想操作的控件
3. 记下控件的标识信息(control_type、title、auto_id 等)
4. 使用这些信息来定位控件
示例输出:
Button - '登录' (L123, T456, R789, B012)
| Edit - '' (L123, T456, R789, B012)
| | ['Edit', 'Edit0', 'Edit1']
| | child_window(title="", auto_id="username", control_type="Edit")
"""
print("\n窗口控件树:")
print("-" * 50)
self.main_window.print_control_identifiers()
print("-" * 50)
def take_screenshot(self, name="screenshot"):
"""
截取窗口截图
参数:
name: 截图文件名(不含扩展名)
返回:
截图文件路径
说明:
- 优先使用 pywinauto 的截图功能
- 如果失败,使用 PIL 的备用方案
- 截图保存在 screenshots 目录
"""
screenshot_path = self.screenshots_dir / f"{name}.png"
# 方式1: 使用 pywinauto 的截图功能
try:
img = self.main_window.capture_as_image()
if img:
img.save(screenshot_path)
print(f"截图已保存: {screenshot_path}")
return screenshot_path
except Exception as e:
print(f"pywinauto 截图失败: {e}")
# 方式2: 使用 PIL 直接截图(备用方案)
try:
from PIL import ImageGrab
import win32gui
# 获取窗口句柄和位置
hwnd = self.main_window.handle
rect = win32gui.GetWindowRect(hwnd) # (left, top, right, bottom)
# 截取指定区域
img = ImageGrab.grab(rect)
img.save(screenshot_path)
print(f"截图已保存(备用方案): {screenshot_path}")
return screenshot_path
except Exception as e:
print(f"备用截图也失败: {e}")
return None
def find_element(self, **kwargs):
"""
查找子控件
参数:
**kwargs: 控件属性,支持以下参数:
- title: 控件标题/文本
- auto_id: AutomationId
- control_type: 控件类型(Button、Edit、Text、ComboBox 等)
- class_name: 类名
- found_index: 索引(当有多个匹配时)
返回:
控件对象
常用控件类型:
- Button: 按钮
- Edit: 文本输入框
- Text: 静态文本
- ComboBox: 下拉框
- CheckBox: 复选框
- RadioButton: 单选按钮
- ListBox: 列表框
- TreeView: 树形控件
- TabControl: 选项卡
示例:
# 查找第一个输入框
input1 = automator.find_element(control_type="Edit", found_index=0)
# 查找标题为"登录"的按钮
login_btn = automator.find_element(title="登录", control_type="Button")
# 通过 AutomationId 查找
username_input = automator.find_element(auto_id="username")
"""
return self.main_window.child_window(**kwargs)
def input_text(self, element_kwargs, text):
"""
在输入框中输入文本
参数:
element_kwargs: 查找控件的参数(字典)
text: 要输入的文本
说明:
- 会先设置焦点到控件
- 使用 set_edit_text 直接设置文本(快速)
- 也可以使用 type_keys 模拟键盘输入(慢但更真实)
示例:
# 在第一个输入框输入用户名
automator.input_text({"control_type": "Edit", "found_index": 0}, "admin")
"""
element = self.find_element(**element_kwargs)
element.set_focus() # 设置焦点
element.set_edit_text(text) # 设置文本
print(f"已输入文本: {text}")
def click_button(self, element_kwargs):
"""
点击按钮
参数:
element_kwargs: 查找控件的参数(字典)
示例:
# 点击"登录"按钮
automator.click_button({"title": "登录", "control_type": "Button"})
"""
element = self.find_element(**element_kwargs)
element.click()
print(f"已点击按钮")
def test_login(self, username, password):
"""
测试登录功能
这个方法演示了如何使用 pywinauto 进行实际的自动化测试
步骤:
1. 查找登录窗口
2. 打印控件树(调试用)
3. 截图记录初始状态
4. 输入用户名(尝试多种方式)
5. 切换到密码框
6. 输入密码
7. 点击登录按钮
8. 截图记录最终状态
"""
print("\n开始登录测试...")
# ========== 步骤1: 查找登录窗口 ==========
try:
print("正在查找登录窗口...")
# 使用正则表达式查找标题包含"登录"的窗口
# ".*登录.*" 表示匹配任何包含"登录"的字符串
self.get_window(title_re=".*登录.*", timeout=10)
except:
print("未找到包含'登录'的窗口,尝试获取主窗口...")
# 如果找不到,就使用主窗口
self.get_window()
time.sleep(1) # 等待窗口稳定
# ========== 步骤2: 打印控件树(重要!) ==========
# 这一步会显示窗口中所有控件的信息
# 通过这些信息,你可以知道如何定位每个控件
try:
self.print_controls()
except Exception as e:
print(f"打印控件树失败: {e}")
# ========== 步骤3: 截图记录初始状态 ==========
self.take_screenshot("01_login_window")
# ========== 步骤4: 输入用户名 ==========
print("\n尝试输入用户名...")
username_success = False
# 方式1: 通过 descendants 获取所有 Edit 控件,然后选择第一个
# descendants() 会递归查找所有子控件
try:
# 查找所有类型为 "Edit" 的控件(输入框)
edit_controls = self.main_window.descendants(control_type="Edit")
print(f"找到 {len(edit_controls)} 个输入框")
if len(edit_controls) >= 1:
# 使用第一个输入框(通常是用户名框)
edit_controls[0].set_focus() # 设置焦点
# type_keys 模拟键盘输入,with_spaces=True 允许输入空格
edit_controls[0].type_keys(username, with_spaces=True)
print(f"✓ 已输入用户名: {username}")
username_success = True
except Exception as e:
print(f"方式1失败: {e}")
# 方式2: 如果方式1失败,直接使用键盘输入
# send_keys 是全局键盘输入,会发送到当前焦点的控件
if not username_success:
try:
print("使用键盘输入用户名")
send_keys(username) # 直接输入文本
username_success = True
except Exception as e:
print(f"方式2失败: {e}")
time.sleep(0.5)
self.take_screenshot("02_username_entered")
# ========== 步骤5: 切换到密码框 ==========
print("\n切换到密码框...")
# {TAB} 表示按 Tab 键,用于在控件间切换焦点
# 其他特殊键: {ENTER}, {ESC}, {BACKSPACE}, {DELETE} 等
send_keys("{TAB}")
time.sleep(0.5)
# ========== 步骤6: 输入密码 ==========
print("尝试输入密码...")
password_success = False
# 方式1: 使用第二个输入框
try:
edit_controls = self.main_window.descendants(control_type="Edit")
if len(edit_controls) >= 2:
# 使用第二个输入框(通常是密码框)
edit_controls[1].set_focus()
edit_controls[1].type_keys(password, with_spaces=True)
print("✓ 已输入密码")
password_success = True
except Exception as e:
print(f"方式1失败: {e}")
# 方式2: 键盘输入
if not password_success:
try:
print("使用键盘输入密码")
send_keys(password)
password_success = True
except Exception as e:
print(f"方式2失败: {e}")
time.sleep(0.5)
self.take_screenshot("03_password_entered")
# ========== 步骤7: 点击登录按钮 ==========
print("\n尝试点击登录按钮...")
login_success = False
# 方式1: 查找所有按钮,找到包含"登录"文字的按钮
try:
# 获取所有按钮控件
buttons = self.main_window.descendants(control_type="Button")
print(f"找到 {len(buttons)} 个按钮")
for btn in buttons:
# 获取按钮的文本
btn_text = btn.window_text()
print(f" 找到按钮: {btn_text}")
# 检查按钮文本是否包含"登录"
if "登录" in btn_text or "Login" in btn_text:
btn.click() # 点击按钮
print(f"✓ 已点击按钮: {btn_text}")
login_success = True
break
except Exception as e:
print(f"方式1失败: {e}")
# 方式2: 如果找不到按钮,直接按回车键
# 很多登录界面支持按回车提交
if not login_success:
try:
print("使用回车键登录")
send_keys("{ENTER}") # 按回车键
login_success = True
except Exception as e:
print(f"方式2失败: {e}")
print("已提交登录")
time.sleep(3) # 等待登录完成
# ========== 步骤8: 登录后截图 ==========
self.take_screenshot("04_after_login")
print("\n✓ 登录测试完成")
def close_app(self):
"""
关闭应用
说明:
- 使用 kill() 强制终止应用进程
- 如果需要优雅关闭,可以使用窗口的 close() 方法
"""
if self.app:
try:
self.app.kill() # 强制终止
print("应用已关闭")
except:
pass
def main():
"""
主函数 - 程序入口
这里演示了完整的自动化测试流程
"""
# ========== 配置 ==========
app_path = r"D:\***\***.exe" # 应用程序路径
username = "********" # 用户名
password = "******" # 密码
# ========== 创建自动化器 ==========
automator = WindowsAutomator(app_path)
try:
# ========== 启动应用 ==========
automator.start_app()
# ========== 执行登录测试 ==========
automator.test_login(username, password)
# ========== 等待用户查看结果 ==========
input("\n按回车键关闭应用...")
except Exception as e:
print(f"错误: {e}")
import traceback
traceback.print_exc()
finally:
# ========== 清理:关闭应用 ==========
automator.close_app()
if __name__ == "__main__":
"""
程序入口点
学习建议:
1. 先运行这个脚本,查看控件树输出
2. 根据控件树的信息,调整 test_login 方法中的控件定位
3. 查看 screenshots 目录中的截图,验证每一步是否正确
4. 如果遇到问题,尝试:
- 切换 backend("uia" <-> "win32")
- 增加等待时间
- 使用不同的控件定位方式
常见问题:
Q: 找不到控件?
A: 运行 print_controls() 查看控件树,确认控件的正确标识
Q: 操作太快导致失败?
A: 增加 time.sleep() 的等待时间
Q: Electron 应用控件识别不到?
A: 确保使用 backend="uia",并等待窗口完全加载
更多学习资源:
- pywinauto 官方文档: https://pywinauto.readthedocs.io/
- 控件类型参考: https://docs.microsoft.com/en-us/windows/win32/winauto/uiauto-controlpatterns
"""
print("=" * 60)
print("pywinauto Windows 自动化教程")
print("=" * 60)
print("\n提示:")
print("1. 这个脚本会启动应用并尝试自动登录")
print("2. 请仔细查看控件树输出,了解如何定位控件")
print("3. 截图会保存在 screenshots 目录")
print("4. 如果登录失败,根据控件树调整代码")
print("-" * 60)
main()