简介:这份资源面向需要在Windows桌面端嵌入网页渲染能力的C++开发者,尤其是使用MFC框架、希望用Chromium内核替代传统IE控件的工程师。内容围绕谷歌CEF(Chromium 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 起步,客户还嫌启动慢。这时候 CEF(Chromium 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 里判断命令行参数,如果带有--type=renderer就进入渲染进程逻辑,否则走主进程初始化。这个分支不写对,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: CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; } CefRefPtr<CefLoadHandler> GetLoadHandler() override { return this; } void OnAfterCreated(CefRefPtr<CefBrowser> browser) override { // 浏览器实例创建完成,此时可以安全获取窗口句柄 browser_ = browser; } void OnBeforeClose(CefRefPtr<CefBrowser> browser) override { // 必须在此处释放引用,否则 CefShutdown 会卡死 browser_ = nullptr; } void OnLoadEnd(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, int httpStatusCode) override { if (frame->IsMain()) { // 主框架加载完成,可在此注入初始化脚本 frame->ExecuteJavaScript("console.log('CEF ready');", frame->GetURL(), 0); } } private: CefRefPtr<CefBrowser> browser_; IMPLEMENT_REFCOUNTING(SimpleClient); };OnAfterCreated里拿到的 browser 指针是跨进程代理,不能直接转成窗口句柄,需要通过browser->GetHost()->GetWindowHandle()获取。OnBeforeClose必须把browser_置空,否则 CefShutdown 会等待引用计数归零而永久阻塞——这是血泪经验,调试时表现为程序退出后进程残留。
3. C++ 与 JavaScript 双向通信:从 CefV8Handler 到 CefMessageRouter
3.1 注册 C++ 函数供 JS 调用
CEF 提供了 CefV8Handler 接口,允许你把 C++ 函数暴露到 JS 的 window 对象上。典型场景是 JS 调用 C++ 的文件保存对话框、读取注册表、或者调用本地硬件接口。
class NativeHandler : public CefV8Handler { public: bool Execute(const CefString& name, CefRefPtr<CefV8Value> object, const CefV8ValueList& arguments, CefRefPtr<CefV8Value>& 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(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context) override { CefRefPtr<CefV8Value> global = context->GetGlobal(); CefRefPtr<NativeHandler> handler = new NativeHandler(); CefRefPtr<CefV8Value> 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(CefRefPtr<CefFrame> 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"; // 取消查询的函数名 CefRefPtr<CefMessageRouterBrowserSide> 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) {} CefRefPtr<CefResourceHandler> Create(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, const CefString& scheme_name, CefRefPtr<CefRequest> request) override { return new LocalResourceHandler(root_, request->GetURL()); } private: std::string root_; IMPLEMENT_REFCOUNTING(LocalResourceHandlerFactory); };LocalResourceHandler需要实现ProcessRequest、GetResponseHeaders、ReadResponse三个方法。ProcessRequest里解析 URL 路径,拼接本地文件路径,判断文件是否存在;GetResponseHeaders设置 MIME 类型和 Content-Length;ReadResponse分块读取文件内容。注意ReadResponse的bytes_to_read参数是建议值,实际返回可以小于它,但返回 0 表示结束。
4.2 请求拦截与响应修改
有些场景需要拦截特定请求,比如给所有 API 请求加上认证头,或者把远程图片替换成本地缓存。CefRequestHandler 的OnBeforeResourceLoad提供了修改请求头的机会。
class RequestInterceptor : public CefRequestHandler { public: CefRefPtr<CefResourceRequestHandler> GetResourceRequestHandler( CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefRequest> 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( CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefRequest> request, CefRefPtr<CefCallback> 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(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> 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 面板,确认首屏加载没有长任务阻塞,再关掉端口出包。这个习惯帮我省掉了至少三次“页面卡顿但找不到原因”的返工。希望帮到你。
本文还有配套的精品资源,点击获取