用 Python 写的自动化测试 WPF 程序的一个案例

背景

在日常开发和测试工作中,我们需要对 WPF 桌面应用程序进行功能验证。传统的手工点击方式效率低、容易出错,特别是一些需要重复执行的操作,人工测试不仅耗时,而且难以保证一致性。

因此,我尝试用 Python 来实现自动化测试,通过脚本来启动和操作 WPF 程序,完成一些表单填写和操作的验证。

遇到的问题

在实现过程中,主要遇到几个问题:

  1. 进程连接困难

    一开始通过 psutil 找到目标进程 PID 没问题,但在用 pywinauto 连接时,总是提示 Process with PID=xxxx not found

    原因是目标进程虽然启动了,但窗口还没完全初始化,或者运行权限不一致。

  2. 控件识别不稳定

    WPF 程序的控件有时用 uia 后端才能识别,有时需要 win32。一开始只用单一后端会导致找不到控件。

  3. 执行速度与同步问题

    自动化脚本运行太快,程序界面还没加载完就开始操作,导致控件查找失败

解决思路

针对以上问题,我采取了以下思路:

  1. 封装进程连接方法

    写了 get_app_window 方法,先用 psutil 找到目标进程,再用 pywinauto.Application.connect 连接窗口,并增加了 backend(默认为 uia)和 wait_time(默认为 5 秒)的参数,确保能灵活切换后端并等待窗口加载完成。

  2. 默认值和重试机制

    在方法内部设置默认参数,避免每次调用都要传。并预留了超时和重试机制,保证在窗口加载慢时也能成功。

  3. 封装操作逻辑

    对常用操作(如填写表单)进行了函数封装,比如 fill_subject_form(),这样测试用例更清晰,后续要换别的数据也很方便。

具体方案

代码如下:

python 复制代码
import psutil
import time
import os
from pywinauto import Application
from pywinauto.mouse import click


def get_app_window(process_name: str, backend: str = "uia", wait_time: float = 5.0):
    pid = None
    for proc in psutil.process_iter(['pid', 'name']):
        if proc.info['name'] and proc.info['name'].lower() == process_name.lower():
            pid = proc.info['pid']
            break
    if not pid:
        raise RuntimeError(f"未找到进程:{process_name}")

    # 直接用默认 backend 和 wait_time
    app = Application(backend=backend).connect(process=pid, timeout=wait_time)

    win = app.top_window()
    return win


def generate_incremental_num(counter_file="counter.txt"):
    """递增编号生成器,返回 3 位字符串编号"""
    if not os.path.exists(counter_file):
        with open(counter_file, "w") as f:
            f.write("1")

    with open(counter_file, "r") as f:
        num = int(f.read().strip())

    new_num = num + 1
    with open(counter_file, "w") as f:
        f.write(str(new_num))

    return f"{num:03d}"  # 格式化为 '001'


def select_position(win, main_area, sub_area=None):
    """点击主部位和子部位按钮"""
    try:
        win.child_window(title=main_area, control_type="Button").click_input()
        print(f"✅ 已点击主部位:{main_area}")
        time.sleep(0.5)
        if sub_area:
            win.child_window(title=sub_area, control_type="Button").click_input()
            print(f"✅ 已点击子部位:{sub_area}")
    except Exception as e:
        print(f"❌ 点击部位失败:{e}")

def wait_for_checkboxes(win, timeout=5):
    """等待复选框出现"""
    for _ in range(timeout * 2):  # 每0.5秒查一次
        checkboxes = win.descendants(control_type="CheckBox")
        if len(checkboxes) > 1:
            return checkboxes
        time.sleep(0.5)
    raise RuntimeError("超时:复选框仍未出现")

