简介:这份资源是面向Delphi开发者的Chrome浏览器控件集成方案,核心基于CEF4Delphi封装库,帮助开发者在Delphi环境中嵌入Chromium内核,快速构建具备现代Web能力的桌面应用。资源包共503个文件,约1.66MB,以206个pas单元文件、48个dfm窗体、47个dproj工程与47个dpr源码为主,辅以bat批处理脚本、inc头文件、html页面及png、bmp、ico等图标资源,覆盖从工程配置到界面素材的完整结构。已有1876人学习下载。内容围绕TChromium控件的属性、方法与事件展开,涉及URL加载、导航拦截、加载错误处理、标题更新,以及隐藏地址栏、自定义UI、ExecuteJavascript脚本交互、OnJSDialog对话框处理等实用技巧;同时包含SSL证书检查、CORS策略、Cookie管理、缓存路径与大小设置、多线程渲染调优等安全与性能要点,适合需要为Delphi程序集成浏览器功能的中高级开发者参考。
1. 当桌面程序需要一颗浏览器心脏:Chrome浏览器控件Delphi版能解决什么
做桌面端工具的人迟早会撞上一个需求:界面里要嵌一个能跑现代网页的浏览器。不是那种老掉牙的 TWebBrowser——它内核停留在 IE 时代,打开一个用 Vue 或 React 写的页面直接白屏,CSS Grid 不认,ES6 语法报错。你需要的是一颗 Chromium 内核,能渲染现代前端页面,同时还能被 Delphi 代码控制:加载指定 URL、执行 JavaScript、拦截请求、读取页面内容、和原生窗口无缝共存。
Chrome浏览器控件Delphi版,说的就是这件事。它把 Chromium 的渲染能力封装成 Delphi 可调用的组件,让你在 VCL 或 FMX 窗体上像放一个 TPanel 那样放一个浏览器控件。适合谁?做企业内部工具、自动化测试辅助工具、数据采集前端、桌面客户端混合开发的 Delphi 工程师。如果你手头有存量 Delphi 项目需要嵌入 Web 视图,或者想用 Delphi 快速搭一个带浏览器能力的桌面工具,这条路值得走一遍。
2. 选型与原理:Delphi 嵌 Chromium 到底有哪几条路
2.1 三种主流方案的能力边界对比
在 Delphi 生态里嵌 Chromium,常见做法有三类。第一类是基于 CEF(Chromium Embedded Framework)的 Delphi 封装,把 CEF 的 C++ 接口翻译成 Delphi 的接口单元,功能最全,但编译配置最重。第二类是走 WebView2 路线,依赖系统里已安装的 Edge WebView2 Runtime,轻量、部署简单,但受限于系统环境,且 Delphi 侧的封装成熟度参差不齐。第三类是第三方商业组件,开箱即用但授权和定制受限。
| 方案 | 内核来源 | 部署体积 | Delphi 控制粒度 | 适用场景 |
|---|---|---|---|---|
| CEF 封装 | 自带 Chromium | 大(100MB+) | 高,可拦截请求、注入脚本 | 需要深度控制的工具 |
| WebView2 | 系统 Edge Runtime | 小 | 中,依赖 Runtime 版本 | 轻量嵌入、内部工具 |
| 商业组件 | 自带或系统 | 视产品而定 | 中到高 | 快速交付、预算充足 |
我一般会先问自己一个问题:这个工具要不要分发到别人机器上?如果要,CEF 自带内核虽然体积大,但行为可控,不会因为对方系统没装 Runtime 而翻车。如果只是内部几台机器用,WebView2 的轻量优势很明显。
2.2 CEF 封装的核心加载流程
以 CEF 路线为例,Delphi 侧要跑通一个浏览器控件,核心是初始化 CEF 运行时、创建浏览器实例、挂接事件回调。下面是一个最小化的初始化骨架,展示关键步骤的顺序和参数含义。
// 初始化 CEF 运行时,必须在创建任何浏览器窗口之前调用 // 参数说明: // SubProcessPath: 子进程可执行文件路径,CEF 多进程架构必需 // CachePath: 缓存目录,不设会用临时目录,建议显式指定便于清理 // LocalesPath: 本地化资源目录,缺失会导致部分语言页面渲染异常 procedure TMainForm.InitCEF; var Settings: TCefSettings; begin FillChar(Settings, SizeOf(Settings), 0); Settings.MultiThreadedMessageLoop := 1; // 多线程消息循环,避免阻塞 UI Settings.NoSandbox := 1; // 内部工具可关沙箱,正式分发建议开启 Settings.LogSeverity := LOGSEVERITY_WARNING; // 日志级别,调试时调成 INFO Settings.CachePath := ExtractFilePath(ParamStr(0)) + 'cache'; Settings.LocalesDirPath := ExtractFilePath(ParamStr(0)) + 'locales'; Settings.SubProcessPath := ExtractFilePath(ParamStr(0)) + 'subprocess.exe'; if not CefInitialize(Settings, nil) then raise Exception.Create('CEF 初始化失败,检查子进程和资源目录'); end;这段代码的逻辑顺序不能乱:先填充设置结构体,再指定子进程路径和资源目录,最后调用初始化。MultiThreadedMessageLoop设为 1 是桌面应用的常规选择,让 CEF 在自己的线程里跑消息循环,Delphi 主线程继续处理 VCL 消息。NoSandbox在开发阶段可以关掉,减少权限问题导致的启动失败,但正式分发时建议开启,否则页面里的恶意脚本可能影响宿主进程。
2.3 创建浏览器实例与事件挂接
初始化完成后,创建浏览器实例并挂接到 Delphi 的窗体控件上。关键是要把 CEF 的窗口句柄和 Delphi 控件的句柄对齐,否则会出现浏览器内容画在错误位置的情况。
// 在窗体上创建一个浏览器实例,绑定到指定区域 // 参数说明: // ParentHandle: Delphi 控件的句柄,浏览器内容会绘制在这个区域内 // URL: 初始加载地址,留空则显示空白页 // OnLoadEnd: 页面加载完成回调,用于注入脚本或读取内容 procedure TMainForm.CreateBrowser; var WindowInfo: TCefWindowInfo; BrowserSettings: TCefBrowserSettings; begin FillChar(WindowInfo, SizeOf(WindowInfo), 0); WindowInfo.Style := WS_CHILD or WS_VISIBLE; WindowInfo.ParentWindow := PanelHost.Handle; // PanelHost 是窗体上的容器面板 WindowInfo.Window := PanelHost.Handle; WindowInfo.Width := PanelHost.Width; WindowInfo.Height := PanelHost.Height; FillChar(BrowserSettings, SizeOf(BrowserSettings), 0); BrowserSettings.WindowlessFrameRate := 30; // 无窗口模式下的帧率,影响流畅度 CefBrowserHostCreateBrowser( WindowInfo, TCefClientOwn.Create, // 自定义客户端,处理加载、脚本等事件 'https://example.com', BrowserSettings, nil ); end;这里有个容易忽略的点:WindowInfo.ParentWindow和WindowInfo.Window都设成同一个容器面板句柄,这是有窗口模式下的常规做法。如果你用的是无窗口模式(离屏渲染),则需要走另一套回调,把渲染结果画到 Delphi 的 Canvas 上,复杂度会高不少。新手建议先用有窗口模式跑通,再考虑无窗口。
3. 从零跑通:Delphi 项目集成 Chromium 控件的完整步骤
3.1 准备 CEF 二进制包与目录结构
CEF 的二进制包解压后有一堆文件,不能随便乱放。Delphi 程序运行时需要能找到子进程、资源文件和本地化目录。我一般会按下面的结构组织,把 CEF 相关文件集中放在一个子目录里,避免和主程序文件混在一起。
MyBrowserTool/ ├── MyBrowserTool.exe # Delphi 编译出的主程序 ├── cef/ │ ├── libcef.dll # CEF 核心动态库 │ ├── subprocess.exe # 子进程可执行文件 │ ├── chrome_elf.dll # 崩溃处理相关 │ ├── locales/ # 本地化资源 │ │ └── zh-CN.pak │ └── resources/ # 其他资源文件 └── cache/ # 运行时生成的缓存目录目录结构定好后,代码里的路径要跟着调整。SubProcessPath指向cef/subprocess.exe,LocalesDirPath指向cef/locales,CachePath指向cache。这些路径建议用相对路径加ExtractFilePath(ParamStr(0))拼接,避免硬编码绝对路径导致换台机器就跑不起来。
3.2 在 Delphi 中加载 CEF 接口单元并初始化
Delphi 项目要引用 CEF 封装提供的接口单元。不同封装库的单元名不一样,常见的有ceflib、uCEFApplication这类。引用之后,在项目启动阶段调用初始化,在退出阶段调用关闭。
// 项目文件中的初始化与清理顺序 program MyBrowserTool; uses Vcl.Forms, uCEFApplication, // CEF 应用级初始化单元 MainForm in 'MainForm.pas' {MainForm}; begin // 创建全局 CEF 应用对象,必须在 Application.Initialize 之前 GlobalCEFApp := TCefApplication.Create; GlobalCEFApp.MultiThreadedMessageLoop := True; GlobalCEFApp.NoSandbox := True; GlobalCEFApp.Cache := 'cache'; GlobalCEFApp.LocalesDirPath := 'cef\locales'; GlobalCEFApp.SubProcessPath := 'cef\subprocess.exe'; if GlobalCEFApp.StartMainProcess then begin Application.Initialize; Application.CreateForm(TMainForm, MainForm); Application.Run; end; // 退出时销毁 CEF 应用对象,释放资源 GlobalCEFApp.Free; end.这段代码的关键在于StartMainProcess的返回值判断。如果返回 False,说明当前进程是子进程,不应该继续走主程序的窗体创建逻辑。这是 CEF 多进程架构的典型模式:主进程负责界面,子进程负责渲染和网络。如果你忽略这个判断,子进程也会尝试创建窗体,导致行为异常。
3.3 加载页面并执行 JavaScript 读取结果
浏览器控件跑起来之后,最常见的需求是加载一个页面,等它加载完,然后执行一段 JavaScript 把结果取回来。这个过程是异步的,不能像调用普通函数那样直接拿返回值。
// 页面加载完成后执行脚本,通过回调获取结果 // 参数说明: // script: 要执行的 JavaScript 代码,最后一句表达式的值会被返回 // callback: 结果回调,参数为脚本返回值 procedure TMainForm.ExecuteScript(const script: string; callback: TProc<string>); begin if FBrowser = nil then Exit; FBrowser.MainFrame.ExecuteJavaScript( script, 'about:blank', // 脚本来源标识,影响同源策略判断 0 // 起始行号,一般填 0 ); end; // 在加载完成事件里调用 procedure TMainForm.OnLoadEnd(Sender: TObject); begin ExecuteScript( 'document.title + "|" + document.querySelectorAll("a").length', procedure(const result: string) begin MemoLog.Lines.Add('页面标题和链接数:' + result); end ); end;这里要注意ExecuteJavaScript的第二个参数。它标识脚本的来源 URL,会影响同源策略。如果你填了一个和当前页面不同源的地址,某些操作可能被浏览器拦截。一般填about:blank或者当前页面的 URL 都可以,但涉及跨域操作时要特别留意。
3.4 拦截请求与修改响应头
有些场景需要拦截页面发出的请求,比如替换某个接口的返回内容,或者给所有请求加上自定义头。CEF 提供了请求拦截的回调接口,在 Delphi 侧实现对应的处理逻辑。
// 自定义请求处理器,拦截指定 URL 并修改响应 // 返回 True 表示已处理,不再走默认流程 function TMainForm.OnBeforeResourceLoad( const request: ICefRequest; const callback: ICefRequestCallback): Boolean; var url: string; begin url := request.Url; // 拦截特定接口,返回本地模拟数据 if Pos('/api/config', url) > 0 then begin callback.Continue( request, True // True 表示允许继续加载,但后续可改写响应 ); Result := True; Exit; end; Result := False; // 其他请求走默认流程 end;请求拦截的坑在于:不是所有请求都能在OnBeforeResourceLoad阶段拿到完整信息。POST 请求的 body 可能还没读完,重定向的 URL 可能已经变了。如果你要修改 POST 数据,需要在更早的阶段介入,或者用 CEF 提供的GetUploadData接口。另外,拦截回调里不要做耗时操作,否则会阻塞整个网络线程,页面加载会明显变慢。
4. 避坑指南:Delphi 嵌 Chromium 最常见的五类翻车
4.1 子进程启动失败导致白屏
现象:主窗口出来了,浏览器区域一片空白,日志里能看到 CEF 初始化成功,但页面就是不渲染。
原因:子进程可执行文件路径不对,或者子进程依赖的 DLL 缺失。CEF 的多进程架构要求主进程能正确启动子进程,如果SubProcessPath指向的文件不存在,或者子进程启动时找不到libcef.dll,渲染进程就起不来。
解决:先确认subprocess.exe和libcef.dll在同一个目录下,且路径拼接正确。可以在代码里打印出完整的SubProcessPath,然后手动在文件管理器里确认文件存在。如果路径没问题,用依赖查看工具检查subprocess.exe是否缺少其他 DLL。
4.2 关闭窗口时程序卡死
现象:点关闭按钮,窗口消失了,但进程还在任务管理器里,CPU 占用不低,过一会儿才退出,或者干脆不退。
原因:CEF 的浏览器对象没有正确销毁,或者消息循环没有正常退出。常见于在FormClose里只调用了Close而没有等待浏览器销毁完成。
解决:在窗体关闭事件里,先调用浏览器的CloseBrowser方法,等OnBeforeClose回调触发后再真正关闭窗体。如果用了多线程消息循环,退出前要调用CefShutdown并等待其完成。不要用Application.Terminate暴力退出,那样 CEF 的清理逻辑跑不完。
4.3 JavaScript 执行返回空值
现象:调用ExecuteJavaScript后,回调里拿到的结果是空字符串,但脚本在浏览器控制台里手动执行是正常的。
原因:脚本执行时页面还没加载完,或者脚本本身有语法错误被静默吞掉了。另外,如果脚本返回的是一个异步结果(比如 Promise),ExecuteJavaScript不会等待 Promise 完成,直接返回空。
解决:确保在OnLoadEnd之后再执行脚本。对于异步操作,把结果写到全局变量里,然后用轮询或事件通知的方式取回。脚本里加try-catch把错误信息返回出来,便于定位问题。
4.4 中文页面乱码
现象:加载中文网站时,页面文字显示成方块或乱码。
原因:本地化资源目录缺失或路径错误,导致 CEF 找不到中文字体映射。另外,如果系统缺少某些中文字体,也会出现方块。
解决:确认LocalesDirPath指向的目录里有zh-CN.pak文件。如果还是没有,检查系统字体目录里是否有常用的中文字体。在 CEF 设置里可以指定默认字体,但更可靠的做法是保证本地化资源完整。
4.5 高 DPI 下界面模糊
现象:在 4K 显示器上,浏览器内容模糊,文字发虚。
原因:Delphi 程序没有声明 DPI 感知,或者 CEF 的 DPI 缩放设置和系统不匹配。
解决:在项目文件里加上 DPI 感知声明,同时在 CEF 初始化时设置Settings.HighDpiSupport相关参数。如果用的是有窗口模式,还要确保父窗口的 DPI 缩放和 CEF 的缩放一致。这个问题的排查比较玄学,不同 Windows 版本表现不一样,建议在目标系统上实测。
5. 进阶技巧:用 Delphi 和 Chromium 做自动化数据采集
5.1 注入脚本批量提取页面结构化数据
浏览器控件跑通之后,一个很自然的延伸是做数据采集。和传统的 HTTP 请求加 HTML 解析相比,用浏览器控件的好处是能拿到 JavaScript 渲染后的 DOM,对付那些靠前端框架动态生成内容的页面特别有效。
// 注入采集脚本,提取页面中所有商品卡片的标题和价格 // 返回 JSON 字符串,由 Delphi 侧解析 const COLLECT_SCRIPT = '(function(){' + ' var items = [];' + ' document.querySelectorAll(".product-card").forEach(function(card){' + ' var title = card.querySelector(".title");' + ' var price = card.querySelector(".price");' + ' if (title && price) {' + ' items.push({' + ' title: title.innerText.trim(),' + ' price: price.innerText.trim()' + ' });' + ' }' + ' });' + ' return JSON.stringify(items);' + '})()'; procedure TMainForm.CollectData; begin ExecuteScript(COLLECT_SCRIPT, procedure(const result: string) var jsonArr: TJSONArray; i: Integer; item: TJSONObject; begin if result = '' then Exit; jsonArr := TJSONArray.ParseJSONValue(result) as TJSONArray; try for i := 0 to jsonArr.Count - 1 do begin item := jsonArr.Items[i] as TJSONObject; MemoLog.Lines.Add( item.GetValue('title').Value + ' -> ' + item.GetValue('price').Value ); end; finally jsonArr.Free; end; end ); end;这段脚本的逻辑是:用querySelectorAll拿到所有商品卡片,遍历每个卡片,提取标题和价格,拼成一个 JSON 数组返回。Delphi 侧用TJSONArray解析,逐条输出。关键点是脚本最后要return一个字符串,因为ExecuteJavaScript只能把基本类型的值传回来,复杂对象需要序列化。
5.2 翻页与滚动加载的处理策略
很多列表页是滚动加载的,一屏只渲染一部分,滚动到底部才加载更多。用浏览器控件处理这种页面,需要模拟滚动并等待新内容出现。
// 滚动到页面底部,等待新内容加载 // 参数说明: // maxScrolls: 最大滚动次数,防止无限循环 // intervalMs: 每次滚动后的等待时间,太短会漏掉加载 procedure TMainForm.ScrollToBottom(maxScrolls: Integer; intervalMs: Integer); var i: Integer; begin for i := 1 to maxScrolls do begin ExecuteScript( 'window.scrollTo(0, document.body.scrollHeight);' + 'document.body.scrollHeight', procedure(const result: string) begin // 可以在这里记录滚动高度,判断是否还有新内容 end ); Sleep(intervalMs); // 等待新内容渲染,实际项目中建议用异步等待 end; end;这里用Sleep是简化写法,实际项目中不建议在主线程里睡眠,会卡住界面。更好的做法是用定时器或者异步回调,每次滚动后检查页面高度是否变化,变化了就继续滚,没变化就停止。intervalMs设多少取决于目标网站的加载速度,一般 500 到 1000 毫秒比较稳妥,设太短会漏掉内容,设太长采集效率低。
5.3 用请求拦截加速静态资源加载
采集场景下,图片、字体、样式这些静态资源往往不需要真正加载,拦截掉可以大幅提升页面加载速度。
// 在请求拦截中屏蔽图片和字体,只保留文档和接口请求 function TMainForm.OnBeforeResourceLoad( const request: ICefRequest; const callback: ICefRequestCallback): Boolean; var resourceType: TCefResourceType; begin resourceType := request.ResourceType; // 屏蔽图片、字体、媒体资源 if resourceType in [RT_IMAGE, RT_FONT_RESOURCE, RT_MEDIA] then begin callback.Cancel; // 直接取消,不加载 Result := True; Exit; end; Result := False; end;ResourceType是 CEF 对请求的分类,常见的有RT_MAIN_FRAME(主文档)、RT_SCRIPT(脚本)、RT_IMAGE(图片)、RT_XHR(接口请求)。把图片和字体取消掉,页面渲染速度会明显提升,尤其是那些图片很多的列表页。但要注意,有些网站会用图片的加载事件来触发后续逻辑,屏蔽后可能导致页面行为异常,需要根据实际情况调整。
5.4 采集结果的去重与增量保存
采集到的数据要落地保存,直接追加写入会导致重复。我一般会在 Delphi 侧维护一个已采集 URL 的集合,每次采集前先查重。
// 用 TDictionary 做 URL 去重,采集结果追加到文件 // 参数说明: // url: 当前页面的 URL,作为去重键 // data: 采集到的 JSON 数据 procedure TMainForm.SaveResult(const url, data: string); var lines: TStringList; begin if FVisited.ContainsKey(url) then Exit; // 已采集过,跳过 FVisited.Add(url, True); lines := TStringList.Create; try if FileExists('result.jsonl') then lines.LoadFromFile('result.jsonl'); lines.Add(data); // 每行一条 JSON,方便后续处理 lines.SaveToFile('result.jsonl'); finally lines.Free; end; end;用 JSONL 格式保存的好处是每行独立,追加写入方便,后续用脚本处理也简单。TDictionary做去重键,内存占用小,查询快。如果采集量特别大,可以考虑用 SQLite 做本地存储,但那是另一个话题了。
这套方案我前后调了挺久,最大的教训是:不要在主线程里做任何等待操作,所有和浏览器交互的逻辑都要异步化。另外,CEF 的版本和 Delphi 封装库的版本要匹配,升级其中一个之前先确认兼容性,不然会浪费很多时间在环境问题上。希望帮到你。
本文还有配套的精品资源,点击获取