拓冰建站拓冰建站
首页 / 资讯中心 / 正文

CEF4Delphi从入门到实战:在Delphi中嵌入现代Chromium浏览器

简介CEF4Delphi一个基于Chromium Embedded Framework的Delphi组件库让开发者能在传统Delphi桌面应用中嵌入Chromium内核借助HTML5、CSS3和JavaScript打造现代交互界面非常适用于办公系统、数据看板及混合形态的客户端。压缩包内含该库完整源码工程共525个文件、仅1.71MB包括210个.pas源文件、50个.dfm窗体定义、49个.dpr/dproj工程文件以及批处理脚本、编译配置、示例页面与图片资源等结构紧凑便于逐层复用。当前已有348人学习浏览。通过阅读源码和示例可掌握TChromium组件的加载URL、导航事件、JS与Delphi对象双向调用等核心用法同时参考不同Delphi版本的项目文件与构建脚本能快速完成工具链配置免去自行编译CEF库的繁琐过程。对中高级Delphi开发者而言这份代码尤为实用既能跟随Demo上手也能深入理解浏览器进程与消息通信机制压缩包中还收录了Lazarus跨平台入口、.inc条件编译脚本、应用图标和资源文件为跨平台适配和二次封装提供了完整参考。1. 为什么是CEF4Delphi而不是TWebBrowser做桌面端的老Delphi项目最尴尬的场景不是数据库慢而是界面里嵌的那个浏览器今天连CSS3动画都跑不明白。TWebBrowser封装的是IE内核面对现代前端页面要么渲染错位要么交互失效更别提WebGL和WebSocket。CEF4Delphi把Chromium Embedded Framework搬进Delphi让Delphi 7到10.x的项目都能直接获得Chrome同级别的渲染能力而且不是套壳网页是原生窗体里嵌一个真正的Chromium进程。这篇博文会顺着“解包资源、配全局参数、初始化组件、处理导航事件、打通JS双向通信”这条路线把关键代码和参数边界都拆开讲最后收在CEF多进程模型和资源释放上——那是几乎所有自测通过但上线崩框的项目都会踩到的地方。2. CEF4Delphi项目结构与环境初始化从bat文件到全局参数拿到CEF4Delphi-master.zip之后第一件事不是急着往Delphi里装组件而是先看清楚这个zip里躺着的是一套“源码包构建脚本示例工程”的集合。项目里一长串重复的00-DeleteDCUs.bat看似冗余其实对应不同子目录下的批处理入口比如00-CreateLazarusResources.bat是为了在Lazarus环境下准备CEF的.res资源文件。如果只用Delphi这几个bat基本碰不着但一定要知道它们的存在——很多从GitHub直接拉源码的开发者会误以为缺了bat就无法编译其实真正决定成败的是后续的全局初始化参数。2.1 解压后那些00开头的bat是干什么的这些bat属于项目整理步骤00-CreateLazarusResources.bat用于为Lazarus IDE生成CEF4Delphi需要的资源描述文件00-DeleteDCUs.bat则是递归清除各子项目残留的.dcu编译缓存。为什么这么在意删除DCUCEF4Delphi跨多个Delphi版本不同版本的RTL/VCL单元对接口布局有细微差异旧DCU残留轻则链接混乱重则弹出“单元版本不匹配”的提示。我一般把zip解压到D:\libs\CEF4Delphi-master这种无空格目录先跑一遍删除脚本再用Delphi打开Packages目录下的组件包安装——注意装组件包时要确认CEF4Delphi的源码搜索路径是绝对的还是相对的。安装组件后拖放面板上会出现TChromium和TCEFWindowParent。TChromium是逻辑控制器管理浏览器实例、事件、JS绑定TCEFWindowParent是继承自TWinControl的承载窗口必须在表单上可见才能显示渲染画面。不推荐只拖TChromium它没有窗口句柄。2.2 全局CEF参数的启动顺序CEF4Delphi要求在Application.Initialize之前完成GlobalCEFApp的参数设置因为CEF的老大哥Chromium进程需要尽早拉起。下面这段是dpr文件的典型写法program CEFDemo; uses Vcl.Forms, uCEFApplication, uCEFConstants, MainForm in MainForm.pas {Form1}; {$R *.res} begin // 必须放在VCL初始化之前否则CEF内部消息循环会和其他初始化动作竞争 GlobalCEFApp.BrowserSubprocessPath : cef\Release\CefSharp.BrowserSubprocess.exe; GlobalCEFApp.ResourcesDirPath : cef\Release\Resources; GlobalCEFApp.LocalesDirPath : cef\Release\Resources\locales; GlobalCEFApp.CacheDirPath : cef\Cache; GlobalCEFApp.WorkerProcessCount : 2; GlobalCEFApp.EnableGPU : False; GlobalCEFApp.StartMainProcess; Application.Initialize; Application.MainFormOnTaskbar : True; Application.CreateForm(TForm1, Form1); Application.Run; GlobalCEFApp.ShutdownMainProcess; end.BrowserSubprocessPath指向渲染/GPU子进程的可执行文件若留空CEF4Delphi会自动从libcef.dll同目录查找。ResourcesDirPath放置cef.pak、icudtl.dat等Chromium资源路径错误时窗口会一片白。EnableGPU设为False是应对老显卡驱动崩溃的保守做法代价是CSS动画的流畅度稍有下降。WorkerProcessCount限定子进程数量为2避免低配机器上出现几十个CEF进程。设置这些参数时我习惯用相对路径而不是硬编码C:\因为目标客户机器上软件可能装在任意带空格或中文的目录——而路径带空格时有些CEF版本对LocalesDirPath的解析会失败表现为右键菜单和工具提示全部变成英文。2.3 最小浏览器Form代码与TChromium组件表单上放一个TCEFWindowParent设为alClient和一个TChromium不可见。在FormShow里创建浏览器procedure TForm1.FormShow(Sender: TObject); begin // 必须先让窗口获得实际尺寸否则初始viewport为0x0 CEFWindowParent1.UpdateSize; // CreateBrowser的第二个参数是要加载的起始URL Chromium1.CreateBrowser(CEFWindowParent1, https://example.com); Timer1.Enabled : True; // 用于轮询创建状态 end;procedure TForm1.Timer1Timer(Sender: TObject); begin if Chromium1.Initialized then begin Timer1.Enabled : False; Caption : 浏览器已就绪PID IntToStr(Chromium1.Browser.Identifier); end; end;CreateBrowser是异步的调用后Chromium1.Browser可能为空所以用定时器轮询Initialized。不要在FormCreate里直接调用CreateBrowser此时窗体句柄还没稳定渲染窗口收不到WM_SIZE会出现黑块。TCEFWindowParent继承自VCL标准控件所以调整大小、Dock等行为都和普通Panel一致这也是它比直接用THandle承载CEF更便于融入界面的原因。3. 导航与生命周期事件处理页面状态如何影响业务逻辑浏览器组件能加载只是一个起点真正要接进业务的是“页面加载到哪一步了”“用户点了什么链接”“弹窗要不要拦”这些运行时事件。CEF4Delphi把CEF的回调整理成了TChromium的一大串事件属性全部以On开头。用好这些事件才能做到在页面跳转时同步UI、在鉴权失败时切回本地页面。3.1 Navigate和LoadURL的选择TChromium暴露了多个加载方法。常见的三个调用是// 方法一直接赋值URL属性等价于导航 Chromium1.LoadURL(https://localhost:8443/app); // 方法二从某个Frame发起导航用于框架页内部跳转 // Chromium1.Browser.MainFrame.LoadURL(/api/bootstrap); // 方法三刷新当前页 Chromium1.Browser.Reload;LoadURL与Browser.MainFrame.LoadURL的区别在于作用范围前者加载到主frame后者可以指定frame名适合页面里有iframe且需要只刷新子框架的场景。日常最常用的还是LoadURL。注意LoadURL不会等待原页面卸载完成连续调用两次会出现一次白屏闪烁因为CEF内部会为每个导航产生独立CefLoadRequest。3.2 关键生命周期事件与参数表下面这张表列出了项目里几乎必挂的事件以及每个事件适合做什么事件名触发时机常见用途OnBeforePopup页面向window.open或target_blank时拦截新窗口改为当前组件内导航OnBeforeBrowse浏览器准备导航时服务端重定向前拦截外部协议如mailto:、自定义myapp://OnLoadStart新页面开始加载时显示等待遮罩、记录加载耗时OnLoadEnd页面主frame加载完成时注入JS、更新状态栏OnLoadingStateChange加载状态变化时是否在加载同步按钮的“停止/刷新”状态OnRenderProcessTerminated渲染进程崩溃或被kill时提示用户并重载页面一个典型业务场景内嵌后台系统需要检测登录Cookie是否过期通常在OnLoadEnd里执行一段JS读取document.cookie再调用Delphi回传状态。但OnLoadEnd对页面内异步接口并不保证数据都到位所以严格判断登录态还是得依靠OnBeforeBrowse拦截HTTP状态码或JS侧的MutationObserver。3.3 弹窗拦截与多标签页处理CEF默认遇到target_blank会新建一个独立窗口这对单窗口应用来说很突兀。我习惯在OnBeforePopup里把链接强行导航到当前组件procedure TForm1.Chromium1BeforePopup(Sender: TObject; const browser: ICefBrowser; const frame: ICefFrame; const targetUrl, targetFrameName: string; const targetDisposition: TCefWindowOpenDisposition; userGesture: Boolean; const popupFeatures: TCefPopupFeatures; var windowInfo: TCefWindowInfo; var client: ICefClient; var settings: TCefBrowserSettings; var noJavascriptAccess: Boolean; var Result: Boolean); begin // 无论新标签还是新窗口都被当成当前页面的导航 if targetUrl then begin Chromium1.LoadURL(targetUrl); // 阻止CEF创建新浏览器窗口 Result : True; end; end;targetDisposition可以区分是WOD_NEW_FOREGROUND_TAB还是WOD_NEW_WINDOW如果需要把某些外链丢给系统默认浏览器可以在这里判断域名白名单然后调用ShellExecute(0,open,PChar(targetUrl),nil,nil,SW_SHOWNORMAL)。关键点只要把Result设为True新窗口就不会创建但导航动作不会自动发生必须手动调用LoadURL。很多新手只设Result : True结果点击链接后页面没有任何反应。4. JavaScript与Delphi双向通信的完整实现嵌入浏览器的核心价值在于复用Web端UI同时又不放弃Delphi的业务能力。双向通信意味着Delphi对象能被JS直接“调用”JS函数也能被Delphi主动触发。CEF4Delphi把这件事封装成了两个方向注册绑定对象、执行JavaScript。4.1 注册Delphi对象到JS上下文CEF4Delphi提供了一个简化基类TCefSimpleJSObject继承它就能把自己的方法暴露给JS。以下代码注册一个bridge对象JS端可以调用bridge.showMessage(hello)type TDelphiBridge class(TCefSimpleJSObject) protected procedure showMessage(const event: ICefEvent; const arguments: TCefValueArray); override; end; procedure TDelphiBridge.showMessage(const event: ICefEvent; const arguments: TCefValueArray); begin if Length(arguments) 0 then ShowMessage(JS调用了Delphi: arguments[0].GetStringValue); end;在Form的OnLoadEnd事件确保JS上下文已存在里注册procedure TForm1.Chromium1LoadEnd(Sender: TObject; const browser: ICefBrowser; const frame: ICefFrame; httpStatusCode: Integer); begin // 只向主frame注册避免iframe里重复绑定导致变量被覆盖 if frame.IsMain then Chromium1.RegisterJavaScriptObject(bridge, TDelphiBridge.Create); end;TDelphiBridge.Create不需要手动释放CEF的v8引擎会持有引用。JS侧用法就像调用本地函数bridge.showMessage(表格行双击事件);注意方法名大小写要保持一致CEF4Delphi默认绑定函数时严格区分大小写。arguments是TCefValueArray取字符串用GetStringValue取整数用GetIntValue取布尔用GetBoolValue。一旦类型不匹配CEF不会抛异常而是静默转换或返回0这常常让调试陷入迷雾。我一般会在showMessage里先打印arguments[0].Type帮助排查。4.2 从Delphi调用JavaScript函数反向调用不需要注册任何对象直接用ExecuteJavaScript。比如点击Delphi按钮把当前编辑框内容同步到页面的一个输入框procedure TForm1.Button1Click(Sender: TObject); var js: string; begin js : Format(document.getElementById(title).value %s;, [QuotedStr(Edit1.Text)]); Chromium1.Browser.MainFrame.ExecuteJavaScript(js, , 0); end;ExecuteJavaScript的三个参数依次是JS代码、来源URL不影响执行一般传空串、行号用于调试时定位脚本错误。还可以在页面加载完成后调用页内的全局函数procedure TForm1.Button2Click(Sender: TObject); begin Chromium1.Browser.MainFrame.ExecuteJavaScript( if (window.exportData) exportData(fromDelphi);, local, 1); end;调用时机特别重要。如果页面正在跳转旧的MainFrame可能已经失效这时执行JS会抛出“无框架上下文”的错误。稳妥做法是先用Chromium1.IsLoading判断或者在OnLoadEnd里做延迟调用。4.3 参数类型和返回值边界下表列出JS值和Pascal类型的大致对应关系避免你取出空对象时一头雾水JS类型TCefValue.Get方法注意事项stringGetStringValueUTF-8中文不需要额外转码numberGetIntValue或GetDoubleValue超过2^31的数字要用GetDoubleValuebooleanGetBoolValueJS的1和0不自动转布尔object/array无直接方法需要再套一层FindValue或转换成JSON字符串null/undefinedGetType判断访问成员前务必判空当需要传复杂结构比如一个订单对象时我建议不要在v8层面逐字段读取而是在JS侧JSON.stringify(obj)后传字符串Delphi端用SuperObject或同等级的JSON库解析。CEF4Delphi的v8对象模型设计得比较底层直接在Delphi里遍历JS Object容易踩到生命周期管理上的坑——项对象可能在一次跨进程调用后被意外释放。5. 容易被忽略的进程模型与资源释放陷阱到前面为止浏览器能跑、能跳转、能和JS说话了看起来一切正常。但如果现在就打正式包大概率会遇到“关闭窗口后Delphi进程不结束”或“程序第二次启动时崩溃”的问题。这既不是CEF4Delphi的bug也不是代码逻辑错误而是没理解CEF的多进程模型和VCL窗体生命周期的耦合关系。5.1 CEF的多进程模型对Delphi的影响CEF不是一个纯DLL嵌入方案它启动时会产生一个主进程browser process和多个子进程renderer, GPU, utility。在任务管理器里你会看到你的exe被运行了7、8次这是CEF的正常行为。子进程由libcef.dll拉起而它们的终止信号来自GlobalCEFApp.ShutdownMainProcess。如果代码在Application.Run结束后直接走默认退出逻辑而没有调用ShutdownMainProcess子进程就成了“孤儿”主进程的退出标志位也清不掉表现为任务管理器里残留若干同名exe。5.2 正确关闭CEFWindowParent的顺序直接关窗体而不管浏览器实例会导致浏览器窗口销毁时回调还没完成就释放了VCL句柄。下面是我在一套支付客户端里验证过的关闭顺序procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean); begin // 先告诉浏览器用户要关闭了停止后台JS Chromium1.CloseBrowser(True); // 每次关闭前都缓存一个字段等待OnBeforeClose真正触发 FCanClose : False; CanClose : FCanClose; // 启动一个短超时保护防止某些页面拒绝关闭 TimerForceClose.Enabled : True; end;procedure TForm1.Chromium1BeforeClose(Sender: TObject; const browser: ICefBrowser); begin // 到这里浏览器内部的窗口句柄已经销毁VCL组件可以安全释放 FCanClose : True; Close; end;procedure TForm1.TimerForceCloseTimer(Sender: TObject); begin // 3秒还没有触发BeforeClose强制关闭并清理CEF TimerForceClose.Enabled : False; GlobalCEFApp.ShutdownMainProcess; Halt(0); end;Chromium1.CloseBrowser(True)表示强制关闭但页面里的beforeunload事件如果弹窗确认CEF会暂时挂起关闭流程。所以必须有一个超时兜底否则用户点了一个“你确定要离开吗”确认框未响应窗口就假死在那里。5.3 一个通用的退出流程模板把上面两段拼起来就是一个可以复制的退出模板在FormCloseQuery里调用Chromium1.CloseBrowser(True),设置CanClose : False。在TChromium.OnBeforeClose中把标志位置为True并重新Close。在FormDestroy中只TCEFWindowParent.Free不操作Chromium1。在dpr的Application.Run之后无条件调用GlobalCEFApp.ShutdownMainProcess。这个顺序之所以重要是因为OnBeforeClose触发时CEF的C对象还活着但窗口已销毁此时若在事件里直接释放TCEFWindowParent会重复释放同一个句柄导致访问冲突。正确的做法是让VCL的消息循环接管后续释放。如果遇到个别页面脚本无限循环导致beforeunload永不返回超时强制Halt(0)虽然粗暴但对于桌面应用来说总比进程残留被用户骂“软件关不掉”要好。另一个相关技巧开发时把GlobalCEFApp.LogFile指向一个文件路径LogSeverity设为LOGSEVERITY_VERBOSE退出崩溃时能看到是哪一步卡住。CEF4Delphi的日志里会明确打印Browser process shutdown sequence的每一阶段对照日志就能定位是渲染进程挂起还是GPU进程没退干净。这份zip里自带的Demo工程关闭流程基本都遵循上述模板直接抄它们的FormCloseQuery实现是最省事的。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门