☰
Delphi 7 项目集成 Chrome 内核:CEF4Delphi 实战指南
2026/10/27 4:02:14 网站建设 项目流程

简介:这份资源是面向 Delphi 开发者的 CEF4Delphi 控件整合包,用于在 Delphi 7 等版本中嵌入 Chrome 浏览器内核,解决传统 WebBrowser 控件内核老旧、兼容性差的问题,适合需要为桌面程序加入现代网页渲染能力的中高级开发者。压缩包共 1330 个文件,约 229.27MB,以 434 个 pas 单元、130 个 inc 头文件、79 个 dpr 工程与 75 个 dfm 窗体为主,另含 dproj、lpi、lpr 等工程配置及 res、ico、png 等资源文件,覆盖 VCL 与 FMX 多套示例工程。资源同时附带 cef_binary 运行库,按说明将 Release 与 Resources 目录内容拷入 bin 后即可编译 demo 验证效果。目前已有 2824 人学习下载,包内示例工程与批处理脚本齐全,便于快速对照接入、排查初始化与资源加载问题。

1. 把 Chrome 内核塞进 Delphi 窗体:一个老牌桌面技术栈的续命方案

上周有个做工业上位机的朋友找我,说他手上一个跑了十几年的 Delphi 7 项目要加 Web 页面展示,客户还指定要能正常渲染现代前端框架打包出来的界面。他试过 TWebBrowser,也就是 IE 那套内核,页面一打开满屏错位,Vue 的响应式直接罢工。这种场景其实很典型:老 Delphi 项目不能推倒重来,但又必须跟上现代 Web 的渲染能力。CEF4Delphi 就是干这件事的——它把 Chromium 嵌入式框架(CEF)封装成 Delphi 控件,让你在 VCL 或 FMX 窗体里直接嵌一个真正的 Chrome 内核浏览器。这份资源是截至目前较新的版本,明确支持 Delphi 7 到较新的 RAD Studio 版本,对还在维护老项目的开发者来说,算是把「前端现代化」这条路重新打通了。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序,把这个控件的结构和使用方式拆一遍。

2. 技术选型与工程结构:CEF4Delphi 到底封装了什么

2.1 为什么是 CEF 而不是系统 WebView

Delphi 桌面程序嵌网页,常见有三条路:系统自带的 TWebBrowser(Windows 上就是 IE/EdgeHTML 内核)、第三方轻量浏览器控件、以及 CEF。前两者的问题是渲染引擎版本被系统绑死,Windows 7 上跑的还是 IE11 级别的能力,现代 CSS Grid、ES6 模块、WebAssembly 基本别想。CEF 的思路不一样,它自带一份完整的 Chromium 运行时,渲染能力和同期的 Chrome 基本对齐,不依赖用户机器上装了什么浏览器。

代价是体积。CEF 的运行时目录动辄一两百 MB,打包发布时要把整个Release或Debug目录跟着 exe 一起发。这是选型时必须接受的现实:你要的是渲染一致性,就得付出分发体积。我在工业场景里一般会跟客户说清楚,安装包大一点,换来的是「页面在每台机器上长得一样」,这笔账通常划算。

CEF4Delphi 在 CEF 之上做了一层 Pascal 封装,核心价值是把 CEF 那套 C++ 的引用计数、回调接口、进程模型翻译成 Delphi 开发者熟悉的对象和事件。它不是一个「简化版浏览器」,而是尽量完整地暴露 CEF 的能力:多进程、JS 与 Delphi 双向调用、自定义协议、请求拦截、下载处理,这些都有对应封装。

2.2 源码包的目录骨架

拿到源码包后,先别急着编译,花十分钟把目录认清楚,后面改代码会省很多事。典型的 CEF4Delphi 源码包结构大致是这样:

cef4delphi/ ├── src/ # 核心封装单元 │ ├── uCEFApplication.pas # 应用级初始化、进程启动 │ ├── uCEFChromium.pas # 浏览器控件主体 │ ├── uCEFWindowParent.pas # 承载浏览器窗口的父容器 │ ├── uCEFInterfaces.pas # 所有 CEF 接口的 Pascal 声明 │ ├── uCEFClient.pas # 客户端回调实现 │ ├── uCEFLoadHandler.pas # 页面加载生命周期回调 │ ├── uCEFRequestHandler.pas # 请求拦截 │ ├── uCEFResourceHandler.pas # 自定义资源响应 │ └── uCEFTypes.pas # 枚举、记录、常量 ├── demos/ # 示例工程,按 Delphi 版本分目录 │ ├── Delphi7/ │ ├── Delphi10/ │ └── ... ├── bin/ # CEF 运行时二进制(libcef.dll 等) └── packages/ # 设计期包

