news 2026/9/7 10:16:17

CEF控件嵌入桌面程序:从版本选型到双向通信的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CEF控件嵌入桌面程序:从版本选型到双向通信的完整实践

简介:这是一份面向MFC桌面开发者的CEF浏览器控件集成示例工程,解决在传统Windows程序中嵌入Chromium内核、实现现代网页展示与交互的常见需求。资源以完整项目形式呈现,共450个文件、约167MB,h头文件用于接口声明,cc/cpp为功能源码,lib/dll支持编译链接与运行,pak为Chromium所需的Web资源包,vcxproj则是Visual Studio工程配置文件,整体目录结构清晰,便于对照学习或二次修改。内容覆盖从CEF库获取、项目编译路径配置,到创建CMFCCEFView封装类、利用CefBrowserHost::CreateBrowserSync创建浏览器实例,再到实现CefLifeSpanHandler、CefLoadHandler、CefRequestHandler等回调接口的完整链路,可处理窗口创建关闭、页面加载状态、HTTP请求拦截等关键逻辑;同时专门提示了CEF多线程环境下的调用时机与注意事项。示例中保留了对OnSize调整视口、OnDestroy释放资源等MFC事件的处理,能帮助开发者规避集成中的常见坑。已有1209人学习下载,适合需要快速掌握CEF与MFC集成方法、希望参考可编译工程结构的中高级C++桌面开发者。 不知道你有没有遇到过这种情况:手里的桌面程序跑得好好的,突然需求方说要在里面加一个动态页面,最好还是网页做的。改完UI发现不现实,用系统自带的WebView又各种兼容问题,页面渲染出来跟Chrome里完全两个样。最后查了一圈资料,决定用谷歌开源Chromium项目底下的CEF浏览器控件来搞定这件事。这个方案在实践中相当能打,但网上大多是零散的片段,要么是英文Demo,要么只告诉你某几个API,真正从一个完整例子入手讲清楚的很少。我最近正好做完一个项目,把CEF控件嵌进了自家工具里做数据报表展示,从选版本到双端通信再到退出崩溃排查,一路踩了不少坑,这篇文章就把这个过程完整拆给你看。

1. 为什么我会在桌面程序里硬塞一个浏览器控件

1.1 一个具体的桌面端需求

当时的需求其实不复杂:公司内部的设备巡检工具,原来全是MFC那套老界面,每次巡检完要把结果以一份图文报表的形式展示给客户。报表里要有设备的热力分布图、实时曲线、历史对比表格,还要支持导出PDF。如果全用原生控件重写,工作量少说也要一两个月,而且图表交互、样式调整会非常痛苦。

我当时的判断是,页面展示这块直接用HTML来做,桌面壳子负责调用本机资源。但问题来了,界面怎么嵌进去?那时候系统自带WebBrowser控件用的是IE内核,渲染CSS3、Canvas、ECharts图表会出现各种莫名其妙的兼容问题,字体会变、动画卡顿、透明背景变黑。后来试了WebView2,效果不错,但部署时有些老客户机器没有WebView2运行时,还得额外装环境。CEF的好处是它把Chromium内核完整打包在程序目录里,不依赖系统组件,发布的时候一份目录拷过去就能跑,版本自己控制,页面渲染行为和Chrome几乎一致。

1.2 CEF相比其他方案,强在哪

单说嵌入能力,CEF的定位很直接——它是一个纯的浏览器嵌入框架,不像PyQt里的QWebEngineView需要依赖整个Qt框架,也不像Electron那样整个应用都要用Node生态。你可以把它理解成一块"浏览器内核积木",想塞进什么桌面程序都行:MFC、Qt、WinForms、WPF、甚至是纯Win32窗口,都可以。

另外它有几个对开发者特别友好的设计:

  • 多进程架构,页面卡死不会拖挂整个程序
  • 提供完整的C++接口,同时也有CefGlue、CefSharp等语言绑定
  • 支持JavaScript与原生代码双向调用,这个在对接业务时几乎是必需品
  • 渲染模式分窗口嵌入和无窗口离屏渲染,不同场景都有对应API

