frmWindowTest工程解析:从Delphi窗体到pywinauto自动化测试
2026/9/23 12:24:26 网站建设 项目流程

简介:这是一份面向 C# 与机器视觉开发者的 WinForm 工程示例,演示如何调用 Halcon 图像处理库,并利用笔记本内置摄像头通过 DirectShow 获取实时视频流,最终完成二维码识别。工程文件完整,共 36 个文件,以 C# 源码为主,包含动态链接库、可执行程序、配置文件、资源文件以及测试图片,压缩包大小约 11.05 MB。目录结构包括窗体设计、程序入口、项目配置等模块,适合学习桌面程序开发、视频采集与视觉算法之间的集成方式,也方便直接打开工程进行二次修改。包内提供的测试二维码和示例图像有助于验证从摄像头采集到二维码解析的完整流程,减少环境配置与参数调节的时间。目前已有 460 人学习,对于希望快速上手 Halcon 与 C# 结合的开发者,是一份实操性较强的参考资料。

1. frmWindowTest.rar 是给谁用的:先把窗体测试这件事的边界划清楚

frmWindowTest.rar 这个文件名一眼就能看出大半信息:frm是 Delphi / C++Builder 工程里窗体(Form)文件的传统命名前缀,WindowTest说明它是一个窗口行为验证工程,rar表示作者把整个工程目录打完包对外分发。这类压缩包经常出现在两类场景里:要么是组内分享的 GUI 自测工具,要么是某个业务模块的窗体测试工程,解压之后用 IDE 打开就能编译运行,窗口弹出来之后你可以手动点它,也可以用自动化脚本去驱动它。

我要先说实话:这个 rar 里大概率不是什么复杂框架,而是一个能编译运行的窗体程序,外加它依赖的单元文件。它的价值不在于代码量,而在于它给你提供了一个稳定的、可重复的窗体测试对象。你后续要做的 UI 自动化、控件识别、窗口布局验证,都可以拿它当靶子练手,而不是每天都在业务系统上试探。适合读这篇文章的人,是手里拿到这类工程包却不知道怎么下手的人,以及想用 pywinauto 这类工具把窗体测试跑起来的人。我不打算跟你说太多理论,直接拆包、编译、写脚本、踩坑,一条线走完。

2. 拆开 frmWindowTest.rar:先搞懂工程结构,再决定它在你这边怎么跑

拿到任何一个 rar 包,我的习惯是先别急着解压到桌面,先看一眼压缩包里的目录层次。很多窗体工程打包的时候会把整个项目文件夹压进去,解压出来会多一层外层目录;也有作者只挑必要文件打包,解压出来直接就是 .dpr 文件摆在根目录。这两种情况处理方式不一样,前者你直接打开外层目录里的工程文件即可,后者你需要自己新建一个工作目录再放进去。

2.1 从文件名反推工程类型:frm 前缀、rar 后缀代表的来路

在 Delphi 的世界里,frm前缀是一个沿用了二十多年的命名习惯。一个可视化窗体由两个文件组成:.pas 或 .cpp 文件(代码逻辑),以及 .dfm 文件(窗体布局)。例如 frmWindowTest.pas 里会声明TfrmWindowTest类,而 frmWindowTest.dfm 里描述这个窗体的尺寸、按钮位置、控件属性。你用记事本打开 .dfm 文件会看到二进制或文本格式的窗体定义,Delphi 2007 之后的版本默认存成文本格式,可以直接在 IDE 里右键查看文本。

rar 后缀说明作者选择了 WinRAR 或 7-Zip(7-Zip 也能解 rar)来分发。这通常是个人开发者或小团队的做法,因为 Delphi 工程体积不大,去掉临时文件之后整个目录也就几百 KB 到几 MB,打一个 rar 发给别人非常方便。如果你看到的是 zip 或 7z,对应的可能来自 CI 服务器打包,但本质上没有区别。

2.2 解压后的关键文件清单:哪些文件决定能不能编译

解压之后大致会看到这些文件,我把最关键的处理顺序写一下。