src是你要读和改的地方,bin是运行时依赖,demos是最好的学习入口。注意demos按 Delphi 版本分了目录,Delphi 7 的工程文件是.dpr加.dfm,高版本可能是.dproj,别拿错版本去开,否则 IDE 会提示一堆不认识的属性。

2.3 多进程模型:这不是一个普通控件

理解 CEF4Delphi 最关键的一点,是它背后是 Chromium 的多进程架构。你的 Delphi 程序启动后,主进程负责 UI 和窗口,渲染、GPU、网络这些活由独立的子进程干。CEF 通过命令行参数把子进程拉起来,所以你的程序在任务管理器里会看到好几个同名进程,这是正常的,不是内存泄漏。

这个模型带来两个直接影响。第一,初始化必须在主线程、且在创建任何窗体之前完成,因为 CEF 要接管消息循环。第二,子进程的启动依赖 exe 自身的路径,所以调试时如果 exe 被挪了位置或者运行时目录没配对,子进程起不来,表现就是白屏或者直接退出。后面避坑章节会专门讲这个。

3. 跑起来:从零初始化到页面加载的完整链路

3.1 环境准备与运行时文件放置

第一步是把 CEF 运行时放对位置。源码包bin目录下通常有libcef.dll、chrome_elf.dll、icudtl.dat、snapshot_blob.bin以及locales文件夹,这些必须和你的 exe 在同一目录,或者在一个你能通过代码指定的子目录里。我一般会在工程输出目录下建一个cef子目录,把运行时全塞进去,然后初始化时指定路径,这样源码目录和输出目录不会互相污染。

Delphi 7 用户要特别注意:CEF4Delphi 的较新版本用了一些较新的 Pascal 语法和单元,Delphi 7 能编译是因为作者做了兼容处理,但你需要确认src目录里没有引用只有高版本才有的单元。打开demos/Delphi7下的示例工程,先原样编译一次,能过再动自己的项目。

# 典型的输出目录布局(以 Windows 为例) MyApp/ ├── MyApp.exe ├── cef/ │ ├── libcef.dll │ ├── chrome_elf.dll │ ├── icudtl.dat │ ├── snapshot_blob.bin │ ├── v8_context_snapshot.bin │ └── locales/ │ └── zh-CN.pak

locales目录里放的是界面语言资源,缺了它 CEF 会报错退出。如果你只需要中文和英文,可以只保留zh-CN.pak和en-US.pak,能省不少体积。这一步没有代码,但放错位置是新手最常见的翻车点,所以单独拎出来说。

3.2 应用级初始化:uCEFApplication 的正确用法

CEF 的初始化分两段:全局初始化和上下文初始化。CEF4Delphi 把这两段封装在TCefApplication组件里,通常做法是在项目文件.dpr里创建它,而不是在主窗体里。原因是全局初始化必须在窗体创建前完成,放在.dpr里时序最可控。

// 项目文件 MyApp.dpr 中的典型写法 program MyApp; uses Forms, uCEFApplication, // CEF 应用级封装 UnitMain in 'UnitMain.pas' {FormMain}; {$R *.res} var GlobalCEFApp: TCEFApplication; // 全局唯一实例 begin GlobalCEFApp := TCEFApplication.Create(nil); try // 指定 CEF 运行时所在目录,相对 exe 路径 GlobalCEFApp.DllPath := ExtractFilePath(ParamStr(0)) + 'cef'; // 单进程模式仅用于调试,生产环境务必保持多进程 GlobalCEFApp.SingleProcess := False; // 关闭沙箱在部分老系统上更稳,但会降低隔离性 GlobalCEFApp.NoSandbox := True; // 缓存目录,不设会写到系统临时目录 GlobalCEFApp.Cache := ExtractFilePath(ParamStr(0)) + 'cache'; // 启动子进程并完成全局初始化 GlobalCEFApp.StartMainProcess; Application.Initialize; Application.CreateForm(TFormMain, FormMain); Application.Run; finally GlobalCEFApp.Free; end; end.

这段代码有几个参数值得说清楚。DllPath指向运行时目录,CEF 会去那里找libcef.dll,路径写错的表现是启动即报「找不到模块」。SingleProcess设为 True 时所有渲染在主进程里跑,调试断点好打,但稳定性差,页面一崩整个程序跟着崩,所以只建议临时开。NoSandbox在 Windows 7 这类老系统上经常是必须的,沙箱对系统版本有要求,关掉能少一类启动失败。Cache不指定的话 CEF 会往用户临时目录写,多用户环境下容易串数据,显式指定更干净。

