
简介为MFC应用嵌入CEF浏览器内核的完整示例工程源自Code Project社区专家之手面向希望在桌面客户端中集成现代Web技术、增强界面表现力的开发者。压缩包共26个文件包括10个h头文件、7个cpp源文件以及rc资源脚本、html演示页面、ico图标、bmp工具栏图片和vcxproj项目文件整体仅81KB头文件与源文件构成核心实现rc/html/bmp分别承担界面资源与测试页面目录结构清晰便于快速定位关键模块。示例围绕CEF初始化、浏览器视图创建、消息循环协同、生命周期管理四大核心展开通过CefWindowsHelpers、ClientHandler、CefView等类展示了MFC窗口如何承载并驱动Chromium内核附带的html页面可用于验证前端与CEF接口的联动效果同时借助CefV8Context等接口还能实现JavaScript与C的数据交互。已有1225人学习对于初学者和有经验的MFC开发者均具参考价值可直接作为项目起点进行二次开发与功能定制。 先说个场景老旧的 MFC 项目跑得好好的客户突然提了一句“这个窗口能不能直接显示网页”登录页、报表页、H5 活动页全想塞进桌面客户端。最开始我想的是 MFC 自带的 WebBrowser 控件毕竟拖一下就能用但真遇到复杂页面就开始卡壳渲染逻辑落后JS 兼容性问题一堆。所以把目光转向 CEFChromium Embedded Framework直接在 MFC 窗口里嵌一个 Chromium 内核的浏览器页面兼容性、渲染速度、定制能力都解决了。这篇文章我不讲空话就按一个可直接运行的 demo 为主线把从下载 CE F 到嵌入、初始化、关闭、踩坑的全过程都捋一遍适合刚接手 MFC 老项目、又需要内嵌浏览器的同学。1. 为什么选 CEF而不是被 IE 控件逼疯1.1 客户的需求其实只有一句话做桌面端的人都有体会客户说“做个浏览器功能”时真实需求往往很朴素你客户端里现有的窗口列表、菜单、工具栏都保留只是在某个区域 Web 页面能和 C 代码互相通信。比如点页面上一个按钮客户端去调本地接口或者客户端主动通知网页刷新数据。这种需求在 MFC 里有几种实现路径但我劝你别一步就跳进成熟方案的坑。最早我用 WebBrowser 控件时加载个带 ES6 语法的页面直接白屏更别提 WebRTC、Canvas 这些能力了。后来公司内部做过一次选型评估发现团队里大部分浏览器相关的历史包袱全是因为底层内核版本太低导致的兼容性问题。换掉内核这些问题能消掉七八成。1.2 主流嵌入式浏览器方案对比市面上能嵌到 MFC 里的方案大概四个MFC 自带的 WebBrowser 控件、WebView2、CEF、以及自己封装 Chrome 的 ActiveX 版本。横向参数我整理成表格实际选型时会更直观方案内核集成难度体积影响定制能力长期维护WebBrowser 控件IE 内核系统决定低小弱微软已停止迭代 IE 内核功能WebView2ChromiumEdge 同源中中等较强官方持续更新依赖 Edge Runtime 安装环境CEFChromium较高较大约 80~150MB极强可改渲染、资源加载、JS 桥接版本更新自己负责ActiveX 壳封装各厂商封装中不定一般受制于封装方C 端产品可能更偏向 WebView2因为安装包不用自带 Chromium系统装好 Edge Runtime 就能跑。但我们是 MFC 为主、需要离线部署、又要深入定制页面加载和通信逻辑CEF 这种“自己控制发行包”的模式明显更可控。另外 CEF 可以脱离系统浏览器版本限制把内核版本锁在自己手里这就避免了很多“客户电脑浏览器太老页面渲染全乱”的运维灾难。选型时的另一个决策点是团队是否愿意长期维护一个自集成内核。CEF 每次大版本升级都意味着重新适配、回归测试如果你的项目只是偶尔用一下网页那 WebView2 更省心如果你打算把大量业务页面都搬到 Web 侧同时保留桌面端的操作习惯CEF 的投资是值得的。2. 动手之前先把 CEF 的“脾气”摸清2.1 多进程架构为什么会看到一堆 cef.exe第一次跑 CEF demo 的同学大概率会被任务管理器吓到一个主进程后面跟着渲染进程、GPU 进程、网络进程、实用程序进程就像一个缩小版 Chrome。这是 CEF 从 Chromium 继承来的能力每个标签页或站点隔离在独立进程里一个页面崩了不会拖垮整个客户端。这个机制同时也意味着关闭浏览器不等于进程全没了。如果某个渲染进程里还有定时器、Web Worker 或者正在进行的下载任务进程退出就会比预期慢。如果我们的 MFC 代码没有按照 CEF 要求的顺序去关闭浏览器就会出现“exe 已经关了cef 相关进程还挂在后台”的诡异现象。这不是病毒而是生命周期没收干净。另外CEF 有“浏览器进程”和“渲染子进程”的区分。应用第一次启动时CefInitialize 创建的进程负责创建窗口、管理 UI当页面需要渲染时CEF 会用同一个可执行文件再拉起子进程。这种设计决定了我们的程序入口需要具备一些“命令分派”逻辑如果当前命令行参数表明这是一个 CEF 子进程就进入 CEF 的子进程处理函数而不是走 MFC 的界面初始化流程。2.2 版本与工具链匹配要先确认很多教程一上来就让你下载最新版 CEF然后直接配置链接。但这里有个隐藏条件CEF 新版本对编译器和 Windows SDK 版本要求很高。比如现在的新版 CEF基本要求 VS2019/VS2022 和 C17 环境而团队里大量 MFC 老项目还停在 VS2013 或 VS2015。如果你打开一个 VS2013 的 MFC 工程非要硬刚新版 CEF光是编译 libcef_dll_wrapper 就可能报一堆 C 标准库不匹配的错。我自己的建议是老工程该升编译器的就升CEF 版本选最近一年稳定分支。另外如果要支持 Windows 7CEF 110 以上版本基本已经放弃对 Win7 的支持不要为了追新版本把客户的老机器坑了。还有一点容易被忽略CEF 有 Debug 和 Release 两种构建产物。Release 版体积小、运行稳定Debug 版会带完整符号但调试启动特别慢。实际开发时我一般先用 Release 版本做功能联调只在排查内存问题时才换 Debug。很多 demo 跑不起来的直接原因就是把 Debug 和 Release 的 dll 混着放了启动时要么 0xc000007b 要么直接弹 DLL 加载失败。3. 一步步把 CEF 塞进 MFC 工程3.1 下载并配置 CEF 发行包CEF 官方提供预编译发行包选好分支后下载 Standard Distribution 即可。包里目录大概长这样include 头文件目录、libcef_dll 包装器源码、Release 目录下的 libcef.dll、Resources 目录下的资源文件。我们要做的第一件事不是急着建工程而是把目录结构理顺。我习惯建一个ThirdParty/cef根目录将解压后的内容放进去然后在 MFC 工程属性里配置“VC 目录”→“包含目录”添加ThirdParty/cef/include“VC 目录”→“库目录”添加ThirdParty/cef/Release或根据平台选 x86/x64“链接器”→“输入”→“附加依赖项”加上libcef.lib。这里的libcef.lib是导入库真正的libcef.dll还要丢到 exe 同级目录下。另外CEF 的 C API 使用 C 接口导出直接调用非常难受所以官方提供了 libcef_dll_wrapper本质上是把 C API 包成 C 类。这个包装器并不是一个现成 .lib你要么把libcef_dll目录下的源文件加入工程一起编译要么先用 CMake 单独构建出libcef_dll_wrapper.lib。为了方便我通常会建一个静态库工程专门放这些源文件MFC 主工程只引用它。3.2 初始化 CEF 并改造 MFC 消息循环MFC 工程创建好后在 App 类的 InitInstance 里调用 CefInitialize这一步是整个集成的地基。这里有个关键选择要不要让 CEF 使用独立的消息循环。BOOL CMFCCefDemoApp::InitInstance() { // ... MFC 标准初始化代码 CefMainArgs main_args(m_hInstance); CefSettings settings; settings.no_sandbox true; settings.multi_threaded_message_loop true; CefString(settings.log_file) _T(cef.log); CefRefPtrCBrowserApp app(new CBrowserApp()); if (!CefInitialize(main_args, settings, app.get(), nullptr)) { AfxMessageBox(_T(CEF 初始化失败)); return FALSE; } // 创建主窗口、进入消息循环 }multi_threaded_message_loop设为 true 后CEF 会在自己的线程里跑消息循环MFC 的CWinApp::Run()不用动。如果设成 false你就得在 MFC 的 PreTranslateMessage 或空闲处理里反复调用CefDoMessageLoopWork()否则页面会假死。对初学者来说多线程模式是最省心的我后面的 demo 也默认用这个模式。还要注意一点程序入口处最好加上子进程分流。如果在 InitInstance 一开始发现命令行参数里带--typerenderer、--typegpu-process之类的参数就应该直接调用CefExecuteProcess然后退出当前实例而不是继续初始化 MFC 窗口。很多实际项目把子进程当普通窗口启动结果内存暴涨、进程管理混乱就是这个分流没做。3.3 在 MFC 窗口里创建浏览器并完成消息对接界面布局我先放一个占位的CStatic控件名字叫IDC_CEF_PLACEHOLDER。创建浏览器时把 CEF 窗口直接挂在占位控件下面相当于把静态控件当成一个“画布容器”。void CMainFrame::CreateBrowser() { CefWindowInfo info; CRect rc; m_cefPlaceHolder.GetWindowRect(rc); info.SetAsChild(m_cefPlaceHolder.GetSafeHwnd(), rc); CefBrowserSettings browser_settings; // 这里建议先加载一个简单页面验证环境比如本机 demo 页面 CefString url _T(file:///D:/demo/index.html); CefRefPtrCBrowserClient client(new CBrowserClient(GetSafeHwnd())); m_browser CefBrowserHost::CreateBrowserSync(info, client.get(), url, browser_settings, nullptr); }CBrowserClient需要从CefClient派生并实现生命周期管理相关的接口。多数人忽略GetLifeSpanHandler和GetLoadHandler导致页面加载状态拿不到、关闭流程乱套。推荐一开始就把这两个 handler 的骨架写好在OnAfterCreated里保存浏览器对象在OnLoadEnd里通知 MFC“页面加载完成”在DoClose里做特殊处理防止 MFC 主窗口销毁时浏览器连带出问题。窗口大小变化时还需要调用m_browser-GetHost()-WasResized()否则 CEF 子窗口不会重新布局页面会出现白色边条或者拉伸错乱。4. 避坑实录关闭、残留、白屏与崩溃4.1 进程关不掉问题大概率不在 CEF“cef 进程如何关掉”这个话题在社区里问烂了。项目关闭后电脑上还挂着几个cef.exe这些进程既占用内存又会让下一次启动时端口资源冲突。遇到这种情况我最先检查的永远是关闭顺序。CEF 官方推荐流程是先调用CefBrowserHost::CloseBrowser(false)让页面有机会执行beforeunload事件等最后一个浏览器窗口关闭后触发OnBeforeClose这时再去调用CefShutdown。如果 MFC 主窗口先销毁了但 CEF 子窗口没来得及销毁或者你根本没调用CloseBrowser那么渲染进程必然残留。在 MFC 的 OnClose 里我一般这么处理void CMainFrame::OnClose() { if (m_browser m_browser-GetHost()) { m_browser-GetHost()-CloseBrowser(true); // 等待 OnBeforeClose 回调中发出退出消息 return; // 先不销毁主窗口 } CWnd::OnClose(); }CloseBrowser(true)是强制关闭省心但不给页面善后机会生产环境建议用CloseBrowser(false)然后在DoClose回调里向 MFC 主窗口发送自定义消息由主窗口统一销毁。简单来说进程残留十有八九是“MFC 主窗口死得太早CEF 还不知道自己该下班了”。4.2 DLL 加载失败与字符集设置CEF 运行起来依赖的 dll 不少除了libcef.dll还有libEGL.dll、libGLESv2.dll、icudtl.dat、snapshot_blob.bin等资源文件。很多新手只复制了 libcef.dll启动直接报“找不到入口点”或“应用程序无法正常启动(0xc000007b)”。其实 CEF 的 Release 目录和 Resources 目录里的文件几乎都要复制到 exe 同级目录并且要保留locales、swiftshader这样的子文件夹。另一个坑在于 MFC 项目字符集。CEF 要求项目使用 Unicode 字符集打开项目属性“常规”→“字符集”里如果还停留在“多字节字符集”链接阶段就会报一堆字符类型不匹配的错误。这个配置在新建 MFC 工程时默认不是 Unicode很多人栽在这里浪费半天时间排查。如果项目跑在 32 位环境必须确保 libcef.dll 也选的是 x86 版本千万别把 x64 的 Release dll 硬塞给编译出来是 x86 的程序这几乎 100% 会出现 0xc000007b。反之64 位程序要用 x64 dll这条规则简单但必须刻在脑子里。4.3 窗口大小变化白屏或闪烁嵌入 CE F 后最影响观感的问题就是用户拖动窗口边缘时浏览器区域闪烁、白屏。这个问题的根源通常有两个一是父窗口没有正确触发布局刷新二是 CEF 子窗口没有被同步调节尺寸。MFC 窗口在OnSize里应该把占位控件铺满整个客户区再通知浏览器void CMainFrame::OnSize(UINT nType, int cx, int cy) { CWnd::OnSize(nType, cx, cy); if (m_cefPlaceHolder.GetSafeHwnd()) { m_cefPlaceHolder.MoveWindow(0, 0, cx, cy); } if (m_browser m_browser-GetHost()) { m_browser-GetHost()-WasResized(); } }如果还闪烁看一下占位控件是否启用了NotifyParent或者被设置成透明背景。CEF 本质上是一个独立子窗口不要让 MFC 在它上面频繁重绘背景否则视觉上就会“白一下、闪一下”。有时候把主窗口的WS_CLIPCHILDREN样式加上能明显减少闪烁。5. 从 Demo 到生产的扩展建议5.1 JS 与 C 的双向通信Demo 做出来只能证明“能显示网页”实际业务更关心的是“网页和客户端怎么通信”。CEF 的办法不像 WebView2 那么简单但也不复杂。页面侧用window.cefQuery或者自定义 schemeC 侧用CefMessageRouter注册处理器。以cefQuery为例C 侧处理函数接收 JavaScript 传来的请求字符串解析后调用本地逻辑再通过回调把结果返回给 JS。这个机制非常适合“页面报表查询本地数据库”这类场景。我实际项目里把本地加密、读卡器、打印服务全部封装成这类桥接接口网页端只需要统一调用window.cefQuery({request: print, ...})就行。如果你希望做得更底层还可以在OnBeforeResourceLoad里拦截资源请求给特定 URL 加上自定义 Header 做鉴权或者在OnResourceLoadComplete里统计页面资源加载耗时。CEF 的定制空间很大理论上 Chromium 能做的CEF 都能做关键在于投入的开发精力值不值得。5.2 打包与分发要点CEF 集成后的客户端体积不会小。即使已经把 Release 目录剥掉一部分无用 dll一个最小可运行包也基本在 50MB 以上。打包时我不是简单把 dll 丢到 exe 目录就完事还要注意几个细节libcef.dll和resources.pak等文件必须和 exe 同级或者按 CEF 的目录规则放置不能随意改路径如果使用file://加载本地页面要注意本地页面里的相对路径和 JS 模块加载方式很多页面在浏览器里能跑放到 CEF 里就找不到资源多半是路径基座问题发布时建议把 CEF 版本号写进产品说明或版本文件里方便后续排查兼容性和安全补丁。我自己的习惯是把整个 CEF 相关文件作为“运行时组件”单独打一个子目录例如runtime/cef/然后启动时调用CefSettings.browser_subprocess_path和CefSettings.resources_dir_path明确指定路径。这样主系统升级时CEF 组件可以被单独替换不至于每次都要重做整个安装包。按我这几年的经验CEF 集成这事本身不复杂真正复杂的是生命周期和进程管理。如果你第一次搞建议先跑通最小 demo确认消息循环、关闭流程都干净再往里面加业务逻辑。否则后续每加一个功能都有可能在“关闭时残留”或“渲染崩溃”上翻车。等这版跑稳了你再看 WebView2 或新版 CEF 分支心里会踏实很多。本文还有配套的精品资源点击获取