CEF4Delphi 下载回调中创建 TForm 后控件尺寸归零怎么办?
结论:先检查创建窗体的线程
首先确认 ChromiumDownloadUpdated 是否运行在 VCL 主线程。如果不是,就需要把 TOpenSaveForm.Create、控件属性赋值、ShowModal 和窗体释放全部移到主线程执行。
现有代码无法证明 DFM 文件损坏,仅凭 Width=0、Left=0 也无法断定 DFM streaming 失败。先排查跨线程创建 VCL 窗体的问题;如果确认创建过程始终在主线程,再检查 DFM、构造函数和布局代码。
为什么下载事件中直接创建窗体有风险?
代码在 downloadItem.IsComplete 分支内直接执行了:
openSaveForm := TOpenSaveForm.Create(Self);
随后修改标签、单选按钮和图标,再调用 ShowModal。这些都属于 VCL 界面操作,从构造窗体开始就需要遵守线程要求。
CEF 的下载处理回调运行在浏览器进程的 CEF UI thread。它不一定是 Delphi 的 VCL 主线程,两者是否重合取决于消息循环配置,需要在实际程序中确认。CEF 下载处理接口文档
CEF4Delphi 的下载处理实现会将调用转发给 doOnDownloadUpdated。不能因为它表现为组件事件,就假定调用已经切换到了 VCL 主线程。CEF4Delphi 下载处理源码
VCL 对象的界面访问应集中在主线程执行。把事件中的代码放进 try..except,或者只给创建过程加锁,都不能替代主线程调度。Embarcadero 主线程使用说明
第一步:记录实际执行线程
在回调入口和 TOpenSaveForm.Create 之前记录线程 ID。Windows 下可使用:
OutputDebugString(PChar(Format(
'Download callback: thread=%d, main=%d',
[GetCurrentThreadId, MainThreadID])));
相关标识符通常来自 Windows、SysUtils 和 System。建议同时记录下载标识和文件路径,方便区分多个下载任务。
如果两个线程 ID 不同,就确认这里存在不符合 VCL 使用要求的操作。修正线程后再观察布局异常是否仍然出现,不要过早将所有症状归因于 DFM。
第二步:复制下载数据,再通知主线程
回调中只提取界面需要的数据,例如完整路径和已接收字节数。窗体的创建和显示交给主线程。
不要保存 downloadItem,等主线程稍后再读取。CEF 接口说明要求不要在回调范围之外保留该下载项;异步处理应使用回调内复制出的数据。CEF 下载项生命周期说明
Delphi 7 可以使用 PostMessage 和主线程创建的专用通知窗口。下面展示关键接线方式,需要合并到现有窗体类中。
声明通知数据和接收方法
const
WM_DOWNLOAD_COMPLETE = WM_APP + 101;
type
PDownloadInfo = ^TDownloadInfo;
TDownloadInfo = record
FullPath: string;
ReceivedBytes: Int64;
end;
{ Add these members to TMainForm's private section: }
{ FDownloadNotifyWnd: HWND; }
{ procedure DownloadNotifyWndProc(var Msg: TMessage); }
{ procedure ShowDownloadDialog(const AFullPath: string; }
{ AReceivedBytes: Int64); }
WM_APP + 101 是示例消息编号,应确认它没有与项目现有消息冲突。相关单元包括 Windows、Messages、Classes、SysUtils、Graphics 和 Forms。
在主线程中、启动下载之前创建通知窗口:
FDownloadNotifyWnd := AllocateHWnd(DownloadNotifyWndProc);
通知窗口句柄应在下载回调仍可能发生期间保持有效。回调中直接读取这个预先创建的句柄,避免访问可能触发创建或重建的窗体 Handle 属性。
在下载回调中发送数据
将完成分支中的界面操作替换为:
procedure TMainForm.ChromiumDownloadUpdated(Sender: TObject;
const browser: ICefBrowser;
const downloadItem: ICefDownloadItem;
const callback: ICefDownloadItemCallback);
var
Info: PDownloadInfo;
begin
if not downloadItem.IsComplete then
Exit;
New(Info);
try
Info^.FullPath := downloadItem.GetFullPath;
Info^.ReceivedBytes := downloadItem.ReceivedBytes;
if not PostMessage(FDownloadNotifyWnd,
WM_DOWNLOAD_COMPLETE, 0, LPARAM(Info)) then
RaiseLastOSError;
except
Dispose(Info);
raise;
end;
end;
发送成功后,数据由接收方释放;发送失败则由发送方释放。示例没有包含整个 CEF 回调异常边界,实际项目应按既有机制记录发送失败,避免让未处理的 Delphi 异常越过回调边界。
在主线程接收并显示窗体
procedure TMainForm.DownloadNotifyWndProc(var Msg: TMessage);
var
Info: PDownloadInfo;
FullPath: string;
ReceivedBytes: Int64;
begin
if Msg.Msg = WM_DOWNLOAD_COMPLETE then
begin
Msg.Result := 0;
Info := PDownloadInfo(Msg.LParam);
if Info = nil then
Exit;
try
FullPath := Info^.FullPath;
ReceivedBytes := Info^.ReceivedBytes;
finally
Dispose(Info);
end;
try
ShowDownloadDialog(FullPath, ReceivedBytes);
except
Application.HandleException(Self);
end;
end
else
Msg.Result := DefWindowProc(FDownloadNotifyWnd,
Msg.Msg, Msg.WParam, Msg.LParam);
end;
procedure TMainForm.ShowDownloadDialog(
const AFullPath: string; AReceivedBytes: Int64);
var
FileIcon: TIcon;
Descripcion: string;
OpenSaveForm: TOpenSaveForm;
begin
FileIcon := TIcon.Create;
try
OpenSaveForm := TOpenSaveForm.Create(Self);
try
OpenSaveForm.LabelFilename.Caption :=
OpenSaveFormCaptionMsg(
ExtractFileName(AFullPath), AReceivedBytes);
GetFileIconAndDescription(
AFullPath, FileIcon, Descripcion);
OpenSaveForm.RadioButtonOpen.Caption :=
OpenSaveForm.RadioButtonOpen.Caption +
' ' + Descripcion;
OpenSaveForm.FileIcon.Picture.Icon := FileIcon;
if OpenSaveForm.ShowModal = mrOk then
ChooseShowSaveDialog(AFullPath,
OpenSaveForm.RadioButtonSave.Checked);
finally
OpenSaveForm.Free;
end;
finally
FileIcon.Free;
end;
end;
这里沿用了问题中的业务辅助函数,示例不是可独立编译的完整程序。显示名称暂用 ExtractFileName(AFullPath);如果业务需要下载前的原始名称,应在回调阶段一并复制并传递。
还需要处理两种情况:
- 程序关闭时,先确保不再有回调发送通知,再处理或释放尚未消费的通知数据,最后在主线程调用
DeallocateHWnd。窗口销毁并不会自动释放消息指针指向的记录。 - 多个下载同时完成时,
ShowModal的消息循环可能继续处理其他完成通知。实际应用应增加待处理队列和“对话框已打开”状态,按顺序显示,并按下载标识防止重复处理。
较新版本的 Delphi 也可以使用相应版本提供的 TThread.Queue 或 TThread.ForceQueue。Delphi 7 不能直接套用匿名过程写法。Synchronize 会让调用线程等待主线程完成操作,若同步操作里包含长时间打开的模态框,需要额外评估等待和死锁风险。
如果主线程创建后仍然异常,怎么继续查?
先做一个最小对照:从主窗体按钮事件中创建同一个 TOpenSaveForm,不经过下载事件,并反复检查构造完成后的控件属性。
随后按以下顺序定位:
- 检查
{$R *.dfm}是否关联了正确资源,以及 DFM 中的窗体类名、控件名和继承关系是否正确。 - 检查构造函数、
Loaded、OnCreate和自定义控件的初始化逻辑。这些代码都可能在OnShow之前执行,因此“显示前已经归零”不能单独证明读取 DFM 失败。 - 在这些阶段记录异常控件的
BoundsRect、Align、Anchors和父控件尺寸,找出属性首次变为异常值的位置。 - 临时去掉
GetFileIconAndDescription等辅助调用,比较构造完成后与辅助调用后的属性。若异常只在某次调用后出现,再检查对应实现和内存使用。
由于缺少 DFM、这些辅助函数的实现和实际线程日志,目前还无法确定具体根因。
原代码还应修正的地方
FileIcon 创建后,TOpenSaveForm.Create(Self) 在进入 try..finally 之前执行。如果窗体构造抛出异常,图标对象得不到释放。上面的嵌套 try..finally 已覆盖这个路径。
另外,片段中的 originalFilename 没有赋值,会影响文件名显示;开头的 else if 也缺少对应的前置分支。这说明代码片段不完整,但这两点本身不能解释多个控件尺寸偶发归零。
备注:内容仅供参考。