StartMainProcess这个方法名容易让人误解,它不只是「启动主进程」,而是完成全局初始化并根据命令行判断当前进程是主进程还是子进程。子进程走到这里会直接进入 CEF 的消息循环,不会继续执行后面的Application.Run。所以这段代码在主子进程里都会执行,但行为不同,这是 CEF 的标准套路。

3.3 窗体里放浏览器:TChromium 加 TCEFWindowParent

全局初始化搞定后,主窗体上要放两个东西:一个TCEFWindowParent作为容器,一个TChromium作为浏览器逻辑载体。这是 CEF4Delphi 和普通控件最不一样的地方——浏览器不是直接画在TChromium上,而是画在一个独立的子窗口里,TCEFWindowParent负责给这个子窗口腾地方。

// UnitMain.pas 中的窗体创建与加载逻辑 procedure TFormMain.FormCreate(Sender: TObject); begin // 容器铺满窗体客户区 CEFWindowParent1.Align := alClient; // 创建浏览器,必须在容器句柄有效之后 Chromium1.CreateBrowser(CEFWindowParent1, ''); end; procedure TFormMain.Chromium1AfterCreated(Sender: TObject; const browser: ICefBrowser); begin // 浏览器创建完成后才能加载 URL Chromium1.LoadURL('https://example.com'); end; procedure TFormMain.FormResize(Sender: TObject); begin // 窗体尺寸变化时同步容器,浏览器窗口会跟着调整 if Assigned(CEFWindowParent1) then CEFWindowParent1.UpdateSize; end;

CreateBrowser的第一个参数是父容器,第二个是初始 URL,传空串表示先创建再加载。这里有个时序坑:CreateBrowser是异步的,调用返回时浏览器还没真正建好,所以不能紧接着LoadURL,要等AfterCreated事件。我见过不少人把LoadURL写在CreateBrowser下一行,结果页面时好时坏,就是这个原因。

UpdateSize在FormResize里调用,是因为浏览器子窗口不会自动跟随父容器缩放,必须手动通知。漏了这一步的表现是窗体拉大后浏览器还是原来大小,右边留一片空白。

3.4 加载本地页面与自定义协议

工业项目里经常要把前端打包好的静态资源嵌进去,而不是加载远程 URL。直接LoadURL('file:///...')能跑,但file://协议在跨域、fetch 请求上有诸多限制,现代前端框架的路由和接口调用很容易踩雷。更稳的做法是注册一个自定义协议,比如app://,由 Delphi 侧负责把请求映射到本地文件。

// 注册自定义协议 scheme,需在全局初始化前完成 procedure RegisterCustomScheme; begin // 声明 app 协议为安全协议,允许 fetch 和跨域 GlobalCEFApp.RegisterSchemeCustom( 'app', True, True, True, False); end; // 在 TChromium 的 OnGetResourceHandler 中拦截并响应 procedure TFormMain.Chromium1GetResourceHandler(Sender: TObject; const browser: ICefBrowser; const frame: ICefFrame; const request: ICefRequest; var aResourceHandler: ICefResourceHandler); var FilePath: string; begin if Pos('app://', request.Url) = 1 then begin // 把 app://index.html 映射到本地 web 目录 FilePath := ExtractFilePath(ParamStr(0)) + 'web\' + StringReplace(request.Url, 'app://', '', []); aResourceHandler := TCefStreamResourceHandlerRef.New( FilePath, GetMimeType(FilePath)); end; end;

RegisterSchemeCustom的四个布尔参数分别表示是否标准协议、是否安全、是否支持 CORS、是否支持 CSP,具体含义以源码里uCEFApplication.pas的注释为准,不同版本参数顺序可能有微调,改之前先看一眼声明。自定义协议的好处是前端可以像访问普通网站一样用相对路径和 fetch,不用为file://做特殊适配。GetMimeType需要你自己按扩展名返回text/html、application/javascript这类值,返回错了浏览器会拒绝执行脚本。

4. 核心模块拆解:JS 与 Delphi 双向通信怎么落地

4.1 从 Delphi 调 JS:ExecuteJavaScript 的边界

最直接的交互是 Delphi 主动执行一段 JS。TChromium提供了ExecuteJavaScript,适合做「通知前端刷新」「传一个配置对象进去」这类单向操作。

procedure TFormMain.SendConfigToPage; var JSCode: string; begin // 把 Delphi 侧的配置序列化成 JSON 再注入 JSCode := Format( 'window.__APP_CONFIG__ = %s; ' + 'if (window.onConfigReady) window.onConfigReady();', [ConfigToJSON]); Chromium1.ExecuteJavaScript(JSCode, 'app://index.html', 0); end;