文件后缀作用缺失后果
.dpr 或 .dproj工程入口,决定编译入口和项目配置无法编译
.pas / .cpp单元源码,窗体逻辑都在这里编译报错,找不到单元
.dfm / .fmx窗体布局描述窗体类定义不完整
.res资源文件,包含图标、版本信息部分工程编译失败
.dproj / .cbproj工程配置,含编译选项无法在 IDE 里正确加载

拿到这些文件以后,先确认一件事:.pas / .cpp 和 .dfm 是否配套齐全。经常有人把 frmWindowTest.dfm 漏发或者误删,结果一打开工程,Delphi 直接弹窗提示 “Error reading frmWindowTest.dfm: Property does not exist.” 这时候你先别急着改代码,先从备份或压缩包里找原文件,找不到就只能用文本方式打开 .dfm 逐行人工修复,非常痛苦。

2.3 在 Delphi 与 C++Builder 中打开并跑起的最短步骤

常见的做法是:先双击 .dpr 文件(如果关联了 Delphi IDE),或者打开 IDE 之后用 File -> Open Project 选择 .dpr。这时候 IDE 会询问是否要构建新工程,直接选打开历史工程即可。如果你是第一次在这台机器上跑,大概率会遇到两个问题:一是缺少第三方控件库,二是编译环境没切到正确的平台(Win32/Win64)。

缺少第三方控件库是最常见的拦路虎。frmWindowTest 这类工程如果引用了 VCL 自带的控件,那还好;一旦代码里uses了 DevExpress、JVCL 或其它非官方库,IDE 会报File not found。我的处理方式是:先按 Ctrl+F9 完整编译,看错误信息里缺的是哪个单元,再去安装对应控件包。没有捷径,只能装。

2.4 不打开 IDE 也能编译:用命令行 msbuild 做快速验证

如果你只是想把窗口跑起来看看效果,不想等 IDE 加载,可以用命令行编译。Delphi 的现代版本(XE 之后)都支持用 msbuild 编译:

# 在工程目录下执行,需要先把 rsvars.bat 加载进 PATH call "%ProgramFiles(x86)%\Embarcadero\Studio\22.0\bin\rsvars.bat" msbuild frmWindowTest.dproj /p:Config=Debug /p:Platform=Win32 /t:Build

先说逻辑:rsvars.bat是 Embarcadero 提供的环境变量脚本,会自动配好编译器路径和库路径。/p:Config=Debug指定构建配置,/p:Platform=Win32指定目标平台,/t:Build告诉 msbuild 执行编译而不是只做依赖分析。最后生成的 exe 会出现在Win32\Debug目录下,直接运行即可。

参数说明:如果你本机装的是 C++Builder,工程文件后缀是 .cbproj,命令基本不变,只需要把 .dproj 替换成 .cbproj。平台参数里,如果你的测试目标是 64 位系统下的 64 位进程,就把 Platform 改成 Win64,但这里提醒你注意:pywinauto 连接 64 位进程并没有额外障碍,所以如果工程支持,直接编译成 Win64 反而能避免很多旧控件兼容问题。

3. 把它变成自动化靶子:用 pywinauto 把 frmWindowTest 完整驱动起来

工程能编译、窗口能弹出来,只是第一步。真正有价值的用法,是让 frmWindowTest 变成一个可以被脚本操控的窗体,这样你以后回归测试、控件识别、布局校验都有一个稳定的底座。下面我从选型开始讲,再给一套可以直接复制的脚本。

3.1 为什么用 Windows UI Automation 这条链路驱动 Delphi 窗体

驱动 Windows GUI 的技术方案有四条常见路线:Win32 API 消息发送、UI Automation(UIA)、Windows Message 级别的 Hook、以及直接在 Delphi 内部写 DUnit 测试。对于 frmWindowTest 这种 VCL 窗体,我推荐用 pywinauto 走 UIA,原因很简单:VCL 控件本身没有暴露原生的 Accessibility 接口,但 pywinauto 能通过窗口句柄和控件层级把按钮、输入框、标签逐个找出来,兼容性比直接发 WM_CLICK 消息强很多。