我见过有人用CEF做了一个带浏览器的聊天工具,有人用它做游戏内嵌活动页面,还有人直接拿它做了个信息采集客户端。论生态成熟度,它确实是当前把Chromium嵌进桌面程序的最常用选择之一。

2. 选CEF版本这件事,比你想的更讲究

2.1 从哪里拿发行包

CEF官方没有像普通开源库那样给俩Release文件就完事,它的构建产物托管在自动化构建页面,国内下载速度一般,但总归是能拿到的。下载的时候要注意区分几个目录:Standard Distribution是标准发行包,里面包含完整的头文件、库文件、资源和示例工程;Minimal Distribution是精简包,去掉了cmake工程和部分工具,适合只需要运行时文件的情况。

我第一次下载时没仔细看版本,随手点了最新的分支构建号,结果一堆新特性用不习惯,还在某个旧版本示例代码里调用了一个已经被删除的API,编译直接报错。后来学乖了,先看CEF跟Chromium版本号的对应关系,再结合自己项目的编译工具链选一个稍微"成熟"点的构建号。

有个经验:不要太追新。CEF的构建号天天在变,新版本可能优化了渲染性能,但也可能引入新的崩溃问题。生产项目建议选已经发布超过一两个月的构建版本,社区反馈相对充分,资料也齐全。

2.2 版本匹配和位数选择

CEF的版本号和Chromium强相关,目前常见的形式是类似116.0.19加一个构建号。下载页面会同时提供32位和64位的构建,这里有个容易踩的坑:如果你编译的是64位程序,一定要下载64位的CEF发行包,不要以为32位包在64位系统上一定能正常工作。二者混用会出现链接错误,或者运行时诡异崩溃。

另外要注意编译器的ABI兼容性。CEF官方针对不同Visual Studio版本提供了不同的库目录,vs2015、vs2017、vs2019、vs2022的运行时库不同,混用轻则警告,重则直接崩溃。在工程属性里要把运行库设置和CEF发行包保持一致,通常是/MD

2.3 工程目录需要哪些东西

一个可运行的CEF程序,发布目录里最核心的几样东西包括libcef.dlllibegl.dlllibGLESv2.dll.pak资源文件、icudtl.datv8_context_snapshot.bin以及locales目录。这些文件一个都不能少,少了任何一个启动阶段就崩,而且崩得莫名其妙。

我习惯把CEF的Resources目录整个拷进输出目录,再把ReleaseDebug目录下对应的libcef.dll等文件也拷过去,同时把include头文件和libcef.lib导入库配置到工程里。调试的时候经常会遇到"运行程序没反应,进程一闪而过"的情况,多半就是资源文件路径不对。

注意:CEF是基于Chromium的,它继承了Chromium的多进程模型。崩溃时如果只看到libcef.dll的异常,别先怀疑CEF本身,先检查发行目录的文件是否完整。

3. 用最简代码把网页嵌进窗口

3.1 初始化CEF的关键参数

CEF要在程序入口处先初始化,这部分逻辑通常放在main函数里。初始化前要设置好CefSettings,我建议重点关注这几个字段:

CefSettings settings; settings.no_sandbox = true; // 很多业务场景需要关闭沙箱,否则本地文件访问受限 settings.log_severity = LOGSEVERITY_WARNING; // 日志级别,调试时可调成INFO settings.multi_threaded_message_loop = true; // 多线程消息循环,集成MFC/Qt时很关键 settings.remote_debugging_port = 0; // 远程调试端口,调试页面时设为9222之类

如果你的宿主程序有自己的消息循环,比如Qt或MFC,一定要把multi_threaded_message_loop打开,让CEF跑自己的线程,否则消息循环会冲突,最直接的表现是界面拖拽卡顿、窗口响应慢半拍。

初始化代码是:

CefMainArgs main_args(hInstance); CefInitialize(main_args, settings, app.get(), sandbox_info);