第二个参数是脚本执行的 frame URL,传空串表示主 frame,传具体 URL 可以定向到某个 iframe。第三个参数是脚本起始行号,调试用,一般传 0。这里要注意,ExecuteJavaScript是「发射后不管」,拿不到返回值,也不能保证页面已经加载完。如果页面还没 ready 就注入,变量会丢。稳妥做法是等OnLoadEnd事件之后再调,或者像上面这样在 JS 侧留一个回调钩子,由页面主动通知。

4.2 从 JS 调 Delphi:消息路由与注册函数

反方向才是重头戏。前端要调 Delphi 的能力,比如读本地文件、调硬件接口、弹原生对话框,标准做法是通过 CEF 的消息路由。CEF4Delphi 提供了RegisterExtension或者基于ICefMessageRouter的封装,把 JS 的函数调用转成 Delphi 的事件。

// 在 AfterCreated 之后注册一个 JS 可调用的函数 procedure TFormMain.RegisterJsBridge; var Code: string; begin // 注入一个全局函数,内部走 CEF 的消息通道 Code := 'window.nativeBridge = {' + ' readDevice: function(id) {' + ' return cefQuery({request: "readDevice:" + id});' + ' }' + '};'; Chromium1.ExecuteJavaScript(Code, '', 0); end; // Delphi 侧接收查询请求 procedure TFormMain.Chromium1ProcessMessageReceived(Sender: TObject; const browser: ICefBrowser; const frame: ICefFrame; sourceProcess: TCefProcessId; const message: ICefProcessMessage; var aHandled: Boolean); var Req, Resp: string; begin if message.Name = 'cefQuery' then begin Req := message.ArgumentList.GetString(0); if Pos('readDevice:', Req) = 1 then begin Resp := ReadDeviceValue(Copy(Req, 12, MaxInt)); // 通过回调把结果送回 JS SendQueryResponse(message, Resp); aHandled := True; end; end; end;

cefQuery是 CEF 内置的查询通道,请求和响应都是字符串,复杂数据自己用 JSON 序列化。ProcessMessageReceived里判断message.Name来区分消息类型,ArgumentList按下标取参数。aHandled必须置 True,否则消息会继续往默认处理器传,可能触发意外行为。这套机制的好处是跨进程安全,JS 在渲染进程,Delphi 逻辑在主进程,消息由 CEF 负责序列化和转发,你不用自己处理进程间通信。

实际项目里我会把这层再包一下,定义一个消息协议表,请求格式统一成模块:动作:参数,Delphi 侧按模块分发,避免ProcessMessageReceived里堆一长串 if。前端也封装一个 Promise 版的invoke,把cefQuery的回调包成 async 函数,写起来跟调后端接口差不多。

4.3 请求拦截与资源替换:离线场景的关键

有些部署环境完全断网,前端资源必须全部本地化,但页面里又写了https://cdn...的引用。这时候要用OnBeforeResourceLoad做请求拦截,把远程请求重定向到本地文件。

procedure TFormMain.Chromium1BeforeResourceLoad(Sender: TObject; const browser: ICefBrowser; const frame: ICefFrame; const request: ICefRequest; const callback: ICefCallback; var aResult: TCefReturnValue); var Url: string; begin Url := request.Url; // 把 CDN 请求改写到本地 app:// 协议 if Pos('https://cdn.example.com/', Url) = 1 then begin request.Url := StringReplace(Url, 'https://cdn.example.com/', 'app://vendor/', []); aResult := RV_CONTINUE; end else aResult := RV_CONTINUE; end;

aResult返回RV_CONTINUE表示继续加载(可能已改过 URL),RV_CANCEL表示取消。改request.Url之后 CEF 会用新地址重新走一遍资源加载,所以自定义协议处理器要能响应app://vendor/这类路径。这个技巧在离线部署里非常实用,前端代码一行不用改,Delphi 侧统一做映射。

5. 避坑与排查:那些让程序白屏或闪退的细节

5.1 白屏但进程都在:运行时目录没配对

现象是程序启动后窗体出来了,浏览器区域一片白,任务管理器里能看到多个同名进程。原因通常是DllPath指向的目录里缺文件,或者locales子目录不存在。CEF 子进程启动时找不到资源会静默失败,主进程不一定报错。解决方法是先确认libcef.dll、icudtl.dat、locales/zh-CN.pak三样都在,再检查DllPath拼出来的路径是否真的存在。我一般会在初始化前加一句目录存在性检查,不存在就直接弹框提示,比白屏好排查得多。

5.2 调试时正常、发布后闪退:子进程路径依赖

