Chrome浏览器控件Delphi版:桌面程序嵌入Chromium内核实战
简介这份资源是面向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常见做法有三类。第一类是基于 CEFChromium 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.exeLocalesDirPath指向cef/localesCachePath指向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: TProcstring); 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后回调里拿到的结果是空字符串但脚本在浏览器控制台里手动执行是正常的。原因脚本执行时页面还没加载完或者脚本本身有语法错误被静默吞掉了。另外如果脚本返回的是一个异步结果比如 PromiseExecuteJavaScript不会等待 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 封装库的版本要匹配升级其中一个之前先确认兼容性不然会浪费很多时间在环境问题上。希望帮到你。本文还有配套的精品资源点击获取