PAYATHON 2026

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,不经过下载事件,并反复检查构造完成后的控件属性。

随后按以下顺序定位:

  1. 检查 {$R *.dfm} 是否关联了正确资源,以及 DFM 中的窗体类名、控件名和继承关系是否正确。
  2. 检查构造函数、Loaded、OnCreate 和自定义控件的初始化逻辑。这些代码都可能在 OnShow 之前执行,因此“显示前已经归零”不能单独证明读取 DFM 失败。
  3. 在这些阶段记录异常控件的 BoundsRect、Align、Anchors 和父控件尺寸,找出属性首次变为异常值的位置。
  4. 临时去掉 GetFileIconAndDescription 等辅助调用,比较构造完成后与辅助调用后的属性。若异常只在某次调用后出现,再检查对应实现和内存使用。

由于缺少 DFM、这些辅助函数的实现和实际线程日志,目前还无法确定具体根因。

原代码还应修正的地方

FileIcon 创建后,TOpenSaveForm.Create(Self) 在进入 try..finally 之前执行。如果窗体构造抛出异常,图标对象得不到释放。上面的嵌套 try..finally 已覆盖这个路径。

另外,片段中的 originalFilename 没有赋值,会影响文件名显示;开头的 else if 也缺少对应的前置分支。这说明代码片段不完整,但这两点本身不能解释多个控件尺寸偶发归零。

备注:内容仅供参考。