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 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 (获取状态)
  • 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 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\\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. 调试与最佳实践

调试技巧
  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

你可能感兴趣的:(python)