背景
在日常开发和测试工作中,我们需要对 WPF 桌面应用程序进行功能验证。传统的手工点击方式效率低、容易出错,特别是一些需要重复执行的操作,人工测试不仅耗时,而且难以保证一致性。
因此,我尝试用 Python 来实现自动化测试,通过脚本来启动和操作 WPF 程序,完成一些表单填写和操作的验证。
遇到的问题
在实现过程中,主要遇到几个问题:
-
进程连接困难
一开始通过
psutil
找到目标进程 PID 没问题,但在用pywinauto
连接时,总是提示Process with PID=xxxx not found
。原因是目标进程虽然启动了,但窗口还没完全初始化,或者运行权限不一致。
-
控件识别不稳定
WPF 程序的控件有时用
uia
后端才能识别,有时需要win32
。一开始只用单一后端会导致找不到控件。 -
执行速度与同步问题
自动化脚本运行太快,程序界面还没加载完就开始操作,导致控件查找失败
解决思路
针对以上问题,我采取了以下思路:
-
封装进程连接方法
写了
get_app_window
方法,先用psutil
找到目标进程,再用pywinauto.Application.connect
连接窗口,并增加了backend
(默认为uia
)和wait_time
(默认为 5 秒)的参数,确保能灵活切换后端并等待窗口加载完成。 -
默认值和重试机制
在方法内部设置默认参数,避免每次调用都要传。并预留了超时和重试机制,保证在窗口加载慢时也能成功。
-
封装操作逻辑
对常用操作(如填写表单)进行了函数封装,比如
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 窗口并自动化操作"的问题。