ARTICLE DETAIL

资讯详情

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

MFC中嵌入CEF浏览器内核:从初始化到进程管理的完整实践

MFC中嵌入CEF浏览器内核:从初始化到进程管理的完整实践 简介面向MFC桌面开发者的CEF浏览器集成示例项目源自Code Project社区专家之手主要解决在微软基础类库框架中嵌入Chromium内核、实现现代Web界面与本地应用交互的需求适合希望为传统Win32界面引入Web技术栈的开发人员。压缩包仅81KB共收录26个文件涵盖h头文件、cpp源文件、rc资源描述、html示例页面及ico/bmp图标位图等结构紧凑便于对照学习和按需裁剪。目前已有1225人浏览学习工程代码覆盖CEF环境初始化、浏览器视图创建、MFC消息循环协同、生命周期管理及页面交互控制等关键环节并封装了CefView与ClientHandler等核心类同时附有ReadMe说明可支撑本地html加载、自定义协议扩展、JavaScript与C互调等常见功能。整个示例可直接运行调试也可作为二次开发的基础框架帮助开发者快速掌握在MFC中嵌入CEF浏览器的完整实现路径。1. 整体思路拆解为什么要在MFC里嵌一个浏览器做MFC开发的老哥们十有八九都遇到过这种需求程序里要展示个地图、嵌入个网页后台、跑一个在线文档预览或者干脆要把前端同事写的那套Web界面端到端搬到桌面程序里。你总不能每回都“ShellExecute”调外部浏览器吧用户体验割裂、也不像个正经桌面应用。这时候直接在MFC窗口里嵌入一个浏览器内核就成了最务实的解法。CEFChromium Embedded Framework这个框架干的事说白了就是往你的桌面程序里塞一个完整可用的Chromium内核而且进程模型、多标签、JS回调和本地交互这些都有现成接口。我这次做的demo就是这么个东西一个基于MFC对话框的壳里面嵌一个CEF控件能加载网页能和JS互相通信并且能控制CEF进程干净地启动、运行和退出不留下“僵尸”Chrome子进程。这个方案最核心的选型逻辑是什么是“Web渲染需求”和“桌面交互需求”的缝合。页面上再不合适的UI前端几周就能改完MFC里再难维护的自绘控件Web里一个样式文件搞定。所以如果你是做Windows桌面开发的这种“桌面壳 Web内容”的架构在未来相当长一段时间里都会是实用组合。对于一个demo而言我要的核心价值就三个CEF能不能在MFC里跑起来进程能不能控制住C和JS之间能不能实时通信。保住这三点后续业务逻辑往上堆就只是时间问题。2. 环境准备与CEF版本选型2.1 版本选型的关键点CEF版本很多、发布频率高但别上来就追最新。试用前先想清楚一件事你的MFC工程用的是哪一代Visual Studio。这会直接影响你能不能用新版CEF。我这里用的是VS2015对应选择的CEF分支是Chromium 3729系列也就是CEF 76版本左右。新版CEF要求C11/14支持VS2015勉强能用再老的编译器建议直接退到CEF 3.2079之类的老版本否则编译能把你坑哭。文件下载也很关键CEF官方发行页的下载链接在国外国内拉取经常速度感人或者直接失败。我实际测试下来本地镜像站或者宽带较好的情况下只下标准发行包Standard Distribution就够了不用下“Sample Application”那个带工程的大包。标准包里的文件结构很清晰include头文件、libcef_dll_wrapper源码、Resources浏览器内核资源和Release/Debug二进制目录。2.2 工程构建前的准备清单构建MFC CEF的demo前先把目录整理好。下面是我习惯的项目布局建议直接抄CefBrowserDemo/ ├── cef/ │ ├── include/ -- CEF头文件 │ ├── libcef_dll_wrapper/ -- 需要编译进MFC程序里的包装层源码 │ └── Release/ -- libcef.dll、icudtl.dat、v8_context_snapshot.bin等 ├── CefBrowserDemo/ │ ├── CefBrowserDemo.vcxproj │ ├── BrowserWnd.cpp -- 浏览器宿主窗口封装 │ └── CefAppImpl.cpp -- CEF应用生命周期管理 └── resources/ └── cef.pak, cef_100_percent.pak等这一步看起来琐碎但目录结构一旦定下来后面工程配置、运行时文件拷贝、版本升级都会省很多事。别随手丢一堆文件到同一个文件夹里时间一长你自己都找不着哪个文件对应哪个版本。CEF的版本兼容性比较特殊libcef.dll和Resources里的资源文件必须是同一版本混搭大概率起不来起来半路也会崩。3. MFC工程内嵌入CEF从初始化到浏览器窗口3.1 MFC里初始化CEF避开坑位CEF初始化只能在主线程UI线程做而且必须在任何CEF调用前完成。这套逻辑和MFC的CWinApp生命周期要匹配好。我实际写的初始化代码是在InitInstance()里完成的// CefBrowserDemoApp.cpp CefSettings settings; settings.no_sandbox true; // 避免沙箱和Windows权限混淆 settings.multi_threaded_message_loop true; // 关键让CEF自己跑消息循环 settings.remote_debugging_port 9222; // 调试Web内容时非常有用 CefRefPtrCefApp app(new CCefAppImpl); CefMainArgs main_args(GetModuleHandle(NULL)); // 两种进程需要注意 // - 如果是浏览器进程执行此处后返回 // - 如果是渲染进程/GPU进程等子进程走CefExecuteProcess直接处理 int exit_code CefExecuteProcess(main_args, app.get()); if (exit_code 0) return exit_code; CefInitialize(main_args, settings, app.get());这里有一个新手一定会踩的坑很多人以为CefInitialize之后直接创建浏览器就行。但如果你在MFC里用的是CWinApp的消息循环而CEF又默认使用自己的消息循环两者会打架。我上面配置了multi_threaded_message_loop true就是让CEF自己去内部创建一个线程处理消息循环MFC这边照常跑自己的互不干扰这样省掉了一大堆关于OnIdle()和PumpMessage()的破事。注意CefExecuteProcess这条分支一定不能省。CEF会为标签页和GPU等创建大量子进程每个子进程重新进入你的入口函数走CefExecuteProcess去分流。如果你漏了这步直接就是各种崩溃和白屏而且是那种看日志都看不出原因的玄学问题。3.2 创建浏览器窗口把CEF“塞”进MFC子窗口里CEF浏览器创建需要两个东西一个父窗口句柄之后网页内容就渲染在这个窗口里一个CefWindowInfo结构体。我的做法是在MFC对话框上放置一个自定义控件或者一个Static控件改ID也行然后用CreateBrowser把CEF绑定到这个控件窗口上// BrowserWnd.cpp BOOL CBrowserWnd::CreateBrowser(HWND hParentWnd, const CString url) { // 自定义控件位置建议用GetClientRect动态算别写死 CRect rect; ::GetClientRect(hParentWnd, rect); CefWindowInfo window_info; // 在MFC子窗口内嵌入渲染 window_info.SetAsChild(hParentWnd, rect); CefBrowserSettings browser_settings; browser_settings.javascript_close_windows STATE_ENABLED; return CefBrowserHost::CreateBrowser(window_info, new CCefClientImpl(this), // CefClient负责处理各种回调 url, browser_settings, nullptr, CefRequestContext::GetGlobalContext()); }SetAsChild的语义就是把渲染区域绑定到父窗口客户区上。接着CCefClientImpl需要继承CefClient然后重写GetLifeSpanHandler、GetLoadHandler、GetRequestHandler等几个关键子处理器。任何一个返回空指针都可能导致网页交互失效比如右键菜单弹不出来、页面打不开新窗口或者页面前端调用不到C回调这些装配关系一开始就要理顺。3.3 尺寸自适应别让浏览器卡死在固定大小上MFC窗口拉大缩小时CEF渲染区域不会自动跟着变需要手动响应WM_SIZE消息void CBrowserWnd::OnSize(UINT nType, int cx, int cy) { if (IsWindowVisible() m_browser) { CefWindowHandle hwnd m_browser-GetHost()-GetWindowHandle(); if (hwnd) ::SetWindowPos(hwnd, NULL, 0, 0, cx, cy, SWP_NOZORDER); } }这段代码不写你会在运行时看到一种很滑稽的现象页面只画在原始创建尺寸的那一块区域拉伸窗口后其他地方一片灰白。这种低级问题集成的时候很容易忽略先提前打好补丁能避免后面做自适应控件那一大堆麻烦。4. CEF生命周期管理启动、退出、进程回收4.1 CEF和MFC的退出顺序之争CEF进程模型下有browser进程、render进程、GPU进程等一堆子进程。如果你在MFC退出时不做任何善后进程就会残留。这个就是热词里“cef进程如何关掉”的来源后台挂着几十个cef_subprocess或者chrome_child进程CPU还狂吃用户看到心态就直接崩了。我在这套demo里的退出管理是明确按顺序来的先关闭所有浏览器窗口调用m_browser-GetHost()-CloseBrowser(true)强制关闭。在对应的CefLifeSpanHandler::OnBeforeClose回调里等待最后一个浏览器关闭。关闭完成后再调用CefShutdown()回收CEF全局资源。最后才退出MFC的ExitInstance()。手动操作时更粗暴一点的方案是直接TerminateProcess干掉所有CEF进程。但那样做有风险比如页面正处于写本地缓存的状态强行结束可能导致缓存文件损坏下次启动直接白屏。进程清理不是小事平时开发可以偷懒线上版本不行。4.2 我用的干净退出策略在MFC对话框的OnClose()里拦截关闭消息void CBrowserDlg::OnClose() { if (m_browser) { m_browser-GetHost()-CloseBrowser(true); // 强制关不等待确认 m_browser-GetHost()-TryCloseBrowser(); // 双保险 } // 不要立即CDialog::OnClose() // 而是等CefLifeSpanHandler通知所有浏览器都关闭后再真的退出 }对应的CefLifeSpanHandler里的回调逻辑bool CCefClientImpl::DoClose(CefRefPtrCefBrowser browser) { if (m_browser_count 1) { // 通知MFC主窗口CEF侧已准备关闭 ::PostMessage(m_hMainWnd, WM_CEF_BROWSER_CLOSED, 0, 0); } return false; // false允许CEF继续触发OnBeforeClose } void CCefClientImpl::OnBeforeClose(CefRefPtrCefBrowser browser) { m_browser_count--; if (m_browser_count 0) { CefQuitMessageLoop(); // 如果只是CEF自己的循环直接退出 // 如果是multi_threaded_message_loop模式这里发送MFC消息 } }这套流程跑通之后进程残留的问题就基本绝迹了。测试时可以盯着任务管理器观察程序点关闭后所有带CEF字样的进程在两三秒内全部消失不会像某些劣质集成一样关掉主窗口后一堆子进程还赖在后台。5. C与JavaScript通信让MFC和网页真正“对话”5.1 C侧暴露方法给JS调用嵌入浏览器的场景十个里有九个需要和网页交互。要么是网页页面里的数据要传给C去处理要么是C事件要推送到页面上展示。其实两边通信路子很宽比较常用的是通过CefV8Handler暴露一个原生对象给JS。大致拆解为三步在渲染进程里注册一个名为MfcBridge的JS对象对象里挂几个方法比如GetMachineId()、OpenFileDialog()当前端调用MfcBridge.OpenFileDialog()时C侧收到请求弹出原生对话框然后把结果通过JS回调返回前端class CMfcV8Handler : public CefV8Handler { public: virtual bool Execute(const CefString name, CefRefPtrCefV8Value object, const CefV8ValueList arguments, CefRefPtrCefV8Value retval, CefString exception) override { if (name OpenFileDialog) { // 这里注意线程V8回调通常在渲染进程线程 // 要弹原生UI必须切回MFC UI线程 PostMessage(m_hMainWnd, WM_OPEN_FILE_DIALOG, 0, 0); return true; } return false; } private: IMPLEMENT_REFCOUNTING(CMfcV8Handler); };在实际demo里我注册这个对象的代码更多但核心就是这个Execute方法。JS那边只需要调window.MfcBridge.OpenFileDialog()就能唤起一次Windows原生文件对话框。注意回调线程问题别在V8线程里直接创建窗口Windows会非常“不开心”。5.2 JS侧回调C以及C主动调用JS反过来C想主动调JS函数比如向页面推送一条新数据这个用ExecuteJavaScript在浏览器进程侧执行即可void CBrowserWnd::NotifyJs(const CString message) { CefRefPtrCefFrame frame m_browser-GetMainFrame(); if (!frame) return; CString script; script.Format(Lwindow.onNativeMessage(%s), message); frame-ExecuteJavaScript(script, CefString(), 0); }但要注意ExecuteJavaScript在执行复杂字符串时容易因为转义和引号匹配出问题。稳妥做法是前端主动暴露一个函数放在window上C侧把参数用CefV8Value包装好再调用。如果涉及大量数据传输比如实时把C采集的内存数据流推给Web页面做图表刷新那就要考虑走CEF的CefProcessMessage走异步通道而不是每个数据包都ExecuteJavaScript一次性能和安全性都会好很多。5.3 通信链路一个实际可跑的例程我demo里做的最简单却也最典型的一件事页面上有两个按钮和一个文本框。点按钮1拉起MFC原生文件选择对话框选中的完整路径回传到网页文本框里页面加载完成后MFC侧每隔一秒给页面推送一条随机数页面把这个数字渲染到画面里。这样一套走下来CEF的双向通信链路就都通了。前端和C各干各的活页面负责界面展示MFC负责和系统底层打交道彼此之间靠CEF消息通信衔接调试起来也比较直观。6. 常见问题排查与避坑指南6.1 页面白屏、无法加载网页白屏是集成CEF最常见的现象我列出几个高频原因基本覆盖90%的问题场景现象大概率原因解决方式空白但无报错资源文件缺失或版本不匹配检查icudtl.dat、cef.pak等Resources是否放入指定目录CEF版本是否与libcef.dll一致页面一直转圈网络请求被沙箱拦截settings.no_sandbox true先关掉沙箱窗口出现但黑屏GPU进程起不来或显卡驱动问题设置settings.use_gpu false或环境变量禁用硬件加速有页面但JS不执行CefApp没有正确传递确认CefApp::OnRegisterCustomSchemes等回调有实现或返回空6.2 编译时报libcef_dll_wrapper相关错误CEF的libcef_dll_wrapper是一套源码不是lib文件你要把它加进工程里一起编译。建议把cef目录下的libcef_dll_wrapper文件夹整个导入工程同时确认预处理器中有WIN32和_WINDOWS。另外MFC工程默认字符集是UnicodeCEF接口大多数也是宽字符这个匹配问题不大。但如果你某些源文件还在用多字节字符集可能引发类型转换冲突。统一设为Unicode再出问题就用CStringW显式转换。6.3 关闭时进程残留问题进程残留最核心的原因通常是主窗口已经被销毁但CEF浏览器对象引用还没有全部释放。尤其是CefRefPtr循环引用或者回调未触发导致OnBeforeClose永远不会执行。排查方法很简单在OnBeforeClose里加输出日志如果日志到退出时一直没打出就说明CloseBrowser后还有东西引用着浏览器对象。检查CefClient内部是否还持有CefBrowser的强引用以及CefV8Handler是否还持有窗口句柄导致无法释放。我的习惯是给CEF相关类统一规范BrowserWnd持有的m_browser是强引用但CefClientImpl里只存弱引用或裸指针绝不让客户端反过来再持有窗口句柄造成循环。这个设计在排除进程残留问题时帮了我大忙。6.4 调试利器远程调试端口CEF最有价值的调试方式之一就是remote_debugging_port。启动程序前在CefSettings里设置settings.remote_debugging_port 9222然后打开Chrome浏览器访问http://localhost:9222你会看到当前CEF渲染页面的DOM、Console、Network、Sources全都能像普通网页一样调试。这个功能在排查JS报错、C回调没触发、或者页面布局异常时极其好用。你真要开发完整的MFCCEF项目这一步几乎是必须掌握的别只靠MessageBox输出日志去排前端问题效率太低了。7. 实操过程中的一些个人经验这个demo从零到跑通我前后大概折腾了一个工作日。最大体会有两点第一CEF的接口很啰嗦但你只要把CefApp、CefClient、CefLifeSpanHandler、CefLoadHandler这几个核心接口对象的职责分清搭好框架之后就是往里面填业务代码不会越写越乱第二嵌入浏览器不是“塞进去就完事”从初始化、创建窗口、消息循环适配、生命周期管理到双向通信每一环都紧密咬合像一条流水线任何一环断了整体体验都会特别别扭。我在那次开发中还漏过一个小点CEF初始化时如果用了multi_threaded_message_loop true那么整个MFC的消息循环基本不参与CEF的调度。表面看省事了但如果你在页面里用了window.open弹出新窗口那个新窗口的尺寸和位置可能完全不受你控制。这个时候要用CefWindowInfo配合CefBrowserHost::CreateBrowser的动态创建逻辑手动接管新窗口不能指望CEF默认行为细节方面值得深挖一番。最后提一个建议这种项目不要等全部功能都设计好再动手先做一个能跑起来的最小demo也就是我前面讲的“加载一个网页 双向传一条消息 退出后进程干净”。这个demo跑到位了后续业务往里叠加就有了坚实的基础。这种“最小闭环 - 逐步扩展”的路子在CEF这种复杂集成场景里真的比一上来就铺全量功能要稳妥得多。本文还有配套的精品资源点击获取
返回列表