有人会质疑:既然 VCL 控件没有原生 UIA 支持,为什么还推荐 pywinauto?因为 pywinauto 在uia后端下会通过系统的 UIA 桥接机制去匹配控件,即使是老式 VCL 控件,也能识别出编辑框和按钮的NameControlType,这就够了。如果你追求更精确的控件间关系,可以用win32后端走句柄枚举,但对窗口位置、大小、焦点状态的测试,uia后端明显更省心。

3.2 最小可用脚本:连接窗体、枚举控件、完成一次点击和断言

先把整个工程跑起来,获得 exe 的绝对路径。这里我假设编译输出在C:\work\frmWindowTest\Win32\Debug\frmWindowTest.exe。下面是一个完整的 pywinauto 脚本:

from pywinauto import Application EXE_PATH = r"C:\work\frmWindowTest\Win32\Debug\frmWindowTest.exe" # 启动应用并连接主窗口 app = Application(backend="uia").start(EXE_PATH, timeout=10) main_win = app.window(auto_id="frmWindowTest") main_win.wait("visible", timeout=10) # 打印当前窗口内的所有控件,方便确认控件名 main_win.print_control_identifiers() # 往编辑框里输入文本,再点击提交按钮 edit = main_win.child_window(auto_id="edtInput") edit.wait("ready", timeout=5) edit.set_text("hello frmWindowTest") btn = main_win.child_window(auto_id="btnSubmit") btn.click() # 读取结果标签的文本,并断言 result_label = main_win.child_window(auto_id="lblResult") result_text = result_label.window_text() assert "hello frmWindowTest" in result_text, f"断言失败,实际输出:{result_text}" print("测试通过,窗体响应正常")

逻辑说明:整个脚本从启动应用到断言一共五步。Application.start()负责拉起进程,app.window(auto_id="frmWindowTest")用控件 ID 定位主窗口,少绕很多弯路。print_control_identifiers()是调试利器,第一次跑的时候一定先执行这一行,把控件 ID 和类型打印出来看一眼,避免你猜的 auto_id 跟实际不符。后面的set_textclick是标准操作,就不展开了。

参数说明:backend="uia"是后端选择,如果连接不上再试win32,后面避坑章会展开讲。timeout统一设成 10 秒,是因为窗体在 Debug 模式下启动可能要加载调试符号,启动稍慢;脚本里每个控件操作之前都加一个wait,防止窗体出来了但控件还没创建完。

3.3 控件识别与参数细节:auto_id、timeout、backend 的取舍

很多新手会踩同一个坑:直接把控件标题当 auto_id 写进脚本。比如按钮上显示“提交”,就写child_window(title="提交"),这在大部分 VCL 窗体里是能命中的,因为 pywinauto 会按 title 匹配。但如果窗体里有两个按钮都叫“提交”,就要用 auto_id(VCL 里对应 Name 属性,例如btnSubmit)来区分。

关于 timeout,经验值是这样:窗体启动等待用 10 秒,控件 ready 等待用 5 秒,窗口 close 等待用 3 秒。不要全都用 30 秒,那样测试失败时等待成本太高。关于 backend 的选择,我把两者做个小对比。

维度uia 后端win32 后端
控件识别能力强,支持层级和模式弱,基于句柄枚举
VCL 控件兼容性一般,部分控件需自适应好,老控件识别率高
获取文本速度稍慢
适用场景现代应用、WPF、浏览器传统 Win32、Delphi 老工程

这个表不是绝对的。frmWindowTest 如果是老工程且使用了大量自定义绘制控件,uia 后端很可能识别不出内部元素,这时候切到 win32 后端反而轻松。我的建议是:脚本里先写 uia,跑不通再加一行切 win32 的注释,两套方案都留着,毕竟没人愿意在排查控件识别上耗太久。

3.4 和 DUnit 单元测试的分工:哪些用例该放窗体层

