简介这份资源面向需要在Windows桌面端嵌入网页渲染能力的C开发者尤其是使用MFC框架、希望用Chromium内核替代传统IE控件的工程师。内容围绕谷歌CEFChromium Embedded Framework浏览器控件的集成与使用展开覆盖从CEF库引入、项目配置到浏览器实例创建、生命周期管理、回调接口实现及多线程处理的完整链路适合具备一定MFC与C基础、想快速搭建内嵌浏览器框架的中级开发者参考。压缩包共450个文件约166.98MB包含250个C/C头文件、116个pak资源包、14个动态库、若干cpp与cc源码、lib静态库、vcxproj工程文件及exe可执行程序等覆盖编译、运行与资源加载所需的主要组件。目前已有1210人学习下载。通过该示例读者可掌握在MFC应用中创建CefBrowser、处理窗口尺寸变化与销毁流程、实现CefLifeSpanHandler与CefLoadHandler等回调并理解CEF多线程上下文约束为后续定制插件、本地文件访问或构建Web桌面客户端打下基础。1. 桌面端嵌入浏览器的真实选择CEF 控件到底能解决什么做 Windows 桌面客户端时产品经理一句“内嵌一个网页展示后台数据”往往就是噩梦的开始。用系统自带的 WebBrowser 控件内核停留在 IE7 时代Vue 打包出来的页面直接白屏用 Electron安装包直接飙到 150MB 起步客户还嫌启动慢。这时候 CEFChromium Embedded Framework就成了绕不开的选项——它把 Chromium 内核封装成动态库让你在 C、C#、Delphi 甚至易语言里都能塞进一个现代浏览器。这份资源是一套 CEF 浏览器控件的使用示例集合核心价值在于把“怎么把 Chromium 嵌进原生窗口”这件事拆成了可运行的代码。它适合两类人一是需要给传统桌面软件加 Web 界面的客户端开发者二是想理解浏览器内核与宿主程序如何通信的工程师。你不需要从零编译 Chromium但需要知道 CEF 的多进程架构、CefClient 的回调分发、以及 CefBrowser 的生命周期管理——这些才是决定你能不能跑通的关键。2. CEF 多进程架构与初始化为什么你的窗口一片空白2.1 主进程与渲染进程的分工逻辑CEF 默认采用多进程模型主进程Browser Process负责窗口管理、网络请求调度和资源加载渲染进程Render Process负责解析 HTML、执行 JavaScript。两者通过 IPC 通道通信所有跨进程调用都是异步的。很多新手第一次跑 CEF 时窗口一片空白八成是因为没理解这个模型——你在主进程里直接操作 DOM 是无效的必须通过 CefBrowserHost::SendDevToolsMessage 或者自定义 CefV8Handler 来桥接。常见做法是在主进程的 CefExecuteProcess 里判断命令行参数如果带有--typerenderer就进入渲染进程逻辑否则走主进程初始化。这个分支不写对CEF 会直接崩溃或者卡死在启动阶段。// main.cpp 入口分支区分主进程与渲染进程 int main(int argc, char* argv[]) { CefMainArgs main_args(argc, argv); // CefExecuteProcess 返回 -1 表示当前是主进程 // 返回 0 表示是子进程直接退出即可 int exit_code CefExecuteProcess(main_args, nullptr, nullptr); if (exit_code 0) { return exit_code; // 渲染进程在此结束 } CefSettings settings; settings.no_sandbox true; // 调试阶段关闭沙箱避免权限问题 settings.multi_threaded_message_loop true; // 消息循环交给独立线程 settings.log_severity LOGSEVERITY_INFO; // 日志级别排查时调成 VERBOSE CefInitialize(main_args, settings, nullptr, nullptr); // ... 创建窗口和浏览器实例 CefRunMessageLoop(); CefShutdown(); return 0; }这段代码里no_sandbox在开发阶段建议打开生产环境再关掉multi_threaded_message_loop设为 true 后主线程不会被 CEF 的消息循环阻塞适合 MFC 或 Qt 这类已有自己事件循环的框架。log_severity是排查问题的第一入口CEF 会把渲染进程的崩溃信息写到 debug.log路径由settings.log_file指定。2.2 初始化参数与缓存路径配置CEF 的缓存、Cookie、LocalStorage 默认写在可执行文件同级目录但 Windows 下 Program Files 没有写权限导致页面加载失败。我一般会显式指定cache_path和user_data_path到 AppData 目录。CefSettings settings; // 缓存路径必须可写否则页面白屏且日志报 access denied CefString(settings.cache_path).FromASCII(C:\\Users\\Public\\AppData\\Local\\MyApp\\cache); CefString(settings.user_data_path).FromASCII(C:\\Users\\Public\\AppData\\Local\\MyApp\\userdata); // 关闭 GPU 加速可规避部分老显卡驱动崩溃 settings.windowless_rendering_enabled false; settings.background_color 0xFFFFFFFF; // 白色背景避免透明窗口闪烁参数说明cache_path影响所有 HTTP 缓存和 Service Worker 存储user_data_path存放 Cookie 和 LocalStorage多实例场景下必须隔离否则两个客户端会互相覆盖登录态。background_color在窗口less模式下才生效普通窗口模式忽略。2.3 创建浏览器窗口与消息循环绑定初始化完成后创建 CefBrowser 实例需要提供 CefClient 实现其中至少覆盖GetLifeSpanHandler和GetLoadHandler。生命周期回调决定了窗口何时显示、何时销毁。class SimpleClient : public CefClient, public CefLifeSpanHandler, public CefLoadHandler { public: CefRefPtrCefLifeSpanHandler GetLifeSpanHandler() override { return this; } CefRefPtrCefLoadHandler GetLoadHandler() override { return this; } void OnAfterCreated(CefRefPtrCefBrowser browser) override { // 浏览器实例创建完成此时可以安全获取窗口句柄 browser_ browser; } void OnBeforeClose(CefRefPtrCefBrowser browser) override { // 必须在此处释放引用否则 CefShutdown 会卡死 browser_ nullptr; } void OnLoadEnd(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, int httpStatusCode) override { if (frame-IsMain()) { // 主框架加载完成可在此注入初始化脚本 frame-ExecuteJavaScript(console.log(CEF ready);, frame-GetURL(), 0); } } private: CefRefPtrCefBrowser browser_; IMPLEMENT_REFCOUNTING(SimpleClient); };OnAfterCreated里拿到的 browser 指针是跨进程代理不能直接转成窗口句柄需要通过browser-GetHost()-GetWindowHandle()获取。OnBeforeClose必须把browser_置空否则 CefShutdown 会等待引用计数归零而永久阻塞——这是血泪经验调试时表现为程序退出后进程残留。3. C 与 JavaScript 双向通信从 CefV8Handler 到 CefMessageRouter3.1 注册 C 函数供 JS 调用CEF 提供了 CefV8Handler 接口允许你把 C 函数暴露到 JS 的 window 对象上。典型场景是 JS 调用 C 的文件保存对话框、读取注册表、或者调用本地硬件接口。class NativeHandler : public CefV8Handler { public: bool Execute(const CefString name, CefRefPtrCefV8Value object, const CefV8ValueList arguments, CefRefPtrCefV8Value retval, CefString exception) override { if (name saveFile) { if (arguments.size() ! 2 || !arguments[0]-IsString()) { exception saveFile requires (filename, content); return true; } std::string filename arguments[0]-GetStringValue(); std::string content arguments[1]-GetStringValue(); // 实际写入逻辑此处省略文件 IO retval CefV8Value::CreateBool(true); return true; } return false; } IMPLEMENT_REFCOUNTING(NativeHandler); }; // 在 OnContextCreated 中绑定 void OnContextCreated(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefV8Context context) override { CefRefPtrCefV8Value global context-GetGlobal(); CefRefPtrNativeHandler handler new NativeHandler(); CefRefPtrCefV8Value func CefV8Value::CreateFunction(saveFile, handler); global-SetValue(nativeSaveFile, func, V8_PROPERTY_ATTRIBUTE_NONE); }逻辑说明Execute方法根据函数名分发arguments是 JS 传入的参数列表retval是返回值。注意exception用于抛出 JS 异常比返回 false 更友好。参数校验必须做否则 JS 传错类型会导致 C 侧崩溃。OnContextCreated在每个 frame 创建时触发包括 iframe所以要用frame-IsMain()过滤避免重复绑定。3.2 JS 调用 C 的异步回调封装同步调用会阻塞渲染进程如果 C 侧执行耗时操作比如网络请求页面会卡死。正确做法是 C 立即返回一个 Promise 的 resolve 句柄异步完成后通过CefFrame::ExecuteJavaScript回调。// C 侧异步完成后回调 JS void OnTaskComplete(CefRefPtrCefFrame frame, int taskId, const std::string result) { std::string script window.__cefCallback( std::to_string(taskId) , result );; frame-ExecuteJavaScript(script, frame-GetURL(), 0); }JS 侧维护一个回调映射表// 前端封装将 C 异步调用包装成 Promise const pendingTasks new Map(); let taskId 0; window.__cefCallback (id, result) { const resolve pendingTasks.get(id); if (resolve) { resolve(result); pendingTasks.delete(id); } }; function callNativeAsync(method, params) { return new Promise((resolve) { const id taskId; pendingTasks.set(id, resolve); // 调用 C 暴露的同步函数传入 taskId window.nativeInvoke(method, JSON.stringify(params), id); }); }参数说明taskId由 JS 侧自增生成C 侧原样回传避免多任务并发时回调错乱。JSON.stringify保证复杂对象能跨语言传递C 侧用 JSON 解析库反序列化。注意ExecuteJavaScript的第二个参数是脚本来源 URL传空字符串在某些版本会触发安全警告建议传当前页面 URL。3.3 CefMessageRouter 的适用场景与配置如果通信逻辑复杂手写回调映射容易乱CEF 官方提供了 CefMessageRouter。它在渲染进程和主进程之间建立了一条命名通道支持同步和异步两种模式。CefMessageRouterConfig config; config.js_query_function cefQuery; // JS 侧调用的函数名 config.js_cancel_function cefQueryCancel; // 取消查询的函数名 CefRefPtrCefMessageRouterBrowserSide router CefMessageRouterBrowserSide::Create(config); router-AddHandler(new MyQueryHandler(), true); // true 表示支持异步MyQueryHandler需要实现OnQuery方法返回 true 表示异步处理稍后调用callback-Success()或callback-Failure()。同步模式下直接返回结果但会阻塞渲染进程只适合极轻量的操作。我一般只在需要取消长时间查询的场景用这个普通通信还是走 V8Handler 更直观。4. 资源加载与网络拦截CefResourceHandler 的实战用法4.1 自定义协议注册与本地资源映射桌面客户端经常需要加载本地 HTML 文件但直接file://协议会有跨域限制而且相对路径处理麻烦。常见做法是注册一个自定义协议比如app://把请求映射到本地目录。// 注册自定义协议 CefRegisterSchemeHandlerFactory(app, local, new LocalResourceHandlerFactory(C:\\MyApp\\web)); // 工厂类实现 class LocalResourceHandlerFactory : public CefSchemeHandlerFactory { public: LocalResourceHandlerFactory(const std::string root) : root_(root) {} CefRefPtrCefResourceHandler Create(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, const CefString scheme_name, CefRefPtrCefRequest request) override { return new LocalResourceHandler(root_, request-GetURL()); } private: std::string root_; IMPLEMENT_REFCOUNTING(LocalResourceHandlerFactory); };LocalResourceHandler需要实现ProcessRequest、GetResponseHeaders、ReadResponse三个方法。ProcessRequest里解析 URL 路径拼接本地文件路径判断文件是否存在GetResponseHeaders设置 MIME 类型和 Content-LengthReadResponse分块读取文件内容。注意ReadResponse的bytes_to_read参数是建议值实际返回可以小于它但返回 0 表示结束。4.2 请求拦截与响应修改有些场景需要拦截特定请求比如给所有 API 请求加上认证头或者把远程图片替换成本地缓存。CefRequestHandler 的OnBeforeResourceLoad提供了修改请求头的机会。class RequestInterceptor : public CefRequestHandler { public: CefRefPtrCefResourceRequestHandler GetResourceRequestHandler( CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefRequest request, bool is_navigation, bool is_download, const CefString request_initiator, bool disable_default_handling) override { return new InterceptHandler(); } IMPLEMENT_REFCOUNTING(RequestInterceptor); }; class InterceptHandler : public CefResourceRequestHandler { public: cef_return_value_t OnBeforeResourceLoad( CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefRequest request, CefRefPtrCefCallback callback) override { CefRequest::HeaderMap headers; request-GetHeaderMap(headers); // 给所有请求加上自定义标识 headers.insert(std::make_pair(X-Client-Version, 1.0.0)); request-SetHeaderMap(headers); return RV_CONTINUE; // 继续加载 } IMPLEMENT_REFCOUNTING(InterceptHandler); };参数说明OnBeforeResourceLoad返回RV_CONTINUE表示放行RV_CANCEL表示拦截。如果要修改响应内容需要实现GetResourceHandler返回自定义的 CefResourceHandler在ReadResponse里替换数据。注意这个回调在 IO 线程执行不能做耗时操作否则会阻塞所有网络请求。4.3 缓存策略与离线加载CEF 的缓存默认遵循 HTTP 头但桌面客户端经常需要强制缓存某些资源。可以通过CefRequest::SetFlags设置UR_FLAG_SKIP_CACHE或UR_FLAG_ONLY_FROM_CACHE。// 强制从缓存读取离线场景使用 request-SetFlags(UR_FLAG_ONLY_FROM_CACHE); // 跳过缓存强制刷新 request-SetFlags(UR_FLAG_SKIP_CACHE);注意这两个标志互斥同时设置行为未定义。离线加载的完整方案是首次启动时把资源包解压到本地注册自定义协议指向该目录然后所有页面请求走app://协议完全不依赖网络。这样即使断网客户端也能正常显示界面。5. 避坑与排查CEF 集成中最容易翻车的五个点5.1 程序退出后进程残留现象关闭主窗口后任务管理器里还有 CEF 相关进程再次启动时报“另一个实例正在运行”。原因OnBeforeClose里没有释放 browser 引用或者CefShutdown被跳过。多进程模型下渲染进程需要主进程通知才会退出。解决确保OnBeforeClose中browser_ nullptr并且在主窗口销毁后调用CefShutdown()。如果用了multi_threaded_message_loop需要在CefShutdown前先CefRunMessageLoop退出。我一般会在OnBeforeClose里发一个自定义消息给主线程等所有 browser 关闭后再走 shutdown 流程。5.2 页面白屏但日志无报错现象窗口创建成功但内容区域全白debug.log 里只有正常的加载记录。原因大概率是缓存路径不可写或者OnContextCreated里绑定的 JS 函数抛了异常导致后续脚本中断。解决先检查cache_path是否指向了有写权限的目录然后打开LOGSEVERITY_VERBOSE看渲染进程日志。如果日志正常用frame-ExecuteJavaScript(document.body.innerHTML, ...)在OnLoadEnd里打印页面内容确认是加载失败还是渲染失败。还有一种可能是 GPU 加速导致的黑屏设置settings.windowless_rendering_enabled false并禁用 GPU 试试。5.3 JS 调用 C 时崩溃现象页面里调用window.nativeSaveFile(...)后整个客户端闪退。原因CefV8ValueList的参数类型没校验或者GetStringValue()在非字符串类型上调用。CEF 的 V8 接口对类型敏感传错直接触发断言。解决在Execute里对每个参数做IsString()、IsInt()判断不匹配就设置exception并返回 true。另外注意CefV8Value的生命周期不要在异步回调里持有它因为 V8 上下文可能已经销毁。需要跨线程传递的数据先转成 std::string 或 JSON。5.4 多显示器下窗口位置错乱现象把窗口拖到副屏后CEF 内容区域偏移或者不刷新。原因CEF 的窗口句柄是子窗口父窗口移动时没有触发重绘。高 DPI 场景下还有缩放比例不匹配的问题。解决监听父窗口的WM_MOVE和WM_SIZE消息调用browser-GetHost()-WasResized()通知 CEF 重新布局。高 DPI 需要在 manifest 里声明dpiAware并且 CEF 初始化时设置settings.force_device_scale_factor 1.0来统一缩放。如果副屏缩放比例不同建议用CefBrowserHost::SetWindowlessFrameRate限制帧率减少渲染压力。5.5 加载本地文件时的跨域限制现象用file://协议加载 HTML里面的 AJAX 请求被 CORS 拦截。原因Chromium 对file://协议有严格的安全限制默认禁止跨域请求。解决不要用file://注册自定义协议如app://并设置CefRegisterSchemeHandlerFactory。自定义协议默认被视为安全来源可以正常发起 AJAX。如果必须用file://启动时加--allow-file-access-from-files命令行参数但生产环境不推荐有安全风险。6. 进阶技巧用 DevTools 远程调试与性能分析CEF 内置了 Chromium DevTools但默认不开启远程调试端口。在CefSettings里设置remote_debugging_port后就可以用 Chrome 浏览器访问http://localhost:端口来调试内嵌页面。CefSettings settings; settings.remote_debugging_port 9222; // 监听 9222 端口 // 启动后浏览器打开 http://localhost:9222 即可看到可调试页面列表这个技巧在排查 JS 错误时特别有用因为桌面客户端的控制台输出往往被重定向到日志文件不如 DevTools 直观。注意生产环境要关掉这个端口或者加访问密码否则等于给客户端开了一个后门。另一个实用技巧是注入初始化脚本。在OnContextCreated里通过frame-ExecuteJavaScript注入一段代码可以在页面加载前修改window对象比如注入 polyfill 或者拦截fetch请求。void OnContextCreated(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefV8Context context) override { if (!frame-IsMain()) return; std::string script R( (function() { const originalFetch window.fetch; window.fetch function(...args) { console.log(fetch intercepted:, args[0]); return originalFetch.apply(this, args); }; })(); ); frame-ExecuteJavaScript(script, frame-GetURL(), 0); }这段脚本在页面任何 JS 执行之前运行适合做请求监控、错误上报或者环境变量注入。注意脚本里不要用 ES6 模块语法因为注入时页面上下文可能还没完全初始化。性能分析方面CEF 支持CefBrowserHost::SendDevToolsMessage发送 DevTools 协议命令可以程序化获取 FPS、内存占用、网络瀑布图。我一般会在客户端里加一个隐藏的调试面板按快捷键触发Tracing.start把 trace 数据保存成 JSON然后用 Chrome 的chrome://tracing打开分析。这样排查卡顿问题时能精确到某个 JS 函数执行了多久、哪次网络请求拖慢了首屏。从那以后我每次集成 CEF都会先把remote_debugging_port打开跑一遍 DevTools 的 Performance 面板确认首屏加载没有长任务阻塞再关掉端口出包。这个习惯帮我省掉了至少三次“页面卡顿但找不到原因”的返工。希望帮到你。本文还有配套的精品资源点击获取