尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Delphi 7 老工程嵌入 Chromium:CEF4Delphi 实践

Delphi 7 老工程嵌入 Chromium:CEF4Delphi 实践 简介面向仍在使用 Delphi 7 及后续版本、希望在桌面程序中内嵌 Chrome 浏览器的开发者这是一套较新的 CEF4Delphi 控件与配套源码包可用于构建内置网页浏览、前端页面与本地代码交互的视窗客户端。压缩包共 1330 个文件主体由四百余个 Pascal 源码单元、一百三十余个批处理脚本、一百三十余个头文件以及多套工程配置、窗体界面和资源图片组成整体约 229.27MB目录覆盖组件封装、示例工程与不同版本的工程文件便于按模块查阅。已有 2826 人学习或下载。对需要兼容旧版 Delphi、又希望复用 Chromium 能力的项目包内源码与示例可提供组件集成参考、工程组织样例和资源部署结构也能帮助开发者理解网页加载、前后端交互及界面嵌入的实现方式。适合桌面端浏览器控件选型、旧项目升级评估与示例代码学习。1. 接手老工程CEF4Delphi 在 Delphi 7 里能解决什么问题接手一个跑了八年的老 MIS 系统界面全是 TDBGrid 加 TPageControl新需求只有一个把网页版的报表嵌进去。工程是 Delphi 7 编的升到新版本意味着几十个第三方控件全部重编译没人愿意担这个风险。最后落地的方案是 CEF4Delphi——一份用 Pascal 重写的 CEF 封装源码把 Chromium 内核以 VCL 控件的形式挂进窗体Delphi 7 也能直接编译。它不是“放个浏览器”那么简单前端页面、本地资源包、Delphi 业务代码三方之间的桥都得靠它搭。下面按架构、跑起来、改起来三步拆一遍。2. 架构拆解CEF4Delphi 把浏览器拆成了哪几层2.1 多进程模型你的 exe 只是宿主进程CEF 默认是多进程架构。browser 进程就是你自己编译出来的那个 exe负责窗口、导航、V8 上下文调度renderer 进程负责解析 HTML、跑 JavaScript此外还有 GPU、network utility 等按需拉起的辅助进程。在 Windows 上这些子进程用的还是同一个 exe靠命令行参数区分角色。这就决定了 DPR 里那句判断不能省if GlobalCEFApp.StartMainProcess then begin Application.Initialize; Application.CreateForm(TForm1, Form1); Application.Run; end;StartMainProcess内部会解析命令行如果当前进程是 CEF 拉起来的子进程它就直接把控制权交给 CEF 的入口函数并返回 False此时你的窗体代码一行都不该跑。反过来如果漏了这个判断子进程会继续往下执行Application.CreateForm于是每开一个页面就多弹一个窗体任务管理器里同名进程越堆越多看着像内存泄漏其实是被自己拉起来的。另一个开关是GlobalCEFApp.SingleProcess。设成 True 后所有渲染都挤在宿主进程里调试时断点能一路跟到页面脚本但页面一复杂就卡崩溃也会把整个程序带走。我一般只在排查“到底是渲染崩溃还是宿主崩溃”的时候临时打开验证完立刻改回去。还有一条容易忽略VCL 的Application.Run提供的是 Windows 消息泵而 CEF 默认走多线程消息循环GlobalCEFApp.MultiThreadedMessageLoop为 True浏览器自己的线程在跑不需要你喂消息。只有在把该开关关掉、改由主线程驱动时才需要在OnScheduleMessagePumpWork里回调GlobalCEFApp.DoMessageLoopWork。Delphi 7 的 VCL 主循环比较老能不折腾就别折腾保持默认即可。2.2 源码包结构Core、Runtime、VCL、FMX 各管什么拿到手的源码包大体分四块理清楚各自的职责后面定位问题才不会乱翻CEF4Delphi/ source/ 所有 uCEF 开头的单元核心逻辑全在这 packages/ 按 Delphi 版本分的设计期包.dpk / .dproj demos/ VCL、FMX、Lazarus 各一套示例工程 bin/ CEF 二进制、资源包、localessource/里真正每天打交道的就那么几个单元关键内容什么时候会打开它uCEFApplicationTCefApplication、GlobalCEFApp配缓存路径、日志、自定义协议、注册 V8 扩展uCEFChromiumTChromium浏览器控制器导航、执行脚本、注册事件uCEFWindowParentTCEFWindowParent 容器处理尺寸跟随、父子窗口uCEFInterfacesICefBrowser、ICefFrame、ICefRequest 等接口事件签名对不上时查uCEFTypes枚举、常量、回调类型查 LOGSEVERITY、菜单类型这类值uCEFResourceHandlerTCefResourceHandlerOwn 基类接管本地资源加载uCEFV8HandlerTCefv8HandlerOwn 基类实现 JS 调 Delphipackages/那层要注意设计期包通常只覆盖到较新的 Delphi 版本Delphi 7 直接装包大概率装不上。我的做法是根本不装包把source目录加进项目搜索路径窗体上需要控件就在代码里手工创建或者建一个TFrame包起来。少一层 IDE 集成反而少一堆“包版本不匹配”的报错。2.3 组件协作TChromium 是控制器不是窗口初学者最容易搞错的一点TChromium本身不画任何东西。它是个无窗口的控制器对象真正显示页面的是 CEF 自己创建的子窗口。所以它必须有个宿主官方推荐配TCEFWindowParentChromium1.CreateBrowser(CEFWindowParent1, );第一个参数传的是“父控件的句柄来源”第二个是初始 URL传空串就先建一个空白页。TCEFWindowParent内部会在自己的客户区里建一个真正的子窗口Chromium 的渲染结果就贴在上面。如果偷懒直接把TPanel塞进去当 parent短期能跑但窗口缩放时会和 VCL 的双缓冲打架出现闪烁、黑边甚至面板重绘后浏览器内容被擦掉。这类问题排查起来极其难受属于典型的玄学故障——明明代码没错就是看着别扭。所以一开始就老老实实用官方容器。另外还有个离屏渲染模式OSR不需要子窗口CEF 把画面渲染到位图里交给你自己Canvas.Draw上去。适合做无边框、异形、需要录屏或者做水印叠加的场景代价是性能下降、鼠标键盘事件要自己转发、输入法处理更麻烦。做后台管理系统这类常规界面不值得上做自助终端那种全屏无边框的才考虑。3. 环境搭建把 Chromium 二进制和 Delphi 7 拼成能跑的工程3.1 目录铺法二进制、资源包、locales 一个都不能少CEF 不是单个 DLL 就能跑的它需要一整套运行时文件。源码包bin目录里的东西要原样拷到编译产物旁边缺一个可能就是白屏或者启动即退。典型的目录长这样BrowserDemo/ BrowserDemo.exe libcef.dll 核心库最重要的一个 chrome_elf.dll Crashpad / 注入相关 icudtl.dat 国际化数据缺了中文直接失效 snapshot_blob.bin V8 启动快照 v8_context_snapshot.bin 上下文快照新版 CEF 才有 resources.pak chrome_100_percent.pak locales/ zh-CN.pak en-US.pak cache/ 运行时生成的缓存目录Delphi 7 只能编 32 位程序所以这里必须是 32 位的libcef.dll。网上随手下的 CEF 发行包很多默认只有 64 位混用之后的表现是进程一启动就消失连异常对话框都不给你——因为加载 DLL 的失败发生在你的代码之前调试器都拦不住。版本匹配同样硬性CEF4Delphi 的封装层对着某个 CEF 大版本写死的接口结构体一旦错位程序在创建浏览器时直接访问越界。经验做法是只用发布包里自带的二进制不要自己去别处下“更新的”。真需要升级内核就整包换源码和二进制一起换。3.2 DPR 里的初始化代码工程入口要把路径、缓存、日志这些一次性配好顺序也很关键GlobalCEFApp必须在Application.CreateForm之前创建在Application.Run返回之后再释放。program BrowserDemo; uses Forms, Unit1 in Unit1.pas {Form1}, uCEFApplication; // GlobalCEFApp 定义在这 {$R *.res} var BaseDir : string; begin BaseDir : ExtractFilePath(ParamStr(0)); GlobalCEFApp : TCefApplication.Create; // 告诉 CEF 去哪儿找 libcef.dll 和资源默认是 exe 所在目录 GlobalCEFApp.FrameworkDirPath : BaseDir; GlobalCEFApp.ResourcesDirPath : BaseDir; GlobalCEFApp.LocalesDirPath : IncludeTrailingPathDelimiter(BaseDir locales); // 缓存目录必须可写程序装在 Program Files 下时要换成 CSIDL_LOCAL_APPDATA GlobalCEFApp.Cache : IncludeTrailingPathDelimiter(BaseDir cache); // 打开日志出问题时这里比 IDE 的调用栈有用得多 GlobalCEFApp.LogFile : BaseDir debug.log; GlobalCEFApp.LogSeverity : LOGSEVERITY_INFO; // 调试页面时用浏览器连这个端口比在代码里打日志快 GlobalCEFApp.RemoteDebuggingPort : 8088; if GlobalCEFApp.StartMainProcess then begin Application.Initialize; Application.Title : BrowserDemo; Application.CreateForm(TForm1, Form1); Application.Run; end; GlobalCEFApp.Free; GlobalCEFApp : nil; end.几个参数值得单独说。FrameworkDirPath决定 libcef.dll 的查找位置如果把它写死成开发机的绝对路径换台机器上线就废了所以统一用ParamStr(0)推相对路径。Cache目录如果不可写CEF 不会报错而是悄悄用内存缓存表现为每次启动都要重新加载用户看着慢但找不到原因。LogSeverity平时可以调成LOGSEVERITY_DISABLE排查完再关掉避免日志文件把客户磁盘写满。3.3 第一个窗体和第一个页面窗体上放三个东西一个TPanel当工具栏里面塞TEdit和TButton一个TCEFWindowParent占满剩余区域一个TChromium挂在窗体上它没有位置属性摆哪儿都行。然后写事件unit Unit1; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls, uCEFChromium, uCEFWindowParent, uCEFInterfaces, uCEFTypes; type TForm1 class(TForm) pnlTop: TPanel; edtURL: TEdit; btnGo: TButton; CEFWindowParent1: TCEFWindowParent; Chromium1: TChromium; procedure FormCreate(Sender: TObject); procedure FormShow(Sender: TObject); procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean); procedure btnGoClick(Sender: TObject); procedure Chromium1AfterCreated(Sender: TObject; const browser: ICefBrowser); procedure Chromium1BeforeClose(Sender: TObject; const browser: ICefBrowser); procedure Chromium1LoadEnd(Sender: TObject; const browser: ICefBrowser; const frame: ICefFrame; httpStatusCode: Integer); procedure Chromium1TitleChange(Sender: TObject; const browser: ICefBrowser; const title: ustring); private FCanClose : Boolean; FClosing : Boolean; end; implementation procedure TForm1.FormCreate(Sender: TObject); begin FCanClose : False; FClosing : False; end; procedure TForm1.FormShow(Sender: TObject); begin // 一定要等窗体真正显示出来此时 CEFWindowParent1 才有有效的窗口句柄和尺寸 Chromium1.CreateBrowser(CEFWindowParent1, ); end; procedure TForm1.Chromium1AfterCreated(Sender: TObject; const browser: ICefBrowser); begin // 内核初始化完成从现在开始才能安全导航 Chromium1.LoadURL(about:blank); end; procedure TForm1.btnGoClick(Sender: TObject); begin if Chromium1.Initialized then Chromium1.LoadURL(edtURL.Text); end; procedure TForm1.Chromium1LoadEnd(Sender: TObject; const browser: ICefBrowser; const frame: ICefFrame; httpStatusCode: Integer); begin // 只关心主框架iframe 也会触发这个事件 if (frame nil) and frame.IsMain then Caption : BrowserDemo - IntToStr(httpStatusCode); end; procedure TForm1.Chromium1TitleChange(Sender: TObject; const browser: ICefBrowser; const title: ustring); begin edtURL.Text : title; end; procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean); begin CanClose : FCanClose; if not FClosing then begin FClosing : True; Chromium1.CloseBrowser(True); // 异步真正退出要等 OnBeforeClose end; end; procedure TForm1.Chromium1BeforeClose(Sender: TObject; const browser: ICefBrowser); begin FCanClose : True; Close; // 再走一次关闭流程这次能过 end; end.这段代码里有两处是踩过坑才写对的。一是CreateBrowser放在FormShow而不是FormCreateFormCreate阶段控件还没完成句柄创建TCEFWindowParent的客户区尺寸是 0×0CEF 拿到这个尺寸后不会自己重算结果就是一片空白。二是关闭流程——CloseBrowser是异步的调用之后 CEF 还得销毁渲染进程、会话、缓存这时如果直接Free掉TChromium必定模糊的AV。必须用一个标志位挡住第一次关闭等OnBeforeClose回调回来再真正关窗。3.4 编译选项与搜索路径Delphi 7 下的项目设置没几项但每项都影响能不能跑。Project Options Directories/Conditionals里把source目录加进 Search path不要加子目录CEF4Delphi 的单元都在同一层。Compiler页里建议把 Range checking、Overflow checking 关掉——这俩在跨语言回调的边界上会干扰栈帧一旦触发异常调用栈经常是错乱的纯属给自己添堵。还有一个现实问题源码包里的单元是滚动更新的偶尔有某个单元开始用TStringHelper、泛型容器这类 Delphi 7 没有的语法编译会直接报错。遇到这种情况不用慌先在单元里搜{$IFDEF}看有没有条件编译开关没有就把那个单元从项目里排除只保留实际用到的。我一般还会在工程目录里留一份“能编译的单元清单”升级源码包时对照着替换避免整包覆盖后一堆莫名其妙的不兼容。4. 核心链路一个 URL 从 Delphi 走到 Chromium 再回来4.1 生命周期与导航事件TChromium的事件看起来不少实际按顺序记住几个关键的就够用事件触发时机常见用途OnAfterCreated浏览器对象创建完成首次导航、注入初始化脚本OnLoadStart主框架开始加载显示 loading 状态、禁用按钮OnLoadEnd加载结束含失败隐藏 loading、读 httpStatusCode 判断成败OnTitleChange页面标题变化更新窗口标题OnBeforeBrowse用户点击链接、发生跳转前拦截外链、拦掉非白名单域名OnBeforeClose内核即将销毁置关闭标志、兑现“可以关了”OnConsoleMessage页面 console.log前端日志转进 Delphi 日志文件OnBeforeBrowse是最值得写的一个它能在导航真正发生前把请求掐掉procedure TForm1.Chromium1BeforeBrowse(Sender: TObject; const browser: ICefBrowser; const frame: ICefFrame; const request: ICefRequest; isRedirect: Boolean; out Result: Boolean); var URL : string; begin Result : False; if (request nil) then Exit; URL : request.URL; // 只允许白名单内的站内地址其余一律拦下用系统浏览器打开 if (Pos(app://, URL) 1) and (Pos(about:, URL) 1) then begin Result : True; // True 取消本次导航 ShellExecute(0, open, PChar(URL), nil, nil, SW_SHOWNORMAL); end; end;Result : True表示取消这是 CEF 的约定别写反。里面顺手把外链甩给系统浏览器是为了避免用户在嵌入式浏览器里跳到外部站点后没有后退按钮被困在一个页面里出不来。这类体验问题上线后一定会有反馈。加载失败也是靠OnLoadEnd里的httpStatusCode判断404、500 都会走到这里状态码是 0 则通常意味着连接被拒绝或者域名解析失败。要给用户展示统一的错误页就在这里LoadURL到本地的一个error.html把状态码当参数带过去。4.2 本地资源接管自定义 scheme 加工 ResourceHandler把前端打包的 HTML/JS/CSS 塞进 exe 的资源段当然可以但每次改页面都要重编译调试时太痛苦。更实用的是注册一个自定义协议让 CEF 把app://开头的请求交给 Delphi 处理文件从磁盘读路径还能做白名单。第一步在 DPR 里注册协议和处理器工厂位置要在StartMainProcess之前GlobalCEFApp.AddCustomScheme(app, standard,secure); GlobalCEFApp.RegisterSchemeHandlerFactory(app, , TLocalSchemeFactory.Create);standard表示这是个标准协议会走正常的 URL 解析和安全策略不加这个标记页面里的相对路径、fetch、Cookie 都会出各种怪问题。第二步实现资源处理器继承TCefResourceHandlerOwn重写三个方法type TLocalResourceHandler class(TCefResourceHandlerOwn) protected FStream : TMemoryStream; FMime : ustring; function ProcessRequest(const request: ICefRequest; const callback: ICefCallback; var continuation: Boolean): Boolean; override; procedure GetResponseHeaders(const response: ICefResponse; var responseLength: Int64; var redirectUrl: ustring); override; function ReadResponse(const dataOut: Pointer; bytesToRead: NativeUInt; var bytesRead: NativeUInt; const callback: ICefCallback): Boolean; override; public constructor Create(const aFileName, aMime: string); destructor Destroy; override; end; constructor TLocalResourceHandler.Create(const aFileName, aMime: string); begin inherited Create; FMime : aMime; FStream : TMemoryStream.Create; if FileExists(aFileName) then FStream.LoadFromFile(aFileName); // 找不到就返回空流由 GetResponseHeaders 给 404 FStream.Position : 0; end; destructor TLocalResourceHandler.Destroy; begin FStream.Free; inherited; end; function TLocalResourceHandler.ProcessRequest(const request: ICefRequest; const callback: ICefCallback; var continuation: Boolean): Boolean; begin Result : True; // True 同步处理完成CEF 紧接着调 GetResponseHeaders continuation : False; end; procedure TLocalResourceHandler.GetResponseHeaders(const response: ICefResponse; var responseLength: Int64; var redirectUrl: ustring); begin if FStream.Size 0 then begin response.Status : 200; response.MimeType : FMime; responseLength : FStream.Size; end else begin response.Status : 404; responseLength : 0; end; end; function TLocalResourceHandler.ReadResponse(const dataOut: Pointer; bytesToRead: NativeUInt; var bytesRead: NativeUInt; const callback: ICefCallback): Boolean; begin bytesRead : FStream.Read(dataOut^, bytesToRead); Result : bytesRead 0; // 返回 False 表示这一轮没数据了CEF 会再来问 end;三个方法的分工很清晰ProcessRequest决定同步还是异步本地文件同步读就行网络请求才需要异步GetResponseHeaders里给出状态码、MIME 和长度ReadResponse被反复调用直到返回 False。注意ReadResponse的返回值语义是“这次读到数据了吗”不是“读完了吗”写错会导致页面只加载出一半内容。工厂类负责根据 URL 生成上面的 handlerfunction TLocalSchemeFactory.New(const browser: ICefBrowser; const frame: ICefFrame; const schemeName: ustring; const request: ICefRequest): ICefResourceHandler; var RelPath, FullPath : string; begin Result : nil; if (request nil) then Exit; // app://web/index.html - /web/index.html去掉主机名部分 RelPath : StringReplace(string(request.URL), app://web, , []); RelPath : StringReplace(RelPath, /, \, [rfReplaceAll]); // 关键一步挡掉 ../ 穿越只允许访问程序目录下的 web 文件夹 FullPath : ExpandFileName(ExtractFilePath(ParamStr(0)) web RelPath); if Pos(UpperCase(ExtractFilePath(ParamStr(0)) WEB), UpperCase(FullPath)) 1 then Exit; Result : TLocalResourceHandler.Create(FullPath, MimeTypeFromExt(FullPath)); end;路径穿越这一条必须写。前端页面里如果有用户可控的 URL 拼接没做校验的话app://web/../../../windows/win.ini就能把系统文件读出来这种漏洞在桌面端往往被忽略但只要前端里有任何一处把外部输入拼进 iframe 的 src就成立了。4.3 Delphi 与前端双向通信最省事的通信方式是做一份 JS 扩展页面里调一个不存在的函数CEF 回调到 Pascal 里执行。这样前端写起来就跟调本地函数一样不用拼字符串、不用轮询。先写 V8 handlertype TBridgeHandler class(TCefv8HandlerOwn) protected function Execute(const name: ustring; const obj: ICefV8Value; const arguments: TCefV8ValueArray; var retval: ICefV8Value; var exception: ustring): Boolean; override; end; function TBridgeHandler.Execute(const name: ustring; const obj: ICefV8Value; const arguments: TCefV8ValueArray; var retval: ICefV8Value; var exception: ustring): Boolean; var Msg : string; begin Result : False; if (name nativePost) and (Length(arguments) 1) and arguments[0].IsString then begin Msg : string(arguments[0].GetStringValue); // 只做分发具体业务丢回主线程队列别在 V8 线程里操作 VCL PostBridgeMessage(Msg); Result : True; // True 表示这个函数被我们处理了 end else exception : unknown native function: string(name); end;Execute是在渲染线程里被调用的这里绝对不能碰任何 VCL 控件。我第一次写的时候顺手在里面改了edtURL.Text单步调试时一切正常正常跑起来就随机报“控件没有窗口句柄”。正确做法是丢进一个线程安全的队列用PostMessage通知主窗体去取。扩展本身在 DPR 里注册注册的 JS 代码会在每个页面加载前注入GlobalCEFApp.RegisterExtension(v8/bridge.js, var appBridge {}; appBridge.post function(m){ return nativePost(m); }; appBridge.onMessage function(){ };, // 占位等 Delphi 覆盖 TBridgeHandler.Create);注入之后页面里直接appBridge.post(JSON.stringify({cmd:save, id: 12}))就能把消息送到 Pascal 侧。协议建议固定成三段cmd是方法名、data是负载、seq是请求序号回包带上同样的seq前端好做回调管理。别用裸字符串拼参数页面上出现中文或者引号就要出事。反向调用简单得多Chromium1.ExecuteJavaScript( appBridge.onMessage({cmd:refresh,data:{total:128}});, app://web/index.html, 0);第二个参数是脚本来源 URL主要影响 DevTools 里显示的归属第三个参数是起始行号。注意ExecuteJavaScript也是往渲染线程丢任务不保证立刻执行完别紧接着就去读页面里的变量——真要读结果就让前端把结果再 post 回来。5. 避坑与排查Delphi 7 下最容易翻车的五件事5.1 启动阶段闪退、白屏、进程残留现象一双击 exe一秒内消失连异常框都没有。原因是 CEF 二进制和源码版本对不上或者位数不对Delphi 7 编出来的是 32 位却放了 64 位的libcef.dll。这种失败发生在Application.Initialize之前Delphi 的异常处理根本接管不了。解决方式先确认libcef.dll是 32 位方法是看文件属性或者用依赖查看工具然后确认二进制来自源码包自带的bin目录不要自己另外下。检查顺序上先看debug.log里有没有生成内容——如果日志文件压根没创建说明 CEF 还没走到初始化就挂了问题一定在 DLL 加载。现象二任务管理器里一堆同名进程关掉主窗口还在跑。两个原因。一是 DPR 漏了StartMainProcess判断子进程把窗体代码又跑了一遍二是SingleProcess被误设成 True看起来进程只剩一个其实渲染和宿主挤在一起关窗时互相卡死。解决办法是把 DPR 的入口代码重新核对一遍StartMainProcess必须包住整个Application.Run块GlobalCEFApp.Free必须放在块外面否则子进程退出时会去释放一个不属于它的对象。5.2 运行阶段乱码、白边、关窗崩溃现象三页面里的中文全变成问号标题栏也是乱码。Delphi 7 的string是 ANSICEF4Delphi 里的ustring在这个版本下映射成WideString。所有从页面传过来的字符串默认按系统 ANSI 代码页转换只要代码页对不上就成了问号。处理原则是接口层统一用 WideString 进出落到文件或数据库时显式转 UTF-8中间不要依赖隐式转换。前端侧保证meta charsetutf-8本地资源 handler 里MimeType给text/html; charsetutf-8两边 charset 说清楚了问题基本就没了。现象四拖动窗口边缘放大会看到右侧留一条白边内容不跟着走。TCEFWindowParent自己会跟随父控件尺寸变化但浏览器子窗口的重绘要额外通知。老版本里调Chromium1.WasResized新版本改名叫UpdateSize具体按你手上uCEFChromium里声明的方法名来写在FormResize里判断Chromium1.Initialized之后再调。如果父控件不是TCEFWindowParent而是普通面板那就更麻烦——得自己算客户区坐标再SetBounds所以还是建议一开始就用官方容器。现象五点关闭按钮瞬间弹出 AV或者窗口消失了进程还在。这是最典型的一个。CloseBrowser只是发起关闭CEF 会在后台销毁渲染进程、落盘缓存、回收会话全部完成后才回调OnBeforeClose。在此之前TChromium和TCEFWindowParent都不能释放。写法就是前面那套FormCloseQuery里先挡住CloseBrowser(True)发起关闭OnBeforeClose里把标志位置真再Close一次。如果程序里有多处关闭入口托盘菜单、快捷键、任务栏右键记得全部收敛到同一个函数里避免某条路径绕过了这套流程。6. 二次开发把浏览器控件收成一个可复用的业务组件真正上线时业务代码不应该直接碰Chromium1。我的做法是建一个TFrame把TCEFWindowParentTChromium包起来对外只暴露四个东西Navigate(URL)、PostToJS(JSON字符串)、OnJSMessage事件、Close。业务窗体只跟这个 Frame 打交道将来换内核、加离屏渲染、改成多标签改动都收敛在一处。桥接协议我固定成三个字段前端 Delphi 两侧共用一份约定字段类型说明cmdstring方法名禁止带点号和空格便于路由分发seqinteger请求序号回包原样带回前端用它找 callbackdataobject负载统一 JSON不用位置参数分发侧就是一个TStringList或者字典cmd找注册的处理函数找不到就回一条{cmd:error,data:unknown command}。这样前端加了新功能Delphi 侧只要注册一个 handler不用改窗体代码。本地资源那套也值得再往前走本文还有配套的精品资源点击获取
返回列表