frmWindowTest 如果本身挂接了测试框架(比如 DUnit),你要想清楚每一类验证放在哪一层。窗体自动化适合测的是:窗口能否正常创建、按钮是否能点击、点击后界面状态是否符合预期、控件间的联动是否正常。而具体到一个数学计算函数、字符串处理过程,应该直接写 DUnit 用例在逻辑层测,不要通过界面输入去间接验证。

举例说明:假如 frmWindowTest 里有一个把输入的句子转换成大写并显示在标签上的功能,正确的分层是:函数UpperCase的结果用 DUnit 断言;而“用户在编辑框输入小写、点击按钮、看到标签变成大写”这个链路,才属于窗体自动化用例。两层结合覆盖才算完整,只有界面测试的话,一个逻辑 bug 会绕个大圈才能被发现。

4. 窗体实测路上的坑:从解压到跑通,最容易翻车的五个位置

这一章我直接写踩坑记录,每条都是我在实际跑窗体测试时真实遇到过的。 frmWindowTest 听起来简单,但工程从解压到能被脚本稳定驱动,中间隔着不少暗坑。没有废话,直接看现象、原因和解决方式。

4.1 路径里的空格和中文让编译链路直接断掉

现象:把 frmWindowTest.rar 解压到C:\Users\我的文档\测试项目\之后,双击 .dpr 打开工程,点编译,IDE 报错 “Cannot open file: "C:\Users\我的文档\测试项目\frmWindowTest.dpr"”。问题是文件明明在那里。

原因:Delphi 的编译器在处理包含中文和空格的路径时,某些版本会生成错误的面板路径,导致资源编译(BRCC32)失败。这算是一个历史遗留问题,Delphi 2007 及之前的版本尤其严重,高版本有所缓解但没根治。

解决:解压路径只用英文和数字,根目录放到C:\work\或者D:\test\这样干净的地方,并且不要带空格。我的习惯是一切自动化相关工程都用C:\work\打底,省掉这类玄学问题。

4.2 控件名对不上:Name 属性和窗口标题不是一回事

现象:脚本里写main_win.child_window(title="frmWindowTest")永远找不到控件,报 ElementNotFoundError,但用截图工具明明能看到窗口上有这些元素。

原因:Delphi 窗体的 Name 属性(比如TfrmWindowTest)是窗体类的类名,而窗口标题(Caption)是显示在标题栏上的文字,两者独立。pywinauto 按 title 匹配时,匹配的是窗口标题,不是类名。如果窗体标题被改成“窗口测试工具”,那 title 就变成了“窗口测试工具”。

解决:统一用auto_idclass_name来定位,不要依赖 title。auto_id在 VCL 里对应 Delphi 控件的 Name 属性,稳定性远高于用户可修改的标题。如果你实在拿不到 auto_id,先在脚本里加一行print_control_identifiers()把控件树完整打出来,看到真实属性再写定位代码。

4.3 等待不稳定:窗体出现了但控件还没就绪

现象:脚本连上主窗口之后立刻去找编辑框,结果偶尔能找到,偶尔报TimeoutError。尤其是加载慢的机器上,十次里有三次不稳定。

原因:app.window()只确认窗口句柄存在,不代表窗体内的所有 VCL 控件都已经创建完成。Delphi 创建窗体时是先创建窗体对象,再逐个子控件加载,整个过程是异步的,窗口显示出来并不等于控件树完整。

解决:不要只等主窗口,等你要操作的控件自己。把main_win.wait("visible")改成edit.wait("ready", timeout=10),并且在每次操作后加必要的sleep(0.5)。这是很多人写脚本时最容易忽略的环节,也是窗体自动化从“偶尔能跑通”到“稳定能跑通”的关键一步。

4.4 DPI 缩放让坐标点击全部偏位

现象:同样的脚本,在 1080p 100% 缩放的机器上能跑通,拿到 2K 屏 150% 缩放的机器上,click()点到了按钮外面,或者读到的窗口尺寸比实际尺寸大。

