ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

VS2010下CEF3嵌入MFC实现JS调C++完整指南

VS2010下CEF3嵌入MFC实现JS调C++完整指南 简介面向使用Visual Studio 2010做MFC桌面开发的C工程师这份RAR压缩包提供了CEF3 90.6.0版本嵌入MFC窗口并实现JavaScript与C互调所需的完整工程与运行依赖。包内共629个文件约457MB其中269个h头文件与9个cpp源文件用于编译集成168个pak资源文件与28个dll支撑Chromium内核运行14个lib提供链接库另有sln工程、vcxproj项目配置及Release/Debug输出基本涵盖从代码编译到程序运行的各环节。已有1555人学习下载说明该方案在VS2010老项目升级或Web混合客户端场景中具有较高的参考价值。借助其中的BWQ工程结构、CEF配置参数以及JS调用C的示例逻辑读者可以快速理解CEF3嵌入MFC窗口、消息循环同步和自定义scheme通信的关键流程减少环境配置和编译排错的摸索时间。1. 老项目接住新内核VS2010 里跑通 CEF3 90.6.0让 JS 调 C手上有一批 VS2010 时代留下来的 MFC 程序客户却要求在窗口里直接打开现代网页页面里的 JS 还要能调本地 C 函数。我最后落地方案是 CEF3 的 90.6.0 版预编译包把浏览器视图嵌进 MFC 对话框JS 通过 CefV8Handler 反向调用 CC 用 ExecuteJavaScript 再驱动页面。整个过程一半工作量在工程配置另一半在消息循环真正写互调代码反而是最快的那一步。这篇笔记写给同样被 VS2010 绑住手脚的从业者无论是接手旧系统的运维开发还是想在老平台上做混合界面都可以直接照着走。2. 版本选型与结构拆解为什么 90.6.0 能在 VS2010 下工作2.1 CEF 的进程模型与预编译包里的家底CEF 全称 Chromium Embedded Framework本质是给桌面程序嵌一个完整 Chromium。CEF3 是纯异步接口版本现在主流分发版本走的都是这套 API。90.6.0 这个版本号对应 Chromium 90 这一代发布线这一代接口形态还算稳定官方同时提供 32 位和 64 位分发包。我选 90.6.0 的核心原因很现实项目要求必须在 VS2010 下编译而我实际验证下来这代 wrapper 源码是 VC10 能啃得动的最后一条线。再往上走CEF 源码里用的 C14、更复杂模板会让 VS2010 的编译器彻底罢工。解压官方分发包之后有效文件基本是这几类libcef.dll和libcef.lib是主库include/下是 C API 头文件libcef_dll/里是 wrapper 的 C 源码Resources/下则是icudtl.dat、*.pak、v8_context_snapshot.bin和locales/目录。很多人只关心libcef.dll忽略了icudtl.dat和locales结果启动秒崩或者界面全是英文。icudtl.dat是 Chromium 的 ICU 数据文件丢了页面文字直接变方块locales/少了则回退到英文资源MFC 程序里看着特别违和。还要注意 CEF 是独立多进程模型CreateBrowser之后系统会自动拉起 GPU、Renderer 等子进程。你的 exe 不只是浏览器宿主它还会作为子进程入口被反复以--typerenderer、--typegpu-process这样的命令行参数重新加载。所以工程入口函数不能乱改CEF 内部会根据命令行区分主进程还是子进程这也是后面“白屏但进程在”这类问题的根源。2.2 VS2010 的编译器边界Unicode、C11 和 wrapper 源码VS2010 的编译器是 VC10对 C11 的支持非常残。CEF 的 wrapper 源码大量使用CefRefPtr、nullptr、auto、override这些偏新语法VC10 对其中一部分能用但遇到std::move、std::unique_ptr、变参模板或range-for就会翻车。90.6.0 的 wrapper 源码刚好卡在 VC10 能编译的临界线上这是我能在这个老环境里跑通它的主要原因。CEF 内部统一使用 UTF-16 字符串CefString在 Windows 下默认就是宽字符实现。MFC 老项目默认往往开“多字节字符集”结果写第一行CefBrowserHost::CreateBrowser就报一大堆类型转换错误。我接手这类项目第一件事永远是把工程改成“使用 Unicode 字符集”否则后续每个参数都要手动转一把代码会变得没法看。另一个边界是运行库。CEF 预编译包里的libcef.dll以及 wrapper 编译出来的libcef_dll_wrapper.lib都是按动态运行库生成的。VS2010 工程如果开了/MT静态运行库调试时会出现内存分配释放跨模块的诡异问题表现形式是数组、字符串随机损坏。所以下面的参数表会把运行库单独列出来这点不要省。2.3 选型参数表平台、字符集与 CEF 包配置项建议值原因平台工具集Visual Studio 2010 (v100)项目要求不能换编译器目标平台Win3232 位 CEF 包兼容性最好字符集使用 UnicodeCEF 字符串统一 UTF-16运行库Debug 用 /MDdRelease 用 /MD与预编译 libcef 动态库匹配CEF 包32 位 90.6.0与工程目标平台一一对应输出目录exe 同目录libcef.dll 和 Resources 必须能找到这套参数不是随便定的。字符集决定了你能不能过编译运行库决定了你运行稳不稳定CEF 包的位数决定了子进程能不能被正确拉起。后面第 5 章的五个坑里至少有三个能直接对上这张表。3. 工程搭建与窗口嵌入配置 VS2010 项目并让 CEF 视图上窗口3.1 目录规划与文件拷贝新建一个 MFC 对话框程序工程名假设是CefMfcDemo。我把 CEF 分发包解压到工程目录下的third_party\cef90结构保持官方原样CefMfcDemo/ src/ third_party/cef90/ include/ libcef_dll/ Release/ Resources/然后在主工程的“VC 目录”里配置附加包含目录为$(ProjectDir)..\third_party\cef90附加库目录为$(ProjectDir)..\third_party\cef90\Release。这样代码里写#include include/cef_app.h就能直接命中不需要再拼长路径。接下来要把运行期文件准备到输出目录。libcef.dll必须和 exe 在同一目录Resources需要完整拷过去包括里面的locales子目录。我一般写在工程“生成后事件”里而不是手动拖文件xcopy /Y /E /I $(ProjectDir)..\third_party\cef90\Resources $(OutDir)Resources copy /Y $(ProjectDir)..\third_party\cef90\Release\libcef.dll $(OutDir) copy /Y $(ProjectDir)..\third_party\cef90\Release\icudtl.dat $(OutDir)这里$(ProjectDir)在 VS2010 里自带末尾反斜杠拼路径时不要重复加\。/E表示连空目录一起复制/I允许目标目录不存在时自动创建。如果少了locales程序能启动但界面文本是英文排查起来很莫名。3.2 链接器输入与预处理器接着把libcef.lib和libcef_dll_wrapper.lib加进“链接器 - 输入 - 附加依赖项”。libcef.lib在分发包的Release或Debug目录里而libcef_dll_wrapper.lib需要自己编译因为官方包只带 wrapper 源码。我习惯不单独建静态库工程直接把libcef_dll目录下的*.cc和*.cpp全部加入主工程省去工程间依赖的折腾。改完源文件列表还要检查预处理器定义WIN32;_WINDOWS;UNICODE;_UNICODE;NOMINMAX逻辑说明UNICODE和_UNICODE是 Windows 头文件和 C 运行库头文件的宏开关让TCHAR全部展开成宽字符NOMINMAX用来禁止 Windows.h 定义min和max宏CEF wrapper 源码里大量使用std::min少了这个宏会编译出一堆“minis not a member ofstd”的报错。这段宏列表在 Project Properties - C/C - Preprocessor 里维护VS2010 对宏展开顺序比较敏感建议直接黏贴而不是手动逐条输入。3.3 初始化 CEFCefSettings 与 multi_threaded_message_loop在所有浏览器创建之前先初始化 CEF。在 MFC 工程的InitInstance里写一个独立的InitCef函数#include include/cef_app.h void InitCef() { CefSettings settings; memset(settings, 0, sizeof(settings)); settings.size sizeof(CefSettings); settings.no_sandbox true; settings.multi_threaded_message_loop true; settings.log_severity LOGSEVERITY_WARNING; CefMainArgs args(GetModuleHandle(nullptr)); CefRefPtrGCefApp app(new GCefApp()); CefInitialize(args, settings, app); }逻辑说明settings.size必须赋值为sizeof(CefSettings)CEF 靠这个字段判断结构体版本不写会按旧结构解析轻则配置不生效重则越界崩溃。multi_threaded_message_loop true是嵌入 MFC 的关键开关设成 true 之后 CEF 内部自己处理消息循环MFC 原来的消息泵不用动。如果保持 false就必须在 MFC 主循环里周期性调用CefDoMessageLoopWork()否则浏览器窗口像死了一样不刷新很多白屏卡死就是这么来的。no_sandbox true是为了减少 VS2010 老工程和沙箱模块的兼容问题本地工具类程序可以开生产环境按安全规范自行评估。log_severity调到 WARNING 级别是为了让 CEF 把调试信息写进debug.log真出问题时有据可查。3.4 创建浏览器窗口并把它挂在 MFC 窗口上MFC 对话框在OnInitDialog完成窗口创建后就可以创建浏览器视图了BOOL CefMfcDemoDlg::OnInitDialog() { CDialogEx::OnInitDialog(); CRect rc; GetClientRect(rc); CefWindowInfo window_info; window_info.SetAsChild(m_hWnd, CefRect(0, 0, rc.Width(), rc.Height())); CefBrowserSettings browser_settings; CefRefPtrCefClient client(new CefBrowserClient); m_viewClient client; CefBrowserHost::CreateBrowser(window_info, client, Lfile:///C:/cef_mfc_demo/index.html, browser_settings, nullptr, nullptr); return TRUE; }逻辑说明SetAsChild告诉 CEF“把浏览器视图创建为指定 HWND 的子窗口”。之后 CEF 会自己创建一个Chrome_WidgetWin_*窗口并挂到m_hWnd下面所以这里传的矩形决定了浏览器初始显示区域。CefBrowserClient是CefClient的派生类哪怕暂时不处理任何回调也必须传一个合法实例它负责浏览器生命周期和后续事件分发。窗口创建之后还要处理 WM_SIZE 和 WM_SETFOCUS。浏览器子窗口不会自动跟随 MFC 父窗口缩放必须手动同步void CefMfcDemoDlg::OnSize(UINT nType, int cx, int cy) { CDialogEx::OnSize(nType, cx, cy); if (m_browser.get()) { HWND hwnd m_browser-GetHost()-GetWindowHandle(); if (hwnd) { SetWindowPos(hwnd, nullptr, 0, 0, cx, cy, SWP_NOZORDER); } m_browser-GetHost()-WasResized(); } }WasResized()通知 CEF 内部重新计算视口尺寸漏了它页面会一直按初始尺寸渲染拉伸窗口时出现大片空白。GetWindowHandle()拿到的是 CEF 内部真实窗口SetWindowPos把它的位置和大小设置成父窗口客户区。m_browser在创建回调完成前可能为空所以要先判空。4. JS 与 C 互调从 ExecuteJavaScript 到注册原生函数4.1 渲染进程里注册全局函数CefV8HandlerCEF 的 JS 引擎跑在渲染进程所以“JS 调用 C”必须先让你的CefApp同时实现CefRenderProcessHandler在OnContextCreated回调里把原生函数挂到全局window对象上。下面的 handler 用来处理页面里的showMessage调用#include include/cef_v8.h class GJsHandler : public CefV8Handler { public: bool Execute(const CefString name, CefRefPtrCefV8Value object, const CefV8ValueList arguments, CefRefPtrCefV8Value retval, CefString exception) override { if (name showMessage) { if (arguments.size() 0 || !arguments[0]-IsString()) { exception 参数必须是字符串; return true; } std::wstring msg arguments[0]-GetStringValue().ToString(); PostMessage(g_mainHwnd, WM_CEF_JS_CALL, 0, 0); retval CefV8Value::CreateString(LOK); return true; } return false; } private: IMPLEMENT_REFCOUNTING(GJsHandler); };逻辑说明Execute的返回值表示“这个函数我处理了”。如果返回 falseCEF 会继续找其他同名 handler。exception字符串会抛回给 JS 的异常机制在页面里可以用 try/catch 捕获。PostMessage只是通知 MFC 主窗口“JS 来消息了”实际传参数时常见做法是new std::wstring后通过 WPARAM/LPARAM 传指针接收方用delete释放避免全局变量和跨线程同步问题。接着让CefApp同时继承CefRenderProcessHandler并在OnContextCreated里完成注册class GCefApp : public CefApp, public CefBrowserProcessHandler, public CefRenderProcessHandler { public: CefRefPtrCefRenderProcessHandler GetRenderProcessHandler() override { return this; } CefRefPtrCefBrowserProcessHandler GetBrowserProcessHandler() override { return this; } void OnContextCreated(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefV8Context context) override { CefRefPtrCefV8Value window context-GetGlobal(); CefRefPtrCefV8Handler handler new GJsHandler(); CefRefPtrCefV8Value func CefV8Value::CreateFunction(showMessage, handler); window-SetValue(showMessage, func, V8_PROPERTY_ATTRIBUTE_NONE); } private: IMPLEMENT_REFCOUNTING(GCefApp); };window-SetValue把showMessage注册成全局函数JS 里直接写showMessage(hello)就能进入 C 的Execute。V8_PROPERTY_ATTRIBUTE_NONE表示该属性可读、可写、可删除如果不想让页面覆盖可以换成V8_PROPERTY_ATTRIBUTE_READONLY。4.2 JS 侧调用 C 并传参数页面端非常简单一个按钮加一个输出区域就够!DOCTYPE html html headmeta charsetutf-8/head body button idbtncall cpp/button div idout/div script document.getElementById(btn).onclick function () { if (typeof showMessage ! function) { document.getElementById(out).innerText showMessage 未注册; return; } var ret showMessage(hello from js, timestamp Date.now()); document.getElementById(out).innerText cpp返回: ret; }; /script /body /html这段 HTML 里typeof showMessage的判断非常有用。虽然OnContextCreated理论上在页面脚本执行前就已经完成注册但一旦遇到 iframe 或动态创建 context 的异常时序直接调用会抛TypeError。加了判断之后至少能把问题定位到“注册没生效”而不是“页面代码写错”。参数传递是同步的JS 调用showMessage时会阻塞到 C 的Execute返回。但 C 内部的PostMessage是异步的所以返回值OK只代表“函数已接收”不代表 MFC 窗口已经处理完业务。如果业务需要“处理完再反馈给页面”那就反过来用 C 主动执行 JS。4.3 C 主动执行 JSExecuteJavaScript浏览器进程里拿到CefFrame后可以主动执行任意 JS。MFC 按钮里我封装了这样一个方法void CefMfcDemoDlg::CallJsFromCpp(const CString script) { if (!m_browser.get() || !m_browser-GetMainFrame()) { return; } std::wstring wideScript script.GetString(); m_browser-GetMainFrame()-ExecuteJavaScript( CefString(wideScript), Lfile:///C:/cef_mfc_demo/index.html, 1); }逻辑说明ExecuteJavaScript有三个参数第一是 JS 代码第二是source_url用来给报错信息定位到当前页面第三是起始行号。这里的source_url最好和实际页面 URL 一致否则控制台里看到的堆栈来源不对。行号从 1 开始如果 JS 代码是多行拼接的行号不准确会影响调错体验。我特别强调不要用CT2A去转字符串。VC2010 的CT2A依赖当前代码页遇到中文页面会乱码更稳妥的做法是用 Unicode 宽字符直接构造CefString。反正工程已经改成了 Unicode 字符集这一步并不麻烦。4.4 生命周期别把 V8Handler 当成员变量缓存一个很容易踩的坑是把GJsHandler实例存到 MFC 类的成员变量里。OnContextCreated每次创建新 context 时都会调用页面刷新一次就创建一个新 context旧 context 和它上面的 JS 函数会被销毁。如果你在成员变量里保存的是第一次创建的 handler第二次刷新后 JS 函数没了但 C 成员还指向旧对象表现就是“第一次能调第二次没反应”。正确做法是像上面那样在OnContextCreated里临时创建 handler让CefRefPtr和 V8 context 绑定生命周期不要手动缓存。还要注意CefV8Value、CefV8Handler只能在渲染进程的 V8 上下文里创建绝不能在 MFC 主线程里直接搞一个CefV8Value::CreateString再塞给页面。CEF 的接口设计上不允许跨进程操作 V8 对象硬来要么崩溃要么接口直接断言。5. 避坑手册VS2010 CEF90.6.0 的 5 个踩坑现场5.1 编译错误 C2440字符串类型互转失败现象编译到CefString相关代码报error C2440: cannot convert from const char [19] to CefString或者从CefString转std::string时报错。原因工程字符集是多字节而 CEF 的CefString在 Windows 下默认是CefStringWide。VS2010 老 MFC 工程默认就是多字节字符集编译器看到宽窄字符混转会直接拒绝。解决项目属性 - 常规 - 字符集改成“使用 Unicode 字符集”。然后把代码里多余的CStringA、CT2A清理掉统一用CefString构造和赋值。改完字符集后其他模块如果出现C2664多半是TCHAR相关代码还没跟上要连带检查。5.2 链接错误 LNK1123转换到 COFF 期间失败现象VS2010 链接时报fatal error LNK1123: failure during conversion to COFF: file invalid or corrupt而且往往出现在安装了多个 Visual Studio 版本的机器上。原因系统 PATH 里高版本 VS 的cvtres.exe被 VS2010 链接器调用了工具版本不兼容导致中间文件转不了 COFF。这不是代码问题是工具链冲突。解决先到项目属性 - 链接器 - 常规把“启用增量链接”改成“否”这一招能绕开一部分冲突。如果还报错把 VS2010 自己的cvtres.exe路径C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin放到 PATH 最前面。最省事的是安装 VS2010 SP1SP1 之后混装冲突明显减少。不要去删高版本的cvtres.exe那样会影响其他工具链。5.3 运行时提示 libcef.dll 找不到现象编译通过F5 启动直接弹窗The program cant start because libcef.dll is missing。原因libcef.dll没复制到 exe 同目录。VS2010 调试器的工作目录默认是$(ProjectDir)而不是$(OutDir)就算 DLL 拷到了Debug目录也可能因为工作目录不对加载失败。解决生成后事件里把 DLL 和 Resources 统一拷到$(OutDir)然后在“调试 - 工作目录”里显式填$(OutDir)。同时确认 CEF 包的位数和工程目标平台一致。拿dumpbin /headers libcef.dll | findstr machine看输出是 x86 还是 x6432 位 DLL 永远配不了 x64 的 exe这种错在 VS2010 下报得非常隐晦经常是子进程起不来而不是 DLL 缺失。5.4 白屏但子进程在消息循环与父窗口句柄现象程序能启动主窗口出现但浏览器区域白屏任务管理器里能看到不少CefMfcDemo.exe --typerenderer之类的子进程。原因最常见是multi_threaded_message_loop为 false而 MFC 消息循环没有调用CefDoMessageLoopWork()CEF 的消息泵一次都没跑。另一个常见原因是SetAsChild传的父窗口句柄不对或者 rect 传了 0浏览器视图创建在不可见区域。解决先把settings.multi_threaded_message_loop true。这一条能解决绝大多数“CEF 自己的循环没转起来”的问题。如果设了还是白屏在CefLifeSpanHandler::OnAfterCreated里断点看browser-GetHost()-GetWindowHandle()是否非空再用IsWindowVisible确认窗口真的可见。很多时候窗口句柄拿到了但 rect 被传成(0,0,0,0)导致浏览器子窗口只有 1 像素甚至不可见。5.5 CefInitialize 闪退或返回 false现象F5 后窗口还没弹出来程序直接崩在CefInitialize或者函数返回 false后面CreateBrowser没反应。原因三个高频原因一是 64 位 exe 配了 32 位 CEF 包二是settings.size没赋值被当成旧版结构三是 Debug 工程误用了 Release 目录下的libcef_dll_wrapper.lib。VS2010 用/MDd调试运行库却链接 Release 版 wrapper初始化时内部对象模型不一致直接断言崩溃。解决核对包位数代码里保留settings.size sizeof(CefSettings);Debug 工程用分发包Debug目录下的 lib 和 dllRelease 工程用Release目录不要混。运行库统一/MDd或/MD不要用/MT系列除非你能确认libcef.dll也是静态运行库编译的。6. 收尾实战最小自检页面验证 C/JS 双向往来6.1 自检页面结构等全部工程跑起来我建议先别写业务代码放一个最小自检页面把两条链路验证通。页面里保留两个方向JS 调 C 用nativeFuncC 调 JS 用window.cpp。页面代码!DOCTYPE html html langzh-CN headmeta charsetutf-8/head body h3CEF 自检/h3 div idstatuswaiting/div button onclickcallNative()JS -gt; C/button script window.cpp function (msg) { document.getElementById(status).textContent C -gt; JS: msg; }; function callNative() { if (typeof nativeFunc ! function) { document.getElementById(status).textContent nativeFunc 未注册; return; } var ret nativeFunc(ping- Date.now()); document.getElementById(status).textContent JS -gt; C 返回: ret; } /script /body /htmlC 侧的GJsHandler里把showMessage改成nativeFunc返回字符串pong。MFC 按钮里执行一行std::wstring script Lwindow.cpp(from-cpp- Date.now());; m_browser-GetMainFrame()-ExecuteJavaScript(CefString(script), Lfile:///C:/cef_mfc_demo/index.html, 1);6.2 验证顺序与常见误判启动程序后先看 CEF 区域是否正常渲染自检页面。如果白屏直接打开debug.log看最后几行90.6.0 会把关键错误写在这里。然后点页面上的按钮如果 MFC 日志里收到了pong说明 JS 调 C 通如果typeof nativeFunc是undefined多半是GCefApp没有正确继承CefRenderProcessHandler或者GetRenderProcessHandler返回了空。再点 MFC 按钮页面状态文字变成C - JS: from-cpp-xxx说明 C 执行 JS 通了。一个常见误判是页面里写了nativeFunc(ping)但忘记在Execute里检查name是否等于nativeFunc。CreateFunction传入的名字和Execute拿到的name必须一致拼写错一个字符就会永远命中不了。另一个误判是在OnContextCreated里注册多个 frame 时重复SetValue覆盖了第一次注册的函数排查时总会觉得“代码没问题”其实是被同名属性覆盖了。从那以后我每次在新机器或新版本上搭 CEF都会先跑一遍这个自检。不是保守而是这一步至少能把工程配置、渲染进程注册、消息路由三层问题一次筛完后面再接业务代码时才不会在定位阶段反复折腾。希望帮到你。本文还有配套的精品资源点击获取
返回列表