
简介这是一份面向 C# 与机器视觉开发者的 WinForm 工程示例演示如何调用 Halcon 图像处理库并利用笔记本内置摄像头通过 DirectShow 获取实时视频流最终完成二维码识别。工程文件完整共 36 个文件以 C# 源码为主包含动态链接库、可执行程序、配置文件、资源文件以及测试图片压缩包大小约 11.05 MB。目录结构包括窗体设计、程序入口、项目配置等模块适合学习桌面程序开发、视频采集与视觉算法之间的集成方式也方便直接打开工程进行二次修改。包内提供的测试二维码和示例图像有助于验证从摄像头采集到二维码解析的完整流程减少环境配置与参数调节的时间。目前已有 460 人学习对于希望快速上手 Halcon 与 C# 结合的开发者是一份实操性较强的参考资料。1. frmWindowTest.rar 是给谁用的先把窗体测试这件事的边界划清楚frmWindowTest.rar 这个文件名一眼就能看出大半信息frm是 Delphi / CBuilder 工程里窗体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-Zip7-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 与 CBuilder 中打开并跑起的最短步骤常见的做法是先双击 .dpr 文件如果关联了 Delphi IDE或者打开 IDE 之后用 File - Open Project 选择 .dpr。这时候 IDE 会询问是否要构建新工程直接选打开历史工程即可。如果你是第一次在这台机器上跑大概率会遇到两个问题一是缺少第三方控件库二是编译环境没切到正确的平台Win32/Win64。缺少第三方控件库是最常见的拦路虎。frmWindowTest 这类工程如果引用了 VCL 自带的控件那还好一旦代码里uses了 DevExpress、JVCL 或其它非官方库IDE 会报File not found。我的处理方式是先按 CtrlF9 完整编译看错误信息里缺的是哪个单元再去安装对应控件包。没有捷径只能装。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:ConfigDebug /p:PlatformWin32 /t:Build先说逻辑rsvars.bat是 Embarcadero 提供的环境变量脚本会自动配好编译器路径和库路径。/p:ConfigDebug指定构建配置/p:PlatformWin32指定目标平台/t:Build告诉 msbuild 执行编译而不是只做依赖分析。最后生成的 exe 会出现在Win32\Debug目录下直接运行即可。参数说明如果你本机装的是 CBuilder工程文件后缀是 .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 AutomationUIA、Windows Message 级别的 Hook、以及直接在 Delphi 内部写 DUnit 测试。对于 frmWindowTest 这种 VCL 窗体我推荐用 pywinauto 走 UIA原因很简单VCL 控件本身没有暴露原生的 Accessibility 接口但 pywinauto 能通过窗口句柄和控件层级把按钮、输入框、标签逐个找出来兼容性比直接发 WM_CLICK 消息强很多。有人会质疑既然 VCL 控件没有原生 UIA 支持为什么还推荐 pywinauto因为 pywinauto 在uia后端下会通过系统的 UIA 桥接机制去匹配控件即使是老式 VCL 控件也能识别出编辑框和按钮的Name、ControlType这就够了。如果你追求更精确的控件间关系可以用win32后端走句柄枚举但对窗口位置、大小、焦点状态的测试uia后端明显更省心。3.2 最小可用脚本连接窗体、枚举控件、完成一次点击和断言先把整个工程跑起来获得 exe 的绝对路径。这里我假设编译输出在C:\work\frmWindowTest\Win32\Debug\frmWindowTest.exe。下面是一个完整的 pywinauto 脚本from pywinauto import Application EXE_PATH rC:\work\frmWindowTest\Win32\Debug\frmWindowTest.exe # 启动应用并连接主窗口 app Application(backenduia).start(EXE_PATH, timeout10) main_win app.window(auto_idfrmWindowTest) main_win.wait(visible, timeout10) # 打印当前窗口内的所有控件方便确认控件名 main_win.print_control_identifiers() # 往编辑框里输入文本再点击提交按钮 edit main_win.child_window(auto_idedtInput) edit.wait(ready, timeout5) edit.set_text(hello frmWindowTest) btn main_win.child_window(auto_idbtnSubmit) btn.click() # 读取结果标签的文本并断言 result_label main_win.child_window(auto_idlblResult) result_text result_label.window_text() assert hello frmWindowTest in result_text, f断言失败实际输出{result_text} print(测试通过窗体响应正常)逻辑说明整个脚本从启动应用到断言一共五步。Application.start()负责拉起进程app.window(auto_idfrmWindowTest)用控件 ID 定位主窗口少绕很多弯路。print_control_identifiers()是调试利器第一次跑的时候一定先执行这一行把控件 ID 和类型打印出来看一眼避免你猜的 auto_id 跟实际不符。后面的set_text和click是标准操作就不展开了。参数说明backenduia是后端选择如果连接不上再试win32后面避坑章会展开讲。timeout统一设成 10 秒是因为窗体在 Debug 模式下启动可能要加载调试符号启动稍慢脚本里每个控件操作之前都加一个wait防止窗体出来了但控件还没创建完。3.3 控件识别与参数细节auto_id、timeout、backend 的取舍很多新手会踩同一个坑直接把控件标题当 auto_id 写进脚本。比如按钮上显示“提交”就写child_window(title提交)这在大部分 VCL 窗体里是能命中的因为 pywinauto 会按 title 匹配。但如果窗体里有两个按钮都叫“提交”就要用 auto_idVCL 里对应 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(titlefrmWindowTest)永远找不到控件报 ElementNotFoundError但用截图工具明明能看到窗口上有这些元素。原因Delphi 窗体的 Name 属性比如TfrmWindowTest是窗体类的类名而窗口标题Caption是显示在标题栏上的文字两者独立。pywinauto 按 title 匹配时匹配的是窗口标题不是类名。如果窗体标题被改成“窗口测试工具”那 title 就变成了“窗口测试工具”。解决统一用auto_id或class_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, timeout10)并且在每次操作后加必要的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 这样的测试窗体建一份“控件树快照 截图基线”每次改动后自动比对任何意外变化都会被揪出来。先写一个快照脚本把窗体的控件树和关键属性落盘成 JSONimport json from pywinauto import Application app Application(backenduia).connect(pathrC:\work\frmWindowTest\Win32\Debug\frmWindowTest.exe, timeout10) win app.window(auto_idfrmWindowTest) win.wait(visible, timeout10) def dump_ctrl(node, depth0): 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, encodingutf-8) as f: json.dump(dump_ctrl(win), f, ensure_asciiFalse, indent2) win.capture_as_image().save(window_baseline.png) print(快照与截图已保存)这段脚本先连接窗体递归遍历控件树把控件类型、名称、矩形区域和启用状态塞进嵌套字典最后整体落盘 JSON。同时把当前窗口截一张图保存。第一次跑完这两份文件就是你的回归基线。之后每次功能改动完重跑一遍用 Beyond Compare 或任何 diff 工具对比 JSON 内容能一眼看到哪个按钮变了位置、哪个控件被禁用。这个技巧最爽的用法是接进测试流程做自动比对改动代码前跑一次生成 basline改动后跑一次生成 current然后比对如果你还愿意多花半天时间可以把这个比对步骤直接写进 CI 脚本里每次提交代码自动截窗比对。以后不管谁动了这个窗体布局只要控件树和基线上有差异CI 就会亮红这比任何人肉回归都可靠。我个人的习惯是把这份快照脚本固化成一个模块所有拿 frmWindowTest 类型工程做界面回归测试时都调用它。时间久了你会发现维护的其实不是某一个脚本而是一套稳定的窗体验证体系。想真正把窗体测试做得有底气照着这套组合拳会少走很多弯路。希望帮到你。本文还有配套的精品资源点击获取