简介:这是一份面向C#桌面开发者与机器视觉入门者的示例工程,演示如何在WinForm窗体中集成Halcon图像库,借助DirectShow调取笔记本摄像头实时画面,并完成二维码识别。项目提供完整源码与测试二维码,适合想快速上手C#与Halcon联合开发、处理相机视频流的读者参考。压缩包共36个文件,体积约11MB,包含9个C#源码文件(窗体逻辑、程序入口)、4个DLL动态库、3个EXE可执行程序以及配置、资源、工程文件等,目录结构清晰,打开解决方案即可查看工程全貌。目前已有460人学习。通过学习该工程,可理解WinForm中托管DirectShow滤镜图的搭建方式,掌握Halcon条码读取参数的配置思路,并可直接运行程序验证摄像头采集与解码效果,为后续开发视觉检测工具提供可复用起点。
1. 拿到 frmWindowTest.rar 先别急着解压:这个 Delphi 窗口测试工具包解决什么问题
看到 frmWindowTest.rar 这个名字,老 Delphi 开发基本能猜出七八分:frm 是表单前缀,WindowTest 是测试对象,rar 是传阅压缩。这类压缩包里跑不出什么大框架,大概率是 .dpr、.pas、.dfm 三件套加上一两个资源文件——一个典型的窗口自测工具。它解决的问题很具体:验证目标窗口是否存在、句柄能不能拿到、发一条消息过去窗口有没有反应,以及目标窗口对 WM_CLOSE、WM_SETTEXT 这类常用消息的处理是否符合预期。适合接手老桌面项目时快速摸清界面行为,也适合给 UI 自动化脚本做前置探针。别把它当病毒,也别指望它是通用测试框架——它就是一把趁手的小刀。
2. 拆开 frmWindowTest 的骨架:.dpr、.pas、.dfm 如何共同撑起一个窗体测试程序
2.1 命名规则泄露的信息:frm 前缀、TfrmWindowTest 类与三个核心文件
Delphi 工程的命名规矩几十年没变:单元文件叫 uMain.pas,里面的窗体类就叫 TfrmMain;叫 uWindowTest.pas,窗体类就是 TfrmWindowTest。frm 前缀是 Team 代码规范里最常见的一条,看到这个压缩包名,说明原作者至少守住了「窗体类必须有 frm 前缀」这条红线。
解压之后你基本会看到三个文件:
| 文件 | 作用 | 测试工具里承担的角色 |
|---|---|---|
| .dpr | 工程入口,负责 Application.Initialize 和 CreateForm | 启动时决定先显示哪个窗体,frmWindowTest 通常是主窗体 |
| .pas | 单元源码,写窗体类的字段、方法和事件响应 | 句柄探测、消息发送的全部逻辑都在这 |
| .dfm | 窗体资源,描述控件布局、属性和事件绑定 | 决定测试工具长什么样:输入框、按钮、日志框 |
.dfm 看着像文本,其实是窗体资源,丢一行格式不对整个窗体都加载不了。Delphi 的 .dfm 支持文本和二进制两种存储,老工程里常见二进制 .dfm,用右键 Open As Text 才能转文本查看。frmWindowTest 这种自测工具,.dfm 一般不会太复杂,几个 TEdit、TButton、TMemo 就够了。
打开 .pas,你会看到类似这样的骨架:
unit uWindowTest; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Classes, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls; type TfrmWindowTest = class(TForm) edtTarget: TEdit; // 输入目标窗口标题关键字 btnFind: TButton; // 触发窗口查找 btnSend: TButton; // 给目标窗口发消息 mmLog: TMemo; // 显示查找结果和消息发送结果 procedure btnFindClick(Sender: TObject); procedure btnSendClick(Sender: TObject); private FTargetHandle: THandle; procedure Log(const AMsg: string); end; var frmWindowTest: TfrmWindowTest; implementation {$R *.dfm} procedure TfrmWindowTest.Log(const AMsg: string); begin mmLog.Lines.Add(Format('[%s] %s', [TimeToStr(Now), AMsg])); end; end.这个骨架透露了两个关键设计:一是 FTargetHandle 字段用来保存找到的窗口句柄,二是 Log 方法统一输出带时间戳的日志到 TMemo。这两个东西是整个窗口测试工具的地基——句柄是测试对象,日志是观察手段。
2.2 窗口测试的本质:句柄可获取、消息有响应、闭环可观察
窗口测试为什么不能只靠「看界面有没有弹出来」?因为 Windows 的窗口本质是一个消息驱动的对象:窗口的显示、移动、关闭、重绘,全部靠消息队列和窗口过程驱动。你调用 ShowWindow、SendMessage,最终都变成一条条 WM_ 开头的消息投递到目标窗口过程里。所以窗口测试做三件事就能覆盖绝大多数场景:拿句柄、发消息、观察结果。
拿句柄解决「窗口在不在」的问题,发消息解决「窗口听不听话」的问题,观察结果解决「消息到底有没有生效」的问题。三者凑成一个闭环:
function IsWindowResponsive(AHandle: HWND; const ANewTitle: string): Boolean; var OldTitle, CurTitle: array[0..511] of Char; begin Result := False; if not IsWindow(AHandle) then Exit; GetWindowText(AHandle, OldTitle, Length(OldTitle)); // 发 WM_SETTEXT,同步等待执行完成 if SendMessage(AHandle, WM_SETTEXT, 0, LPARAM(PChar(ANewTitle))) = 1 then begin // 读回标题,确认改动真的生效 GetWindowText(AHandle, CurTitle, Length(CurTitle)); Result := string(CurTitle) = ANewTitle; end; end;这里最容易忽略的一点是 SendMessage 是同步调用。它会一直等到目标窗口过程处理完 WM_SETTEXT 才返回,返回值等于 1 表示处理成功。但「返回值等于 1」和「标题真的改了」是两回事——有的窗口处理函数偷懒,收到消息直接返回默认值,什么也没干。所以测试逻辑里必须把标题读回来做二次确认,这也是 frmWindowTest 这类工具比人眼观察更可靠的地方:它验证的是结果,不是意图。
这个小函数就是后面整个工具的核心模型。后面要写的窗体,本质上就是把「目标窗口列表、消息类型、验证方式」做成可视化输入,让测试逻辑复用这一套闭环。
3. 复现 frmWindowTest 的核心功能:句柄探测、消息发送与结果回显
3.1 最小界面:TEdit 输标题、TMemo 看日志、TButton 触发动作
自己动手写一个窗口测试工具,界面不用花哨。核心就三个输入输出点:一个 TEdit 接收目标窗口标题关键字,一个 TMemo 做日志回显,几个 TButton 触发查找、发消息、清空日志的操作。
.dfm 里布局大概长这样:
object frmWindowTest: TfrmWindowTest Left = 0 Top = 0 Caption = 'frmWindowTest - 窗口测试工具' ClientHeight = 330 ClientWidth = 520 object edtTarget: TEdit Left = 12 Top = 12 Width = 260 Hint = '输入目标窗口标题的关键字' end object btnFind: TButton Left = 280 Top = 10 Width = 110 Height = 25 Caption = '查找窗口' OnClick = btnFindClick end object btnClose: TButton Left = 396 Top = 10 Width = 110 Height = 25 Caption = '发送 WM_CLOSE' OnClick = btnCloseClick end object mmLog: TMemo Left = 12 Top = 44 Width = 496 Height = 274 ReadOnly = True ScrollBars = ssVertical end end为什么用 TEdit 而不是 TComboBox?因为测试窗口时,标题关键字经常是反复试出来的——先输入一个可能的名字,找不到就换一个。TEdit 天然适合这种快速改写的场景。TMemo 的 ReadOnly 必须设 True,防止测试过程中不小心动到日志内容。ScrollBars 设 ssVertical,因为窗口多了日志会长得很快,没有滚动条后面几条日志根本看不到。
窗体加载时顺手做一件事:把自己的窗口信息打一条日志。这是调试期最便宜的验证手段——如果连自己的句柄都拿不对,后面测别人的窗口全是白搭。
procedure TfrmWindowTest.FormShow(Sender: TObject); var ClsName: array[0..255] of Char; begin GetClassName(Self.Handle, ClsName, Length(ClsName)); Log(Format('本窗口句柄: %d, 类名: %s', [Self.Handle, ClsName])); end;这里 Self.Handle 就是当前窗体的窗口句柄。Delphi 的 TForm 创建后,Handle 才真正指向一个 Win32 窗口,之前访问 Handle 会触发隐式创建,这个行为在窗口测试里经常被忽略——你拿到的 Handle 可能不是你以为的那个窗口的。
3.2 用 FindWindow 和 EnumWindows 拿到目标窗口句柄
窗口测试的第一步永远是「找到窗口」。两种方式:按标题/类名精确找用 FindWindow,按条件枚举找用 EnumWindows。
FindWindow 用起来最简单,但限制也明显——只能按类名和窗口标题精确匹配,不能模糊匹配。实际测试中目标窗口的标题往往带动态后缀,比如「文档1 - 记事本」,所以 frmWindowTest 这类工具的核心查找逻辑更推荐用 EnumWindows 自己过滤:
function EnumFindByTitle(AWindow: HWND; AParam: LPARAM): BOOL; stdcall; var Buf: array[0..511] of Char; begin Result := True; // 默认继续枚举 if GetWindowText(AWindow, Buf, Length(Buf)) = 0 then Exit; // AParam 传进来的是 TfrmWindowTest 对象自身 if Pos(TfrmWindowTest(AParam).FKeyword, string(Buf)) > 0 then begin TfrmWindowTest(AParam).FTargetHandle := AWindow; Result := False; // 找到目标,停止枚举 end; end; procedure TfrmWindowTest.btnFindClick(Sender: TObject); begin FTargetHandle := 0; FKeyword := Trim(edtTarget.Text); if FKeyword = '' then begin Log('请输入窗口标题关键字'); Exit; end; EnumWindows(@EnumFindByTitle, LPARAM(Self)); if FTargetHandle <> 0 then Log(Format('找到窗口: %s, 句柄: %d', [edtTarget.Text, FTargetHandle])) else Log(Format('未找到标题包含 [%s] 的窗口', [FKeyword])); end;回调函数里有几个细节值得说。第一,Result 必须初始化为 True,系统依靠返回值决定是否继续枚举,返回 False 会立即终止整个枚举过程,所以在找到目标后返回 False 能省掉大量无效遍历。第二,AParam 这个 LPARAM 参数是唯一的上下文通道,回调函数里拿不到 Self,只能靠 AParam 把对象自身传进来。第三,GetWindowText 拿到的标题会被截断到 511 个字符,对窗口测试来说足够用。
这套 EnumWindows 方案比 FindWindow 灵活得多:你可以在回调里加类名匹配、进程 ID 过滤、可见性判断,后面所有进阶玩法都是在这个基础上叠加条件。
3.3 发送 WM_CLOSE 与 WM_SETTEXT:同步消息的返回值怎么判
拿到句柄以后,测试工具的发消息环节就好办了。最常用的测试消息就两个:WM_CLOSE 验证窗口能不能被正常关闭,WM_SETTEXT 验证窗口标题能不能被修改。
procedure TfrmWindowTest.btnCloseClick(Sender: TObject); begin if not IsWindow(FTargetHandle) then begin Log('目标句柄已失效,请重新查找窗口'); Exit; end; // 同步发送关闭消息,等待窗口处理完 SendMessage(FTargetHandle, WM_CLOSE, 0, 0); Log(Format('已向句柄 %d 发送 WM_CLOSE', [FTargetHandle])); end; procedure TfrmWindowTest.btnSetTextClick(Sender: TObject); var NewTitle: string; begin if not IsWindow(FTargetHandle) then begin Log('目标句柄已失效,请重新查找窗口'); Exit; end; NewTitle := 'WindowTest_' + FormatDateTime('hhnnss', Now); // WM_SETTEXT 返回 TRUE 表示处理成功 if SendMessage(FTargetHandle, WM_SETTEXT, 0, LPARAM(PChar(NewTitle))) = 1 then Log(Format('标题修改成功: %s', [NewTitle])) else Log('标题修改失败或被拒绝'); end;写这段代码时有两个必须注意的坑。一是 PChar(NewTitle) 的临时转换——SendMessage 是同步的,消息处理完才返回,所以 PChar 指向的内存只要在调用期间有效就行,NewTitle 作用域覆盖了整段代码,安全。但如果换成 PostMessage,这里就成悬垂指针了,必须用 StrNew 或全局变量保住字符串生命周期。二是 WM_CLOSE 的返回值没有实际意义,它只是把关闭消息投递给目标窗口,窗口可能收到消息但拒绝关闭——所以测试逻辑里对 WM_CLOSE 的判断要看窗口是否真的消失,而不是看 SendMessage 返回什么。
4. 不玄学的参数设定:匹配模式、枚举回调耗时与轮询间隔的落地取值
4.1 三种标题匹配模式:精确、前缀和正则,分场景选型
很多人在写窗口匹配时吃过亏:用了精确匹配,测试目标窗口标题加了版本号就找不到了;换了模糊匹配,又误匹配到一堆同名窗口。frmWindowTest 这类工具里,匹配模式不是玄学,按场景选就行。
| 模式 | 实现方式 | 适用场景 | 误匹风险 |
|---|---|---|---|
| 精确匹配 | CompareStr(标题, 关键字) = 0 | 窗口标题固定,如登录框 | 低 |
| 前缀匹配 | 标题以关键字开头 | 标题带动态后缀,「文档 - 记事本」类 | 中 |
| 包含匹配 | Pos(关键字, 标题) > 0 | 只记得标题一部分 | 高 |
实际工程中,包含匹配是默认选项,因为它是三者里唯一能在「只知道一个词」的情况下工作的。但用包含匹配时必须配合控件层级过滤——比如先按类名定位到目标窗体的主窗口,再在子窗口里做标题包含匹配,把误匹范围压缩到可控区间。
正则匹配要不要上?我一般不建议在窗口测试工具里用正则,原因很现实:窗口标题数量级通常只有两位数,正则带来的表达力提升远小于调试成本。窗口测试的核心问题是「找到对的那个窗口」,不是「写出炫的匹配式」。
4.2 EnumWindows 回调里的时间预算:为什么不能 Sleep
EnumWindows 回调是跑在系统枚举上下文里的,它不属于你的业务线程。回调里做耗时操作,卡住的不是你自己的代码,是整个窗口枚举机制,严重时会让系统觉得你的进程无响应,直接给你弹一个「程序未响应」的窗口。
我见过最离谱的写法是在回调里调 Sleep(100) 做节流,理由是想「慢一点枚举,防止漏掉窗口」。这是完全没理解枚举机制:EnumWindows 是同步抓取系统窗口快照,不是持续监听。你在回调里 Sleep,系统就挂着等你,等完了继续枚举下一个,除了把整个调用拖慢,没有任何过滤效果。
回调代码保持一个原则:只收集、不处理。把命中的句柄和标题存进列表,EnumWindows 返回后回到主线程再慢慢分析。
var TempList: TList<THandle>; function EnumCollect(AWindow: HWND; AParam: LPARAM): BOOL; stdcall; begin // 回调里只做收集 if IsWindowVisible(AWindow) then TList<THandle>(AParam).Add(AWindow); Result := True; end; procedure TfrmWindowTest.btnCollectClick(Sender: TObject); var I: Integer; begin TempList := TList<THandle>.Create; try EnumWindows(@EnumCollect, LPARAM(TempList)); // 返回后再做耗时处理 for I := 0 to TempList.Count - 1 do Log(Format('可见窗口句柄: %d', [TempList[I]])); finally TempList.Free; end; end;这里有另一个隐蔽问题:回调里调用 IsWindowVisible 是快的,但如果你在这基础上再调 GetWindowText 加 GetWindowThreadProcessId,单次回调的开销就会上去。系统里窗口数量多的时候,累计的时间差会被放大。所以判断逻辑尽量精简,能在回调外做的判断绝不放回调里。
4.3 TTimer 轮询自测:间隔、消息堵塞与 UI 线程的取舍
窗口测试里经常要轮询一个窗口的创建和销毁——比如点了一个按钮,目标程序弹出一个新窗口,你要等它出现然后自动抓取。这种场景用 TTimer 轮询最省事。
但 TTimer 的参数别随便填。Windows 默认计时器精度是 15.6 毫秒,你设一个 Interval = 10 毫秒,实际触发频率还是 64 Hz 左右,白白浪费 CPU。轮询窗口出现这种场景,间隔 200 毫秒足够;轮询窗口销毁,间隔可以放到 500 毫秒;如果轮询里还带着 SendMessage 调用,间隔就得上千毫秒。
procedure TfrmWindowTest.tmrPollTimer(Sender: TObject); begin tmrPoll.Enabled := False; try FTargetHandle := FindWindow(nil, PChar(FExpectedTitle)); if FTargetHandle <> 0 then begin Log(Format('轮询发现窗口: %d', [FTargetHandle])); // 这里再发测试消息 SendMessage(FTargetHandle, WM_CLOSE, 0, 0); end; finally // 没找到的话继续下一轮 if FTargetHandle = 0 then tmrPoll.Enabled := True; end; end;这里的关键是把 Timer 先停掉再干活,干完活再开。如果开着 Timer 直接在里面发 SendMessage,而目标窗口正好不响应,SendMessage 会卡住 UI 线程,Timer 消息全排在队列里,等 SendMessage 超时返回,积压的 Timer 消息会连续触发——表现出来就是日志刷屏、界面卡死。停掉再干活,能把这个连锁反应切断。
5. 避坑:窗口测试工具最常见的 5 个翻车现场
5.1 句柄存进 32 位整型被截断,窗口永远匹配不上
现象:FindWindow 或 EnumWindows 明明返回了非 0 句柄,但 Log 里显示的句柄是负数,或者拿这个句柄去调 IsWindow 永远返回 False。
原因:老 Delphi 工程的通病——有人把 HWND 塞进 LongInt 或 Integer。Windows 64 位系统上句柄是 64 位指针,截图存进 32 位整型,高位直接被截断。这个问题在 32 位系统上跑不出来,一上 64 位系统就现原形。
解决:所有窗口句柄的存储统一用 THandle 类型,不要用 LongInt、Integer、LongWord 等历史遗留类型。检查代码里有没有 LongInt(FindWindow(...)) 这类强转,一个都不能留。
// 错误写法:句柄被截断 var H: LongInt; begin H := LongInt(FindWindow(nil, '记事本')); end; // 正确写法 var H: THandle; begin H := FindWindow(nil, '记事本'); end;THandle 在 64 位编译下就是 64 位无符号整型,和 HWND 完全对齐。
5.2 目标窗口被「最小化到托盘」后 FindWindow 扑空
现象:程序明明在运行,任务管理器里能看到进程,但 FindWindow 返回 0。怀疑是标题不对,换了各种方式匹配都找不到。
原因:很多程序「最小化到托盘」不是真的最小化,而是把主窗口隐藏甚至销毁了。隐藏窗口调用 ShowWindow(SW_HIDE) 后窗口还在,FindWindow 理论上能找到;但如果程序直接把主窗口 DestroyWindow,然后只在托盘区保留一个图标,那窗口对象已经没了,FindWindow 当然扑空。
解决:先用 EnumWindows 把所有可见窗口打出来看看目标窗口到底还在不在。如果窗口对象销毁了,只能从进程角度入手——用 CreateToolhelp32Snapshot 按进程名找到 PID,再 SetupDiGetClassDevs 之类的设备接口去拿主窗口句柄。一个更实用的方式是:如果你的目标程序是自家产品,直接在程序里加一个 /autotest 参数,启动时把窗口句柄写到共享内存或文件里,测试工具直接从那里读,省去一切猜测。
5.3 SendMessage 卡死调用线程:UAC 与跨 session 消息隔离
现象:向某个窗口 SendMessage(WM_CLOSE) 后,测试工具整个界面卡住,鼠标转圈,过几秒才恢复。不是每次都卡,只针对某些目标窗口出现。
原因:SendMessage 是同步消息,发送线程会一直等待目标窗口过程处理完才继续执行。如果目标窗口属于更高完整性级别的进程——比如以管理员权限运行的窗口——普通权限进程的消息会被 UAC 隔离机制拦截,SendMessage 的等待时间被拖长。目标窗口消息循环如果正忙,卡顿更明显。
解决:能用 PostMessage 就不用 SendMessage。PostMessage 是异步投递,发完就返回,不会卡调用线程。如果必须拿返回值,用 SendMessageTimeout 并设置超时:
var Res: LRESULT; begin // 500ms 超时,目标不响应就直接放弃 SendMessageTimeout(FTargetHandle, WM_SETTEXT, 0, LPARAM(PChar('NewTitle')), SMTO_ABORTIFHUNG, 500, Res); if Res = 1 then Log('消息发送成功') else Log('消息发送超时或失败'); end;SMTO_ABORTIFHUNG 这个标志位是关键——它告诉系统,如果目标窗口在超时时间内不处理消息,直接放弃而不是无限等待。窗口测试工具里这个参数几乎是必设的。
5.4 中文窗口标题匹配失败,问题出在 ANSI 和 Wide 的转换
现象:目标窗口标题是中文,FindWindow 找不到;但用 Spy++ 看,窗口明明就挂在那里,标题也确实是你要找的那个。
原因:Windows 窗口标题内部是 UTF-16 存储,FindWindow 的 A 版本(FindWindowA)会把参数按 ANSI 代码页转换后再比较。如果你的测试代码里硬编码了中文标题,但工程没有启用 Unicode,编译器会走 FindWindowA,中文字符经过 ANSI 转换就变了样。
解决:Delphi 2009 之后默认编译模式就是 Unicode,string 直接是 UTF-16,直接调用 FindWindow 会走 W 版本,不会出问题。老工程或者用了第三方非 Unicode 控件的工程,显式调用宽字符版本:
var WTitle: array[0..511] of WideChar; begin StringToWideChar('中文标题测试', WTitle, Length(WTitle)); FTargetHandle := FindWindowW(nil, WTitle); end;读标题同理,用 GetWindowTextW 加 WideChar 数组,避免回 ANSI 时丢字。
5.5 枚举回调里弹 MessageBox,把整个窗口测试卡死
现象:点击「查找窗口」,程序界面直接变白,任务管理器显示「未响应」。强杀进程后重新打开,只要不点查找就没事。
原因:EnumWindows 回调函数里调用了 MessageBox 或者 Application.ProcessMessages。回调执行期间系统持有一个内部锁,此时弹模态窗口会进入嵌套消息循环,系统在等回调返回,回调在等 MessageBox 关闭,死锁。
解决:回调里绝不允许出现任何可能触发消息循环的调用。收集完句柄,等 EnumWindows 返回,再弹出结果。
// 回调里只把结果放到列表 function EnumCollectOnly(AWindow: HWND; AParam: LPARAM): BOOL; stdcall; begin TList<HWND>(AParam).Add(AWindow); Result := True; end; // EnumWindows 返回之后再弹窗 procedure TfrmWindowTest.btnSafeFindClick(Sender: TObject); var L: TList<HWND>; I: Integer; begin L := TList<HWND>.Create; try EnumWindows(@EnumCollectOnly, LPARAM(L)); for I := 0 to L.Count - 1 do Log(Format('句柄采集 %d', [L[I]])); // 到这里才能弹窗或者做耗时处理 finally L.Free; end; end;这条是最容易踩的,也是最难从报错信息里定位的——因为根本没有报错,就是界面假死。血泪经验:凡是回调函数,先问自己一句「里面会不会弹窗」,会就绝对不写。
6. 进阶:给窗口测试工具加 WndProc 消息日志,验证窗口到底吃没吃下这条消息
窗口测试到这一步已经能找窗口、发消息、验证结果了,但还有一个悬而未决的问题:你发了 WM_CLOSE,窗口没关,到底是因为消息没送达,还是窗口收到后拒绝了?不把这个区分开,前面所有验证都像在黑匣子外面猜。
解法是Override主窗体的 WndProc,把经过该窗体的所有消息记录到日志。这是同进程内窗口测试最直接的观察手段——frmWindowTest 要测自己,把消息日志打开,窗体的每一个动作都能看得清清楚楚。
procedure TfrmWindowTest.WndProc(var Message: TMessage); begin // 记录感兴趣的窗口消息 if (Message.Msg = WM_CLOSE) or (Message.Msg = WM_SETTEXT) or (Message.Msg >= WM_USER) then begin Log(Format('[消息] ID=%d wParam=%d lParam=%d', [Message.Msg, Message.WParam, Message.LParam])); end; // 必须调用 inherited,否则窗口收不到任何消息 inherited WndProc(Message); end;注意 inherited WndProc(Message) 这行不能漏。很多人写消息日志时容易改成 inherited 加在 else 分支里,导致日志记录过的消息没走默认处理,窗体直接失去响应。正确做法是先记录再传递,确保消息流程不断裂。
验证这个日志面板是否可靠,有一个简单方法:在按钮事件里给自己发一条 WM_USER 消息,看日志面板是否立刻弹出对应记录。
procedure TfrmWindowTest.btnSelfTestClick(Sender: TObject); begin // 发一条自定义消息验证 WndProc 日志链路 PostMessage(Self.Handle, WM_USER + 100, 1, 2); end;点击自测按钮,如果日志里出现「ID=1025 wParam=1 lParam=2」,说明消息从 PostMessage 到 WndProc 的整条链路是通的。之后再去测外部窗口,日志里看到 WM_CLOSE 未被处理,就能下结论说问题在目标窗口的处理逻辑,不在消息发送环节。
我一直有个习惯:把日志同步写一份到文本文件,用 Append 模式,每行带时间戳。窗口测试最好只信落盘的数据,你盯着屏幕看日志的瞬间,程序崩了,内存里的日志全没,文件里至少留了后悔药。frmWindowTest 这种小工具看着不起眼,但把句柄、消息、日志三个环节串起来以后,排查界面疑难问题比大型测试框架好使——没有框架依赖,没有复杂配置,解压一个 rar 就能跑。希望帮到你。
本文还有配套的精品资源,点击获取