uiautomation` 库的高级教程
Microsoft UI Automation (UIA) 是 Windows 平台上的一种辅助功能框架,它允许应用程序(包括自动化脚本)以编程方式访问、识别和操作另一个应用程序的用户界面 (UI) 元素。模式是 UIA 的核心概念,它将控件的功能标准化。例如,无论一个按钮长什么样,只要它支持,你就可以调用Invoke()方法来“点击”它。是一个强大的 Python 库,适用于 Windows 桌
uiautomation 是一个用于 Windows GUI 自动化的 Python 库,它封装了 Microsoft UI Automation API,使得我们可以通过编程方式查找和操作 Windows 应用程序的控件(如按钮、文本框、菜单等)。
教程大纲:
- 简介与安装
- 什么是 UIAutomation?
- 为什么使用
uiautomation? - 安装
uiautomation
- 基础概念
- 控件 (Control) 与窗口 (Window)
- 控件树 (Control Tree)
- 定位控件:核心方法
- 基本操作
- 启动和附加到应用程序
- 查找控件 (按名称、类名、AutomationId 等)
- 控件交互 (点击、输入文本、获取文本等)
- 等待机制
- 高级控件定位
- 使用
searchDepth控制搜索深度 - 使用正则表达式
RegexName - 组合条件搜索
- 遍历控件树 (父、子、兄弟节点)
- 查找所有匹配的控件
- 使用
- 控件模式 (Control Patterns)
- 什么是控件模式?
- 常用模式:
ValuePattern(读写值)InvokePattern(执行操作,如按钮点击)TogglePattern(切换状态,如复选框)ExpandCollapsePattern(展开折叠,如树视图)SelectionItemPattern和SelectionPattern(选择项)ScrollPattern(滚动)TextPattern(高级文本操作)
- 如何检查和使用模式
- 高级技巧与实践
- 处理动态变化的控件
- 错误处理与日志记录
- 使用 Inspect.exe 或 UISpy.exe 辅助定位
uiautomation库自带的控件检查工具- 与键盘和鼠标的底层交互 (可选,
uiautomation自带方法通常足够) - 多线程/异步操作注意事项 (简单提及)
- 实战案例
- 自动化记事本
- 自动化计算器 (新版 Windows 计算器可能较复杂,可换用经典版或部分功能)
- 调试与最佳实践
- 调试技巧
- 提高脚本稳定性和可维护性的建议
- 总结与资源
1. 简介与安装
什么是 UIAutomation?
Microsoft UI Automation (UIA) 是 Windows 平台上的一种辅助功能框架,它允许应用程序(包括自动化脚本)以编程方式访问、识别和操作另一个应用程序的用户界面 (UI) 元素。
为什么使用 uiautomation?
- 原生 Windows 支持:直接利用 Windows 底层 API,对大多数标准 Windows 应用兼容性好。
- 无需应用源码或API:即使应用没有提供专门的自动化接口,只要其 UI 元素符合 UIA规范,就可以被操作。
- Python 封装:
uiautomation库提供了简洁易用的 Python 接口,大大降低了使用 UIA 的门槛。 - 替代方案:当 Selenium (Web)、Appium (Mobile/Desktop) 或其他特定框架不适用时,
uiautomation是一个强大的选择,尤其适合传统桌面应用。
安装 uiautomation
使用 pip 进行安装:
pip install uiautomation
建议在一个虚拟环境中安装。
2. 基础概念
控件 (Control) 与窗口 (Window)
- 控件 (Control):UI 上的基本元素,如按钮 (Button)、文本框 (Edit)、标签 (Text)、列表框 (ListBox)、菜单项 (MenuItem) 等。
- 窗口 (Window):通常指应用程序的主窗口,但对话框、弹出菜单等也是一种特殊的窗口。在
uiautomation中,窗口本身也是一个控件,通常是其他控件的根容器。
控件树 (Control Tree)
Windows 应用程序的 UI 元素以层级树状结构组织。顶层通常是桌面 (Desktop),然后是应用程序窗口,窗口内包含面板、工具栏,再往下是具体的按钮、文本框等。uiautomation 通过遍历这个树来查找控件。
定位控件:核心方法
uiautomation 主要通过指定控件的属性来查找它们。常用的属性有:
Name: 控件的文本标签或名称。AutomationId: 由开发者为控件设置的唯一标识符,最稳定可靠。ClassName: 控件的窗口类名 (Windows Class Name)。ControlType: 控件的类型,如ControlType.ButtonControl,ControlType.EditControl。
3. 基本操作
import uiautomation as auto
import time
import subprocess
# 设置全局搜索超时时间 (秒)
auto.uiautomation.SetGlobalSearchTimeout(10) # 默认是10秒
# 打印控件的详细信息,方便调试
def print_control_info(control):
if not control:
print("控件未找到")
return
print(f"控件名称: {control.Name}")
print(f"AutomationId: {control.AutomationId}")
print(f"类名: {control.ClassName}")
print(f"控件类型: {control.ControlTypeName}")
print(f"是否可见: {control.IsVisible()}")
print(f"是否启用: {control.IsEnabled()}")
print("-" * 20)
# --- 启动和附加到应用程序 ---
# 示例1: 启动记事本
subprocess.Popen('notepad.exe')
time.sleep(1) # 等待程序启动
# 附加到已运行的记事本窗口
# searchDepth=1 表示只在桌面的直接子窗口中搜索
# 建议优先使用 Name 和 ClassName 组合,或 AutomationId
notepad_window = auto.WindowControl(searchDepth=1, ClassName="Notepad", Name="无标题 - 记事本")
# 如果记事本已打开文件,Name 会是 "文件名 - 记事本"
# notepad_window = auto.WindowControl(searchDepth=1, ClassName="Notepad", RegexName=".*记事本") # 使用正则匹配标题
if not notepad_window.Exists(maxSearchTime=3):
print("记事本窗口未找到!")
exit()
print("成功附加到记事本窗口:")
print_control_info(notepad_window)
# --- 查找控件 ---
# 记事本的编辑区域通常是 EditControl
edit_area = notepad_window.EditControl() # 查找第一个 EditControl
# 如果有多个同类型控件,需要更精确的定位
# edit_area = notepad_window.EditControl(AutomationId="15") # 假设知道 AutomationId
# edit_area = notepad_window.EditControl(Name="文本编辑器") # 某些版本的记事本可能有Name
if not edit_area.Exists(maxSearchTime=2):
print("编辑区域未找到!")
else:
print("找到编辑区域:")
print_control_info(edit_area)
# --- 控件交互 ---
# 输入文本
edit_area.SendKeys("你好,UIAutomation 世界!{Enter}", interval=0.05) # interval是按键间隔
edit_area.SendKeys("这是第二行。\n", interval=0.05) # \n 也可以换行
# 获取文本 (对于EditControl,更推荐使用ValuePattern)
if edit_area. 패턴_지원_여부(auto.PatternId.ValuePattern): # 检查是否支持ValuePattern
current_text = edit_area.GetValuePattern().Value
print(f"当前文本内容: {current_text}")
else:
print(f"编辑区域的Name属性(可能不全): {edit_area.Name}")
# --- 点击菜单 ---
# 文件菜单
menu_file = notepad_window.MenuItemControl(Name="文件(F)")
if menu_file.Exists(maxSearchTime=2):
print("找到文件菜单")
menu_file.Click() # 点击方式1: 直接调用Click
# menu_file.GetInvokePattern().Invoke() # 点击方式2: 使用InvokePattern
time.sleep(0.5)
# 退出菜单项
menu_exit = notepad_window.MenuItemControl(Name="退出(X)") # 注意:菜单项可能在子菜单中,需要重新从父级开始查找或指定深度
if menu_exit.Exists(maxSearchTime=2):
print("找到退出菜单项")
menu_exit.Click()
else:
print("退出菜单项未找到!")
else:
print("文件菜单未找到!")
# --- 等待机制 ---
# 退出时可能会有 "是否保存" 的对话框
# 等待对话框出现,超时时间5秒
save_dialog = auto.WindowControl(searchDepth=1, ClassName="#32770", Name="记事本") # "#32770" 是标准对话框类名
# 或者 save_dialog = auto.WaitForExist(auto.WindowControl(searchDepth=1, ClassName="#32770", Name="记事本"), timeout=5)
if save_dialog.Exists(maxSearchTime=5): # Exists内部也包含了等待
print("找到保存对话框:")
print_control_info(save_dialog)
# 点击 "不保存(N)" 按钮
# 注意: 按钮的 Name 可能因系统语言而异
# 可以用 Inspect.exe 查看确切的 Name 或 AutomationId
# no_save_button = save_dialog.ButtonControl(Name="不保存(N)")
# 或者,如果知道 AutomationId (更可靠)
# no_save_button = save_dialog.ButtonControl(AutomationId="CommandButton_7") # 这个ID不一定对,需要用Inspect工具查看
# 尝试多种语言或通过索引查找
no_save_button = None
possible_names = ["不保存(N)", "Don't Save", "不保存"]
for name in possible_names:
btn = save_dialog.ButtonControl(Name=name)
if btn.Exists(0.1): # 快速检查
no_save_button = btn
break
if not no_save_button: # 如果按名称找不到,尝试按索引(不推荐,但作为后备)
buttons = save_dialog.GetChildren()
for btn_child in buttons:
if btn_child.ControlTypeName == "ButtonControl":
# 这里可以根据按钮的顺序或特定属性进一步判断
# 例如,"不保存" 通常是对话框中的第2或第3个按钮
# 此处仅为演示,实际中应避免硬编码索引
print(f"发现按钮: {btn_child.Name}")
if "不保存" in btn_child.Name or "Don't Save" in btn_child.Name: # 更灵活的匹配
no_save_button = btn_child
break
if no_save_button and no_save_button.Exists(0.1):
print(f"找到按钮: {no_save_button.Name}")
no_save_button.Click()
else:
print("未找到'不保存'按钮,可能已自动关闭或名称不匹配。")
else:
print("未出现保存对话框,可能记事本内容未更改或已自动关闭。")
print("记事本自动化演示完成。")
4. 高级控件定位
# 假设我们有一个更复杂的应用程序窗口
# app_window = auto.WindowControl(Name="我的复杂应用")
# --- 使用 searchDepth 控制搜索深度 ---
# 默认 searchDepth 是无限深。searchDepth=1 只搜索直接子元素。
# control = app_window.Control(searchDepth=2, Name="目标控件") # 搜索到孙子辈
# --- 使用正则表达式 RegexName ---
# control = app_window.ButtonControl(RegexName="提交订单.*") # 匹配以"提交订单"开头的按钮
# --- 组合条件搜索 ---
# control = app_window.EditControl(ClassName="Edit", AutomationId="userTextBox")
# --- 遍历控件树 ---
# parent_control = control.GetParentControl()
# first_child = control.GetFirstChildControl()
# next_sibling = control.GetNextSiblingControl()
# previous_sibling = control.GetPreviousSiblingControl()
# children = control.GetChildren() # 获取所有直接子控件列表
# for child in children:
# print_control_info(child)
# --- 查找所有匹配的控件 ---
# buttons = app_window.FindAllControls(ControlType=auto.ControlType.ButtonControl)
# print(f"共找到 {len(buttons)} 个按钮")
# for btn in buttons:
# print_control_info(btn)
5. 控件模式 (Control Patterns)
控件模式定义了控件可以执行的特定功能。一个控件可以支持零个或多个模式。
什么是控件模式?
模式是 UIA 的核心概念,它将控件的功能标准化。例如,无论一个按钮长什么样,只要它支持 InvokePattern,你就可以调用 Invoke() 方法来“点击”它。
常用模式:
ValuePattern: 用于可以设置和获取值的控件,如文本框、滑块。control.GetValuePattern().Value(获取值)control.GetValuePattern().SetValue("新内容")(设置值,通常比SendKeys更可靠)
InvokePattern: 用于可以被调用的控件,如按钮、菜单项。control.GetInvokePattern().Invoke()(执行动作)
TogglePattern: 用于有开/关或选中/未选中状态的控件,如复选框、单选按钮。control.GetTogglePattern().Toggle()(切换状态)control.GetTogglePattern().ToggleState(获取当前状态,如ToggleState.On,ToggleState.Off,ToggleState.Indeterminate)
ExpandCollapsePattern: 用于可以展开和折叠的控件,如树视图节点、组合框。control.GetExpandCollapsePattern().Expand()control.GetExpandCollapsePattern().Collapse()control.GetExpandCollapsePattern().ExpandCollapseState(获取状态)
SelectionItemPattern和SelectionPattern:SelectionItemPattern: 用于可选择的单个项(如列表项、树节点)。item.GetSelectionItemPattern().Select()item.GetSelectionItemPattern().IsSelected(布尔值)
SelectionPattern: 用于包含可选子项的容器控件(如列表框)。listbox.GetSelectionPattern().GetSelection()(返回选中项的列表)
ScrollPattern: 用于可滚动的控件。control.GetScrollPattern().Scroll(horizontalPercent, verticalPercent)control.GetScrollPattern().SetScrollPercent(horizontalPercent, verticalPercent)
TextPattern: 提供对文本内容的复杂访问,如获取选中文本、按范围操作等(较高级)。
如何检查和使用模式:
# 假设 control 是一个已定位到的控件
if control.IsValuePatternAvailable(): # 检查是否支持 ValuePattern
value_pattern = control.GetValuePattern()
print(f"当前值: {value_pattern.Value}")
value_pattern.SetValue("通过模式设置的值")
else:
print("控件不支持 ValuePattern")
if control.IsInvokePatternAvailable():
invoke_pattern = control.GetInvokePattern()
# invoke_pattern.Invoke() # 执行点击
else:
print("控件不支持 InvokePattern")
# 通用检查方法
if control. 패턴_지원_여부(auto.PatternId.TogglePattern): # PatternId 是一个枚举
toggle_pattern = control.GetTogglePattern()
current_state = toggle_pattern.ToggleState
print(f"Toggle 状态: {current_state}")
if current_state == auto.ToggleState.Off:
toggle_pattern.Toggle() # 切换到 On
6. 高级技巧与实践
处理动态变化的控件
- 使用更稳定的父控件定位:先定位到一个稳定的父控件,再在其下查找动态子控件。
- 部分匹配和正则:如果ID或Name部分固定,部分变化,使用
RegexName。 - 索引定位(慎用):
GetChildren()[index],但UI结构变化时易失效。 - 循环等待:结合
Exists(timeout)或WaitForExist(),在控件出现前轮询。
错误处理与日志记录
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
try:
# ... 你的自动化代码 ...
# button = main_window.ButtonControl(Name="不存在的按钮")
# button.Click() # 这会抛出 LookupError 或 TimeoutError
# 示例:安全地点击
button_to_click = auto.ButtonControl(Name="登录") # 假设这是全局查找
if button_to_click.Exists(3): # 等待3秒
button_to_click.Click()
logging.info("按钮已点击")
else:
logging.warning("按钮未在3秒内找到")
except auto.errors.LookupError as e: # 控件未找到
logging.error(f"控件查找失败: {e}")
except auto.errors.TimeoutError as e: # 操作超时
logging.error(f"操作超时: {e}")
except Exception as e:
logging.error(f"发生未知错误: {e}")
使用 Inspect.exe 或 UISpy.exe 辅助定位
- Inspect.exe: Windows SDK 自带的工具,功能强大,可以查看控件的详细属性 (Name, AutomationId, ClassName, ControlType, 支持的 Patterns 等)。是进行 UIA 自动化必备的辅助工具。
- 通常路径:
C:\Program Files (x86)\Windows Kits\10\bin\<version>\x64\inspect.exe(或 x86)
- 通常路径:
- UISpy.exe: 较旧的工具,功能类似 Inspect.exe,但某些新系统可能不自带。
使用方法:打开 Inspect.exe,将鼠标悬停在目标应用的控件上,Inspect.exe 会显示该控件的属性树和详细信息。
uiautomation 库自带的控件检查工具
uiautomation 库提供了一些方法来帮助你理解控件结构:
control.WalkControl(): 打印控件及其子控件的树状结构和基本信息。# notepad_window.WalkControl(maxDepth=3) # 打印记事本窗口下3层控件信息auto.GetRootControl(): 获取桌面根控件。auto.GetFocusedControl(): 获取当前拥有焦点的控件。auto.GetControlFromPoint(x, y): 获取指定屏幕坐标下的控件。
与键盘和鼠标的底层交互
uiautomation 本身主要关注控件层面的交互。如果需要模拟全局键盘按键或鼠标移动点击,可以:
auto.SendKeys(): 全局发送按键。# auto.SendKeys('{Win}d') # 按 Win + D 显示桌面auto.PressKey(keyCode, scanCode=0, extended=False),auto.ReleaseKey(keyCode, scanCode=0, extended=False): 按下/释放特定虚拟键码。auto.MoveTo(x, y),auto.Click(x, y, button='left'),auto.RightClick(x, y): 全局鼠标操作。
注意:这些全局操作不依赖于特定控件,直接操作屏幕坐标和键盘事件,应谨慎使用,因为它们不如控件级交互稳定。
7. 实战案例
案例1: 自动化记事本 (增强版)
import uiautomation as auto
import subprocess
import time
def run_notepad_automation():
# 1. 打开记事本
subprocess.Popen('notepad.exe')
time.sleep(1) # 等待记事本窗口出现
# 2. 找到记事本窗口
# 使用更通用的方式查找,以防标题变化(例如,如果已打开文件)
notepad_win = None
for i in range(5): # 尝试5秒
# notepad_win = auto.WindowControl(searchDepth=1, ClassName="Notepad", RegexName=".*记事本")
# 对于最新版Win11记事本,ClassName可能是 "RichEditD2DPT" (编辑区) 或者窗口是 "ApplicationFrameWindow"
# 因此,更可靠的是直接通过进程名称获取主窗口
notepad_process_id = None
for proc in auto.ProcessSnapshot().processes:
if proc.name.lower() == "notepad.exe":
notepad_process_id = proc.pid
break
if notepad_process_id:
notepad_win = auto.ControlFromHandle(auto.GetWindowHandleByPid(notepad_process_id)) # 通过PID获取主窗口句柄再转Control
if notepad_win and "记事本" in notepad_win.Name: # 进一步确认
break
# 备用方案:如果通过PID获取的主窗口不理想,尝试传统方法
if not notepad_win or not ("记事本" in notepad_win.Name):
notepad_win = auto.WindowControl(searchDepth=1, ClassName="Notepad", RegexName=".*记事本") # 经典记事本
if notepad_win.Exists(0.2): break
notepad_win = auto.WindowControl(searchDepth=1, NameRegex=".*记事本", ClassName="Window") # Win11 UWP 记事本外框
if notepad_win.Exists(0.2): break
time.sleep(1)
if not notepad_win or not notepad_win.Exists(0.1):
print("错误:未能找到记事本窗口。")
return
print(f"成功找到记事本窗口: {notepad_win.Name}")
notepad_win.SetFocus() # 确保窗口在前台并有焦点
notepad_win.SetActive()
# 3. 定位编辑区
# 经典记事本的编辑区是 EditControl
# Win11 UWP 记事本的编辑区可能是 DocumentControl
edit_area = notepad_win.EditControl()
if not edit_area.Exists(0.5):
edit_area = notepad_win.DocumentControl() # 尝试DocumentControl for Win11 Notepad
if not edit_area.Exists(0.5): # 再尝试通过AutomationId (这个ID可能不通用)
edit_area = notepad_win.Control(AutomationId="RichText Control") # 假设新版记事本有这个ID
if not edit_area.Exists(0.5): # 最后的尝试:通过ControlType和Name模糊查找
edit_area = notepad_win.Control(searchDepth=5, ControlType=auto.ControlType.DocumentControl) # 查找第一个Document
if not edit_area.Exists(0.5):
edit_area = notepad_win.Control(searchDepth=5, ControlType=auto.ControlType.EditControl) # 查找第一个Edit
if not edit_area.Exists(0.1):
print("错误:未能定位到编辑区。")
notepad_win.Close() # 关闭记事本
return
print("成功定位到编辑区。")
# 4. 输入文本 (使用 ValuePattern 更可靠)
if edit_area.IsValuePatternAvailable():
edit_area.GetValuePattern().SetValue("你好,世界!\n这是 `uiautomation` 的高级教程。\n")
edit_area.SendKeys("{Ctrl}{End}{Enter}当前时间: " + time.strftime("%Y-%m-%d %H:%M:%S"), interval=0.01)
else: # 备用方案
edit_area.SendKeys("你好,世界!\n这是 `uiautomation` 的高级教程。\n", interval=0.01)
edit_area.SendKeys("{Ctrl}{End}{Enter}当前时间: " + time.strftime("%Y-%m-%d %H:%M:%S"), interval=0.01)
time.sleep(1)
# 5. 操作菜单 (保存)
# 注意:菜单项的Name可能因系统语言而异
# Win11 UWP 记事本的菜单结构也不同,可能需要用AccessKey或不同的Name
# 以下代码主要针对经典记事本
try:
if "记事本" in notepad_win.Name and notepad_win.ClassName == "Notepad": # 经典记事本
print("尝试经典记事本菜单操作...")
notepad_win.MenuItemControl(Name="文件(F)").Click()
time.sleep(0.5)
# notepad_win.MenuItemControl(Name="另存为(A)...").Click() # 注意有省略号
save_as_item = notepad_win.MenuItemControl(RegexName="另存为.*")
save_as_item.Click()
time.sleep(1)
# "另存为" 对话框
save_dialog = auto.WindowControl(ClassName="#32770", NameRegex="另存为") # 标准对话框
if not save_dialog.Exists(3):
# 尝试通过当前活动窗口获取(如果上一步点击成功,对话框应为活动窗口)
save_dialog = auto.GetFocusedControl().GetTopLevelControl() if auto.GetFocusedControl() else None
if not (save_dialog and "另存为" in save_dialog.Name):
print("错误:未找到另存为对话框。")
return
print("找到另存为对话框。")
# 文件名输入框通常是ComboBox下的EditControl或直接的EditControl
filename_edit = save_dialog.EditControl(AutomationId="1001") # 这个ID比较通用
if not filename_edit.Exists(0.5):
filename_edit = save_dialog.ComboBoxControl(AutomationId="1001").EditControl() # 另一种结构
if not filename_edit.Exists(0.5):
filename_edit = save_dialog.EditControl(Name="文件名:") # 按名称
if filename_edit.Exists(0.1):
filename_edit.GetValuePattern().SetValue("MyTestFile.txt")
else:
print("错误:找不到文件名输入框。")
save_dialog.ButtonControl(Name="取消").Click()
return
save_button = save_dialog.ButtonControl(Name="保存(S)")
save_button.Click()
# 处理可能出现的 "覆盖" 对话框
time.sleep(0.5)
confirm_dialog = auto.WindowControl(ClassName="#32770", NameRegex="确认另存为")
if confirm_dialog.Exists(2):
print("找到文件已存在确认对话框。")
confirm_dialog.ButtonControl(Name="是(Y)").Click()
print("文件已保存。")
else: # UWP 记事本,菜单操作不同,这里仅做演示关闭
print("检测到可能是UWP记事本,菜单操作将跳过,直接关闭。")
except auto.errors.LookupError as e:
print(f"菜单操作失败 (控件未找到): {e}")
except Exception as e:
print(f"菜单操作发生错误: {e}")
finally:
# 6. 关闭记事本 (不保存更改)
time.sleep(1)
# notepad_win.Close() # 这会触发保存对话框(如果内容已更改且未保存)
# 更强制的关闭方式,或者先处理对话框
if notepad_win.Exists(0.1):
if "记事本" in notepad_win.Name and notepad_win.ClassName == "Notepad":
notepad_win.GetWindowPattern().Close() # 尝试正常关闭
time.sleep(0.5)
# 检查是否有保存对话框
save_prompt = auto.WindowControl(ClassName="#32770", Name="记事本")
if save_prompt.Exists(2):
print("关闭时出现保存提示,选择不保存。")
# Name 可能为 "不保存(N)" 或 "Don't Save"
no_save_btn = save_prompt.ButtonControl(RegexName="不保存.*|Don't Save")
if no_save_btn.Exists(0.1):
no_save_btn.Click()
else: # 如果按名称找不到,可能需要用更通用的方式
buttons = save_prompt.GetChildren()
for btn in buttons: # 粗略查找包含“不保存”字样的按钮
if "不保存" in btn.Name:
btn.Click()
break
else: # UWP 或其他版本,尝试用标题栏关闭按钮
close_button = notepad_win.ButtonControl(Name="关闭") # UWP 记事本的关闭按钮Name是"关闭"
if close_button.Exists(0.5):
close_button.Click()
else: # 万能但不优雅的 Alt+F4
notepad_win.SendKeys("{Alt}{F4}")
print("记事本自动化流程结束。")
if __name__ == "__main__":
# 注意:运行此脚本前,请确保没有其他重要内容的记事本窗口打开,以免误操作。
# 最好关闭所有记事本实例。
run_notepad_automation()
案例2: 自动化计算器 (Windows 10/11 标准计算器)
新版计算器是 UWP 应用,其控件结构和属性与传统 Win32 应用有很大不同。AutomationId 非常重要。
import uiautomation as auto
import subprocess
import time
def run_calculator_automation():
# 1. 启动计算器
try:
subprocess.Popen('calc.exe')
except FileNotFoundError:
print("错误:无法启动计算器 (calc.exe)。请确保已安装。")
return
time.sleep(2) # 等待计算器启动和加载
# 2. 找到计算器窗口
# 新版计算器窗口的 Name 可能是 "计算器" 或 "Calculator"
# ClassName 通常是 "ApplicationFrameWindow" (外框) 或 "Windows.UI.Core.CoreWindow" (核心内容)
calc_window = None
for _ in range(5): # 尝试5秒
calc_window = auto.WindowControl(searchDepth=1, NameRegex="计算器|Calculator", ClassName="ApplicationFrameWindow")
if calc_window.Exists(0.2): break
# 某些系统下,直接是这个类名
calc_window = auto.WindowControl(searchDepth=1, NameRegex="计算器|Calculator", ClassName="Windows.UI.Core.CoreWindow")
if calc_window.Exists(0.2): break
time.sleep(1)
if not calc_window or not calc_window.Exists(0.1):
print("错误:未能找到计算器窗口。")
# 可以尝试打印所有顶层窗口来调试
# for win in auto.GetRootControl().GetChildren():
# print(f"顶层窗口: Name='{win.Name}', ClassName='{win.ClassName}'")
return
print(f"成功找到计算器窗口: {calc_window.Name}")
calc_window.SetFocus()
# 3. 定位控件 (依赖 AutomationId,这些 ID 是 Windows 计算器常用的)
# 使用 Inspect.exe 确认这些 ID 在你的系统上是否一致
# 数字按钮通常在 PaneControl (有时名为 "数字键盘") 下
# 有时按钮直接在窗口下,需要调整 searchDepth 或父控件
# 为了更稳定,可以先定位到包含数字按钮的面板
# 首先尝试直接在窗口下查找,如果不行,再深入查找
# calc_window.WalkControl(maxDepth=5) # 打印控件树帮助分析
def get_button(name, automation_id):
# 尝试多种方式定位按钮,因为UWP应用结构可能微调
btn = calc_window.ButtonControl(AutomationId=automation_id)
if btn.Exists(0.1): return btn
btn = calc_window.ButtonControl(Name=name) # 某些情况下Name也可用
if btn.Exists(0.1): return btn
# 尝试在常见的容器内查找
# 例如,数字按钮可能在 "NumberPad" 或类似名称的 Pane 里
# Inspect.exe 可以帮助找到这些容器的 AutomationId 或 Name
# number_pad = calc_window.PaneControl(AutomationId="NumberPad") # 示例
# if number_pad.Exists(0.1):
# btn = number_pad.ButtonControl(AutomationId=automation_id)
# if btn.Exists(0.1): return btn
return None
button_1 = get_button("一", "num1Button")
button_7 = get_button("七", "num7Button")
button_plus = get_button("加", "plusButton")
button_equal = get_button("等于", "equalButton")
results_text_box = calc_window.TextControl(AutomationId="CalculatorResults") # 显示结果的控件
if not all([button_1, button_7, button_plus, button_equal, results_text_box]):
print("错误:未能定位到所有必要的计算器按钮或结果显示区域。")
if not button_1: print("未找到按钮 1 (num1Button)")
if not button_7: print("未找到按钮 7 (num7Button)")
if not button_plus: print("未找到加号按钮 (plusButton)")
if not button_equal: print("未找到等于按钮 (equalButton)")
if not results_text_box: print("未找到结果显示区域 (CalculatorResults)")
print("\n请使用 Inspect.exe 检查计算器控件的 AutomationId 和 Name。")
print("计算器控件结构可能因 Windows 版本或计算器模式(标准、科学等)而异。")
print("以下是当前窗口的部分控件树信息:")
calc_window.WalkControl(maxDepth=5) # 打印控件树帮助分析
# 关闭计算器
calc_window.GetWindowPattern().Close()
return
# 4. 执行计算: 1 + 7 =
# UWP应用有时响应较慢,确保点击间隔
button_1.Click(waitTime=0.1)
time.sleep(0.2)
button_plus.Click(waitTime=0.1)
time.sleep(0.2)
button_7.Click(waitTime=0.1)
time.sleep(0.2)
button_equal.Click(waitTime=0.1)
time.sleep(0.5) # 等待计算结果显示
# 5. 获取结果
# CalculatorResults 的 Name 属性通常会包含显示的值,格式如 "显示为 8"
result_name = results_text_box.Name
print(f"结果显示区域的原始Name: '{result_name}'")
# 从 "显示为 8" 中提取 "8"
# 实际提取逻辑可能需要根据具体语言和格式调整
actual_result = result_name
if "Display is " in result_name: # 英文系统
actual_result = result_name.replace("Display is ", "").strip()
elif "显示为 " in result_name: # 中文系统
actual_result = result_name.replace("显示为 ", "").strip()
# 其他语言环境可能需要添加更多判断
print(f"计算结果: 1 + 7 = {actual_result}")
if actual_result == "8":
print("计算正确!")
else:
print(f"计算错误或结果解析失败。期望 '8',得到 '{actual_result}'")
# 6. 清除 (CE按钮)
# clear_entry_button = get_button("清除条目", "clearEntryButton") # CE
clear_button = get_button("清除", "clearButton") # C
if clear_button and clear_button.Exists(0.1):
clear_button.Click(waitTime=0.1)
print("已点击清除按钮。")
else:
print("未找到清除按钮。")
time.sleep(0.5)
# 7. 关闭计算器
# calc_window.Close() # 可能无法关闭UWP应用
if calc_window.IsWindowPatternAvailable():
calc_window.GetWindowPattern().Close()
else: # 尝试使用标题栏的关闭按钮
# UWP应用的关闭按钮通常有特定的AutomationId或Name
# 例如 "Close" 或 "关闭"
close_btn = calc_window.ButtonControl(Name="关闭") # 假设Name是"关闭"
if close_btn.Exists(0.2):
close_btn.Click()
else: # 最后手段
calc_window.SendKeys("{Alt}{F4}")
print("计算器自动化流程结束。")
if __name__ == "__main__":
run_calculator_automation()
关于计算器案例的注意事项:
- UWP 应用的复杂性:Windows 10/11 的计算器是 UWP (Universal Windows Platform) 应用。它们的 UI 结构可能比传统 Win32 应用更深、更复杂,并且大量依赖
AutomationId。 AutomationId的重要性:对于 UWP 应用,AutomationId是最可靠的定位器。请务必使用 Inspect.exe 来查找正确的AutomationId。- 多语言/版本差异:按钮的
Name属性会随系统语言变化。AutomationId通常是语言无关的。不同 Windows 版本或计算器更新也可能导致AutomationId或控件结构变化。 - 控件容器:有时按钮等控件会嵌套在
PaneControl(面板) 或其他容器控件内。如果直接在窗口下找不到,可能需要先定位到父容器,再查找子控件。 - 响应时间:UWP 应用有时对自动化操作的响应可能稍慢,适当增加
time.sleep()或使用control.Exists(timeout)、control.WaitForExist()等待控件就绪。
8. 调试与最佳实践
调试技巧
- 使用 Inspect.exe:这是最重要的工具。用它查看控件的属性(
Name,AutomationId,ClassName,ControlType),以及控件支持的模式。 control.WalkControl(maxDepth):在代码中打印控件树,了解当前控件的子控件结构。# window = auto.WindowControl(Name="MyApp") # window.WalkControl(maxDepth=5)- 打印控件信息:获取到控件后,打印其关键属性,确认是否找到了正确的控件。
# button = window.ButtonControl(Name="OK") # print(f"Name: {button.Name}, AutoId: {button.AutomationId}, Class: {button.ClassName}") # print(f"Supported patterns: {button.GetSupportedPatternNames()}") - 逐步执行:在复杂的自动化流程中,一步步执行并验证每一步的结果。
- 小范围测试:先针对单个控件或小块功能编写和测试代码,成功后再集成到大流程中。
Exists(timeout)和WaitForExist(timeout):充分利用等待机制,避免因界面加载延迟导致的查找失败。# control = window.EditControl(Name="username") # if control.Exists(timeout=5): # 等待5秒看是否存在 # control.SetValue("test") # else: # print("Username field not found within 5 seconds.") # login_button = auto.ButtonControl(Name="Login") # login_button.WaitForExist(timeout=10, interval=0.5) # 等待10秒,每0.5秒检查一次 # login_button.Click()
提高脚本稳定性和可维护性的建议
- 优先使用
AutomationId:如果开发者为控件设置了AutomationId,这是最稳定、最可靠的定位方式,因为它通常是唯一的且语言无关的。 - 组合定位条件:当
AutomationId不可用时,组合使用Name,ClassName,ControlType等属性来精确定位。 - 避免使用绝对路径或索引:UI 结构容易变化,依赖控件在树中的绝对位置或其在子控件列表中的索引(如
GetChildren()[0])会使脚本非常脆弱。 - 封装常用操作:将重复的控件查找和操作逻辑封装成函数,提高代码复用性和可读性。
def click_button_by_name(parent_control, button_name): button = parent_control.ButtonControl(Name=button_name) if button.Exists(3): button.Click() return True print(f"Button '{button_name}' not found.") return False - 添加显式等待:在执行操作(如点击)后,如果会触发界面变化或加载新内容,请添加适当的等待,确保后续操作的目标控件已就绪。
- 健壮的错误处理:使用
try-except块捕获可能发生的LookupError(控件未找到)、TimeoutError等异常,并给出有意义的错误信息或执行备用逻辑。 - 日志记录:在关键步骤记录日志,方便调试和追踪问题。
- 考虑应用状态:在自动化开始前,确保应用程序处于一个已知的、稳定的初始状态。
- 模块化设计:对于复杂的自动化任务,将流程拆分成小的、独立的模块或函数。
- 注释代码:清晰的注释有助于理解代码的意图,特别是对于复杂的定位逻辑。
- 管理超时设置:
auto.uiautomation.SetGlobalSearchTimeout(seconds)可以设置全局搜索超时。也可以在单个控件查找时通过control.Exists(maxSearchTime=...)或control.WaitForExist(timeout=...)等方法指定局部超时。
9. 总结与资源
uiautomation 是一个强大的 Python 库,适用于 Windows 桌面应用的 GUI 自动化。通过理解其核心概念(控件、控件树、定位器、模式),结合 Inspect.exe 等辅助工具,你可以构建出稳定可靠的自动化脚本。
关键点回顾:
- 安装:
pip install uiautomation - 定位:
Name,AutomationId(首选),ClassName,ControlType,RegexName。 - 交互:
Click(),SendKeys(),GetValuePattern().SetValue(),InvokePattern().Invoke()等。 - 模式:是理解和操作控件高级功能的钥匙。
- 辅助:Inspect.exe 是你最好的朋友。
- 实践:多练习,从简单应用开始,逐步挑战复杂应用。
官方文档和社区(通常是GitHub)是获取最新信息和解决特定问题的好地方:
uiautomationGitHub 仓库 (作者 yinkaisheng): https://github.com/yinkaisheng/Python-UIAutomation-for-Windows (包含 README 和一些示例)
希望这个完整和高级的教程能帮助你掌握 uiautomation!
更多推荐



所有评论(0)