
简介这份资源面向需要在VS2010环境下做桌面端Web集成的C开发者核心是用CEF3 90.6.0版本把Chromium浏览器内核嵌入MFC窗口并打通JavaScript与C的双向调用。包内共629个文件以269个h头文件、168个pak资源包、28个dll动态库、14个lib静态库和9个cpp源码为主另有obj、pdb、tlog等编译中间产物与bin、dat等运行时数据压缩包约457.22MB属于可直接编译运行的完整工程目录。资源围绕CefRenderWidgetHost子类封装、CEF与MFC消息循环同步、CefMessageRouterBrowserSide消息路由注册以及自定义scheme处理工厂等关键环节展开能帮助读者理解浏览器控件在MFC对话框中的创建、尺寸同步与JS回调返回结果的完整链路。目前已有1556人学习下载适合具备一定MFC与C基础、希望快速跑通CEF3嵌入与前后端交互的中高级开发者参考。1. 把 Chromium 塞进 MFC 窗口一个老项目的真实落地场景前阵子接手一个维护了快十年的工业控制上位机界面全是 MFC 那套老控件但客户突然要求内嵌一个能跑现代前端页面的浏览器控件还要支持 JS 调用本地 C 函数。团队第一反应是 WebBrowser 控件结果一跑就翻车——内核太老Vue 3 的页面直接白屏。后来翻到一个基于 VS2010 CEF3 90.6.0 版本的嵌入方案把 Chromium 内核塞进 MFC 窗口同时打通了 JS 与 C 的双向调用。这套东西对还在维护 MFC 老项目、又不得不兼容现代 Web 的开发者来说基本是绕不开的路子。它解决的核心问题就一个让老框架能跑新页面还能让页面里的 JS 直接喊话 C。2. 环境搭建与工程结构从零把 CEF3 编译进 VS20102.1 为什么是 CEF3 90.6.0 配 VS2010CEF3 的版本选择在这个场景里不是随便挑的。90.x 是最后一个对 VS2010 工具链相对友好的分支再往后的版本对 C11 特性依赖加重VS2010 的编译器会直接报一堆语法错误。90.6.0 这个补丁版本在稳定性上也比早期 90.x 好社区里反馈的崩溃问题少很多。VS2010 本身只支持到 C03 加部分 C11 特性所以编译 CEF3 的时候必须用官方提供的二进制分发包而不是自己从 Chromium 源码编。自己编的话光是 depot_tools 那套环境就能耗掉一整天而且 VS2010 根本编不过最新的 Chromium 源码。常见做法是直接下载 cef_binary_90.6.0 的 Windows 包里面已经带好了 libcef.lib、libcef.dll 和头文件。工程结构上我一般会这样组织MyCefMfcApp/ ├── MyCefMfcApp.sln ├── MyCefMfcApp/ │ ├── MyCefMfcApp.h/cpp # MFC 主程序 │ ├── MainFrm.h/cpp # 主框架窗口 │ ├── CefViewWnd.h/cpp # CEF 浏览器窗口封装 │ ├── SimpleHandler.h/cpp # CEF 事件处理 │ └── JsBridge.h/cpp # JS 与 C 通信桥 ├── cef/ │ ├── include/ # CEF 头文件 │ ├── lib/ # libcef.lib │ └── bin/ # libcef.dll 资源文件 └── web/ └── index.html # 前端页面这个结构的关键是把 CEF 相关的东西全部隔离在 cef 目录下业务代码只依赖 CefViewWnd 和 JsBridge 两个封装层。后面升级 CEF 版本的时候只需要替换 cef 目录业务代码基本不用动。2.2 VS2010 工程配置的几处硬性设置VS2010 的工程属性里有几项必须改否则编译能过但运行必崩。第一C/C → 代码生成 → 运行时库必须选多线程调试 DLL (/MDd)或多线程 DLL (/MD)。CEF3 的二进制包是用 /MD 编的如果你的工程用 /MT链接阶段就会报重复符号或者运行时堆冲突。第二链接器 → 输入 → 附加依赖项加上libcef.lib和libcef_dll_wrapper.lib。后者是 CEF 提供的 C 封装层必须和你的工程一起编译。官方包里带的是源码需要把libcef_dll_wrapper这个静态库工程加进解决方案一起编。第三C/C → 预处理器 → 预处理器定义加上WIN32、_WINDOWS、NDEBUGRelease、_DEBUGDebug以及CEF_USE_SANDBOX如果不用沙箱就别加。第四把cef/bin目录下的所有文件libcef.dll、chrome_elf.dll、icudtl.dat、locales 文件夹、resources 文件夹复制到你的输出目录。少一个文件程序启动就静默退出这是最常见的翻车点。提示VS2010 默认的字符集是 UnicodeCEF3 也要求 Unicode所以工程属性里字符集必须选「使用 Unicode 字符集」否则 CEF 初始化会失败。2.3 初始化 CEF一个进程只做一次的事CEF 的初始化必须在 MFC 的InitInstance里做而且整个进程只能做一次。代码大概长这样// MyCefMfcApp.cpp #include include/cef_app.h #include include/cef_browser.h #include include/cef_command_line.h BOOL CMyCefMfcApp::InitInstance() { // 1. 获取当前模块路径CEF 需要知道 dll 和资源在哪 CString strExePath; GetModuleFileName(NULL, strExePath.GetBuffer(MAX_PATH), MAX_PATH); strExePath.ReleaseBuffer(); int nPos strExePath.ReverseFind(_T(\\)); CString strDir strExePath.Left(nPos); // 2. 设置 CEF 的资源和子进程路径 CefMainArgs main_args(GetModuleHandle(NULL)); CefRefPtrCefApp app new CSimpleApp(); // 自定义 App 类 // 3. 子进程处理如果是渲染进程或 GPU 进程走这里 int exit_code CefExecuteProcess(main_args, app.get(), NULL); if (exit_code 0) { return exit_code; // 子进程直接退出 } // 4. 主进程初始化设置 CefSettings settings; settings.no_sandbox true; // VS2010 环境下沙箱容易出问题先关掉 settings.multi_threaded_message_loop false; // 用 MFC 的消息循环 CefString(settings.locales_dir_path) strDir _T(\\locales); CefString(settings.resources_dir_path) strDir _T(\\resources); CefString(settings.cache_path) strDir _T(\\cache); // 5. 初始化 CEF if (!CefInitialize(main_args, settings, app.get(), NULL)) { AfxMessageBox(_T(CEF 初始化失败)); return FALSE; } // 6. 正常走 MFC 的窗口创建流程 CMainFrame* pFrame new CMainFrame(); m_pMainWnd pFrame; pFrame-Create(NULL, _T(CEF MFC Demo)); pFrame-ShowWindow(SW_SHOW); pFrame-UpdateWindow(); return TRUE; }这段代码里有几个参数需要解释。no_sandbox true是因为 VS2010 编译出来的程序在沙箱模式下经常因为权限问题起不来开发阶段先关掉上线前再根据实际情况决定是否开启。multi_threaded_message_loop false表示 CEF 不自己起消息循环而是把消息处理交给 MFC 的PreTranslateMessage这样 MFC 的窗口消息和 CEF 的消息不会打架。CefExecuteProcess那一步很关键它负责处理 CEF 的子进程。如果你漏了这一步浏览器窗口能创建出来但页面渲染会异常因为渲染进程没起来。2.4 退出时的清理顺序CEF 的关闭必须在 MFC 窗口销毁之后、程序退出之前做int CMyCefMfcApp::ExitInstance() { CefShutdown(); // 必须在所有 CEF 对象释放后调用 return CWinApp::ExitInstance(); }顺序反了的话程序退出时会卡死或者报访问违例。血泪经验是CefShutdown之前确保所有CefBrowser对象已经释放否则它会等渲染进程退出而渲染进程又在等主进程的消息互相等就死锁了。3. 核心模块拆解浏览器窗口封装与消息路由3.1 CefViewWnd把浏览器变成 MFC 的子窗口CEF 的浏览器窗口本质上是一个独立的原生窗口要把它嵌到 MFC 的对话框或者 View 里需要拿到它的父窗口句柄。核心做法是在 MFC 窗口的OnCreate里创建浏览器然后把浏览器窗口的父句柄设成当前 MFC 窗口。// CefViewWnd.cpp BOOL CCefViewWnd::CreateBrowser(const CRect rect, CWnd* pParent) { // 保存父窗口 m_pParent pParent; // 浏览器窗口信息 CefWindowInfo window_info; window_info.SetAsChild(pParent-GetSafeHwnd(), CefRect(rect.left, rect.top, rect.Width(), rect.Height())); // 浏览器设置 CefBrowserSettings browser_settings; browser_settings.javascript_access_clipboard STATE_ENABLED; browser_settings.webgl STATE_ENABLED; // 创建浏览器实例 CefRefPtrCSimpleHandler handler new CSimpleHandler(this); CefBrowserHost::CreateBrowser(window_info, handler.get(), _T(file:///web/index.html), browser_settings, NULL, NULL); return TRUE; }SetAsChild这个调用是把 CEF 窗口变成子窗口的关键。第一个参数是父窗口句柄第二个参数是初始位置和大小。注意这里的坐标是相对于父窗口客户区的不是屏幕坐标。CSimpleHandler是继承自CefClient的事件处理器它负责接收浏览器生命周期事件、JS 消息、加载状态变化等。一个 Handler 可以挂多个浏览器实例但通常一个浏览器配一个 Handler 更清晰。3.2 消息路由MFC 消息循环与 CEF 消息的融合因为设置了multi_threaded_message_loop falseCEF 的消息需要由 MFC 的消息循环来驱动。做法是在 MFC 窗口的PreTranslateMessage里把消息转给 CEF// MainFrm.cpp BOOL CMainFrame::PreTranslateMessage(MSG* pMsg) { // 把消息交给 CEF 处理 if (m_pCefView m_pCefView-GetBrowserHost()) { if (m_pCefView-GetBrowserHost()-GetWindowHandle() pMsg-hwnd) { // 如果是 CEF 窗口的消息让 CEF 自己处理 return false; } } return CFrameWnd::PreTranslateMessage(pMsg); }更完整的做法是重写CefViewWnd的WindowProc在里面调用CefBrowserHost::GetWindowHandle()判断消息归属。如果消息的 hwnd 是 CEF 的窗口句柄就交给 CEF 的默认处理流程否则走 MFC 的默认处理。这里有个容易踩的坑CEF 的窗口句柄在浏览器创建完成之前是无效的所以PreTranslateMessage里必须判空。另外CEF 的窗口句柄在页面跳转时可能会变所以不能缓存每次都要重新获取。3.3 窗口大小自适应MFC 窗口缩放时CEF 的窗口不会自动跟着变需要手动调整void CCefViewWnd::OnSize(UINT nType, int cx, int cy) { CWnd::OnSize(nType, cx, cy); if (m_pBrowser m_pBrowser-GetHost()) { // 获取 CEF 窗口句柄 CefWindowHandle hwnd m_pBrowser-GetHost()-GetWindowHandle(); if (hwnd) { // 用 Win32 API 调整 CEF 窗口大小 ::SetWindowPos(hwnd, NULL, 0, 0, cx, cy, SWP_NOZORDER | SWP_NOACTIVATE); } } }SetWindowPos的最后一个参数用SWP_NOZORDER | SWP_NOACTIVATE避免调整大小时抢焦点或者改变 Z 序。如果窗口里有多个子控件CEF 窗口的 Z 序需要特别注意它默认会盖住其他控件需要用SetWindowPos的SWP_NOZORDER配合BringWindowToTop来调整。3.4 加载状态与错误处理页面加载失败时CEF 会回调OnLoadError这时候需要给用户一个提示而不是白屏// SimpleHandler.cpp void CSimpleHandler::OnLoadError(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, ErrorCode errorCode, const CefString errorText, const CefString failedUrl) { if (errorCode ERR_ABORTED) { return; // 用户主动取消的加载不处理 } // 构造一个简单的错误页面 std::stringstream ss; ss htmlbody bgcolor\white\ h2页面加载失败/h2 p错误码: errorCode /p p地址: failedUrl.ToString() /p /body/html; frame-LoadString(ss.str(), failedUrl); }ERR_ABORTED是用户点击链接后快速取消的情况这种不需要显示错误页。其他错误码比如ERR_CONNECTION_REFUSED、ERR_FILE_NOT_FOUND才需要处理。LoadString是 CEF 提供的直接加载 HTML 字符串的方法比写临时文件再加载要干净。4. JS 与 C 双向调用打通前端和后端的任督二脉4.1 C 调用 JSExecuteJavaScriptC 侧调用 JS 最简单的方式是ExecuteJavaScript// 在 C 中调用页面里的 JS 函数 void CCefViewWnd::CallJsFunction(const CString strFuncName, const CString strParam) { if (!m_pBrowser || !m_pBrowser-GetMainFrame()) { return; } // 拼接 JS 调用语句 CString strScript; strScript.Format(_T(%s(%s);), strFuncName, strParam); // 执行 JS m_pBrowser-GetMainFrame()-ExecuteJavaScript( CefString(strScript), m_pBrowser-GetMainFrame()-GetURL(), 0); }ExecuteJavaScript的第二个参数是脚本来源 URL一般传当前页面 URL 就行第三个参数是起始行号传 0。这个方法没有返回值如果需要 JS 返回结果给 C得用CefFrame::ExecuteJavaScript的回调版本或者走 JS 主动调用 C 的通道。注意ExecuteJavaScript必须在 UI 线程调用如果在工作线程里调需要先CefPostTask切回 UI 线程。4.2 JS 调用 C注册原生函数JS 调 C 是这套方案的核心价值。CEF3 提供了CefV8Handler和CefRegisterExtension两种方式我一般用前者因为它更灵活。首先定义一个 V8 Handler// JsBridge.h class CJsBridge : public CefV8Handler { public: CJsBridge(CWnd* pWnd) : m_pWnd(pWnd) {} virtual bool Execute(const CefString name, CefRefPtrCefV8Value object, const CefV8ValueList arguments, CefRefPtrCefV8Value retval, CefString exception) override; private: CWnd* m_pWnd; IMPLEMENT_REFCOUNTING(CJsBridge); };然后在Execute里根据函数名分发// JsBridge.cpp bool CJsBridge::Execute(const CefString name, CefRefPtrCefV8Value object, const CefV8ValueList arguments, CefRefPtrCefV8Value retval, CefString exception) { if (name callNative) { // 检查参数 if (arguments.size() 1 || !arguments[0]-IsString()) { exception 参数错误需要传入字符串; return true; } CString strCmd arguments[0]-GetStringValue().c_str(); // 把命令发给 MFC 窗口处理 if (m_pWnd ::IsWindow(m_pWnd-GetSafeHwnd())) { m_pWnd-PostMessage(WM_NATIVE_CALL, 0, (LPARAM)new CString(strCmd)); } // 返回一个值给 JS retval CefV8Value::CreateString(received); return true; } return false; }Execute的返回值表示是否处理了这个函数调用。返回true表示已处理CEF 不会再找其他 Handler。retval是返回给 JS 的值exception是抛出的异常信息。接下来在页面加载完成后注册这个函数// SimpleHandler.cpp void CSimpleHandler::OnContextCreated(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefV8Context context) { // 获取全局对象 CefRefPtrCefV8Value object context-GetGlobal(); // 创建原生函数 CefRefPtrCefV8Value func CefV8Value::CreateFunction(callNative, m_pJsBridge); // 挂到 window 对象上 object-SetValue(callNative, func, V8_PROPERTY_ATTRIBUTE_NONE); }OnContextCreated在每个 frame 的 JS 上下文创建时调用这时候把callNative挂到 window 上页面里的 JS 就能直接调window.callNative(命令)了。4.3 完整调用链路从 JS 点击到 C 执行把上面的串起来一个完整的调用链路是这样的页面里 JS 执行window.callNative(openFile)CEF 的 V8 引擎捕获到调用进入CJsBridge::ExecuteExecute里把命令通过PostMessage发给 MFC 窗口MFC 窗口的OnNativeCall处理消息执行对应操作如果需要返回结果C 再调ExecuteJavaScript把结果传回页面MFC 侧的消息处理// MainFrm.cpp LRESULT CMainFrame::OnNativeCall(WPARAM wParam, LPARAM lParam) { CString* pStr (CString*)lParam; if (pStr) { if (*pStr _T(openFile)) { // 执行打开文件操作 CFileDialog dlg(TRUE); if (dlg.DoModal() IDOK) { // 把结果传回 JS CString strResult; strResult.Format(_T(onFileSelected(%s)), dlg.GetPathName()); m_pCefView-ExecuteJavaScript(strResult); } } delete pStr; } return 0; }这里用PostMessage而不是SendMessage是因为Execute是在 CEF 的渲染线程里执行的直接SendMessage到 UI 线程可能造成死锁。PostMessage是异步的把命令丢到 UI 线程的消息队列里就返回不会阻塞渲染线程。4.4 异步回调的封装上面的模式有个问题每次调用都要手动管理new CString和delete容易内存泄漏。更好的做法是封装一个异步回调管理器class CAsyncCallbackMgr { public: // 注册一个回调返回回调 ID int RegisterCallback(std::functionvoid(const CString) callback) { int id m_nNextId; m_mapCallbacks[id] callback; return id; } // 触发回调 void TriggerCallback(int id, const CString result) { auto it m_mapCallbacks.find(id); if (it ! m_mapCallbacks.end()) { it-second(result); m_mapCallbacks.erase(it); } } private: int m_nNextId 1; std::mapint, std::functionvoid(const CString) m_mapCallbacks; };JS 侧调用时传入一个回调 IDC 处理完后通过ExecuteJavaScript调用window.onNativeResult(id, result)JS 侧再根据 ID 找到对应的回调函数执行。这样就把异步调用的生命周期管理起来了。5. 避坑与常见问题那些让我加班到凌晨的坑5.1 程序启动就闪退没有任何报错现象编译通过双击 exe 后窗口一闪就没了调试模式下也看不到异常。原因CEF 的资源文件没复制全。最常见的是漏了icudtl.dat或者locales文件夹。CEF 初始化时会去加载这些文件找不到就直接exit连错误对话框都不弹。解决把cef/bin目录下的所有文件复制到 exe 同级目录。特别注意locales文件夹里至少要有zh-CN.pak和en-US.pakresources文件夹里要有cef.pak和cef_100_percent.pak。用 VS2010 的生成后事件自动复制xcopy /Y /E /I $(SolutionDir)cef\bin\*.* $(OutDir)5.2 页面能加载但 JS 调用 C 没反应现象页面正常显示但window.callNative是 undefined控制台报错。原因OnContextCreated没有触发或者注册的时机不对。如果页面里有 iframe每个 iframe 都会触发一次OnContextCreated但只有主 frame 的 window 对象才是页面里直接访问的那个。解决在OnContextCreated里判断frame-IsMain()只给主 frame 注册。另外如果页面是通过LoadString加载的OnContextCreated的触发时机可能比预期晚需要在OnLoadEnd里再检查一次。5.3 关闭窗口时程序卡死现象点关闭按钮后窗口消失但进程还在任务管理器里CPU 占用为 0。原因CefShutdown被调用了但还有CefBrowser对象没释放。CEF 在CefShutdown里会等所有浏览器实例销毁而浏览器实例又在等渲染进程退出渲染进程在等主进程的消息循环但主进程的消息循环已经停了死锁。解决在ExitInstance里先手动关闭所有浏览器int CMyCefMfcApp::ExitInstance() { if (m_pCefView m_pCefView-GetBrowser()) { m_pCefView-GetBrowser()-GetHost()-CloseBrowser(true); } CefShutdown(); return CWinApp::ExitInstance(); }CloseBrowser(true)的true表示强制关闭不等页面确认。5.4 VS2010 编译 libcef_dll_wrapper 报错现象把libcef_dll_wrapper工程加进解决方案后编译报一堆error C2039、error C2065。原因libcef_dll_wrapper的工程配置和你的主工程不一致最常见的是字符集和运行时库不匹配。解决把libcef_dll_wrapper工程的字符集改成 Unicode运行时库改成 /MD 或 /MDd和主工程保持一致。另外CEF 的头文件路径要加到附加包含目录里通常是$(SolutionDir)cef\include。5.5 页面里的中文显示乱码现象HTML 文件里写的中文在浏览器里显示成方块或者问号。原因HTML 文件保存的编码和页面里声明的编码不一致。VS2010 默认保存为 GBK但页面里声明的是 UTF-8。解决把 HTML 文件用 UTF-8 无 BOM 格式保存并在head里加meta charsetutf-8。如果 HTML 文件是动态生成的确保生成时用的是 UTF-8 编码。CEF 内部默认按 UTF-8 处理字符串所以 C 侧传给 JS 的中文也要先转成 UTF-8CStringA strUtf8 CW2A(strChinese, CP_UTF8);6. 二次开发改造从能跑到好用还差哪几步这套方案跑通之后真正花时间的是二次开发。我一般会从三个方向改通信协议、调试工具、打包部署。通信协议方面原生的callNative只支持字符串参数实际项目里经常需要传 JSON。我的做法是在 JS 侧统一用JSON.stringify把对象转成字符串C 侧收到后用CefParseJSON解析CefRefPtrCefValue value CefParseJSON(strJson, JSON_PARSER_RFC); if (value value-GetType() VTYPE_DICTIONARY) { CefRefPtrCefDictionaryValue dict value-GetDictionary(); CString strAction dict-GetString(action).c_str(); // 根据 action 分发 }这样 JS 侧调用就变成了window.callNative(JSON.stringify({action: openFile, path: /tmp/a.txt}))扩展性比纯字符串好很多。调试工具方面CEF 支持远程调试在CefSettings里设置settings.remote_debugging_port 9222然后用 Chrome 打开http://localhost:9222就能看到页面里的 console 和 DOM。这个在排查 JS 报错时非常有用比在 MFC 里打日志快得多。不过要注意远程调试端口不要在生产环境开有安全风险。打包部署方面CEF 的运行时文件加起来有 100 多 MB直接扔给客户不太现实。我一般用 Inno Setup 或者 NSIS 做安装包把libcef.dll、chrome_elf.dll、icudtl.dat、locales、resources这些文件打包进去。安装时注意locales文件夹只需要保留用到的语言包比如只留zh-CN.pak和en-US.pak能省 20 多 MB。还有一个容易忽略的点是缓存目录。CEF 默认会在 exe 同级目录下创建cache文件夹如果程序装在Program Files下普通用户没有写权限会导致页面加载失败。我的做法是在CefSettings里把cache_path指到%LOCALAPPDATA%下CString strCachePath; strCachePath.Format(_T(%s\\MyApp\\cache), (LPCTSTR)SHGetKnownFolderPath(FOLDERID_LocalAppData)); CefString(settings.cache_path) strCachePath;从那以后我每次给 MFC 项目嵌 CEF都会先把缓存路径、资源路径、子进程路径这三项在CefSettings里显式指定不再依赖默认值。默认值在开发机上跑没问题一到客户环境就各种找不到文件。希望帮到你。本文还有配套的精品资源点击获取