简介:一份面向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 与 GlobalCEFApp
CEF4Delphi 本质上是 Chromium Embedded Framework 的 Delphi 封装层,使用前先把组件层次理清,否则后面排查问题会像无头苍蝇。项目里反复出现的 DcefBrowser,可以理解为一层基于 TChromium 的浏览器封装单元,它的存在是为了少写重复代码——比如统一处理OnBeforePopup、OnLoadEnd、JS 上下文创建这些事件。源码里同时出现TChromeTabs,负责把多标签页 UI 与多个TChromium实例做绑定。
| 组件/类 | 作用 | 典型使用场景 |
|---|---|---|
GlobalCEFApp | 全局初始化单例,负责加载 libcef.dll、配置缓存与日志 | 程序入口处初始化一次 |
TChromium | 单个浏览器控件的 VCL 封装,可放在任意容器上 | 嵌入 Panel、TabSheet |
TChromiumWindow | 自带窗体的浏览器组件 | 快速弹出一个独立浏览器窗口 |
TChromeTabs | Chrome 风格多标签页容器组件 | 标签页管理、页面切换 |
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 locale |
| resources.pak | 页面无图标、UI 残缺 | 页面加载后查看控制台 |
额外的调试工具是 CEF 的远程调试端口。在初始化代码里加一行GlobalCEFApp.RemoteDebuggingPort := 9222;,程序运行后,用 Chrome 打开http://localhost:9222,能看到当前所有 CEF 渲染页面的 DOM 树和 Console 输出。这比在 Delphi 里打断点效率高很多,尤其是排查 JS 交互时——页面报错信息全部在 Console 里,Delphi 侧看不到任何异常,远程调试端口就是桌面程序里的“浏览器开发者工具”。结合这四个维度的检查手段,D10.3 下 CEF 的绝大部分异常都能在一个小时内定位到根因。
本文还有配套的精品资源,点击获取