Delphi 7 接入 CEF4Delphi:关键边界与避坑实践
简介:面向仍在使用Delphi7及后续版本、希望在桌面程序中嵌入Chrome内核的开发者,这份CEF4Delphi控件包提供较完整的源码、组件与示例工程,可解决传统WebBrowser控件内核老旧、兼容性不足的问题,也适合评估前端页面与本地程序交互方案。压缩包共1330个文件,约229.27MB,主体为434个pas源码、130个inc头文件及大量dproj、dpr、dfm、lpi、lpr等Delphi与Lazarus工程文件,并含bat脚本、res/ico/png资源、html页面和少量dpk、fmx文件,兼顾VCL与FireMonkey两种界面框架。包内同时提供CEF4Delphi-master与Windows 32位cef_binary运行库,以及demo目录下的可编译示例,便于对照组件封装、事件处理和多平台项目结构进行学习与二次开发。已有2826人学习下载,适合需要嵌入Chrome内核、维护旧版Delphi项目或研究CEF封装的开发者参考。
1. CEF4Delphi 嵌 Chrome:Delphi 7 项目先要判断的三个边界
手头这份最新版 CEF4Delphi 控件包,本质上是一份把 Chrome 内核封装成 Delphi 组件的源码软件,让老 Delphi 7 工程也能在前端页面里用上现代浏览器能力,而不是继续跟TWebBrowser的 IE 内核较劲。它适合三类人:维护 Delphi 7 老系统但需要展示 HTML 报表的人、想用前端技术做界面又不想整体迁移到新工具链的人、以及需要在一个桌面程序里稳定加载 Chrome 页面的开发者。
但别把它当成“装个控件就能跑”的东西。Delphi 7 是 ANSI 时代的编译器,CEF4Delphi 的示例又常常带着新版本 Delphi 的写法,边界判断比装组件更重要:第一,二进制包必须和源码包匹配,缺locales、icudtl.dat这类文件就是白屏;第二,CEF 是多进程模型,子进程路径配错,主窗口能出来但页面永远不渲染;第三,Delphi 7 下字符串、路径、关闭时序都比新版本更容易翻车。先把这三件事想清楚,再动手接源码,后面能少走很多弯路。
2. 把 CEF4Delphi 源码接进 Delphi 7:库路径、二进制与工程骨架
2.1 资源包先拆成四块,不要混着往工程里塞
拿到 CEF4Delphi 的包,先按用途拆开看,而不是双击.dpk一路安装。常见的包结构大致是源码目录、二进制目录、示例目录和设计期包目录,不同发布包命名会有差异,但职责差不多。源码目录放uCEFApplication.pas、uCEFChromium.pas、uCEFWindowParent.pas这些单元;二进制目录放libcef.dll、icudtl.dat、snapshot_blob.bin、locales等运行文件;示例目录是用法参考;包目录用于把组件装进 IDE,但 Delphi 7 下我一般先不装包,直接走源码路径,减少设计期包和运行期包不一致的问题。
| 位置 | 作用 | 建议放法 |
|---|---|---|
| 源码目录 | 提供uCEF*单元 | 固定在工程外某个短路径,加入 Library Path |
| 二进制目录 | CEF 运行必需的 dll 和数据文件 | 按原目录结构复制到输出 exe 同目录 |
| 示例目录 | 参考初始化、关闭、下载、JS 交互写法 | 只读,不直接改 |
| 设计期包 | 把控件注册到 IDE 面板 | 可选,先跑通源码再装 |
这里有个血泪经验:二进制目录不要只复制libcef.dll。CEF 启动时还会找locales、icudtl.dat、snapshot_blob.bin、v8_context_snapshot.bin等资源,少一个就可能在日志里报错,表现却是“窗口出来了,页面空白”。我一般把二进制目录整体复制到bin下,再让工程输出到同一个bin,避免调试时 exe 在Win32\Debug、dll 在别处。
2.2 Delphi 7 的 Library Path 要短、要固定、不要带中文
Delphi 7 的 Library Path 在 IDE 的环境选项里配置,路径一多一长,编译就会变慢,还容易出现“这个单元明明在,但就是找不到”的玄学。我的做法是把 CEF4Delphi 源码目录放在一个短路径下,例如D:\CefSrc\source,然后把source和它下面必要的子目录加入 Library Path。工程自身的 Search Path 也可以加,但不要把同一个目录同时塞进环境路径和工程路径,后面换机器会混乱。
还有一点经常被忽略:路径里不要带中文、空格和特殊符号。Delphi 7 对 Unicode 路径支持本来就弱,CEF 子进程再一掺和,问题会变得很难定位。源码目录、工程目录、输出目录、缓存目录,这四类路径统一用英文短路径,能省掉后面一半的“找不到文件”排查。
如果只是先验证能不能跑,不建议一上来装设计期包。Delphi 7 的包安装对单元版本、运行期包很敏感,装完报Cannot load package反而增加变量。直接建一个 VCL Forms Application,把源码目录加入 Library Path,在窗体里用代码创建TChromium,跑通之后再决定要不要装到面板上。
2.3 项目文件骨架:GlobalCEFApp 必须最先创建,StartMainProcess 不能漏
CEF4Delphi 的入口不是窗体,而是项目文件里的GlobalCEFApp。它负责初始化 CEF、识别子进程、设置缓存和日志。很多“主窗体能显示、浏览器不加载”的问题,根子就在项目文件:要么GlobalCEFApp创建晚了,要么没有调用StartMainProcess。下面这段是 Delphi 7 能接受的最小骨架,重点是顺序。
program CefDelphiDemo; uses Forms, SysUtils, Unit1 in 'Unit1.pas' {Form1}, uCEFApplication, uCEFConstants; {$R *.res} begin // 必须先创建 GlobalCEFApp,再碰 Application GlobalCEFApp := TGlobalCEFApp.Create(nil); try // 子进程路径必须指向当前 exe 的绝对路径;改项目名后这里要同步改 GlobalCEFApp.BrowserSubprocessPath := ExtractFilePath(ParamStr(0)) + 'CefDelphiDemo.exe'; // 开发期先放 exe 同目录;生产环境建议换到可写的用户目录 GlobalCEFApp.Cache := ExtractFilePath(ParamStr(0)) + 'cache'; // 日志是后面排查白屏、子进程失败的第一手材料 GlobalCEFApp.LogFile := ExtractFilePath(ParamStr(0)) + 'cef.log'; GlobalCEFApp.LogSeverity := LOGSEVERITY_INFO; // 开发期保持多进程;SingleProcess 只用于临时定位问题 GlobalCEFApp.SingleProcess := False; // 这一句会判断当前进程是不是 CEF 子进程 GlobalCEFApp.StartMainProcess; Application.Initialize; Application.CreateForm(TForm1, Form1); Application.Run; finally GlobalCEFApp.Free; end; end.这段代码里,StartMainProcess是关键。CEF 启动渲染进程时,会带着--type=renderer之类的参数再次启动同一个 exe;StartMainProcess识别到子进程后执行子进程逻辑并退出,不会再去创建主窗体。如果你把它漏掉,主进程和子进程的逻辑混在一起,轻则页面空白,重则进程反复启动。BrowserSubprocessPath写成固定文件名时,项目改名后一定要同步,否则 CEF 找不到子进程 exe。Cache和LogFile在开发期放 exe 同目录问题不大,但安装到只读目录后必须换位置,后面第 4 章会展开。
3. 第一个 TChromium 窗口:CreateBrowser、LoadURL 与关闭时序
3.1 用 TCEFWindowParent + TChromium 搭最小可运行窗口
CEF4Delphi 里把浏览器塞进窗体,常见做法是准备一个可视的父容器TCEFWindowParent,再创建一个非可视的TChromium,由TChromium.CreateBrowser把浏览器窗口挂到父容器上。不要直接在FormCreate里LoadURL,那个时候浏览器还没创建,调用会丢或者报错。下面这段是 Delphi 7 下比较稳的最小单元写法。
unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, uCEFChromium, uCEFWindowParent, uCEFInterfaces; type TForm1 = class(TForm) procedure FormCreate(Sender: TObject); procedure FormShow(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); private FChromium: TChromium; FWindowParent: TCEFWindowParent; FCanClose: Boolean; procedure ChromiumAfterCreated(Sender: TObject; const browser: ICefBrowser); procedure ChromiumBeforeClose(Sender: TObject; const browser: ICefBrowser); public end; var Form1: TForm1; implementation {$R *.dfm} procedure TForm1.FormCreate(Sender: TObject); begin FCanClose := False; // 先放父容器,浏览器窗口后面挂到它上面 FWindowParent := TCEFWindowParent.Create(Self); FWindowParent.Parent := Self; FWindowParent.Align := alClient; // TChromium 这里用代码创建,避免 Delphi 7 设计期包版本问题 FChromium := TChromium.Create(Self); FChromium.OnAfterCreated := ChromiumAfterCreated; FChromium.OnBeforeClose := ChromiumBeforeClose; end; procedure TForm1.FormShow(Sender: TObject); begin // 父容器已经完成布局,再创建浏览器 FChromium.CreateBrowser(FWindowParent); end; procedure TForm1.ChromiumAfterCreated(Sender: TObject; const browser: ICefBrowser); begin // 浏览器对象创建完成后再加载 URL,不要抢跑 FChromium.LoadURL('https://www.example.com'); end; procedure TForm1.ChromiumBeforeClose(Sender: TObject; const browser: ICefBrowser); begin if FCanClose then Close; end; procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); begin if FCanClose then begin Action := caFree; Exit; end; // 第一次关闭请求先拦住,等 CEF 回调 OnBeforeClose 再真正关 Action := caNone; FCanClose := True; FChromium.CloseBrowser(True); end; end.FormCreate里只创建对象和绑定事件,FormShow里才CreateBrowser,是因为父容器在窗体显示前尺寸和句柄状态还不稳定。OnAfterCreated是浏览器对象真正创建完成的信号,在这里LoadURL才不会白屏。FormClose里第一次先Action := caNone,把关闭权交给CloseBrowser(True),等OnBeforeClose回调回来再真正Close,这是 CEF 生命周期里最容易翻车的一段。
3.2 LoadURL 的时机和路径写法,决定你是秒开还是白屏
LoadURL本身不复杂,难的是“什么时候调”和“传什么路径”。在线地址直接传https://没问题;本地页面常见做法是file:///加绝对路径,但 Windows 路径的反斜杠要换成斜杠,中文目录还要做 URL 编码。Delphi 7 是 ANSI 字符串,直接拼中文路径很容易在 CEF 那边变成乱码,表现就是“页面不存在”或者一直转圈。我一般把前端资源放在纯英文目录下,例如web\index.html,然后这样加载:
var LocalPage: string; begin LocalPage := ExtractFilePath(ParamStr(0)) + 'web\index.html'; LocalPage := StringReplace(LocalPage, '\', '/', [rfReplaceAll]); FChromium.LoadURL('file:///' + LocalPage); end;这段代码只做了路径分隔符替换,没有做完整 URL 编码,所以它适合纯英文、无空格的路径。如果路径里必须带空格,先把空格替换成%20;如果必须带中文,优先改目录名,而不是在 Delphi 7 里硬扛编码转换。LoadURL放在OnAfterCreated里是底线,放在FormShow里直接调,遇到慢加载或者多标签场景就会时好时坏。
3.3 关闭顺序:先 CloseBrowser,再等 OnBeforeClose,最后释放窗体
CEF 的关闭不是FormClose里一句Application.Terminate能解决的。浏览器还在跑渲染进程、JS 还在执行、网络请求还没断,你直接把窗体释放掉,CEF 回调回来就会访问已释放的对象。比较稳的流程是:用户点关闭,FormClose拦截,调用CloseBrowser(True);CEF 触发OnBeforeClose;在回调里再Close窗体;第二次进入FormClose时放行并caFree。
上面代码里的FCanClose就是干这个的。注意CloseBrowser(True)的参数是强制关闭,适合退出场景;如果只是切页面,不要随便强制关。还有一点,OnBeforeClose里不要再做耗时操作,也不要在里面弹窗询问“是否保存”,那时候浏览器对象已经在收尾,弹窗容易把关闭流程卡死。需要询问就在FormClose第一次拦截时问,问完再决定是否走CloseBrowser。
4. 多进程与缓存参数:BrowserSubprocessPath、Cache、日志怎么设
4.1 BrowserSubprocessPath 是多进程的命门,路径错就白屏
CEF 默认是多进程模型,主进程负责窗口和调度,渲染、GPU、网络等逻辑跑在子进程里。子进程不是凭空来的,它就是同一个 exe 带不同命令行参数再启动一次。BrowserSubprocessPath告诉 CEF 去哪里找这个 exe。很多白屏案例里,主窗口正常显示,任务管理器里也能看到几个进程,但页面就是不出来,最后发现BrowserSubprocessPath指向了一个旧文件名,或者用了相对路径,工作目录一变就找不到。
我的习惯是显式写绝对路径,并且和项目输出名保持一致:
// 项目名改了,这里必须同步改;不要依赖默认值 GlobalCEFApp.BrowserSubprocessPath := ExtractFilePath(ParamStr(0)) + 'CefDelphiDemo.exe';ExtractFilePath(ParamStr(0))取的是当前 exe 所在目录,后面拼实际 exe 名。注意不要拼成ParamStr(0)本身带引号的形式,CEF4Delphi 内部会处理命令行。还有人把子进程路径指向libcef.dll,那是完全错的,子进程必须是可执行文件。调试时如果怀疑子进程没起来,先看cef.log,再把SingleProcess临时设为True做对比:单进程能显示、多进程不能显示,基本就是子进程路径或权限问题。
4.2 Cache 和日志目录:不要写在 Program Files 里
开发期把Cache放 exe 同目录很方便,但安装到Program Files后,普通用户没有写权限,CEF 创建缓存失败,表现可能是启动报错、页面加载异常,甚至直接闪退。日志文件也一样,写不进去就没有排查线索。生产环境我一般把缓存和日志放到用户可写目录,例如%LOCALAPPDATA%下以项目名命名的文件夹。
function GetCefDataDir: string; begin // LOCALAPPDATA 是当前用户可写目录,避免安装目录权限问题 Result := IncludeTrailingPathDelimiter(GetEnvironmentVariable('LOCALAPPDATA')) + 'CefDelphiDemo\'; ForceDirectories(Result); end; // 在 GlobalCEFApp 创建后调用 GlobalCEFApp.Cache := GetCefDataDir + 'cache'; GlobalCEFApp.LogFile := GetCefDataDir + 'cef.log';ForceDirectories保证父目录存在,IncludeTrailingPathDelimiter保证拼接不会少斜杠。Delphi 7 下GetEnvironmentVariable的返回值是 ANSI 字符串,LOCALAPPDATA路径里如果带中文用户名,仍然可能出问题。遇到这种情况,优先把缓存目录指到一个纯英文路径,或者用短路径形式,不要在 CEF 初始化阶段跟编码较劲。
4.3 调试端口和 SingleProcess 的使用边界
CEF 支持远程调试端口,开发期打开后可以用 Chrome 打开http://127.0.0.1:9222看页面 DOM、控制台和网络请求。这个功能对排查前端页面特别有用,但它只适合本机开发,不要在生产环境长期开着。
// 仅开发机使用;上线前删掉或改成配置开关 GlobalCEFApp.RemoteDebuggingPort := 9222;SingleProcess则是另一个极端:把所有逻辑塞进一个进程,调试时少一层子进程干扰,但稳定性和性能都不如多进程。我的用法是“只在定位子进程问题时临时打开”,确认问题后立刻关掉。不要把SingleProcess := True当成解决白屏的常规手段,它只是诊断工具。还有一个常见误区:以为Cache设了就能加速所有页面。CEF 的缓存行为受页面响应头、请求方式影响,POST 请求和带no-store的响应不一定按你预期缓存,别把业务逻辑建立在缓存一定命中上。
5. 避坑与排查:Delphi 7 下 CEF4Delphi 的五个高频翻车点
5.1 白屏但主窗口正常:先查子进程路径和二进制文件
现象:窗体出来了,TCEFWindowParent区域一片白,任务管理器里能看到多个同名进程,日志里可能只有几行初始化信息。
原因:最常见的是BrowserSubprocessPath指向错误,或者二进制目录复制不全。CEF 子进程启动失败,主进程还在等渲染结果,页面自然空白。另一个原因是 exe 放在带空格或中文的路径下,子进程命令行解析出问题。
解决:把BrowserSubprocessPath改成绝对路径,确认文件名和实际 exe 完全一致;把二进制目录整体复制到 exe 同目录,重点检查locales、icudtl.dat、snapshot_blob.bin;打开LogFile看有没有Failed to launch之类的记录。临时把SingleProcess设为True,如果单进程能显示,基本可以锁定子进程问题。
5.2 关闭窗体卡死或报访问违例:关闭时序没走完
现象:点右上角关闭,窗口消失一半,进程还在;或者直接弹Access violation,堆栈落在 CEF 回调里。
原因:在FormClose里直接caFree,没有等CloseBrowser的OnBeforeClose回调。CEF 还在往窗体句柄发消息,窗体已经释放,回调访问野指针。
解决:按第 3 章的写法,第一次FormClose先Action := caNone,调用CloseBrowser(True),在OnBeforeClose里再Close,第二次FormClose才caFree。不要在OnBeforeClose里做弹窗、等待网络、释放大量对象,那些操作放到关闭之前。
5.3 本地页面加载失败:中文路径和反斜杠是常客
现象:LoadURL('file:///...')之后页面空白,控制台报找不到文件,或者 URL 里的中文变成乱码。
原因:Windows 路径用反斜杠,file:///要求斜杠;Delphi 7 的 ANSI 字符串拼中文路径,传给 CEF 的ustring时编码不一致;路径里有空格没有转义。
解决:前端资源放纯英文短路径,加载前把\替换成/;空格替换成%20;能不用中文目录就不用。必须用中文时,先确认LoadURL收到的字符串在日志里是否正常,再考虑用 URL 编码函数处理,而不是直接拼字符串。
5.4 安装后启动失败:缓存目录没有写权限
现象:开发机跑得好好的,装到别的机器上,双击没反应或者启动就报错,日志文件也生成不了。
原因:Cache、LogFile用了 exe 同目录,安装到Program Files后普通用户无写权限。CEF 初始化缓存失败,后续流程直接中断。
解决:把Cache和LogFile迁到%LOCALAPPDATA%下的项目目录,启动时用ForceDirectories建目录;安装包不要默认往只读目录写运行数据。日志路径最好在界面上能查到,出问题时让用户能直接发回来。
5.5 Delphi 7 编译报错:新版本示例里的语法不要硬抄
现象:从示例里复制代码,Delphi 7 报Undeclared identifier、E2010 Incompatible types,或者泛型、匿名方法相关错误。
原因:CEF4Delphi 的示例往往面向较新的 Delphi,使用了泛型、匿名方法、Unicode 字符串处理等 Delphi 7 不支持或支持不完整的语法。直接复制就会编译不过。
解决:先确认手头源码包是否包含 Delphi 7 兼容分支;没有就找示例里最基础的TChromium用法,把事件签名改回 Delphi 7 的对象方法形式;遇到字符串类型不匹配,优先用ustring对应的转换,不要强行String和WideString互转。编译错误先解决类型和语法,不要带着错误去调运行期白屏,那样变量太多。
6. 前端页面与 Delphi 消息桥:用 ConsoleMessage 和 ExecuteJavaScript 做闭环
前端页面和 Delphi 之间总得传点东西。CEF4Delphi 里比较轻的做法是用OnConsoleMessage接前端console.log的输出,约定一个前缀,把 JSON 字符串当消息发回来;Delphi 处理完再用ExecuteJavaScript把结果写回页面。这个方案不优雅,但依赖少,Delphi 7 下也能跑,适合先把手头的业务闭环做出来。
// 前端:约定 APP_MSG: 前缀,Delphi 端只处理这个前缀 function sendToDelphi() { const payload = { type: 'search', keyword: document.getElementById('keyword').value }; console.log('APP_MSG:' + JSON.stringify(payload)); document.getElementById('status').innerText = '已发送'; }procedure TForm1.ChromiumConsoleMessage(Sender: TObject; const browser: ICefBrowser; const message, source: ustring; line: Integer; const level: TCefConsoleMessageLevel; out Result: Boolean); var Payload: string; begin Result := False; if Pos('APP_MSG:', message) = 1 then begin // 去掉前缀,剩下的就是前端发来的 JSON 原文 Payload := Copy(message, 9, Length(message)); Memo1.Lines.Add(Payload); // 处理完回写页面;实际项目里先判断 frame 是否为主框架 FChromium.ExecuteJavaScript( 'document.getElementById("status").innerText = "Delphi 已收到";', 'about:blank', 0); end; end;OnConsoleMessage里把Result设为False,控制台消息继续保留,方便调试;设True会吞掉默认输出。Copy(message, 9, ...)是因为APP_MSG:正好 8 个字符,从第 9 位开始才是内容。ExecuteJavaScript的第三个参数是行号,传 0 即可,第二个参数传脚本来源 URL,传about:blank不影响执行。真正要注意的是调用时机:页面还没加载完就注入 JS,document.getElementById可能拿到nil,所以回写最好放在OnLoadEnd之后,或者在前端用DOMContentLoaded包一层。
| 方向 | 方法 | 触发时机 | 注意点 |
|---|---|---|---|
| 前端到 Delphi | OnConsoleMessage+ 前缀 | 前端主动console.log | 消息别太大,高频消息做节流 |
| Delphi 到前端 | ExecuteJavaScript | 页面加载完成后 | 先判断主框架,避免注入到 iframe |
| 调试 | RemoteDebuggingPort | 仅开发机 | 上线关闭,不要长期暴露 |
从那以后我每次接 CEF4Delphi 项目,都先把子进程路径、缓存目录、CloseBrowser时序这三件事写成检查项,再谈前端页面和消息桥。希望帮到你。
本文还有配套的精品资源,点击获取