简介这份资源面向使用VS2010进行Windows桌面开发的C程序员解决在MFC应用程序中嵌入CEF3浏览器内核并实现JavaScript与C双向交互的问题。内容围绕CEF3 90.6.0版本展开涵盖MFC项目创建、CEF库引用配置、CEFRenderWidgetHost子类封装、CEF与MFC消息循环同步以及通过CefMessageRouterBrowserSide::Handler注册JS调用C函数等关键环节适合需要为传统MFC程序引入HTML5、CSS、JavaScript渲染能力的中高级开发者参考。压缩包共629个文件约457.22MB以269个h头文件、168个pak资源包、28个dll动态库、14个lib静态库及9个cpp源文件为主另含obj、pdb、tlog等编译中间产物与解决方案文件完整保留了工程构建痕迹。目前已有1556人学习下载。借助该工程读者可对照目录结构理解CEF3在MFC中的集成方式梳理JS与C通信的注册与回调流程并参考启动参数配置与消息循环处理思路快速搭建可运行的嵌入环境。1. 把浏览器塞进 MFC 窗口一个 VS2010 CEF3 90.6.0 的嵌入实战如果你手上有一个维护了十几年的 MFC 老项目界面还停留在原生控件时代而产品经理突然要求“内嵌一个浏览器能加载网页、能跟 C 双向通信”你会怎么办我最近拆的这个资源就是干这件事的在 VS2010 环境下用 CEF3 的 90.6.0 版本把 Chromium 内核嵌进 MFC 窗口并且打通了 JS 调用 C 的链路。它适合两类人一是被老项目绑住、必须用 VS2010 工具链的 C 开发者二是想搞明白 CEF3 多进程架构到底怎么跟 Win32 消息循环缝合的人。资源里给的是完整可编译的工程不是零散代码片段拿到手就能看到窗口怎么创建、消息怎么转发、JS 怎么调到 C 函数。2. 环境搭建与工程结构VS2010 下把 CEF3 90.6.0 跑起来2.1 为什么是 VS2010 CEF3 90.6.0 这个组合先说清楚这个组合的来由。CEF3 的 90.x 分支对应 Chromium 90是最后一个还能用 VS2010 工具集编译的稳定分支之一。再往后的版本Chromium 的构建系统对 C 标准要求提高VS2010 的 MSVC10 编译器就扛不住了。所以如果你维护的是 VS2010 工程90.6.0 基本是“能用的最新版”。资源里提供的二进制包已经编译好了 libcef.dll 和配套的 lib 文件省去了自己编译 Chromium 这个动辄几小时的苦力活。工程结构上资源分成三块cef_binary目录放 CEF3 的头文件、lib 和 dllMFCEmbed目录是 MFC 主工程包含窗口创建和消息处理js_bridge目录放 JS 调用 C 的接口定义和示例页面。这种分法很清晰改的时候不会互相污染。2.2 工程配置包含目录、库目录和运行时依赖拿到工程第一步不是急着编译而是把 CEF3 的路径配好。VS2010 的工程属性里需要改三个地方# 在 VS2010 项目属性中配置 # 1. C/C - 常规 - 附加包含目录 $(ProjectDir)..\cef_binary\include # 2. 链接器 - 常规 - 附加库目录 $(ProjectDir)..\cef_binary\lib # 3. 链接器 - 输入 - 附加依赖项 libcef.lib libcef_dll_wrapper.lib这里有个细节libcef_dll_wrapper.lib是 CEF3 提供的 C 封装层它把 C 风格的 API 包成了 C 类。如果你只用 C API可以不链它但资源里的代码用的是 C 封装所以必须加。另外libcef.dll和chrome_elf.dll要跟 exe 放同一目录否则运行时会直接报“找不到 libcef.dll”。还有一个容易翻车的地方VS2010 默认用/MDd或/MD运行时而 CEF3 的预编译库通常用/MT。如果链接时报LNK2038或RuntimeLibrary不匹配就把工程改成/MTdDebug或/MTRelease跟 CEF3 库保持一致。2.3 目录结构一览资源解压后的目录长这样CEF3_MFC_Demo/ ├── cef_binary/ │ ├── include/ # CEF3 头文件 │ ├── lib/ # libcef.lib, libcef_dll_wrapper.lib │ ├── Release/ # libcef.dll, chrome_elf.dll │ └── Resources/ # locales, cef.pak 等资源 ├── MFCEmbed/ │ ├── MFCEmbed.cpp # 主程序入口 │ ├── MainFrm.cpp # 主框架窗口 │ ├── CefView.cpp # CEF3 浏览器窗口封装 │ └── JsBridge.cpp # JS 调用 C 的桥接实现 ├── js_bridge/ │ ├── index.html # 示例页面 │ └── bridge.js # JS 侧调用封装 └── README.mdResources目录不能少CEF3 初始化时要加载cef.pak、cef_100_percent.pak和locales下的语言包。如果程序启动后白屏或者直接崩先检查这个目录有没有跟 exe 放一起。3. 核心模块拆解从 CEF3 初始化到 MFC 窗口嵌入3.1 CEF3 多进程初始化CefInitialize 与消息循环CEF3 是多进程架构主进程负责窗口和消息循环渲染进程负责解析 HTML 和跑 JS。初始化必须在 MFC 的InitInstance里做而且要在创建主窗口之前// MFCEmbed.cpp 中的 InitInstance 片段 CefMainArgs main_args(GetModuleHandle(NULL)); // 1. 执行子进程逻辑如果是渲染进程就直接返回 int exit_code CefExecuteProcess(main_args, NULL, NULL); if (exit_code 0) { return exit_code; } // 2. 配置 CEF3 启动参数 CefSettings settings; settings.multi_threaded_message_loop false; // 跟 MFC 消息循环共用主线程 settings.no_sandbox true; // VS2010 下建议关沙箱避免权限问题 CefString(settings.locales_dir_path) LResources\\locales; CefString(settings.resources_dir_path) LResources; // 3. 初始化 CEF3 CefInitialize(main_args, settings, NULL, NULL); // 4. 创建 MFC 主窗口 CMainFrame* pFrame new CMainFrame(); pFrame-Create(NULL, LCEF3 MFC Embed); pFrame-ShowWindow(SW_SHOW); // 5. 进入 MFC 消息循环 int ret CWinApp::Run(); // 6. 退出时关闭 CEF3 CefShutdown(); return ret;这里的关键参数是multi_threaded_message_loop。设成false表示 CEF3 不自己起消息循环线程而是把消息处理挂到 MFC 的主线程上。这样做的好处是 JS 调用 C 时不用考虑跨线程同步坏处是如果 C 侧处理太慢会卡住整个界面。资源里选的是false因为示例场景是轻量级调用没必要引入线程复杂度。no_sandbox true在 VS2010 环境下几乎是必须的。Chromium 的沙箱依赖较新的 Windows API老工具链编译出来的程序开沙箱容易在启动时直接崩。关掉沙箱后渲染进程的权限跟主进程一样安全性降低但开发阶段省心。3.2 把浏览器窗口嵌进 MFCCefView 的创建与布局CEF3 的浏览器窗口本质上是一个子窗口要嵌到 MFC 的框架里得先创建一个静态控件占位再把 CEF3 的窗口贴上去// CefView.cpp 中创建浏览器窗口 void CCefView::CreateBrowser(HWND hParent, const CRect rect) { // 1. 保存父窗口和区域 m_hParent hParent; m_rect rect; // 2. 配置浏览器窗口信息 CefWindowInfo window_info; window_info.SetAsChild(hParent, rect); // 作为子窗口嵌入 // 3. 配置浏览器设置 CefBrowserSettings browser_settings; browser_settings.javascript STATE_ENABLED; // 开启 JS // 4. 创建浏览器实例 CefBrowserHost::CreateBrowser( window_info, m_client.get(), // 实现 CefClient 接口的对象 Lfile:///js_bridge/index.html, browser_settings, NULL, NULL ); }SetAsChild是嵌入的关键它告诉 CEF3 把浏览器窗口创建成hParent的子窗口并指定位置和大小。MFC 这边需要在OnSize里同步调整 CEF3 窗口的大小否则拖动窗口边框时浏览器区域不会跟着变// MainFrm.cpp 中响应 WM_SIZE void CMainFrame::OnSize(UINT nType, int cx, int cy) { CFrameWnd::OnSize(nType, cx, cy); if (m_cefView m_cefView-GetBrowserHost()) { // 获取 CEF3 的窗口句柄并调整大小 HWND hCef m_cefView-GetBrowserHost()-GetWindowHandle(); ::SetWindowPos(hCef, NULL, 0, 0, cx, cy, SWP_NOZORDER); } }这段代码看起来简单但漏掉就会导致浏览器区域跟窗口脱节。我见过有人把 CEF3 窗口创建在对话框上结果对话框一缩放浏览器就跑到屏幕外面去了就是没做这个同步。3.3 JS 调用 CCefV8Handler 与消息路由JS 调 C 是这套方案的核心价值。CEF3 提供了CefV8Handler接口让你在 C 侧注册一个函数JS 里通过window.cefQuery或者自定义对象来调。资源里用的是自定义对象的方式更直观// JsBridge.cpp 中实现 V8 Handler class CJsBridgeHandler : public CefV8Handler { public: virtual bool Execute(const CefString name, CefRefPtrCefV8Value object, const CefV8ValueList arguments, CefRefPtrCefV8Value retval, CefString exception) OVERRIDE { if (name callCpp) { // 1. 从 JS 参数里取出方法名和参数 if (arguments.size() 1 || !arguments[0]-IsString()) { exception 参数错误需要方法名字符串; return true; } CefString method arguments[0]-GetStringValue(); // 2. 根据方法名分发到具体的 C 处理函数 if (method getVersion) { retval CefV8Value::CreateString(CEF3 90.6.0 on VS2010); } else if (method showMessage) { CefString msg arguments[1]-GetStringValue(); ::MessageBox(NULL, msg.ToWString().c_str(), L来自JS, MB_OK); retval CefV8Value::CreateBool(true); } else { exception 未知方法 method; } return true; } return false; } IMPLEMENT_REFCOUNTING(CJsBridgeHandler); };然后在CefClient的OnContextCreated回调里把这个对象挂到window上// 在 OnContextCreated 中注册 void CCefClient::OnContextCreated(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefV8Context context) { CefRefPtrCefV8Value global context-GetGlobal(); CefRefPtrCefV8Handler handler new CJsBridgeHandler(); CefRefPtrCefV8Value bridge CefV8Value::CreateObject(NULL); bridge-SetValue(callCpp, CefV8Value::CreateFunction(callCpp, handler), V8_PROPERTY_ATTRIBUTE_NONE); global-SetValue(cppBridge, bridge, V8_PROPERTY_ATTRIBUTE_NONE); }JS 侧调用就一行// bridge.js var result window.cppBridge.callCpp(getVersion); console.log(result); // 输出 CEF3 90.6.0 on VS2010这里有个坑Execute里如果返回falseCEF3 会继续找其他 handler如果都没有就抛 JS 异常。返回true表示已处理retval就是 JS 拿到的返回值。参数类型必须严格匹配JS 传数字过来C 侧要用IsInt()或IsDouble()判断不能直接GetIntValue()否则会崩。4. 避坑与常见问题VS2010 CEF3 的踩坑记录4.1 启动直接崩没有任何提示现象双击 exe 后进程一闪而过或者弹窗说“应用程序正常初始化失败”。原因最常见的是libcef.dll或chrome_elf.dll没跟 exe 放一起或者Resources目录路径不对。CEF3 初始化时会去加载cef.pak和locales下的语言包找不到就直接崩。解决把cef_binary/Release下的所有 dll 和Resources整个目录复制到 exe 所在目录。如果还是崩在CefInitialize之前加CefEnableHighDPISupport()试试有些 Win10 环境对 DPI 敏感。4.2 编译报 LNK2038 或 RuntimeLibrary 不匹配现象链接阶段报错提示_ITERATOR_DEBUG_LEVEL或RuntimeLibrary不匹配。原因VS2010 工程默认用/MDd而 CEF3 的预编译库用/MTd。两者混用会导致运行时库冲突。解决把工程属性里 C/C - 代码生成 - 运行时库改成/MTdDebug或/MTRelease跟 CEF3 库保持一致。改完后清理解决方案重新编译。4.3 JS 调用 C 没反应控制台也不报错现象JS 里调window.cppBridge.callCppC 侧断点没进去JS 也没抛异常。原因OnContextCreated没触发或者注册的对象名跟 JS 里写的不一致。还有一种可能是页面加载在OnContextCreated之前就执行了 JS导致cppBridge还没挂上去。解决在 JS 里用setTimeout延迟调用或者监听DOMContentLoaded事件。更稳妥的做法是在 C 侧等OnLoadEnd之后再执行 JS。另外检查OnContextCreated是否在CefClient里正确重写了这个回调默认是空的不重写就不会注册任何对象。4.4 窗口缩放后浏览器区域错位现象拖动 MFC 主窗口边框CEF3 的浏览器区域不跟着变或者只变一部分。原因MFC 的OnSize里没有同步调整 CEF3 子窗口的大小。CEF3 窗口是独立的 HWND不会自动跟随父窗口。解决在OnSize里拿到 CEF3 的窗口句柄用SetWindowPos重新设置位置和大小。注意要判断GetBrowserHost()是否为空因为窗口创建初期浏览器还没初始化完。4.5 关闭窗口时进程残留现象点了关闭按钮主窗口消失了但任务管理器里还有进程没退。原因CEF3 的渲染进程和 GPU 进程没有跟着主进程退出。如果CefShutdown()没调用或者调用时机不对就会残留。解决在CWinApp::ExitInstance里调CefShutdown()确保在 MFC 消息循环退出之后执行。另外在CefLifeSpanHandler的OnBeforeClose里做引用计数清理避免浏览器对象没释放导致进程卡住。5. 二次开发改造从示例到生产环境的几个关键调整资源里的示例能跑通但离生产环境还有距离。我一般会做三件事第一把 JS 调用的接口从硬编码的if-else改成注册表模式用std::mapCefString, std::function存方法名和回调新增接口不用改Execute函数第二给CefSettings加上cache_path让浏览器缓存落到本地目录避免每次启动都重新加载资源第三在CefLifeSpanHandler里处理OnBeforeClose确保关闭窗口时先释放浏览器对象再退出消息循环。改造后的接口注册大概长这样// 用 map 管理 JS 可调用的方法 class CJsBridgeRegistry { public: using Handler std::functionCefRefPtrCefV8Value(const CefV8ValueList); void Register(const CefString name, Handler handler) { m_handlers[name] handler; } bool Invoke(const CefString name, const CefV8ValueList args, CefRefPtrCefV8Value retval) { auto it m_handlers.find(name); if (it m_handlers.end()) return false; retval it-second(args); return true; } private: std::mapCefString, Handler m_handlers; };这样新增一个 JS 接口只需要registry.Register(getVersion, ...)不用动Execute里的分发逻辑。验证方法也简单在 JS 里调一个没注册的方法看 C 侧是否返回false并抛异常如果直接崩了说明参数校验没做。还有一个容易忽略的点CEF3 的CefV8Value是引用计数对象跨线程传递时必须用CefRefPtr包住否则渲染进程和主进程之间传参时会出玄学崩溃。我踩过一次JS 传了个对象过来C 侧直接取GetStringValue()结果进程随机挂后来加了IsString()判断才稳定。从那以后我每次改 JS 桥接层都强制走一遍“空参数、错类型、超长字符串”三个边界测试确认不会崩再往下写。希望帮到你。本文还有配套的精品资源点击获取