简介:这份资源面向需要在VS2010环境下做桌面端Web集成的C++开发者,重点解决将CEF3 90.6.0嵌入MFC窗口并打通JavaScript与C++双向调用的问题。包内以头文件、静态库与动态库、预编译头及工程配置为主,包含269个h、14个lib、28个dll、9个cpp以及sln、vcxproj等工程文件,另有pak、bin、dat等CEF运行时资源,压缩包约457.22MB,共629个文件,可直接对照搭建可编译的MFC+CEF3工程骨架。已有1556人学习下载,适合想快速跑通浏览器内核嵌入、消息循环同步与JS调用C++链路的开发者参考,能省去大量环境配置与依赖排查时间。
1. 从一次编译失败说起:vs2010 配 cef3 90.6.0 到底难在哪
前阵子帮一个做工业上位机的朋友收拾一个老项目,环境是 vs2010,要往 MFC 窗口里嵌 cef3,指定版本 90.6.0。他折腾了整整两天,卡在链接阶段,报了一屏 LNK2001 和 LNK2019,最后把工程发我,我打开一看,运行库设置是 MT,而 cef3 的 lib 是 MD 编译的,这种组合在 vs2010 下几乎必炸。这不是个例,vs2010 加 cef3 90.6.0 这套组合,难点从来不在写代码,而在环境对齐:平台工具集、字符集、运行库、lib 版本、dll 放置路径,任何一项对不上,编译能过,运行就崩,或者干脆链接都过不去。
这套环境现在还有谁在用?做工业控制、医疗设备、老金融终端的团队,因为整条产线锁死在 vs2010 的 v100 工具集上,升级成本太高,只能继续用。cef3 90.6.0 是 Chromium 90 内核,支持到 2021 年前后的 Web 标准,对很多内嵌网页场景够用。如果你也是被老项目绑住、必须用 vs2010 把 cef3 塞进 MFC 的人,这篇就是我踩完坑之后的完整记录,从环境清单到编译参数,再到 JS 调用怎么打通,照着走能少熬两个晚上。
2. 环境清单与版本对齐:vs2010 下 cef3 90.6.0 的依赖矩阵
2.1 为什么版本必须锁死在 90.6.0
cef3 的版本号跟 Chromium 主版本绑定,90.6.0 对应 Chromium 90.0.4430 这条线。选这个版本通常有两个现实原因:一是项目里已有的前端页面用了某些 Chromium 90 才稳定的 API,换低版本渲染错位,换高版本又可能因为 CEF 接口变动导致封装层编译不过;二是 90.6.0 是 CEF 官方为 Windows 提供的最后一个对 vs2010 相对友好的二进制分发包之一,再往后的版本,官方预编译包基本都用 vs2019 工具集出了,vs2010 直接链接会碰到 CRT 符号不兼容。
我一般会先把版本对应关系写死在文档里,避免团队里有人手贱去下最新版:
| 组件 | 版本 | 说明 |
|---|---|---|
| Visual Studio | 2010 SP1 | 必须打 SP1,否则 v100 工具集有已知 bug |
| CEF3 | 90.6.0 | Chromium 90 内核,Windows 32/64 位分发包 |
| CMake | 3.20 以下 | 生成 vs2010 工程用,太高版本不认 v100 |
| Windows SDK | 7.1 | vs2010 配套,别用 8.0 以上 |
| 运行库 | MD / MDd | 必须与 CEF 预编译 lib 一致 |
这张表看着简单,但每一条都是血泪。尤其是 CMake 版本,我试过用 3.25 去生成 vs2010 工程,直接报 generator 不支持,降到 3.19 才正常。
2.2 下载 cef3 90.6.0 分发包时该选哪个文件
CEF 官方给 Windows 的二进制包分三种:Standard Distribution、Minimal Distribution、Client Distribution。做 MFC 嵌入,选 Standard Distribution,里面包含 libcef_dll_wrapper 的源码、头文件、release 和 debug 两套 lib、以及所有运行时 dll。下载下来解压,目录结构大致是:
cef_binary_90.6.0_windows32/ ├── cmake/ ├── include/ ├── libcef_dll/ ├── Release/ │ ├── libcef.lib │ ├── libcef.dll │ └── ... ├── Debug/ ├── Resources/ └── CMakeLists.txt注意 32 位和 64 位是两个独立包,别下错。MFC 工程如果是 Win32 配置,就必须用 windows32 包,混用会在链接时报模块机器类型冲突。
2.3 用 CMake 生成 vs2010 工程并编译 libcef_dll_wrapper
CEF 的 lib 不能直接拿来用,必须先编译 libcef_dll_wrapper,它是 CEF C++ 接口的封装层。步骤:
# 进入 cef 解压目录,创建 build 目录 mkdir build cd build # 指定 vs2010 生成器,注意是 "Visual Studio 10" 不是 2010 cmake -G "Visual Studio 10" -DCMAKE_BUILD_TYPE=Release .. # 生成后打开 sln,或者直接用 msbuild 编译 msbuild libcef_dll_wrapper.vcxproj /p:Configuration=Release这里的关键参数是-G "Visual Studio 10",vs2010 在 CMake 里的生成器名字就叫 Visual Studio 10,写 2010 会报错。编译完成后,在build/libcef_dll_wrapper/Release/下会得到libcef_dll_wrapper.lib,这个文件后面要跟 libcef.lib 一起加到 MFC 工程的链接器输入里。
提示:如果 CMake 报找不到编译器,检查 vs2010 的 vcvarsall.bat 有没有先执行,或者直接在 VS2010 命令提示符里跑 cmake。
3. 把 cef3 嵌进 MFC 窗口:工程配置与初始化代码
3.1 MFC 工程的属性页该怎么改
新建一个基于对话框的 MFC 工程,或者用已有的。改属性页之前,先确认工程是 Unicode 字符集,CEF 90.6.0 的接口默认按 UTF-16 处理,多字节字符集下字符串转换会多一层麻烦。配置项按下面来:
- 常规 → 字符集:使用 Unicode 字符集
- C/C++ → 常规 → 附加包含目录:加上
cef_binary_90.6.0_windows32的 include 目录 - C/C++ → 代码生成 → 运行库:Release 用
/MD,Debug 用/MDd - 链接器 → 常规 → 附加库目录:加上
Release和build/libcef_dll_wrapper/Release - 链接器 → 输入 → 附加依赖项:
libcef.lib、libcef_dll_wrapper.lib、user32.lib、gdi32.lib、ole32.lib、shell32.lib
运行库这一项是翻车重灾区。CEF 预编译的 libcef.lib 是 MD 编译的,如果你的 MFC 工程设成 MT,链接时 CRT 符号对不上,报一堆 LNK2005 和 LNK2019。我一般会在项目属性里把运行库显式锁死,并且写进团队规范,谁改谁请奶茶。
3.2 初始化 CEF:CefInitialize 的参数与时机
CEF 必须在 MFC 的InitInstance里初始化,而且要在创建主窗口之前。核心代码:
// 在 App 类的 InitInstance 开头加入 CefMainArgs main_args(GetModuleHandle(NULL)); // 设置 CEF 的 dll 和资源路径,路径不对会直接闪退 CefString(&settings.locale) = "zh-CN"; CefString(&settings.log_file) = "cef_debug.log"; settings.multi_threaded_message_loop = false; // MFC 单线程消息循环 settings.no_sandbox = true; // 老项目一般关沙箱,避免权限问题 // 命令行参数,禁用 GPU 可以规避部分老显卡崩溃 CefRefPtr<CefCommandLine> cmd_line = CefCommandLine::CreateCommandLine(); cmd_line->InitFromString(::GetCommandLineW()); cmd_line->AppendSwitch("disable-gpu"); cmd_line->AppendSwitch("disable-gpu-compositing"); CefInitialize(main_args, settings, nullptr, nullptr);multi_threaded_message_loop设 false 是因为 MFC 的消息循环在主线程,CEF 需要跟它共用。设 true 的话得自己起线程跑 CefRunMessageLoop,MFC 对话框场景下没必要。no_sandbox在 vs2010 老项目里建议开,沙箱对进程权限有要求,工业现场机器往往权限受限,开了反而起不来。
初始化之后,在ExitInstance里调CefShutdown(),顺序不能反,否则退出时可能卡死。
3.3 创建浏览器窗口并嵌入 MFC 控件
CEF 的浏览器窗口可以绑定到一个 MFC 控件的句柄上。常见做法是在对话框上放一个 Static 控件,拿它的 HWND 传给 CEF:
// 假设对话框上有个 IDC_BROWSER_HOST 的 Static 控件 CWnd* pHost = GetDlgItem(IDC_BROWSER_HOST); CRect rect; pHost->GetClientRect(&rect); CefWindowInfo window_info; window_info.SetAsChild(pHost->GetSafeHwnd(), CefRect(0, 0, rect.Width(), rect.Height())); CefBrowserSettings browser_settings; CefRefPtr<CefClient> client = new MyCefClient(); // 自己实现的回调类 CefBrowserHost::CreateBrowser(window_info, client, "about:blank", browser_settings, nullptr, nullptr);SetAsChild把浏览器变成子窗口,跟随 MFC 控件的位置和大小。窗口 resize 的时候,要重写对话框的OnSize,调CefBrowserHost::GetBrowser()->GetHost()->WasResized(),否则网页区域不会跟着变。
4. JS 调用打通:CefV8 与 MFC 的双向通信
4.1 注册 JS 可调用的 C++ 函数
CEF 里 JS 调 C++ 靠CefV8Handler。实现一个 handler,在Execute里根据函数名分发:
class MyV8Handler : public CefV8Handler { public: virtual bool Execute(const CefString& name, CefRefPtr<CefV8Value> object, const CefV8ValueList& arguments, CefRefPtr<CefV8Value>& retval, CefString& exception) override { if (name == "callMfcFunc") { // 从 JS 传来的参数 if (arguments.size() > 0 && arguments[0]->IsString()) { CefString str = arguments[0]->GetStringValue(); // 转成 MFC 侧的处理,比如发消息给主窗口 ::PostMessage(g_hMainWnd, WM_JS_CALL, 0, (LPARAM)new CString(str.c_str())); } retval = CefV8Value::CreateBool(true); return true; } return false; } IMPLEMENT_REFCOUNTING(MyV8Handler); };然后在OnContextCreated回调里把这个函数挂到 window 对象上:
CefRefPtr<CefV8Value> obj = context->GetGlobal(); CefRefPtr<CefV8Handler> handler = new MyV8Handler(); CefRefPtr<CefV8Value> func = CefV8Value::CreateFunction("callMfcFunc", handler); obj->SetValue("callMfcFunc", func, V8_PROPERTY_ATTRIBUTE_NONE);这样网页里window.callMfcFunc("hello")就能触发 C++ 侧逻辑。注意PostMessage里传的new CString要在消息处理函数里 delete,否则内存泄漏,这是老项目里最常见的慢性病。
4.2 C++ 主动调 JS 的两种方式
反过来,MFC 要调 JS,用CefBrowserHost::GetBrowser()->GetMainFrame()->ExecuteJavaScript():
CefRefPtr<CefBrowser> browser = ...; // 保存的 browser 指针 CefString jsCode = "updateStatus('device ready')"; browser->GetMainFrame()->ExecuteJavaScript(jsCode, "", 0);如果 JS 函数有返回值,得用CefV8Value配合CefV8Context,但 MFC 场景下一般不需要同步返回,异步通知够用。另一种方式是通过CefProcessMessage发消息给渲染进程,适合跨进程通信,但配置更复杂,vs2010 下调试成本高,我一般优先用 ExecuteJavaScript。
注意:ExecuteJavaScript 必须在 CEF 的 UI 线程调用,MFC 主线程就是 UI 线程,所以直接在按钮响应里调没问题。如果在工作线程里调,要先 PostMessage 回主线程。
4.3 消息路由:JS 调用怎么落到 MFC 的具体函数
上面用PostMessage把 JS 调用转成 Windows 消息,MFC 侧在对话框类里加消息映射:
// 头文件里定义消息 #define WM_JS_CALL (WM_USER + 100) // 消息处理函数声明 afx_msg LRESULT OnJsCall(WPARAM wParam, LPARAM lParam); // 消息映射 BEGIN_MESSAGE_MAP(CMyDlg, CDialogEx) ON_MESSAGE(WM_JS_CALL, &CMyDlg::OnJsCall) END_MESSAGE_MAP() // 实现 LRESULT CMyDlg::OnJsCall(WPARAM wParam, LPARAM lParam) { CString* pStr = (CString*)lParam; if (pStr) { // 在这里根据字符串内容分发到具体业务函数 if (*pStr == "start") { StartDevice(); } else if (*pStr == "stop") { StopDevice(); } delete pStr; // 必须释放 } return 0; }这套路由的好处是 JS 侧不用关心 MFC 内部结构,只发字符串指令,C++ 侧集中分发。坏处是字符串匹配容易写错,建议在 JS 侧和 C++ 侧共用一份常量定义,别硬编码。
5. 避坑与排查:vs2010 配 cef3 最常见的五个翻车点
5.1 链接报 LNK2001 / LNK2019 找不到 CEF 符号
现象:编译通过,链接时报大量unresolved external symbol,符号名里带Cef或libcef_dll_wrapper。
原因:九成是运行库不匹配。CEF 预编译 lib 用 MD,工程设了 MT,或者 Debug 工程链了 Release 的 lib。
解决:检查工程属性里 C/C++ → 代码生成 → 运行库,Release 必须是/MD,Debug 必须是/MDd。同时确认链接器输入的 lib 路径指向对应配置的目录,别把 Release 和 Debug 混用。
5.2 程序启动闪退,无任何报错
现象:双击 exe 或者调试启动,进程一闪就没了,连日志都没生成。
原因:最常见的是 libcef.dll 没放到 exe 同目录,或者 CEF 的资源文件(icudtl.dat、cef.pak 等)缺失。CEF 初始化时找不到这些文件会直接崩,而且不一定写日志。
解决:把 CEF 分发包里 Release 目录下的所有 dll、Resources 目录下的所有文件,全部拷到 exe 输出目录。我一般会在项目里加一个生成后事件,自动 xcopy,省得每次手动拷。
5.3 网页显示空白,但进程正常
现象:窗口出来了,CEF 也初始化了,但网页区域一片白,或者只有背景色。
原因:SetAsChild传的父窗口句柄不对,或者浏览器窗口大小是 0。也有可能是 disable-gpu 没加,老显卡驱动渲染失败。
解决:确认 Static 控件已经创建完成再调 CreateBrowser,GetClientRect拿到的宽高不能为 0。加上disable-gpu和disable-gpu-compositing开关,工业现场机器显卡驱动往往很老。
5.4 JS 调用 C++ 没反应
现象:网页里调window.callMfcFunc报 undefined,或者调了但 C++ 侧没进消息处理。
原因:OnContextCreated没触发,或者 V8Handler 挂载的 context 不对。CEF 每个 frame 有自己的 context,挂到主 frame 的 global 上才有效。
解决:在OnContextCreated里打印日志确认回调执行了,检查context->GetGlobal()是否为空。另外确认CefV8Value::CreateFunction的 handler 对象没有被提前释放,用CefRefPtr管理。
5.5 退出时卡死或崩溃
现象:关闭对话框后进程不退出,或者退出时弹崩溃报告。
原因:CefShutdown调用时机不对,或者 browser 对象没释放就 shutdown 了。
解决:在ExitInstance里先确保所有 browser 已经关闭,再调CefShutdown。如果用了多线程消息循环,要先CefQuitMessageLoop。MFC 单线程模式下,顺序是:关闭浏览器 → 处理完剩余消息 → CefShutdown。
6. 装完怎么验证:三步确认 cef3 90.6.0 在 MFC 里真的跑通了
环境装没装好,不看编译过不过,看运行时的三个硬指标。我一般按下面顺序验,任何一步不过,前面的配置就得回头查。
第一步,看 CEF 版本号。在OnContextCreated里执行一段 JS,把navigator.userAgent打到日志里:
CefString jsCode = "console.log(navigator.userAgent)"; browser->GetMainFrame()->ExecuteJavaScript(jsCode, "", 0);日志里应该出现Chrome/90.0.4430字样。如果版本号不对,说明链接的 lib 和 dll 不是同一套,或者 dll 被系统里其他版本的 CEF 覆盖了。这一步能排掉大部分“看起来能跑但内核不对”的玄学问题。
第二步,验证 JS 到 C++ 的调用链。在网页里放一个按钮,点击调window.callMfcFunc("test"),C++ 侧消息处理函数里弹个 MessageBox 或者写日志。这一步过了,说明 V8Handler 挂载正确、消息路由通了。如果没反应,回到 4.1 检查OnContextCreated有没有执行,以及函数名大小写是否一致。
第三步,验证 C++ 到 JS 的回调。在 MFC 对话框上加个按钮,点击后调ExecuteJavaScript("updateStatus('ok')"),网页里对应的 JS 函数把状态显示到页面上。这一步过了,双向通信就完整了。
三步都过之后,再做一次压力验证:连续打开关闭浏览器窗口 20 次,看内存有没有持续上涨,进程能不能正常退出。CEF 的引用计数如果哪里漏了Release,这一步会暴露出来。
下面这张表是我自己用的验证清单,每次配新环境都走一遍:
| 验证项 | 操作 | 预期结果 |
|---|---|---|
| 内核版本 | 打印 userAgent | 含 Chrome/90.0.4430 |
| JS→C++ | 网页按钮触发 | C++ 侧收到消息 |
| C++→JS | MFC 按钮触发 | 网页状态更新 |
| 资源加载 | 打开本地 HTML | 样式和图片正常 |
| 退出 | 关闭主窗口 | 进程干净退出 |
从那以后我每次配 CEF 环境,都强制先跑这三步再写业务代码,绝不等到功能堆上去才发现底层没通。希望帮到你。
本文还有配套的精品资源,点击获取