pywinauto初识


# -*- 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()


posted @ 2026-01-08 20:32  PyAj  阅读(15)  评论(0)    收藏  举报