第三个参数是CefApp的实例,通常用来处理进程启动时的回调,比如OnBeforeCommandLineProcessing里加启动参数。这里有一个很多人容易忽略的点:不同进程都会进入入口函数,要通过CefExecuteProcess处理子进程。

3.2 在窗口里创建浏览器实例

初始化完成后,就可以在宿主窗口上创建浏览器了。以Win32窗口为例,我从客户区矩形创建一个CefWindowInfo,然后用SetAsChild把它嵌入到指定的HWND中:

CefWindowInfo window_info; RECT rect; GetClientRect(hwnd, &rect); window_info.SetAsChild(hwnd, rect); CefBrowserSettings browser_settings; CefBrowserHost::CreateBrowserSync(window_info, client.get(), url, browser_settings, nullptr, nullptr);

这里传入的clientCefClient派生对象,它像是一张"接口网",承载着浏览器生命周期、加载状态、右键菜单、弹窗处理等各类回调。大多数人误以为只要把页面加载出来就行,却发现右键菜单、新窗口打开、下载弹窗统统要自己实现,就是因为没有正确挂接CefClient的回调。

3.3 加载页面并处理关闭事件

创建浏览器后,调用browser->GetMainFrame()->LoadURL(...)就能跳转任意地址。这里的URL既可以是远程地址,也可以是file:///D:/report/index.html这样的本地路径。

关闭这步最值得警惕。如果你直接在宿主收到WM_CLOSE时调用DestroyWindow,大概率会在退出时卡死或崩溃。正确做法是向所有浏览器窗口发送关闭请求,然后等待CefLifeSpanHandler::BeforeClose回调触发后,再真正销毁窗口:

// 在生命周期回调里处理 bool DoClose(CefRefPtr<CefBrowser> browser) override { if (browser->GetHost()->GetWindowHandle() == main_hwnd) { // 先关闭原生窗口,触发WM_DESTROY ::DestroyWindow(main_hwnd); } return false; }

如果你把multi_threaded_message_loop设置为true,整个退出流程会稍微简单一些。但无论如何,切记不要在主消息循环退出前直接调用CefShutdown,那样浏览器进程还活着,资源就会被强行回收,轻则崩溃重则系统报错。

4. C++与网页JS的双向通信,这套机制得讲透

4.1 C++主动调用页面里的JS函数

实际业务里,桌面端常常需要把本机读取到的状态推给网页。比如我的报表页面,C++读取巡检结果后要把数值更新到网页图表里。实现思路很简单:拿到CefFrame,然后调用ExecuteJavaScript

CefRenderHandler基础上,最通用的做法是这样:

std::string script = "window.updateData(" + jsonString + ");"; frame->ExecuteJavaScript(script, frame->GetURL(), 0);

这里有个小坑:ExecuteJavaScript必须在UI线程调用,如果从工作线程发起,需要先CefPostTask(TID_UI, ...)把任务派发过去。另一个坑是JSON字符串里的特殊字符,比如换行、引号,直接拼进JS代码会把语法搞坏。所以我都是先把数据序列化简化为JSON,再对字符串做一次转义处理。

4.2 网页里点击按钮,怎么触发C++代码

反向调用稍微麻烦一点。最古老的方式是注册一个JavaScript扩展,在页面里暴露一个CefApp层面的原生对象。我推荐用官方封装的CefMessageRouter,因为它的使用成本低、模型清晰。

注册路由器的典型代码分两端:浏览器进程注册CefMessageRouterBrowserSide,渲染进程那边用CefMessageRouterRendererSideCreate方法监听。两端都绑定同一个query处理器,页面端就能通过window.cefQuery({ request: '...' })发起请求,C++端在OnQuery回调里拿到请求字符串,处理完返回结果。

class MyHandler : public CefMessageRouterBrowserSide::Handler { public: bool OnQuery(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, int64 query_id, const CefString& request, bool persistent, CefRefPtr<Callback> callback) override { if (request == "openFileDialog") { std::string result = DoOpenFileDialog(); callback->Success(result); return true; } return false; } };

