简介:Chrome浏览器控件Delphi版是一份基于CEF(Chromium Embedded Framework)技术封装的开发资源包,面向需要在Delphi应用中嵌入现代Web浏览器的中高级开发者。借助CEF4Delphi库,开发者可快速集成Chromium核心引擎,使用TChromium控件完成页面加载、导航拦截、JavaScript交互、界面定制等常见任务,适合构建办公自动化、混合应用或对Web兼容性要求较高的桌面程序。资源包共503个文件,以206个pas源文件、47个dproj工程文件、41个inc配置为主,辅以dfm窗体定义、bmp图标、html演示页面及批处理脚本等,压缩包仅1.66MB,目录结构清晰,便于移植和二次开发。已有1870人学习下载。通过该包可省去自行编译与适配CEF的大量时间,还能参考其工程组织方式快速打造具备HTML5支持的自定义浏览器窗口,并利用事件机制优化交互体验,为稳定、安全的嵌入式浏览器方案奠定基础。 去年我接手一个维护了十多年的Delphi进销存系统,客户提的第一个需求就让我卡住了:把网页端的实时销售大屏嵌进老程序里。下意识拖了一个TWebBrowser上去,结果页面打开白屏一片——人家前端用的是Vue + ECharts,IE内核根本没法完整解析ES6语法,我折腾了快两周才意识到,再也不能指望这个自带控件承载现代Web内容了。后来我把目光转向Chrome浏览器控件方案,最终用CEF4Delphi把这套老系统从IE内核平滑迁移到了Chromium内核,整个过程涉及选型对比、内核机制理解、消息循环接入、Win7兼容、JS双向通信等一系列问题。这篇文章就把这次改造的完整过程整理出来,重点说清楚为什么这么选、底层原理是什么、实际代码怎么写,以及你一定会遇到的几个坑。如果你也要在Delphi里嵌入现代网页、做混合应用,或者单纯想给老MIS系统换上现代前端界面,这篇值得存下来慢慢看。
1. 为什么要在Delphi里塞一个Chrome内核
1.1 TWebBrowser退场的真正原因
先说结论:TWebBrowser本身没做错什么,错的是它绑定的MSHTML内核。Delphi从很早就提供了TWebBrowser,开发时确实省事,拖上去就能显示网页。但这个控件本质是IE的宿主外壳,内核版本跟着操作系统走,Windows 10上它用的还是老旧版本的Trident/MSHTML引擎。现代前端开发基本默认Chromium内核的渲染行为和V8引擎,于是问题就集中爆发了。
我在这个项目里遇到的典型症状可以列几张纸:Vue/React单页应用打开后白屏、ECharts图表加载不出、flex和grid布局错乱、CSS3动画卡顿、WebSocket连不上、WebGL直接黑屏。还有一次更离谱,客户用了一个带shadow DOM的组件库,页面在Chrome里完全正常,放到TWebBrowser里连组件影子树的样式都加载不出来。你很难说是谁的错,但结果就是:老系统要接新需求,嵌入的网页必须是现代Web,TWebBrowser已经不够用了。
这个问题的本质是"运行时环境"的错位。你可以把TWebBrowser理解成一个老款点唱机,IE内核就是里面的唱片机芯,而现在市面上的唱片都是为现代播放器刻录的,强行播放要么无声要么走调。Chrome浏览器控件的作用,就是给Delphi程序装上一台现代播放器——它把Chromium内核整体打包成可嵌入的组件,你的程序拿到的是一个完整、独立、不依赖系统版本的浏览器运行时。
1.2 这里的"Chrome控件"不是Chrome浏览器
有一点需要先掰扯清楚:我们说的Chrome浏览器控件,并不是把Chrome浏览器本体嵌进程序里,而是使用Chromium Embedded Framework(CEF)这套开源框架。Chrome是Google基于Chromium开源内核做出来的商业化产品,CEF则是把Chromium内核抽出来做成可嵌入库,让第三方桌面程序直接加载渲染网页、执行JavaScript、获取页面事件。所以你在Delphi里集成的,是"Chromium内核 + CEF API封装",这不是套壳浏览器,而是一套可编程的浏览器运行时。
CEF的能力覆盖很全面:HTML5/CSS3/ES6渲染、V8 JavaScript引擎、离线缓存管理、Cookie控制、网络请求拦截、扩展机制(chrome://extensions/ 对应的扩展API有一部分也能用)、远程调试协议等。对于Delphi生态来说,社区里基于CEF出的封装组件,就是我们要选的"Chrome浏览器控件"。它们解决的是同一个问题:让Delphi程序拥有现代浏览器内核,同时保留Delphi原生程序对窗体、数据库、业务逻辑的掌控能力。
2. 选型对比:DCEF、CEF4Delphi、TEmbeddedChrome到底该用哪个
2.1 三款主流封装的差异
在具体动手之前,我把当时能找到的Delphi版CEF封装都过了一遍,主要是三款:DCEF3、CEF4Delphi、TEmbeddedChrome。它们的定位不太一样,做个对比表看得更清楚。
| 方案 | 维护状态 | 封装程度 | 可定制性 | 典型适用场景 |
|---|---|---|---|---|
| DCEF3 | 基本停更,社区零星维护 | 中等 | 中 | 老项目维护、不想换依赖 |
| CEF4Delphi | 活跃,持续跟进CEF版本 | 中等偏底层 | 高 | 新项目、需要深度定制 |
| TEmbeddedChrome | 更新较慢 | 很高,类TWebBrowser操作方式 | 低 | 快速上手、功能需求简单 |
DCEF3算是早期出名的封装,很多老项目都在用,但它的更新已经明显跟不上CEF版本节奏。如果你只是想把现有项目里的TWebBrowser换掉,不怎么需要新特性,DCEF3还能用,前提是别碰太新的网页功能。TEmbeddedChrome封装得更"傻瓜",接口风格接近TWebBrowser,上手最快,但代价是深度定制能力弱,遇到要改请求头、做协议拦截、接入扩展这类需求,会很难受。
CEF4Delphi是我最终的选择。它由Salvador Díaz Fau持续维护,GitHub上非常活跃,会跟着CEF官方版本更新,支持从Delphi老版本到新版的全系。它最大的优点是示例工程齐全,官方带了一个MiniBrowser完整Demo,包含窗体创建、多标签、加载事件、JS回调等几乎所有场景的参考代码。对于需要深度控制浏览器行为的项目来说,这是难得的财富。
2.2 为什么我最终押在CEF4Delphi上
选型逻辑其实很朴素,我给自己列了三项硬指标:能不能长期维护、能不能满足深度定制、能不能兼容目标部署环境。第一项直接pass掉DCEF3,第三项让WebView2方案出局了。
关于WebView2多说一句:它是微软基于Edge Chromium内核推出的现代控件,功能确实不错,设计思路也先进,但它对Windows 7的支持非常有限。客户这边还有一批老工控机跑着Win7,有些机器连系统补丁都没法打,这种情况下WebView2的Runtime分发和系统版本限制就成了硬伤。CEF方案则不同,它是把你的exe同目录的DLL和资源文件作为运行时,系统只负责提供基础Win32 API,所以只要CEF版本选对,Win7上完全能稳定跑。
另外CEF4Delphi保持了较高的底层可访问性,它没有把CEF的API全部藏起来,而是封装成VCL组件但保留了事件和回调入口。这对我后面做请求拦截、JS双向通信、页面截图帮助很大。总而言之,如果你的项目需要长期维护、要部署到老系统,又需要一定程度的浏览器定制能力,CEF4Delphi是当前最稳妥的方案。
3. 嵌入前必须搞懂的三个Chromium底层机制
3.1 多进程架构:任务管理器里多出来的进程不是bug
第一次集成CEF时,运行程序后打开任务管理器,看到进程列表里一下子多出好几个同名的子进程,我第一反应是资源没释放。其实这是Chromium的正常架构,和Chrome浏览器本身一样,CEF跑起来后会按职责拆分为多个进程:主进程(Browser Process)、渲染进程(Renderer Process)、GPU进程、网络进程等。
对Delphi开发者来说,这意味着两件事。第一,你的exe既可能是主进程,也可能是某个子进程——当Windows按CEF命令行为exe传入特殊参数启动它时,这次启动不会进入Delphi的Application逻辑,而是直接进入CEF内置的子进程消息循环,跑完即退出。这就是为什么工程初始化代码里必须判断StartMainProcess的返回值。第二,渲染进程崩溃不一定会带走主程序,但页面会异常,你需要注册相关的事件回调做恢复处理。
这个架构还决定了你打包部署时要注意的细节:exe所在目录需要放完整的CEF运行时文件(DLL、.pak资源、locales目录、icudtl.dat等),一个都不能漏。我后来发现很多白屏问题,根源就是部署时图省事只拷了主DLL,丢了资源文件。
3.2 线程模型与消息循环:Delphi和CEF怎么和平共处
CEF是多线程模型,UI线程、IO线程、渲染线程各司其职。嵌入Delphi程序时最关键的问题,是如何让CEF的消息循环和VCL的消息循环共存。VCL是典型的Win32消息循环,Application.Run里不断取消息、分发消息;CEF也有自己的消息处理需求,如果两边各转各的,界面上会表现为页面卡死、事件不响应。
CEF4Delphi的解决思路是让VCL在空闲时驱动CEF处理消息。官方Demo里通常会在TApplicationEvents.OnIdle事件里调用驱动函数,确保Application的消息队列空闲时,CEF有机会处理自己的内部消息。这里你要记住一个原则:不要阻塞OnIdle事件,不要在里面写耗时操作,否则CEF消息处理会被饿死。
我踩过一次坑:在OnIdle里习惯性地加了一句日志写文件,结果页面加载明显变卡,滚动掉帧。后来把日志挪到单独的线程队列里才恢复流畅。原因是OnIdle触发频率很高,磁盘IO直接拖慢了CEF消息循环。
3.3 生命周期管理:全局只能初始化一次
CEF是全局运行库,一条生命周期约束很硬:进程内只能初始化一次、只能Shutdown一次。在Delphi工程文件里,负责初始化的GlobalCEFApp对象必须在Application.Run之前创建并初始化,然后在Application.Run结束之后再调用Shutdown清理。
更具体地说,初始化过程由GlobalCEFApp.StartMainProcess完成。这个函数会解析命令行参数,判断当前进程是主进程还是子进程。如果是子进程(渲染进程等),它会进入CEF自带的消息循环,函数不会返回,你的Delphi程序也就不会进入Application.Initialize;只有主进程会带着True返回值继续走正常VCL启动流程。这个设计容易被忽略,特别是当你看到程序编译后运行没报错却没有任何窗体出现时,先想想是不是这个流程没写对。
4. 从下载到跑通:CEF4Delphi的完整落地步骤
4.1 环境准备与二进制匹配
先准备好Delphi开发环境,我用的是Delphi 10.4,后来在11和12上也验证过,都没问题。然后从CEF4Delphi官方仓库的Release页面下载两样东西:一是对应你Delphi版本的源码/组件包,二是CEF官方构建的二进制运行时包。这里有一个很容易翻车的点:二进制包分win32和win64,必须和你Delphi程序的目标平台严格匹配。如果程序是32位,就下32位CEF包;64位程序就用64位包,一旦混用,运行时会因为DLL加载失败而直接报错或白屏。
下载后先解压CEF4Delphi组件包,打开其中的Delphi版本分组(比如packages目录下对应10.4的路径),编译并安装运行时包和设计时包。安装成功后Delphi组件面板里会出现TChromium等相关组件。这时候先别急着写功能,建议先把官方MiniBrowser示例编译跑一遍,确认CEF二进制文件放置在示例输出目录下能正常显示网页,再往自己项目里迁移。这一步能帮你把"环境问题"和"代码问题"隔离开。
4.2 工程文件改造:把CEF的初始化嵌进启动流程
在自己的项目里,改造的第一步是工程文件DPR。下面是我实际使用的启动结构,需要注意调用顺序:
program MyBrowser; uses Vcl.Forms, uCEFApplication, UnitMain in 'UnitMain.pas' {frmMain}; {$R *.res} begin GlobalCEFApp := TCefApplication.Create; if not GlobalCEFApp.StartMainProcess then Halt(1); Application.Initialize; Application.CreateForm(TfrmMain, frmMain); Application.Run; GlobalCEFApp.Shutdown; end.这段代码的逻辑线很清楚:先创建全局CEF应用对象,然后调用StartMainProcess判断当前进程身份。如果返回False,说明当前进程是CEF的子进程,程序在这里就halt,由CEF接管后续处理;只有主进程才会继续走到VCL初始化,创建主窗体,进入消息循环。等Application.Run结束后,最后调用GlobalCEFApp.Shutdown做全局清理。
4.3 窗体上嵌入TChromium并加载页面
窗体设计时把TChromium控件拖到窗体上,设置Align为Client,让它填满整个客户区。然后在FormCreate里配置缓存路径并加载目标地址:
procedure TfrmMain.FormCreate(Sender: TObject); begin GlobalCEFApp.CachePath := ExtractFilePath(Application.ExeName) + 'cefcache'; Chromium1.LoadURL('https://www.example.com'); end;缓存路径这个细节容易踩坑:不要把缓存目录设置到Program Files这类系统受保护目录,否则运行时没有写权限,表现为页面偶尔加载缓慢、Cookie保存失败。我习惯把缓存放到exe同级目录下的子文件夹,部署时整个目录拷贝即可。页面加载过程中,几个常用事件值得挂上:OnTitleChange用于把网页标题同步到窗体标题栏,OnLoadStart和OnLoadEnd用于跟踪加载状态,OnBeforePopup用于拦截新窗口弹出。
这里重点讲一下OnBeforePopup。网页里如果执行了window.open或者某些链接带target=_blank,默认行为是触发CEF的弹窗流程,处理不好会直接打开一个新的Chrome窗口脱离你的程序。我在这个项目里就遇到过:点了一下页面上的"打印报表"按钮,结果弹出一个独立的浏览器窗口,客户以为程序出bug了。正确做法是在OnBeforePopup事件里判断弹窗的目标,如果是当前标签页类型的跳转,就把URL交给当前TChromium加载,并置aHandled为True阻止默认行为:
procedure TfrmMain.Chromium1BeforePopup(Sender: TObject; const aURL: string; const aTargetDisposition: TCefStringList; ...); begin if aTargetDisposition = WOD_NEW_FOREGROUND_TAB then begin Chromium1.LoadURL(aURL); aHandled := True; end; end;4.4 处理CEF消息驱动的最后一块拼图
窗体上放一个TApplicationEvents组件,在OnIdle事件里调用CEF的消息处理函数,让它有机会处理内部消息。代码大概是这样:
procedure TfrmMain.ApplicationEvents1Idle(Sender: TObject; var Done: Boolean); begin if Assigned(GlobalCEFApp) then GlobalCEFApp.DoOnIdle; end;具体方法名以你下载的CEF4Delphi版本官方Demo为准,不同版本略有差异。这类函数的核心作用就是给CEF一个"空闲时处理消息"的时机。如果你跳过这一步,常见表现是:程序启动后窗口能显示,但网页区域一直空白,或者点击页面上没有反应。
5. 实战中遇到的高发问题与排查链路
5.1 进程残留:关掉程序后浏览器进程还在跑
这个问题几乎每个用CEF的人都会遇到。现象是程序退出了,任务管理器里还有几个浏览器子进程存活,占着内存不释放。我第一次遇到时做了完整排查,捋出来的原因主要是两个。
第一个原因是GlobalCEFApp.Shutdown没有被执行。只要Application.Run后面没跟上Shutdown,或者中途有异常跳过了,CEF就不会主动清理子进程。第二个原因是动态创建的TChromium或窗体没有正确释放。CEF4Delphi的组件在Destroy时会通知CEF释放对应的浏览器实例,如果组件被泄漏了,浏览器实例自然也就没有释放。
排查链路是这样的:正常退出程序后立刻看任务管理器里残留进程数量,如果残留的是带命令行参数的子进程,先确认Shutdown是否执行;再检查代码里有没有动态Create的TChromium没Free。我的修复方案是在工程文件里用try/finally包住整个流程,确保任何异常情况下Shutdown都会被执行:
try Application.Run; finally GlobalCEFApp.Shutdown; end;5.2 白屏问题:DLL位数不匹配和GPU加速
白屏是第二个高发问题,而且排查链路更长。第一次启动,整个窗体都出来了,但网页区域一片白,打开开发者工具看控制台也没有报错。我按这个顺序排查:
先确认CEF二进制包的位数和exe目标平台一致。这个口子最常见,解决方法就是重新下一份匹配的包,替换掉执行目录里的DLL。然后是资源文件完整性检查,确认locales目录、.pak文件、icudtl.dat都在。这两个排查完如果还白屏,就要考虑GPU加速问题了。老显卡、虚拟机、远程桌面环境下,Chromium的GPU进程可能启动失败导致渲染异常,此时在初始化GlobalCEFApp时关闭GPU加速:
GlobalCEFApp.DisableGPUAcceleration := True;这个配置对大多数业务系统没有影响,普通页面渲染用的是CPU合成,损失的性能几乎感知不到,但稳定性提升很大。
5.3 Win7兼容性:CEF版本锁定很重要
客户那批Win7机器着实让我头疼了一轮。Chrome 109是支持Windows 7的最后一个版本,从110开始官方停止了对Win7的支持,CEF二进制也跟着这个节奏。如果贸然用了新版CEF包,在Win7上运行时会出现各种奇怪的错误,最典型的是进程启动后立刻崩溃。
这个场景下的处理思路是:如果你还需要支持Win7,就把CEF二进制版本锁定在109这个节点,对应的CEF4Delphi版本也选当时匹配的release。我用的是一个老版本的CEF4Delphi release配109的CEF二进制,在Win7工控机上跑了大半年,没有出现内核级问题。需要提醒的是,锁定版本意味着你享受不到后续的安全修复和特性更新,所以建议在项目里把"目标系统是否包含Win7"作为选型前置条件明确写下来。
5.4 回调线程与操作时序:不要直接在事件里操作数据库
我在OnLoadEnd事件里直接执行了一个数据库查询来同步页面数据,结果偶发地报出"cannot perform this operation on an open dataset"的错误。这类问题很迷惑人,因为错误信息和浏览器控件看起来毫无关系。
排查后发现根因是事件回调的线程上下文。CEF4Delphi大部分事件会投递回VCL主线程,但某些时机点并不完全等同于VCL消息循环里的安全时刻,如果你在回调里操作了当前正在使用的DataSet,可能和另一个消息处理逻辑产生竞争。最稳妥的做法是:在回调事件里不做重活,用TThread.Queue把操作投递回主线程的消息队列,或者先设置标志位,在主窗体定时器里再做查询。
procedure TfrmMain.Chromium1LoadEnd(Sender: TObject; const aBrowser: ICefBrowser; const aFrame: ICefFrame; httpStatusCode: Integer); begin if aFrame.IsMain then TThread.Queue(nil, procedure begin DoSyncData; // 实际的数据操作 end); end;这个习惯后来帮我避免了不少潜在崩溃,凡是CEF回调里要触碰VCL组件或数据库的地方,我都统一走TThread.Queue中转。
6. 进阶玩法:双向通信、请求拦截、截图与OCR采集
6.1 从Delphi调用JavaScript:执行页面操作
页面加载完成之后,Delphi需要能主动操作页面元素。CEF4Delphi提供了ExecuteJavaScript方法,可以直接在指定frame里执行JavaScript代码。比如我要把一个字符串写入页面里的输入框并触发点击事件:
Chromium1.ExecuteJavaScript( 'var el = document.getElementById("keyword");' + 'el.value = "入库单";' + 'document.getElementById("searchBtn").click();', '', 0);第三个和第四个参数是脚本URL和起始行号,日常使用传空字符串和0即可。这种调用方式在页面稳定的内部系统里非常好用,我甚至用它实现了自动补录:从旧系统的DataGrid里读出数据,拼接JS代码,把数据填进网页表单,再触发保存。整个过程看起来就像有人在页面上操作一样。
6.2 从JavaScript调用Delphi:自定义协议拦截
反过来,页面上的JavaScript怎么调Delphi方法?最稳妥也最容易理解的方式是自定义协议。在JS里把目标地址赋值为自定义协议URL,Delphi在OnBeforeBrowse事件里拦截这个URL并解析参数:
procedure TfrmMain.Chromium1BeforeBrowse(Sender: TObject; const aBrowser: ICefBrowser; const aFrame: ICefFrame; const aURL: string; const aIsRedirect: Boolean; out aHandled: Boolean); begin if Pos('myapp://action/saveData?', aURL) = 1 then begin HandleSaveAction(aURL); aHandled := True; end; end;对应在页面JS里只需要一行:window.location.href = 'myapp://action/saveData?name=abc&value=123';。这种写法跨版本稳定,不依赖CEF的扩展注册机制,理解起来也直观。等整个通信机制跑通之后,页面相当于成了前端UI层,Delphi成了业务逻辑层,两者通过协议无缝衔接,老系统的改造空间一下就打开了。
6.3 请求拦截:注入Token和修改请求头
系统接入了新的接口认证后,所有XHR请求都要带头Token。改造第三方页面是不可能的,但可以在OnBeforeResourceLoad事件里给所有请求注入请求头:
procedure TfrmMain.Chromium1BeforeResourceLoad(Sender: TObject; const aBrowser: ICefBrowser; const aFrame: ICefFrame; const aURL: string; var aResourceType: TCefResourceType; ...); begin if aURL.Contains('/api/') then begin aHeaders.Add('Authorization: Bearer ' + GetToken); aHeaders.Add('X-Client-App: Delphi'); end; end;这个能力的本质是CEF在请求发出前给了你一次修改机会。做单点登录、接口鉴权、统一报文头的时候,这个回调几乎是必经之路。
6.4 页面截图与OCR数据采集
最后一个我觉得很值钱的玩法,是结合页面截图和OCR做数据采集。CEF4Delphi可以获取当前页面内容,我把网页里的验证码区域截取成位图,再做OCR识别,替代人工输入。具体思路是在页面指定位置截取区域图像,保存为PNG文件,然后交给OCR组件识别。虽然现在很多系统有更好的验证码方案,但在内部老系统里,不少还是四位数字字母验证码,这种套路依然实用。
另外,如果是采集网页表格数据,我更推荐用ExecuteJavaScript把DOM内容序列化成JSON字符串,通过自定义协议传回Delphi解析,而不是用截图OCR。截图OCR的适用场景是:页面本身提供不了结构化数据,或者要采集的是图形界面上的内容。两条路线我都走过,都记录下来供你按需选。
最后说个我自己的习惯:新项目接CEF之前,先做一次10分钟的冒烟验证,拿官方MiniBrowser在目标部署环境上跑一圈,确认页面显示、加载、退出都正常了,再开始写自己的功能。这一下能过滤掉90%的环境问题。后面再遇到疑难杂症,优先开CEF日志,配合远程调试端口定位。你别指望一次集成完完全全不出岔子,但把这里面的机制和坑提前捋清楚,至少能帮你省下好几个周末的排查时间。
本文还有配套的精品资源,点击获取