
简介一份面向Delphi开发者的Chromium内核嵌入方案示例作者在Rad Studio 10.3 Rio下实测通过。集成CEF4Delphi、DcefBrowser与TChromeTabs组件解决在原生Windows程序中加载谷歌浏览器内核、管理多标签页以及JS与网页元素交互控制等常见需求。包内共1831个文件核心包含404个Pascal源文件、333个编译单元、103个项目文件、73个窗体定义另外还有DPK包、DLL动态库、HTML页面和JS脚本等运行资源并附带多个批处理脚本用于清理编译缓存或快速重建工程便于打开工程后对照调试。压缩包为RAR格式整体约71.43MB目前已有1173人学习下载。示例代码覆盖多页面标签创建与切换、网页元素读取与赋值、JavaScript调用Delphi方法等场景且封装了常用的JS注入与事件监听接口二次开发时不需要重复造轮子对需要把CEF嵌入到Delphi业务系统中的中高级开发人员来说这是可直接参考的完整模板能显著减少环境配置和功能联调的时间成本。1. 一个绕不开的尴尬D10.3 上的 CEF 控件为什么这么难落地做 Delphi 桌面端的同行应该都有过这种经历网上翻到一个 CEF4Delphi 的 Demo作者用的 Delphi 11 甚至 12你手头是 D10.3 Rio拉下来编译直接报错不是TChromium找不到就是GlobalCEFApp在Application.CreateForm之前没初始化程序起来白屏、崩溃、弹窗一半就没了。这套“阳光有点冷”系列源码的价值在于它把 CEF4Delphi、DcefBrowser 封装、TChromeTabs 多标签、JS 与网页元素交互这些常用能力打包成了一整套在 D10.3 下实测可跑的工程源码里还带cleanup.bat、00-DeleteDCUs.bat这类清理脚本对需要把 Chromium 内核嵌进旧版 Delphi 工程的团队来说是能直接抄作业的参考实现。适合做混合型客户端、网页数据采集、内部系统自动化操作的人老手看封装思路新手照步骤能跑通。2. CEF4Delphi 组件架构与 D10.3 下的初始化顺序2.1 认识三个层次TChromium、TChromiumWindow 与 GlobalCEFAppCEF4Delphi 本质上是 Chromium Embedded Framework 的 Delphi 封装层使用前先把组件层次理清否则后面排查问题会像无头苍蝇。项目里反复出现的 DcefBrowser可以理解为一层基于 TChromium 的浏览器封装单元它的存在是为了少写重复代码——比如统一处理OnBeforePopup、OnLoadEnd、JS 上下文创建这些事件。源码里同时出现TChromeTabs负责把多标签页 UI 与多个TChromium实例做绑定。组件/类作用典型使用场景GlobalCEFApp全局初始化单例负责加载 libcef.dll、配置缓存与日志程序入口处初始化一次TChromium单个浏览器控件的 VCL 封装可放在任意容器上嵌入 Panel、TabSheetTChromiumWindow自带窗体的浏览器组件快速弹出一个独立浏览器窗口TChromeTabsChrome 风格多标签页容器组件标签页管理、页面切换DcefBrowser.pas对 TChromium 的二次封装集中处理常用事件减少重复事件代码TChromium是核心它内部持有ICefBrowser接口而ICefBrowser.MainFrame才是你执行 JS、遍历 DOM 的操作入口。理解这一点很关键因为后续所有 JS 交互都在MainFrame上做源码里常见的Frame.ExecuteJavaScript也是走这条路。2.2 GlobalCEFApp 初始化顺序错了就是白屏这套源码能在 D10.3 下跑起来最重要的前提是GlobalCEFApp的初始化参数正确。CEF 的初始化是进程级操作必须在创建任何窗体之前完成而且 DLL 路径、资源目录、缓存目录这三项缺一不可。以下是我在项目中验证过的初始化代码program MyCEFApp; uses Vcl.Forms, uMainForm in uMainForm.pas {MainForm}, uCEFApplication; var GlobalCEFApp: TCefApplication; begin GlobalCEFApp : TCefApplication.Create; // 指向 exe 所在目录libcef.dll、icudtl.dat 必须在这里 GlobalCEFApp.FrameworkDirPath : ExtractFilePath(Application.ExeName); // 资源文件目录含 v8_context_snapshot.bin 等 GlobalCEFApp.ResourcesDirPath : ExtractFilePath(Application.ExeName); // locales 子目录缺少会报错或文字乱码 GlobalCEFApp.LocalesDirPath : ExtractFilePath(Application.ExeName) locales; // 缓存目录不设置会在每次启动时重新加载全部资源 GlobalCEFApp.cache : ExtractFilePath(Application.ExeName) cache\; // 日志文件排错非常有用下文会专门讲 GlobalCEFApp.LogFile : ExtractFilePath(Application.ExeName) cef.log; GlobalCEFApp.LogSeverity : LOGSEVERITY_INFO; // 启动主进程注意必须在 CreateForm 之前 GlobalCEFApp.StartMainProcess; Application.Initialize; Application.CreateForm(TMainForm, MainForm); Application.Run; GlobalCEFApp.Free; end.这段代码里FrameworkDirPath、ResourcesDirPath、LocalesDirPath三个路径如果指向错误程序会直接崩溃或闪退而且没有任何可见的 Delphi 异常提示。cache目录建议显式指定否则 CEF 会尝试写系统临时目录在部分受限权限环境里会导致页面加载失败。StartMainProcess是阻塞调用直到浏览器进程初始化完成才返回所以必须放在Application.CreateForm前面。提示D10.3 对应的 CEF4Delphi 版本较老初始化接口可能与新版不同。源码包里如果带uCEFApplication.pas以包内版本为准从 GitHub 拉新版时注意看 Release 说明里对 Delphi 版本的支持范围。2.3 批处理脚本在编译流程里的作用源码根目录里那几个.bat文件不是摆设。00-DeleteDCUs.bat用于清理历史编译产物解决因为dcu文件来自不同版本编译器而导致的链接错误比如F2613 Unit uCEFInterfaces not found这类问题很多时候不是单元缺失而是缓存了旧平台的dcu。cleanup.bat同理删除临时文件和编译中间文件。00-CreateLazarusResources.bat是给 Lazarus 用户生成资源文件的Delphi 用户可以直接跳过。实际编译时我一般先把源码包放到一个纯英文、无空格的路径下比如D:\Demos\CEF4Delphi_D10然后双击00-DeleteDCUs.bat清理一次再打开dpr工程编译。如果报错指向某个dcu路径优先检查工程 Options 里的Search path是否还残留着旧机器上的绝对路径。3. DcefBrowser 封装与 TChromeTabs 多标签集成实战3.1 为什么不用 TWebBrowser而要用 CEF 封装Delphi 自带的TWebBrowser基于 IE 内核最大的痛点是 JS 兼容性差遇到 ES6 语法、fetch、Promise这些现代特性直接罢工更不用说 WebRTC、WebGL 这类能力。CEF 是 Chromium 内核等效于把 Chrome 浏览器嵌进客户端里PWA、Canvas、加密协议都能正常跑。从开发成本看TWebBrowser确实开局容易但后面每遇到一个兼容性问题都要绕路而 CEF 封装的成本集中在首次集成的静态文件部署上。3.2 TChromeTabs 集成每个 Tab 持有独立的 TChromium 实例多标签页是浏览类客户端的基础功能TChromeTabs源码里核心思路是一个标签页对应一个TTabSheet每个TTabSheet上放一个独立的TChromium实例。这里有个关键点TChromium不能共享每个实例对应独立的渲染进程。以下是我惯用的创建方式procedure TMainForm.AddNewTab(const AUrl: string); var LTabSheet: TTabSheet; LChromium: TChromium; begin // 创建标签页容器 LTabSheet : TTabSheet.Create(PageControl1); LTabSheet.PageControl : PageControl1; LTabSheet.Caption : 新页面; // 在标签页中创建独立的 Chromium 实例 LChromium : TChromium.Create(LTabSheet); LChromium.Parent : LTabSheet; LChromium.Align : alClient; // 统一事件处理 LChromium.OnBeforePopup : DoOnBeforePopup; LChromium.OnTitleChange : DoOnTitleChange; LChromium.OnLoadEnd : DoOnLoadEnd; // 记录标签和浏览器实例的映射关系 FTabMap.Add(LTabSheet, LChromium); // 加载目标页面 LChromium.LoadURL(AUrl); PageControl1.ActivePage : LTabSheet; end;这段代码里FTabMap是一个TDictionary用来维护标签和浏览器实例的对应关系。关闭标签时要注意顺序——先释放TChromium关联的资源再销毁TTabSheet顺序反了会触发访问违规错误。TChromeTabs组件内部虽然自带标签 UI但它不管理 TChromium 的生命周期这个责任始终在业务代码里。注意同一个窗口里不要创建超过 15~20 个 TChromium 实例每个实例对应一个独立渲染进程内存开销大约 80~150MB标签多了要引入页面回收策略比如把非活动页面挂起。3.3 Popup 新窗口重定向到新标签网页里常见的window.open()或target_blank默认会触发OnBeforePopup如果不处理CEF 会弹出一个新的原生窗口这不符合客户端的多标签交互逻辑。源码里对 Popup 的处理很规范核心是拦截并改写创建目标procedure TMainForm.DoOnBeforePopup(Sender: TObject; const browser: ICefBrowser; const frame: ICefFrame; const targetUrl, targetFrameName: string; targetDisposition: TCefWindowOpenDisposition; userGesture: Boolean; const popupFeatures: TCefPopupFeatures; var windowInfo: TCefWindowInfo; var client: ICefClient; var settings: TCefBrowserSettings; var extra_info: ICefDictionaryValue; var noJavascriptAccess: Boolean; var Result: Boolean); begin // 将新窗口导航重定向为当前窗口内的新标签页 AddNewTab(targetUrl); // 阻止 CEF 创建原生弹窗 Result : True; end;targetDisposition参数可以区分打开方式常见取值有WOD_NEW_WINDOW和WOD_NEW_FOREGROUND_TAB如果业务上要区分“新窗口”和“新标签”可以在此处做分支。Result : True表示事件已接管CEF 不再执行默认的弹窗动作。这个方法不处理window.close()的回调如果页面需要主动关闭标签还要监听OnBeforeClose并找到对应 TabSheet 释放。3.4 页面导航无反应的排查思路热搜里“delphi 运行edgebrowser.navegate无反应”这类问题放在 CEF 场景里也常见。LoadURL之后页面没反应先检查GlobalCEFApp是否初始化成功再看browser是否处于有效状态。有个容易被忽略的点LoadURL必须在浏览器实例创建完成之后调用如果OnAfterCreated还没触发就执行LoadURL调用会被静默丢弃。源码里如果看到有Timer延迟加载或者OnAfterCreated里做导航就是这个原因。4. JS 与网页元素交互ExecuteJavaScript 与页面数据回传4.1 用 MainFrame.ExecuteJavaScript 操作 DOM在 CEF 里执行 JS 主要走frame.ExecuteJavaScript它接收三个参数要执行的 JS 代码字符串、脚本 URL 标识、起始行号。前两个参数里脚本 URL 在 DevTools 调试时用来定位来源平时传about:blank即可。下面是一段真实的交互示例——自动填充登录表单并点击按钮procedure TMainForm.DoAutoLogin(AMainFrame: ICefFrame); var LScript: string; begin LScript : var userInput document.getElementById(username); if (userInput) { userInput.value test_user; } var pwdInput document.getElementById(password); if (pwdInput) { pwdInput.value test_pass; } var btn document.getElementById(loginBtn); if (btn) { btn.click(); }; AMainFrame.ExecuteJavaScript(LScript, about:blank, 0); end;这段代码的要点在于每个 DOM 操作前都做了空判断。页面结构变化时getElementById可能返回空对象不判断直接访问.value会在渲染进程里抛异常但 Delphi 主进程完全感知不到。执行 JS 后如果想确认页面是否跳转可以配合监听OnLoadEnd判断MainFrame的 URL 变化。4.2 用 TJSObject 把 Delphi 函数暴露给 JS单向执行 JS 只能操作页面如果页面里的 JS 需要回调 Delphi 代码比如把登录 token 传给客户端验证就必须在 CEF 的 V8 上下文里注册全局 JS 对象。CEF4Delphi 提供的TJSObject就是干这个的它能把 Delphi 方法绑定成页面里的全局函数。代码如下procedure TMainForm.DoContextCreated(Sender: TObject; const browser: ICefBrowser; const frame: ICefFrame; const context: ICefv8Context); var LJSObject: TJSObject; begin // 第一个参数是暴露给页面的全局对象名称 LJSObject : TJSObject.Create(delphiBridge); // 把 Delphi 方法绑定为页面里的 delphiBridge.getToken() LJSObject.BindMethod(getToken, HandleGetToken); // 安装到当前 frame 的 V8 上下文 LJSObject.Install(context); end;HandleGetToken的签名必须是函数形式返回一个ICefv8Value页面里调用delphiBridge.getToken()时能同步拿到返回值。这里有一个隐藏的坑DoContextCreated会在每个 frame、每个页面导航时都触发重复注册同一对象会导致页面里的旧引用失效。常见做法是用一个Boolean标记当前browser是否已注册过或者在页面加载后通过ExecuteJavaScript再注入一次。页面侧的使用方式如下// 页面内的 JS 代码 var token delphiBridge.getToken(); if (token) { // 将 token 写入隐藏域或直接发起请求 }这种桥接方式适合同步返回小数据。如果要回传大型数据比如整页 HTML 或 JSON 字符串就要走消息路由把数据通过OnProcessMessageReceived异步送回来否则渲染进程和主进程之间传输大数据会卡住页面。4.3 获取页面元素内容的两种常用方式除了执行 JS 操作页面多数的需求是读取页面数据。第一种方式还是ExecuteJavaScript把结果暂存在 DOM 里再通过请求一个自定义 URL 让 CEF 拦截读取这个方案绕但通用。第二种更直接用 DOM 访问器在OnLoadEnd之后读取procedure TMainForm.DoLoadEnd(Sender: TObject; const browser: ICefBrowser; const frame: ICefFrame; httpStatusCode: Integer); var LDocument: ICefDomDocument; begin // 只处理主框架避免 iframe 触发多次 if not frame.IsMain then Exit; // VisitDom 异步获取 DOM 文档对象 browser.MainFrame.VisitDom( procedure(const doc: ICefDomDocument) var LValue: string; begin LValue : doc.GetElementById(price).GetElementValue; TThread.Queue(nil, procedure begin EditPrice.Text : LValue; end); end); end;VisitDom是异步回调回调里不能直接操作 VCL 控件必须用TThread.Queue切回主线程。这是新手最容易踩的坑在回调里直接赋值给Edit.Text偶尔能用偶尔闪退本质是线程上下文问题。热搜里“js contentwindow 无法找到document”的场景也与此相关——iframe 内容在没加载完成时拿到的 frame 无效必须等它的OnLoadEnd触发后再访问或者用frame.GetParent逐级排查。4.4 JS 执行时机为什么代码没错但页面没反应ExecuteJavaScript调用时机不对是交互失效的最大原因。页面元素还没渲染完成就执行脚本getElementById必然返回空。CEF 的加载完成事件是OnLoadEnd但它只代表主资源加载完成不代表页面里的异步脚本执行完。稳妥的做法是双重保障// 进入页面时执行一次 DoAutoLogin(browser.MainFrame); // 若页面有跳转或异步刷新在 OnLoadEnd 中再次执行 browser.MainFrame.ExecuteJavaScript( if (document.getElementById(loginBtn)) { ... }, about:blank, 0);配合setTimeout延迟执行是省事但不推荐的方案网络慢时 3 秒不够网络快时白等。更好的做法是在页面侧 JS 里监听DOMContentLoaded或者用 CEF 的OnLoadingStateChange判断isLoading为False后再执行。5. 编译部署的一个关键技巧开着 CEF 日志跑一遍很多 D10.3 下 CEF 白屏问题其实不是代码写错而是运行环境缺文件或初始化参数不对。刚拿到这套源码时不要急着改业务逻辑先做一次“裸跑验证”确认 CEF 自身能工作。具体做法是把GlobalCEFApp.LogSeverity调到最详细级别然后在日志里定位崩溃点GlobalCEFApp.LogFile : ExtractFilePath(Application.ExeName) cef_debug.log; GlobalCEFApp.LogSeverity : LOGSEVERITY_VERBOSE;LOGSEVERITY_VERBOSE会输出 CEF 内部的大量调试信息跑一遍程序打开cef_debug.log。如果日志末尾停留在Loading CEF相关条目后没有下文通常是libcef.dll版本与资源文件版本不匹配如果出现Failed to load locale则是locales目录缺失或路径不对。部署目录的检查也有固定清单。exe 同级目录下必须存在libcef.dll、icudtl.dat、v8_context_snapshot.bin、snapshot_blob.bin、chrome_100_percent.pak、chrome_200_percent.pak、resources.pak以及完整的locales目录。少任何一个文件程序都可能在启动期崩溃或渲染空白页。DcefBrowser 源码包里一般自带这些文件但如果是从别处拿的 CEF 版本直接替换 DLL 是行不通的核对这些文件列表即可文件/目录缺失表现排查方式libcef.dll程序启动闪退日志中无任何 CEF 条目icudtl.dat页面乱码、数字格式异常运行时查看页面文字locales 目录启动报 locale 错误日志出现 Failed to load localeresources.pak页面无图标、UI 残缺页面加载后查看控制台额外的调试工具是 CEF 的远程调试端口。在初始化代码里加一行GlobalCEFApp.RemoteDebuggingPort : 9222;程序运行后用 Chrome 打开http://localhost:9222能看到当前所有 CEF 渲染页面的 DOM 树和 Console 输出。这比在 Delphi 里打断点效率高很多尤其是排查 JS 交互时——页面报错信息全部在 Console 里Delphi 侧看不到任何异常远程调试端口就是桌面程序里的“浏览器开发者工具”。结合这四个维度的检查手段D10.3 下 CEF 的绝大部分异常都能在一个小时内定位到根因。本文还有配套的精品资源点击获取