页面里调用:

window.cefQuery({ request: 'openFileDialog', onSuccess: function(response) { console.log('拿到结果:' + response); }, onFailure: function(err) { console.error(err); } });

这个模式下,网页只负责表达,真正打开系统文件选择框、调用本机摄像头等操作全部交给C++,边界特别清晰。我后来把报表导出功能也接到这条链路上:页面点按钮,C++端弹保存对话框,再调打印库输出PDF。

4.3 双向通信里的生命周期与释放

通信回调里最容易忘的是引用计数。CEF的CefRefPtr托管了大部分对象生命周期,但如果你在非UI线程持有了CefBrowserCefFrame指针,页面跳转后这些指针可能已经失效。不要长期保存CefFrame引用,每次用到时直接通过browser->GetMainFrame()获取,避免野指针。

CefMessageRouterBrowserSide的Handler对象生命周期也要跟CefClient保持一致,程序退出时先释放路由器的Handler,再释放CefClient。我写过一版在析构函数里先delete了Handler导致回调崩溃,后来改用CefRefPtr管理Handler,问题就消失了。

5. 进程模型和常见崩溃问题,是我踩坑最多的部分

5.1 CEF的多进程是怎么分工的

很多第一次接触CEF的人会忽略一件事:程序启动后不止有一个进程,而是一个进程组,包括主进程(Browser)、渲染进程(Renderer)、GPU进程等。它们各自分工:主进程负责窗口和调度,渲染进程负责解析HTML和跑JS,GPU进程负责合成位图。

这个架构带来稳定性的同时,也带来了调试复杂度。页面崩溃了,主程序不一定挂;但如果你是64位程序,渲染进程却是32位,两边传递共享内存时可能有兼容隐患。下载发行包时就要确定好位数,整个环境保持一致。

5.2 我最常碰到的退出崩溃和卡死

第一个典型问题:退出时卡在某个进程里不退干净。原因是页面里存在定时器或者死循环,BeforeClose一直没触发。排查办法是给页面里的定时器挂载beforeunload事件统一清理,或者在关闭时先调用LoadURL("about:blank")让页面卸掉再关窗口。

第二个典型问题:主窗口关闭了,但进程列表里还挂着一个renderer子进程。这种情况多半是弹窗或者Popup窗口没处理。CEF默认会把目标为_blank的链接交给OnBeforePopup,如果这个回调你没有接管,也不返回true,系统会莫名其妙创建新窗口或者什么都不显示。正确做法是在OnBeforePopup里选择复用原来的浏览器窗口,或者创建一个新的受控窗口,并返回true阻止默认行为。

第三个问题比较隐蔽:程序退出时CefShutdown调用时机不对。很多时候崩溃栈指向CefShutdown,原因是某个窗口已经被DestroyWindow销毁了,但CEF内部还有浏览器对象未释放。解决思路分几步:

  • 记录当前打开的浏览器窗口数量
  • WM_CLOSE时调用browser->GetHost()->CloseBrowser(true)
  • BeforeClose里递减计数
  • 所有计数归零后退出消息循环,最后再CefShutdown

5.3 内存和GPU设置上的优化点

CEF默认的GPU加速会调起独立GPU进程,但在远控操作或者老显卡机器上,GPU进程经常导致黑屏、花屏甚至崩溃。这种场景直接在命令行参数里加上disable-gpu,换软件渲染,稳定第一。我测试过一台集显老机器,不开GPU时页面滚动反而更顺。

内存方面,如果页面比较重,CEF每个标签页都会开独立渲染进程,内存占用确实不低。如果同一个窗口内始终只加载一个页面,可以在OnBeforeCommandLineProcessing里加process-per-site-instance或者做一些进程复用配置。当然,内存问题本质上是Chromium架构决定的,指望CEF省内存不太现实,设计功能时要控制页面复杂度。

注意:不管怎么优化,发布前一定要在低配置虚拟机里跑一遍。CEF对硬件要求的底线比很多人想象中要高,有些崩溃问题只会在低配机器上复现。

