ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

CEF控件嵌入桌面程序:从版本选型到双向通信的完整实践 简介这是一份面向MFC桌面开发者的CEF浏览器控件集成示例工程解决在传统Windows程序中嵌入Chromium内核、实现现代网页展示与交互的常见需求。资源以完整项目形式呈现共450个文件、约167MBh头文件用于接口声明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.dll、libegl.dll、libGLESv2.dll、.pak资源文件、icudtl.dat、v8_context_snapshot.bin以及locales目录。这些文件一个都不能少少了任何一个启动阶段就崩而且崩得莫名其妙。我习惯把CEF的Resources目录整个拷进输出目录再把Release或Debug目录下对应的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);这里传入的client是CefClient派生对象它像是一张接口网承载着浏览器生命周期、加载状态、右键菜单、弹窗处理等各类回调。大多数人误以为只要把页面加载出来就行却发现右键菜单、新窗口打开、下载弹窗统统要自己实现就是因为没有正确挂接CefClient的回调。3.3 加载页面并处理关闭事件创建浏览器后调用browser-GetMainFrame()-LoadURL(...)就能跳转任意地址。这里的URL既可以是远程地址也可以是file:///D:/report/index.html这样的本地路径。关闭这步最值得警惕。如果你直接在宿主收到WM_CLOSE时调用DestroyWindow大概率会在退出时卡死或崩溃。正确做法是向所有浏览器窗口发送关闭请求然后等待CefLifeSpanHandler::BeforeClose回调触发后再真正销毁窗口// 在生命周期回调里处理 bool DoClose(CefRefPtrCefBrowser 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渲染进程那边用CefMessageRouterRendererSide的Create方法监听。两端都绑定同一个query处理器页面端就能通过window.cefQuery({ request: ... })发起请求C端在OnQuery回调里拿到请求字符串处理完返回结果。class MyHandler : public CefMessageRouterBrowserSide::Handler { public: bool OnQuery(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, int64 query_id, const CefString request, bool persistent, CefRefPtrCallback 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线程持有了CefBrowser或CefFrame指针页面跳转后这些指针可能已经失效。不要长期保存CefFrame引用每次用到时直接通过browser-GetMainFrame()获取避免野指针。CefMessageRouterBrowserSide的Handler对象生命周期也要跟CefClient保持一致程序退出时先释放路由器的Handler再释放CefClient。我写过一版在析构函数里先delete了Handler导致回调崩溃后来改用CefRefPtr管理Handler问题就消失了。5. 进程模型和常见崩溃问题是我踩坑最多的部分5.1 CEF的多进程是怎么分工的很多第一次接触CEF的人会忽略一件事程序启动后不止有一个进程而是一个进程组包括主进程Browser、渲染进程Renderer、GPU进程等。它们各自分工主进程负责窗口和调度渲染进程负责解析HTML和跑JSGPU进程负责合成位图。这个架构带来稳定性的同时也带来了调试复杂度。页面崩溃了主程序不一定挂但如果你是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里递减计数所有计数归零后退出消息循环最后再CefShutdown5.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; CefRefPtrSimpleApp 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里绑上两个最关键的handlerclass SimpleClient : public CefClient, public CefLifeSpanHandler, public CefLoadHandler, public CefMessageRouterBrowserSide::Handler { // 实现各个回调 };创建浏览器时把窗口句柄和初始URL传进去。为了让代码清晰我把创建浏览器封装在WM_CREATE里处理。初始化URL指向本地HTML文件这样一个最小工程就跑起来了。6.3 我自己总结的几条使用经验如果只许说三条最有用的经验我的排序是这样的发布目录必须完整资源文件一个都不能少否则在你机器上好好的拷给别人就崩这问题排错最难。把CefMessageRouter通信机制先跑通再写业务因为多数业务最终都需要页面和原生交互晚接入不如早接入。遇到底层疑似崩溃问题先把远程调试端口打开用Chrome开发者工具连上看渲染进程的报错信息多数页面问题在调试器里原形毕露。第二点再补充一句。很多时候大家觉得CEF只是放个浏览器但实际用起来你会在OnBeforePopup、OnDownloadStart、OnCertificateError这些回调里花费大量精力。与其等出了问题再补不如一开始就按浏览器外壳要自己写的预期来做项目排期。我做完这个报表展示项目后最大的感受是CEF确实颠覆了桌面程序只能长成传统样子的认知。它把Web生态庞大的前端能力无缝释放到了桌面端代价则是你要承担Chromium体系的复杂度。但只要你理解了它的进程模型、生命周期、通信机制这几条主线从能跑通最小例子到能处理真实业务场景中间的路其实并没有想象中那么长。本文还有配套的精品资源点击获取
返回列表