
简介CEF4Delphi控件是一套面向Delphi开发者的Chromium谷歌内核浏览器集成方案适合需要在桌面应用中嵌入现代网页渲染能力的开发者覆盖从Delphi 7到Delphi 10.2的多个版本作者已亲自测试调试安装正常。资源包共462个文件约1.47MB以pas源码、dfm窗体、dpr与dproj工程文件、inc头文件及bat批处理脚本为主另含dpk包、res资源、lrs与lpi等Lazarus相关文件并附带详细帮助文档方便查阅接口与配置说明。需注意Chromium 32位与64位框架需另行下载属于运行必需组件。目前已有785人学习关注适合希望以较低成本为Delphi项目引入浏览器内核、研究控件封装结构或排查安装配置问题的开发者参考使用。1. 当 Delphi 遇上 ChromiumCEF4Delphi 控件到底能解决什么如果你手头维护着一套 Delphi 桌面系统又不得不把网页内容嵌进窗体里大概率经历过这种尴尬TWebBrowser 用的是系统自带的旧内核打开现代前端页面要么白屏要么脚本报错CSS 栅格全乱。客户拿着 Chrome 里跑得好好的页面截图来问“为什么你们软件里显示成这样”你只能解释内核版本问题。CEF4Delphi 就是冲着这个场景来的——它把 Chromium 内核封装成一套 Delphi 的 VCL/FMX 控件让你在 Object Pascal 里直接创建浏览器实例、加载页面、执行 JavaScript、拦截网络请求。适合两类人一是需要把 Web 管理后台、在线报表、地图服务嵌进桌面客户端的 Delphi 开发者二是想用 Pascal 做自动化页面操作、抓取渲染后 DOM 的工程师。它不是一个独立浏览器而是一层绑定理解这一点后面的配置和踩坑才有落脚点。2. 拆开 CEF4Delphi 的封装进程模型与核心单元2.1 为什么它必须多进程单进程为什么跑不起来Chromium 的架构决定了它不能像普通控件那样在单个进程里跑完。渲染、GPU 加速、网络请求、插件各自跑在独立子进程里主进程只负责调度和窗口消息。CEF4Delphi 把这套模型原样暴露给 Delphi你的 exe 启动后主进程加载 libcef.dll然后由 CEF 去拉起子进程。子进程的可执行文件默认就是你的 exe 本身靠命令行参数区分角色。这意味着两件事。第一你的程序目录里必须同时存在 libcef.dll、chrome_elf.dll、icudtl.dat、snapshot_blob.bin、v8_context_snapshot.bin 以及 locales 文件夹缺一个都可能启动即崩。第二子进程启动路径必须能被 CEF 找到否则你会看到主窗口出来了页面区域一片空白任务管理器里却多出好几个同名进程在反复重启。常见做法是在项目里放一个uCEFApplication单元在Application.Initialize之后、创建主窗体之前调用全局初始化// 项目主文件中的初始化顺序顺序错了会直接崩 program MyBrowserApp; uses Vcl.Forms, uCEFApplication, // CEF 全局初始化单元 uMainForm in uMainForm.pas {MainForm}; begin // 必须在 Application.Initialize 之后 Application.Initialize; // 全局初始化指定子进程路径、缓存目录、日志级别 // 参数含义见下方说明 GlobalCEFApp.StartMainProcess; Application.CreateForm(TMainForm, MainForm); Application.Run; end.GlobalCEFApp是全局单例StartMainProcess内部会检查命令行参数判断当前是主进程还是子进程。如果是子进程它会走另一条分支直接执行 CEF 的子进程逻辑不会创建窗体。参数上GlobalCEFApp.BrowserSubprocessPath一般不用改默认取当前 exeGlobalCEFApp.Cache建议显式设成一个可写目录比如ExtractFilePath(ParamStr(0)) cache否则默认往系统临时目录写多用户环境下容易权限打架GlobalCEFApp.LogFile在排查启动失败时非常关键设成固定路径后CEF 自己的错误会写进去比 Windows 事件查看器直观得多。2.2 核心单元分工别把所有逻辑塞进一个 FormCEF4Delphi 的单元命名有规律理解分工能少走很多弯路。uCEFApplication管全局生命周期uCEFChromium是浏览器实例的核心负责创建、加载、导航、执行脚本uCEFWindowParent是一个 TWinControl 容器Chromium 的窗口句柄挂到它上面uCEFBrowser和uCEFLoadHandler、uCEFRequestHandler这些是事件接口的实现基类你要拦截请求、修改响应头、处理下载就继承对应的 Handler。一个典型的窗体创建流程是这样的// 在 Form 的 OnCreate 或 OnShow 中创建浏览器 procedure TMainForm.FormCreate(Sender: TObject); begin // Chromium 组件在窗体上WindowParent 作为容器 Chromium1.DefaultURL : about:blank; // 关键必须在主窗口句柄就绪后再创建 if not Chromium1.CreateBrowser(CEFWindowParent1) then Timer1.Enabled : True; // 句柄未就绪延迟重试 end; // 延迟重试的定时器只跑一次 procedure TMainForm.Timer1Timer(Sender: TObject); begin Timer1.Enabled : False; Chromium1.CreateBrowser(CEFWindowParent1); end;CreateBrowser返回 False 是新手最常遇到的“玄学”问题原因通常是父容器句柄还没生成。CEF 需要一个有效的窗口句柄来挂载渲染表面而 Delphi 窗体的句柄在OnCreate阶段可能还没创建完。稳妥做法是放在OnShow里或者像上面这样用定时器兜一次。DefaultURL设成about:blank而不是留空能避免某些版本下创建时就去请求空地址导致的超时。2.3 事件驱动模型LoadHandler 与 RequestHandler 的介入时机CEF 的绝大多数行为都是异步回调你不能在LoadURL之后立刻去读 DOM因为页面还没加载完。uCEFLoadHandler提供了OnLoadStart、OnLoadEnd、OnLoadingStateChange等回调OnLoadEnd触发时 DOM 基本就绪但如果有异步脚本还在跑内容可能仍在变。真正稳妥的“页面稳定”判断常见做法是结合OnLoadingStateChange里isLoading变为 False再配合一个短延时。请求拦截则走uCEFRequestHandler。比如你要给所有请求加自定义头或者拦截某个 API 返回本地 mock 数据// 继承 TCEFRequestHandler重写 GetResourceHandler 或 OnBeforeResourceLoad type TMyRequestHandler class(TCEFRequestHandler) protected function OnBeforeResourceLoad(const browser: ICefBrowser; const frame: ICefFrame; const request: ICefRequest; const callback: ICefCallback): Boolean; override; end; function TMyRequestHandler.OnBeforeResourceLoad(...): Boolean; begin // 给所有请求追加一个标识头服务端据此区分来源 request.SetHeaderByName(X-Client, DelphiApp, True); Result : False; // False 表示不取消请求继续加载 end;SetHeaderByName第三个参数overwrite为 True 时会覆盖同名头。返回 False 是放行返回 True 是取消该请求。注意这个回调在 IO 线程上执行不要在里面直接操作 VCL 控件需要同步回主线程就用TThread.Queue或TCEFApplication提供的调度方法。3. 从零跑通第一个页面目录、参数与调试3.1 二进制包与 Delphi 版本的对应关系CEF4Delphi 本身是 Pascal 源码但它依赖 CEF 的二进制发行包。这两者的版本必须匹配CEF4Delphi 的某个版本对应某个 CEF 分支CEF 分支又对应某个 Chromium 大版本。拿错组合的典型症状是编译能过运行时libcef.dll加载失败或者初始化直接抛异常。选型时先确认你的 Delphi 版本。老版本 Delphi如 2007 以前对 Unicode 和泛型支持有限需要用较旧的 CEF4Delphi 分支Delphi 10.x 和 11/12 Alexandria 系列则用较新的。常见做法是去 CEF4Delphi 的发布页看它标注的“CEF version”和“Delphi compatibility”然后下载对应的 CEF 二进制包。目录结构一般是这样目录/文件作用放错位置的后果libcef.dllChromium 核心动态库找不到则启动即报错chrome_elf.dll早期加载的辅助库缺失导致初始化崩溃icudtl.dat国际化数据缺失时页面文字乱码或空白snapshot_blob.binV8 快照缺失导致 JS 引擎起不来v8_context_snapshot.binV8 上下文快照同上新版本必需locales/多语言资源缺失时界面语言异常resources/部分版本需要影响开发者工具等这些文件必须和你的 exe 在同一目录或者通过GlobalCEFApp.FrameworkDirPath、GlobalCEFApp.ResourcesDirPath显式指定。我一般直接全放 exe 旁边省得路径拼接出错。3.2 最小可运行工程窗体、容器、Chromium 三件套新建一个 VCL 应用放一个TCEFWindowParent和一个TChromium再放一个按钮触发加载。关键属性设置如下// 窗体 OnShow 中初始化此时句柄已就绪 procedure TMainForm.FormShow(Sender: TObject); begin // 指定缓存目录避免写系统临时目录 GlobalCEFApp.Cache : ExtractFilePath(ParamStr(0)) cache; // 开启开发者工具调试页面时用 GlobalCEFApp.EnableDevTools : True; Chromium1.DefaultURL : https://example.com; if not Chromium1.CreateBrowser(CEFWindowParent1) then ShowMessage(浏览器创建失败检查句柄和二进制文件); end; // 按钮点击加载新地址 procedure TMainForm.Button1Click(Sender: TObject); begin Chromium1.LoadURL(https://example.com/page2); end;EnableDevTools打开后可以调用Chromium1.ShowDevTools弹出调试窗口看 Console 报错、Network 请求这一步对排查页面白屏极其有用。Cache目录如果设成只读路径CEF 会静默失败或反复重试表现是页面加载极慢。3.3 执行 JavaScript 与取返回值嵌入页面的核心诉求之一是和页面脚本交互。CEF4Delphi 提供ExecuteJavaScript但取返回值是异步的不能像函数调用那样直接拿结果// 执行脚本并通过回调拿结果 procedure TMainForm.ButtonEvalClick(Sender: TObject); begin Chromium1.ExecuteJavaScript( document.title, // 要执行的表达式 about:blank, // 脚本所属的 frame URL空则主 frame 0 // 起始行号一般填 0 ); end; // 在 Chromium 的 OnExecuteJavaScript 事件里接收结果 procedure TMainForm.Chromium1ExecuteJavaScript(Sender: TObject; const browser: ICefBrowser; const frame: ICefFrame; const script: string; const result: ICefValue); begin // result 是 ICefValue按类型取值 if Assigned(result) then Memo1.Lines.Add(标题 result.GetString); end;ExecuteJavaScript的第二个参数指定在哪个 frame 执行多 iframe 页面里如果填错脚本会在错误的上下文里跑拿到的结果自然不对。result的类型取决于脚本返回值字符串用GetString数字用GetDouble对象和数组需要进一步遍历。注意这个事件也在非 UI 线程触发更新 Memo 要同步。4. 避坑与排查那些让程序起不来的细节4.1 现象程序启动后无窗口进程列表里一堆同名进程原因子进程启动失败主进程在等待子进程就绪时卡死或退出。常见触发点是BrowserSubprocessPath指向了不存在的路径或者 exe 所在目录没有写权限导致子进程无法创建缓存。解决先设GlobalCEFApp.LogFile和GlobalCEFApp.LogSeverity : LOGSEVERITY_INFO跑一次看日志里子进程启动命令和报错。确认 exe 目录可写必要时把缓存目录改到用户目录下。如果用了自定义的BrowserSubprocessPath确保那个路径是绝对路径且文件存在。4.2 现象页面能加载但中文全是方块原因locales文件夹缺失或路径不对CEF 找不到对应的 locale pak 文件回退到默认语言但字体渲染资源不全。解决确认locales目录和libcef.dll同级里面至少有zh-CN.pak和en-US.pak。如果通过GlobalCEFApp.LocalesDirPath指定了其他位置检查路径拼接有没有多一层或少一层。另外GlobalCEFApp.Locale可以显式设成zh-CN。4.3 现象ExecuteJavaScript 拿不到返回值回调不触发原因脚本执行所在的 frame 还没加载完或者OnExecuteJavaScript事件没有正确挂接。另一个常见原因是脚本本身抛了异常CEF 不会把异常当返回值传回。解决把ExecuteJavaScript放到OnLoadEnd之后再调用确保 DOM 就绪。在脚本里用try/catch包一层把错误信息作为字符串返回这样至少能在回调里看到线索。确认Chromium1.OnExecuteJavaScript已经在设计器或代码里赋值。4.4 现象多显示器或缩放比例变化时页面模糊原因CEF 默认按 100% 缩放渲染Windows 高 DPI 下系统做了位图拉伸。解决在项目主文件里调用SetProcessDpiAwareness或设置清单文件声明 DPI 感知然后通过GlobalCEFApp.ForceDeviceScaleFactor指定缩放比或者让 CEF 跟随系统。Delphi 11 以后对高 DPI 支持较好但仍需确认TCEFWindowParent的Align和父容器缩放设置一致。4.5 现象关闭窗体时进程残留原因CEF 的子进程和浏览器实例没有正确销毁主进程退出时子进程变成孤儿。解决在窗体OnClose里先调用Chromium1.CloseBrowser(True)再在OnBeforeClose回调里释放资源最后在项目结束时调用GlobalCEFApp.Free。顺序反了会导致访问已释放对象。如果用了多个 Chromium 实例每个都要走一遍关闭流程。5. 进阶用 RequestHandler 做本地 mock 与资源替换把 CEF4Delphi 用顺之后最有价值的进阶方向是拦截请求、替换响应这在离线演示、接口联调、页面注入场景里非常实用。核心是继承TCEFRequestHandler并重写GetResourceHandler返回一个自定义的ICefResourceHandler由它来提供数据。下面是一个把特定 URL 替换成本地 HTML 的骨架type // 自定义资源处理器返回一段内存中的 HTML TLocalHtmlResourceHandler class(TCefResourceHandlerOwn) protected function ProcessRequest(const request: ICefRequest; const callback: ICefCallback): Boolean; override; procedure GetResponseHeaders(const response: ICefResponse; out responseLength: Int64; out redirectUrl: string); override; function ReadResponse(const dataOut: Pointer; bytesToRead: Integer; out bytesRead: Integer; const callback: ICefCallback): Boolean; override; end; function TLocalHtmlResourceHandler.ProcessRequest(...): Boolean; begin Result : True; // 表示已处理不再走网络 end; procedure TLocalHtmlResourceHandler.GetResponseHeaders(...); begin response.Status : 200; response.MimeType : text/html; responseLength : Length(FHtmlContent); // 内容长度必须准确 end; function TLocalHtmlResourceHandler.ReadResponse(...): Boolean; begin // 把 FHtmlContent 按 bytesToRead 分段拷到 dataOut // 返回 False 表示读取结束 end;然后在TMyRequestHandler.GetResourceHandler里判断 URL命中就返回这个 handler 的实例。GetResponseHeaders里的responseLength必须和实际内容长度一致否则浏览器会一直等待或提前截断。ReadResponse要处理分段读取不能假设一次读完。另一个常用技巧是注入脚本。与其在页面加载后手动ExecuteJavaScript不如在OnLoadStart时通过frame.ExecuteJavaScript注入一段初始化代码这样脚本在页面自身脚本之前执行能提前挂载钩子或修改全局对象。注意注入时机太早可能 DOM 还不存在需要脚本内部自己监听DOMContentLoaded。验证替换是否生效最直接的办法是打开 DevTools 的 Network 面板看对应请求的 Status 和 Size 列。如果显示(from cache)或状态异常检查GetResourceHandler的匹配条件是否写错以及ProcessRequest是否返回了 True。我一般会在 handler 里加一行日志输出命中的 URL跑一次就能确认拦截范围对不对。从那以后我每次集成 CEF4Delphi都会先把日志级别开到 INFO跑通一个空白页再逐步加拦截和脚本注入绝不一次性把所有功能堆上去。希望帮到你。本文还有配套的精品资源点击获取