> 技术文档 > uiautomation` 库的高级教程_uiautomation教程

uiautomation` 库的高级教程_uiautomation教程

uiautomation 是一个用于 Windows GUI 自动化的 Python 库,它封装了 Microsoft UI Automation API,使得我们可以通过编程方式查找和操作 Windows 应用程序的控件(如按钮、文本框、菜单等)。

教程大纲:

  1. 简介与安装
    • 什么是 UIAutomation?
    • 为什么使用 uiautomation
    • 安装 uiautomation
  2. 基础概念
    • 控件 (Control) 与窗口 (Window)
    • 控件树 (Control Tree)
    • 定位控件:核心方法
  3. 基本操作
    • 启动和附加到应用程序
    • 查找控件 (按名称、类名、AutomationId 等)
    • 控件交互 (点击、输入文本、获取文本等)
    • 等待机制
  4. 高级控件定位
    • 使用 searchDepth 控制搜索深度
    • 使用正则表达式 RegexName
    • 组合条件搜索
    • 遍历控件树 (父、子、兄弟节点)
    • 查找所有匹配的控件
  5. 控件模式 (Control Patterns)
    • 什么是控件模式?
    • 常用模式:
      • ValuePattern (读写值)
      • InvokePattern (执行操作,如按钮点击)
      • TogglePattern (切换状态,如复选框)
      • ExpandCollapsePattern (展开折叠,如树视图)
      • SelectionItemPatternSelectionPattern (选择项)
      • ScrollPattern (滚动)
      • TextPattern (高级文本操作)
    • 如何检查和使用模式
  6. 高级技巧与实践
    • 处理动态变化的控件
    • 错误处理与日志记录
    • 使用 Inspect.exe 或 UISpy.exe 辅助定位
    • uiautomation 库自带的控件检查工具
    • 与键盘和鼠标的底层交互 (可选,uiautomation 自带方法通常足够)
    • 多线程/异步操作注意事项 (简单提及)
  7. 实战案例
    • 自动化记事本
    • 自动化计算器 (新版 Windows 计算器可能较复杂,可换用经典版或部分功能)
  8. 调试与最佳实践
    • 调试技巧
    • 提高脚本稳定性和可维护性的建议
  9. 总结与资源

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 autoimport timeimport 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 组合,或 AutomationIdnotepad_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)# --- 查找控件 ---# 记事本的编辑区域通常是 EditControledit_area = notepad_window.EditControl() # 查找第一个 EditControl# 如果有多个同类型控件,需要更精确的定位# edit_area = notepad_window.EditControl(AutomationId=\"15\") # 假设知道 AutomationId# edit_area = notepad_window.EditControl(Name=\"文本编辑器\") # 某些版本的记事本可能有Nameif 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 (获取状态)
  • SelectionItemPatternSelectionPattern:
    • 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 logginglogging.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\\\\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 autoimport subprocessimport timedef 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 autoimport subprocessimport timedef 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. 调试与最佳实践

调试技巧
  1. 使用 Inspect.exe:这是最重要的工具。用它查看控件的属性(Name, AutomationId, ClassName, ControlType),以及控件支持的模式。
  2. control.WalkControl(maxDepth):在代码中打印控件树,了解当前控件的子控件结构。
    # window = auto.WindowControl(Name=\"MyApp\")# window.WalkControl(maxDepth=5)
  3. 打印控件信息:获取到控件后,打印其关键属性,确认是否找到了正确的控件。
    # button = window.ButtonControl(Name=\"OK\")# print(f\"Name: {button.Name}, AutoId: {button.AutomationId}, Class: {button.ClassName}\")# print(f\"Supported patterns: {button.GetSupportedPatternNames()}\")
  4. 逐步执行:在复杂的自动化流程中,一步步执行并验证每一步的结果。
  5. 小范围测试:先针对单个控件或小块功能编写和测试代码,成功后再集成到大流程中。
  6. 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()
提高脚本稳定性和可维护性的建议
  1. 优先使用 AutomationId:如果开发者为控件设置了 AutomationId,这是最稳定、最可靠的定位方式,因为它通常是唯一的且语言无关的。
  2. 组合定位条件:当 AutomationId 不可用时,组合使用 Name, ClassName, ControlType 等属性来精确定位。
  3. 避免使用绝对路径或索引:UI 结构容易变化,依赖控件在树中的绝对位置或其在子控件列表中的索引(如 GetChildren()[0])会使脚本非常脆弱。
  4. 封装常用操作:将重复的控件查找和操作逻辑封装成函数,提高代码复用性和可读性。
    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
  5. 添加显式等待:在执行操作(如点击)后,如果会触发界面变化或加载新内容,请添加适当的等待,确保后续操作的目标控件已就绪。
  6. 健壮的错误处理:使用 try-except 块捕获可能发生的 LookupError (控件未找到)、TimeoutError 等异常,并给出有意义的错误信息或执行备用逻辑。
  7. 日志记录:在关键步骤记录日志,方便调试和追踪问题。
  8. 考虑应用状态:在自动化开始前,确保应用程序处于一个已知的、稳定的初始状态。
  9. 模块化设计:对于复杂的自动化任务,将流程拆分成小的、独立的模块或函数。
  10. 注释代码:清晰的注释有助于理解代码的意图,特别是对于复杂的定位逻辑。
  11. 管理超时设置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)是获取最新信息和解决特定问题的好地方:

  • uiautomation GitHub 仓库 (作者 yinkaisheng): https://github.com/yinkaisheng/Python-UIAutomation-for-Windows (包含 README 和一些示例)

希望这个完整和高级的教程能帮助你掌握 uiautomation