def click_first_row_of_patient_grid(win):
    """模拟点击 patientGrid 第一行(通过坐标)"""
    try:
        grid = win.child_window(auto_id="patientGrid", control_type="DataGrid")
        rect = grid.rectangle()

        # 假设表头约 40 像素,点击第 1 行中间位置
        x = rect.left + 20
        y = rect.top + 50  # 跳过表头高度

        from pywinauto.mouse import click
        click(button='left', coords=(x, y))

        print(f"✅ 已模拟点击 patientGrid 第一行坐标 ({x}, {y})")
        time.sleep(0.5)

    except Exception as e:
        import traceback
        print(f"❌ 点击 patientGrid 第一行失败:{e}")
        traceback.print_exc()

def click_detect_button(win):
    try:
        btnExamination = win.child_window(auto_id="btnExamination", control_type="Button")
        btnExamination.click_input()
        print("✅ 已点击"检测"按钮")
    except Exception as e:
        print(f"❌ 点击"检测"失败:{e}")


def perform_detection_action(win):
    """在检测界面依次点击 btnLive 和 btnSnap"""
    try:
        print("🎬 开始检测页面点击操作...")
        # 点击 btnLive
        for i in range(10000):
                print(f"\n🔁 第 {i + 1}/10000 次执行...")
                btn_live = win.child_window(auto_id="btnLive", control_type="Button")
                btn_live.click_input()
                print("✅ 已点击 btnLive")
                time.sleep(10)  # 等待 8 秒

                # 点击 btnSnap
                btn_snap = win.child_window(auto_id="btnSnap", control_type="Button")
                btn_snap.click_input()
                print("✅ 已点击 btnSnap")
                time.sleep(8)

                # 点击 btnZstackFast
                btn_zstack = win.child_window(auto_id="btnZstackFast", control_type="Button")
                btn_zstack.click_input()
                print("✅ 已点击 btnZstackFast")
                time.sleep(60)
    except Exception as e:
        print(f"❌ 检测页面按钮点击失败:{e}")

def fill_subject_form(win, name, num, sex, age, height, weight, project, main_area, sub_area):
    print("⏳ 正在填写表单...")

    # 点击"新增"
    try:
        btnAdd = win.child_window(auto_id="btnAdd", control_type="Button")
        btnAdd.click_input()
        print("✅ 已点击"新增"按钮")
        time.sleep(1.0)
    except:
        raise RuntimeError("❌ 找不到"新增"按钮")

    # 获取 Edit 控件组
    edits = win.descendants(control_type="Edit")

    # 姓名
    win.child_window(auto_id="_SubjectNameTextBox_", control_type="Edit").set_edit_text(name)

    # 编号
    edits[1].set_edit_text(num)


    # 年龄、身高、体重
    edits[2].set_edit_text(str(age))
    edits[3].set_edit_text(str(height))
    edits[4].set_edit_text(str(weight))


    # 受试部位(文本框 + 按钮点击)
    win.child_window(auto_id="PositionResult", control_type="Edit").set_edit_text(sub_area)
    select_position(win, main_area, sub_area)

    # 点击"确认"
    btnOk = win.child_window(auto_id="btnOk", control_type="Button")
    btnOk.click_input()
    print(f"✅ 编号 {num} 提交完成")
    time.sleep(2)
    win = get_app_window("780.exe")
    # 等复选框加载完成,再点击"检测"
    click_first_row_of_patient_grid(win)
    click_detect_button(win)
    time.sleep(1.5)

    # 再次获取当前窗口(检测界面可能为新窗口)
    detect_win = get_app_window("780.exe")
    perform_detection_action(detect_win)


if __name__ == "__main__":
    try:
        win = get_app_window("780.exe")

        # 获取自动编号
        num = generate_incremental_num()

        # 调用填写函数
        fill_subject_form(
            win=win,
            name="张三",
            num=num,
            sex="Male",
            age=30,
            height=175,
            weight=70,
            project="111",
            main_area="头颈部",
            sub_area="左面颊"
        )

    except Exception as e:
        print(f"❌ 自动化失败:{e}")

总结

通过这个小案例,我基本跑通了 Python + psutil + pywinauto 的组合,解决了"如何定位 WPF 窗口并自动化操作"的问题。