现象是 IDE 里跑得好好的,拷到别的机器上双击就退。原因是 CEF 启动子进程时用的是当前 exe 的完整路径,如果你把 exe 单独拷走而没带运行时目录,子进程起不来。另外某些打包工具会把 exe 解压到临时目录再运行,路径一变,DllPath的相对路径就失效了。解决办法是用ExtractFilePath(ParamStr(0))拼绝对路径,并且确保发布包里 exe 和cef目录的相对位置不变。打包成单文件 exe 的方案在 CEF 场景下基本行不通,运行时文件必须落地。

5.3 页面加载了但 JS 报错:自定义协议没声明为安全

现象是app://页面能显示,但 fetch 请求被拦,控制台报跨域或安全上下文错误。原因是注册协议时安全标志没打开,浏览器把它当成不安全来源,很多现代 API 直接禁用。解决方法是RegisterSchemeCustom里把「标准协议」和「安全」两个标志都设为 True,并且这个注册必须在StartMainProcess之前完成,晚了不生效。改完记得清一次缓存目录,旧的协议注册可能被缓存住。

5.4 关闭窗体时卡死或报访问冲突

现象是点关闭按钮后程序挂住几秒,或者弹出「Access violation」。原因是浏览器对象还在异步回调,窗体已经销毁,回调访问了野指针。正确顺序是先调Chromium1.CloseBrowser(True)等待浏览器真正关闭,在OnBeforeClose事件里再释放容器和窗体。CloseBrowser的参数 True 表示强制关闭,会触发清理流程。我习惯在FormCloseQuery里设一个标志位,等OnBeforeClose回来再真正Close,这样退出干净,不会留僵尸进程。

5.5 Delphi 7 下字符串与编码的坑

现象是传给 JS 的中文变成乱码,或者 JS 传回来的中文在 Delphi 里显示成问号。原因是 Delphi 7 的string是 ANSI,而 CEF 内部走 UTF-8。解决方法是所有跨边界的字符串统一转成 UTF-8:Delphi 侧用UTF8Encode再传,收到 JS 数据先UTF8Decode。如果项目里混用了WideString,注意转换时机,别在中间环节丢编码信息。这个坑在纯英文项目里不会暴露,一上中文界面就集中爆发。

6. 二次开发:把浏览器控件改造成可复用的业务组件

真正落地时,没人会把TChromium直接拖到每个窗体上,那样初始化逻辑、消息路由、资源映射会散落一地。我的做法是封装一个TWebViewHost组件,内部持有TCEFWindowParent和TChromium,对外只暴露LoadPage、Invoke、OnNativeCall三个接口,业务窗体只管用,不碰 CEF 细节。

封装时第一个要处理的是生命周期。组件在Loaded之后创建浏览器,在Destroy之前走完关闭流程,中间用一个状态机标记「未创建 / 创建中 / 就绪 / 关闭中」,所有对外方法先检查状态,避免在错误时机调用。第二个是消息路由表,用TDictionary<string, TFunc<string, string>>把「动作名」映射到处理函数,新增一个原生能力只需要注册一行,不用改分发逻辑。

// 简化的路由注册示例 procedure TWebViewHost.RegisterHandler(const Action: string; Handler: TFunc<string, string>); begin FHandlers.AddOrSetValue(Action, Handler); end; function TWebViewHost.Dispatch(const Req: string): string; var Action, Param: string; Handler: TFunc<string, string>; begin // 协议格式:动作|参数 Action := Copy(Req, 1, Pos('|', Req) - 1); Param := Copy(Req, Pos('|', Req) + 1, MaxInt); if FHandlers.TryGetValue(Action, Handler) then Result := Handler(Param) else Result := '{"error":"unknown action"}'; end;

验证改造是否成功,我一般跑三个用例:页面加载后调一个无参原生方法看返回值、传一个含中文的参数看编码、连续快速调用十次看有没有消息丢失或顺序错乱。第三个用例最容易暴露问题,因为 CEF 的消息通道在高频调用下需要确认回调是否成对,漏一个响应前端就会挂起等待。

还有一个值得做的改造是资源热更新。把web目录做成可配置路径,启动时读一个版本文件,版本变了就清缓存重新加载。这样前端发版不用重新打包 exe,运维换个目录就行。注意清缓存要清对地方,Cache目录和GPUCache都要处理,只清一个可能还是加载到旧资源。

从那以后我每次接这类嵌浏览器的项目,都会先把「运行时目录检查 + 关闭时序 + 中文编码」这三件事写成启动自检,跑通了再写业务。这三样不出问题,后面基本都是顺的。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询