6. 一个可以直接用的最小工程模板

6.1 目录结构参考

以Visual Studio工程为例,通常这样安排:

DemoCEF/ ├─ include/ # CEF头文件 │ └─ cef/ # 各种cef_*.h ├─ lib/ │ ├─ Debug/ # libcef.lib 等 │ └─ Release/ ├─ resources/ │ ├─ locales/ │ ├─ *.pak │ ├─ icudtl.dat │ └─ v8_context_snapshot.bin └─ src/ ├─ main.cpp ├─ simple_app.h/cpp # CefApp 派生 ├─ simple_client.h/cpp # CefClient 派生 └─ my_handler.h/cpp # 生命周期/加载/消息路由

6.2 核心代码要点

main.cpp里按顺序做三件事:初始化CEF、创建宿主窗口、进入消息循环。

int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrev, PWSTR cmdLine, int nCmdShow) { CefMainArgs args(hInstance); CefSettings settings; settings.no_sandbox = true; settings.multi_threaded_message_loop = true; CefRefPtr<SimpleApp> app(new SimpleApp()); CefInitialize(args, settings, app.get(), nullptr); // 创建原生窗口,并在WM_CREATE时创建浏览器 RegisterAndCreateMainWindow(hInstance); // 消息循环,CEF运行在自己的线程里 while (GetMessage(&msg, nullptr, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } CefShutdown(); return 0; }

SimpleClient里绑上两个最关键的handler:

class SimpleClient : public CefClient, public CefLifeSpanHandler, public CefLoadHandler, public CefMessageRouterBrowserSide::Handler { // 实现各个回调 };

创建浏览器时,把窗口句柄和初始URL传进去。为了让代码清晰,我把创建浏览器封装在WM_CREATE里处理。初始化URL指向本地HTML文件,这样一个最小工程就跑起来了。

6.3 我自己总结的几条使用经验

如果只许说三条最有用的经验,我的排序是这样的:

  • 发布目录必须完整,资源文件一个都不能少,否则在你机器上好好的,拷给别人就崩,这问题排错最难。
  • CefMessageRouter通信机制先跑通再写业务,因为多数业务最终都需要页面和原生交互,晚接入不如早接入。
  • 遇到底层疑似崩溃问题,先把远程调试端口打开,用Chrome开发者工具连上看渲染进程的报错信息,多数页面问题在调试器里原形毕露。

第二点再补充一句。很多时候大家觉得CEF只是"放个浏览器",但实际用起来,你会在OnBeforePopupOnDownloadStartOnCertificateError这些回调里花费大量精力。与其等出了问题再补,不如一开始就按"浏览器外壳要自己写"的预期来做项目排期。

我做完这个报表展示项目后,最大的感受是CEF确实颠覆了"桌面程序只能长成传统样子的认知"。它把Web生态庞大的前端能力无缝释放到了桌面端,代价则是你要承担Chromium体系的复杂度。但只要你理解了它的进程模型、生命周期、通信机制这几条主线,从能跑通最小例子到能处理真实业务场景,中间的路其实并没有想象中那么长。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 10:12:22

MCU选型先核对外设接口:以RX66T为例的实用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 10:11:20

单片机毕设项目:基于 STM32/51 单片机与 GSM 模块的体征异常远程提醒装置 基于 STM32/51 单片机的体征数据本地显示与异常报警系统实现(024106)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/7 10:10:59

C# WinForms ListView自绘按钮列跨平台实现与命中测试详解

简介&#xff1a;面向C# Mono开发者的ListView带按钮事件完整实例&#xff0c;覆盖Windows窗体与Android移动端两类应用场景&#xff0c;解决数据列表展示与行内按钮交互的常见需求。实例项目演示了从ListView初始化、多列标题设置、Item子控件添加&#xff0c;到Click事件绑定…

作者头像 李华
网站建设 2026/9/7 10:10:54

AMD与AI巨头认股权证交易背后的技术合作与算力方案评估

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华