用 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(),这样测试用例更清晰,后续要换别的数据也很方便。

具体方案

代码如下:

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 窗口并自动化操作”的问题。

posted @ 2025-08-25 10:28  yjbjingcha  阅读(20)  评论(0)    收藏  举报