原因:Windows 在 DPI 缩放大于 100% 时,会做逻辑坐标与物理坐标的转换。pywinauto 默认按逻辑坐标操作,但如果 Delphi 工程没有声明 Per-Monitor DPI Aware,系统会对它的窗口做位图拉伸,导致控件实际位置与传给 UIA 的坐标不一致。

解决:给 exe 添加 DPI 感知声明。在 Delphi 工程的 .dpr 里加一句SetProcessDPIAware(),或者在项目属性里声明 DPI Awareness。清理做法是在工程文件的开头加一行 Manifest 设置,让进程自己告诉我们它要按物理像素工作。改完重新编译,再跑脚本就准了。

4.5 32 位 / 64 位进程和后台运行模式的识别问题

现象:脚本能连接进程,但window()返回的对象找不到控件;或者脚本在远程桌面会话里跑的时候,窗口一直显示不出来。

原因:32 位和 64 位进程的窗口结构有差异,pywinauto 的win32后端在连接 64 位进程时偶尔会拿不到完整的控件列表,需要切uia。远程桌面场景则是 Windows 的会话隔离机制导致 GUI 程序在非交互会话中无法正常创建窗口。

解决:进程是 32 位就保持 Win32 编译,是 64 位就保持 Win64,然后据此选 backend。远程桌面跑自动化时,确认你是在登录后的会话里执行脚本,不要用计划任务 + 未登录状态下跑,那必挂。如果确实要无人值守跑,需要额外做虚拟桌面方案,但这对 frmWindowTest 这种单窗体工程来说投入产出比不高。

5. 一个高性价比的窗体验证技巧:控件树快照 + 截图基线

窗体测试跑到后期,你会面临一个常见烦恼:改了一行布局代码,不知道有没有把按钮的可用状态或窗口尺寸改坏。逐条人工核对太慢,纯用断言又覆盖不全。我的做法是给 frmWindowTest 这样的测试窗体建一份“控件树快照 + 截图基线”,每次改动后自动比对,任何意外变化都会被揪出来。

先写一个快照脚本,把窗体的控件树和关键属性落盘成 JSON:

import json from pywinauto import Application app = Application(backend="uia").connect(path=r"C:\work\frmWindowTest\Win32\Debug\frmWindowTest.exe", timeout=10) win = app.window(auto_id="frmWindowTest") win.wait("visible", timeout=10) def dump_ctrl(node, depth=0): info = { "type": node.element_info.control_type, "name": node.element_info.name, "rect": node.rectangle()._asdict() if node.exists() else None, "enabled": node.is_enabled(), } children = [dump_ctrl(child, depth + 1) for child in node.children()] if children: info["children"] = children return info with open("control_snapshot.json", "w", encoding="utf-8") as f: json.dump(dump_ctrl(win), f, ensure_ascii=False, indent=2) win.capture_as_image().save("window_baseline.png") print("快照与截图已保存")

这段脚本先连接窗体,递归遍历控件树,把控件类型、名称、矩形区域和启用状态塞进嵌套字典,最后整体落盘 JSON。同时把当前窗口截一张图保存。第一次跑完,这两份文件就是你的回归基线。之后每次功能改动完,重跑一遍,用 Beyond Compare 或任何 diff 工具对比 JSON 内容,能一眼看到哪个按钮变了位置、哪个控件被禁用。

这个技巧最爽的用法是接进测试流程做自动比对:改动代码前跑一次生成 basline,改动后跑一次生成 current,然后比对;如果你还愿意多花半天时间,可以把这个比对步骤直接写进 CI 脚本里,每次提交代码自动截窗比对。以后不管谁动了这个窗体布局,只要控件树和基线上有差异,CI 就会亮红,这比任何人肉回归都可靠。

我个人的习惯是把这份快照脚本固化成一个模块,所有拿 frmWindowTest 类型工程做界面回归测试时都调用它。时间久了你会发现,维护的其实不是某一个脚本,而是一套稳定的窗体验证体系。想真正把窗体测试做得有底气,照着这套组合拳,会少走很多弯路。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询