资讯动态

MFC嵌入CEF实战:从配置到进程管理的完整指南

发布时间:2026/9/9 13:29:46 来源:尧图企业网站定制
简介一套面向MFC桌面开发者的CEF集成示例包演示如何在传统MFC框架中嵌入Chromium内核并以自定义CWebClient类封装浏览器实例、页面加载、JavaScript与C消息桥接等常用能力适合有MFC基础的C开发者快速上手。压缩包仅6KB共4个文件由2个头文件与2个源文件组成分别对应浏览器客户端封装和下载处理器模块代码精简便于快速移植与二次改造。内容同时涵盖下载管理器与DOM事件监听机制通过CWebClient、CDownloadHandler等类封装可减少重复配置直接获得浏览器实例管理、下载进度回调与前端事件响应能力。其中还给出了初始化CEF、创建浏览器窗口、注册下载回调的完整思路并展示如何通过JavaScript与C桥接响应网页事件。目前已有360人学习适合正在寻找CEFMFC落地参考、希望以少量代码实现现代Web渲染与文件下载控制的开发者。 说起CEF在MFC里的使用我想先讲一段自己的经历。去年接手一个维护了快十年的MFC桌面项目客户提了一堆Web化的需求什么数据大屏、可视化报表、在线审批流程用原来的界面框架一个个做过去工作量能把人淹没。第一反应是挂个IE控件进去结果一测前端同事用Vue3写的页面在IE内核里直接白屏连路由都跑不起来。被逼到墙角之后把CEFChromium Embedded Framework拉出来研究了一番最后硬是把这套组合用到了生产环境。这个组合的价值很直接MFC负责稳定的业务骨架、底层通信和系统级能力CEF提供现代Web渲染、前端生态和交互体验。对那种老系统不能推倒重来但UI能力必须升级的场景这是目前最务实的出路。这篇文章写给准备在MFC项目里嵌入CEF的C开发我把从配置工程到进程回收的完整链路加上实际踩过的一系列坑一次性说清楚。1. 为什么老MFC项目非要请CEF这个外来户1.1 MFC程序最大的短板不在界面框架MFC这套东西论做传统工具类界面CListCtrl、CTreeCtrl、CTabCtrl配上自绘按钮和分割窗口二十年前是生产力放到今天也不算过时。真正的短板是面对高交互、数据可视化、富媒体内容这些需求时开发成本成倍上升。前端生态里一个开源的ECharts图表库能在十分钟内做出一套炫酷的数据面板换成MFC从零绘制没有几周时间下不来而且效果还不一定比得上。CEF把Chromium的渲染引擎、JavaScript引擎、网络栈、GPU加速这些能力打包成一套可以嵌入桌面应用的SDK。简单理解就是你的MFC程序里住进了一个和Chrome同内核的浏览器房间。业务页面全部用Web技术开发MFC只负责框架、系统能力、本地资源调度这些Web做不了或做起来很别扭的事情。1.2 三条路线对比IE控件、WebView2、CEF提到嵌入Web不能只说CEF。我实际对比过另外两条路各有各的适用场景。方案内核部署依赖典型问题IE WebBrowser控件老Trident系统自带不支持现代前端框架微软已放弃维护Edge WebView2Chromium需安装WebView2 Runtime依赖系统环境Win7等老系统部署困难CEFChromium全部DLL和资源随程序打包集成复杂度高包体积大WebView2其实是微软官方推荐的路线COM接口封装得也不错。问题是它依赖一个独立的Runtime在客户内网环境、离线部署、老系统上往往要先装一个几百MB的运行时光这一点就被我们客户否决了。CEF胜在全家桶随程序走目录拷到哪都能跑可控性最强。1.3 什么场景下值得引入CEF不是所有MFC项目都需要CEF。如果只是弹几个简单的网页链接ShellExecute调系统浏览器就够了。但如果你遇到这几种情况CEF基本是绕不开的选项业务页面需要用高版本CSS3/ES6或者Vue、React等现代前端框架需要做地图可视化、WebGL三维展示、音视频通话这类重交互内容页面需要和C桌面测频繁双向调用且数据量不小部署环境不可控程序必须做到拷贝即运行我们做的是一个工业设备监控软件需要一个能实时刷新设备状态、支持自定义图表的仪表盘页面。用MFC硬画第一版花了三周效果还差换成CEF加载网页版仪表盘前端同学两天就把原型做出来了。这就是引入CEF最直接的收益。2. 准备阶段最容易翻车的三个细节2.1 版本位数和SDK目录不能想当然CEF官方发布页提供的是编译好的二进制包按分支分为Stable、Beta、Canary按位数分为32位和64位。第一步就是在官网下载Stable分支、与你MFC工程匹配位数的包。我们用的VS2013配合32位工程就老老实实选了win32版本。一个关键点是你下载的CEF包是Release构建默认用的运行库可能是/MD而MFC项目如果配成了/MT两者链接时经常出符号冲突或者运行时崩溃。所以工程属性里运行库这一项统一用多线程DLL (/MD)Debug则用多线程调试DLL (/MDd)避免混用。SDK目录解压后长这样cef_binary_xxx/ ├─ include/ C头文件 ├─ lib/ libcef.lib 等导入库 ├─ Resources/ cef.pak、devtools_resources.pak 等 ├─ Release/ 运行时DLL和可执行文件 └─ locales/ 国际化语言包工程配置上把include目录加进附加包含目录把lib目录加进附加库目录然后在附加依赖项里补上libcef.lib。还需要在预处理定义里加上NOMINMAX防止Windows头文件里的min/max宏和CEF头文件里的std::min/max冲突。这个坑很隐蔽不加的话编译报错能让你折腾半小时。2.2 工程属性字符集是重灾区MFC项目默认可能是多字节字符集而CEF的API大量使用CefString底层是UTF-16的字符串。最省心的方案是把项目字符集统一改成使用Unicode字符集后续CString到CefString的转换会顺滑很多。我最初就是没改字符集结果传URL进CEF时中文参数全部乱码页面加载直接404。排查到最后发现是字符集不一致CString转出去的窄字符串CEF内部按宽字符解析完全对不上。用Unicode字符集之后代码里CString和std::wstring互转再给CEF整条链路就干净了。2.3 输出目录里少了文件界面就是一片白这是新手最容易遇到的问题没有之一。CEF运行时依赖一批动态库和资源文件漏掉任何一个表现都是启动正常但界面白屏或者渲染不出来。我整理了一个最小清单你的输出目录/ ├─ YourApp.exe ├─ libcef.dll ├─ icudtl.dat ├─ snapshot_blob.bin ├─ v8_context_snapshot.bin ├─ cef.pak ├─ cef_100_percent.pak ├─ cef_200_percent.pak ├─ devtools_resources.pak ├─ locales/ 按需保留比如zh-CN.pak └─ chrome_elf.dll这些文件在CEF包的Release和Resources目录里。建议直接在工程的后生成事件里加一条xcopy命令每次编译自动把整个目录同步过去省得手工拷贝遗漏。我后来受够了手拷文件写了一个bat脚本放到后生成事件里再没出过白屏问题。3. 初始化和消息循环CEF与MFC的第一次握手3.1 先搞清进程模型再看初始化代码CEF是多进程架构主进程叫browser进程负责窗口创建、网络请求、UI管理页面渲染在renderer子进程里跑还有GPU进程、utility进程等。你的MFC主程序既是browser进程又要能变身成子进程。所以在MFC启动的早期必须先做一次进程类型的判断。// CMainApp::InitInstance() 里最先执行 CefMainArgs main_args(m_hInstance); CefRefPtrCefApp app(new CMyCefApp); // 如果当前进程是CEF的子进程CefExecuteProcess会接管并在这里运行 int exit_code CefExecuteProcess(main_args, app.get(), nullptr); if (exit_code 0) { // 子进程逻辑跑完直接退出不走MFC消息循环 return FALSE; } // 走到这里说明是browser进程继续正常初始化这里的关键逻辑是程序启动时自己也不知道自己是主进程还是子进程由CefExecuteProcess根据启动参数判断。如果是子进程它会在内部处理消息循环返回后直接退出如果是主进程返回值为-1继续执行后续的MFC初始化。我见过有人把CefExecuteProcess的返回值判断写反了结果主程序什么都不显示就退出或者渲染进程反复重启。3.2 CefSettings里几个保命参数初始化browser进程时CefSettings的配置决定了很多行为。以下是我实践下来最关键的几个CefSettings settings; settings.multi_threaded_message_loop true; settings.no_sandbox true; settings.log_severity LOGSEVERITY_ERROR; CefString(settings.browser_subprocess_path) CefString(_T(LYourApp.exe)); // 子进程指向自身 CefString(settings.cache_path) CefString(_T(L./cef_cache));multi_threaded_message_loop设为true是给MFC项目省事的关键。默认情况下CEF需要宿主程序不断调用CefDoMessageLoopWork来喂消息循环否则页面交互会卡顿。但MFC自己的消息循环和CEF的消息循环协调起来很别扭尤其是模态对话框、菜单操作这些场景消息循环嵌套会引发各种诡异问题。设为true之后CEF自己开消息循环线程MFC这边完全不用操心两边各跑各的接口上也不冲突。no_sandbox在开发和内网部署阶段建议直接打开。CEF沙箱机制在某些精简系统或权限受限环境会导致子进程启动失败表现为页面白屏、进程反复闪退。关掉沙箱会损失一点安全性但对内网工具类软件换来的稳定运行更实际。cache_path建议设成程序目录下的子目录让Cookie、localStorage有持久化落盘的位置。我们曾经不设这个字段导致每次重启程序登录态全部丢失客户反馈又要重新登录太烦。3.3 消息循环接入推荐multi_threaded_message_loop前面提到了如果用外部消息循环模式就得在MFC的OnIdle或定时器里调用CefDoMessageLoopWork。我再补充一点为什么我不推荐这么干。OnIdle是MFC空闲时刻的处理时机但频率不稳定定时器虽然稳定又会有输入延迟——鼠标点击、键盘事件的响应要等下一次CefDoMessageLoopWork才能送达快的时候还好慢的时候明显感觉交互黏手。而且模态框循环和窗口拖动过程中MFC主循环被阻塞CEF这边也跟着卡死。所以MFC项目我强烈建议用multi_threaded_message_loop true。省下的调试时间拿去给前端同事点一杯奶茶不香吗3.4 CefRefPtr和MFC的兼容问题CEF的引用计数对象统一用CefRefPtr管理它的Release语义和MFC的COM指针不一样不能混用。一个常见场景是CEF回调里拿到CefRefPtr 在MFC窗口销毁时还要访问它。这时要小心别把CefRefPtr存成全局裸指针。// 全局或成员变量 CefRefPtrCefBrowser g_browser; // 回调里赋值 g_browser browser; // 正确引用计数1CefRefPtr支持赋值和拷贝直接用就对了不要试图把CefBrowser*析出来单独存。我早期图省事存了裸指针结果OnBeforeClose里引用计数归零对象销毁再访问就直接崩了。后来一律用CefRefPtr做成员变量在OnBeforeClose中置空世界清净了。4. 把Chromium窗口完整塞进MFC对话框4.1 用Picture控件当浏览器容器把CEF窗口嵌入MFC对话框标准做法是先弄一个容器窗口再让CEF在里面创建子窗口。我用的是对话框资源里的Picture控件改一下属性就能当容器用。// 对话框初始化完成后 CWnd* pContainer GetDlgItem(IDC_BROWSER_CONTAINER); CRect rc; pContainer-GetClientRect(rc); HWND hWndContainer pContainer-GetSafeHwnd(); CefWindowInfo info; info.SetAsChild(hWndContainer, rc); CefBrowserSettings browser_settings; CefString url CefString(_T(https://localhost/your_page.html)); CefBrowserHost::CreateBrowser(info, handler, url, browser_settings, nullptr, nullptr);SetAsChild的作用是把CEF窗口创建成指定句柄的子窗口。注意GetClientRect拿到的坐标是容器客户区坐标CEF窗口会正好铺满整个容器。还有一个建议把Picture控件的可见属性保持勾选但把通知属性去掉避免容器窗口抢走CEF子窗口的焦点消息。4.2 自适应窗口大小和防闪烁MFC对话框在用户拖动拉伸时CEF窗口不会自动跟着变。需要处理WM_SIZE消息通知CEF刷新视图区域。void CMyDialog::OnSize(UINT nType, int cx, int cy) { CDialogEx::OnSize(nType, cx, cy); if (g_browser.get()) { HWND hWndContainer GetDlgItem(IDC_BROWSER_CONTAINER)-GetSafeHwnd(); ::SetWindowPos(g_browser-GetHost()-GetWindowHandle(), nullptr, 0, 0, cx, cy, SWP_NOZORDER); g_browser-GetHost()-WasResized(); } }这个函数的逻辑是对话框尺寸变化时把CEF子窗口一起拉伸到容器的新尺寸再调用WasResized通知CEF重新布局页面。如果你想在对话框上做一个自定义按钮或工具栏把按钮放在容器区域之外别让CEF窗口覆盖上去。实际项目里我还遇到过mfc静态文本覆盖的问题——弹窗提示一会儿就显示不出来了排查后确认是CEF窗口把Static控件盖住了。解决方法是调整Z序或者让CEF容器和Static控件不要有重叠区域。另外不要忘记处理WM_ERASEBKGND对话框重绘时背景擦除会引发CEF窗口闪烁。简单粗暴的做法是直接返回TRUE禁止擦除背景让CEF自己刷新内容。BOOL CMyDialog::OnEraseBkgnd(CDC* pDC) { // 防止背景擦除导致闪屏 return TRUE; }4.3 多页面与Z序问题同一个MFC框架里开多个CEF窗口也是一个很常见的需求。比如左边树形菜单选设备右边多个标签页展示不同页面。维护多个CefRefPtr 每个映射到一个容器句柄逻辑并不复杂。真正要注意的是窗口Z序。CEF窗口是Top-Level风格的重叠窗口即使它是子窗口也可能在切换标签页时盖住MFC的其他控件。我写过一段兜底逻辑每次切换标签页时把当前激活的CEF窗口提到最前其他CEF窗口隐藏或置底。::SetWindowPos(browser-GetHost()-GetWindowHandle(), HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);5. C与JS双向通信业务打通的关键一步5.1 C主动调用页面里的JS把CEF嵌入MFC只是第一步真正让客户觉得这套系统活了的是MFC和Web页面里的数据能互相传递。C主动调用页面里的JS函数用ExecuteJavaScript就能实现。bool CppCallJs(const CefString functionName, const CefString jsonArgs) { if (!g_browser.get()) return false; CefRefPtrCefFrame frame g_browser-GetMainFrame(); if (!frame) return false; CefString jsCode window. functionName ( jsonArgs );; frame-ExecuteJavaScript(jsCode, frame-GetURL(), 0); return true; }一个典型的用法是设备状态变化时把状态数据打包成JSON字符串通过这个函数推给页面页面上订阅对应函数实时刷新UI。前提是调用时机要等页面加载完成否则在页面还没定义这个函数时调用会静默失败。我习惯在CefLoadHandler::OnLoadEnd里设置一个页面就绪标志位确保JS环境可用后再发数据。注意线程问题。ExecuteJavaScript必须在browser UI线程调用如果你在子线程里拿设备数据先要切回UI线程。用CefPostTask很顺手CefPostTask(TID_UI, base::BindOnce([](CefString js) { // 在UI线程里执行JavaScript g_browser-GetMainFrame()-ExecuteJavaScript(js, L, 0); }, jsCode));5.2 JS回调CCefMessageRouter还是CefV8Handler反向链路也就是页面里的JS调用C方法是CEF集成的另一半核心。两条主要路线CefV8Handler绑定JS对象到渲染进程适合做简单的函数调用。但V8Handler逻辑跑在renderer进程如果需要访问browser进程的资源或者操作MFC窗口就必须通过进程间消息传过去自己封装一套消息协议工作量大且容易踩线程坑。我更推荐直接用CefMessageRouter它把上面的进程间通信包装成了简洁的query/response模型。先派生子类注册路由class CMyMessageHandler : public CefMessageRouterBrowserSide::Handler { public: virtual bool OnQuery(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, const CefString request, bool persistent, CefRefPtrCallback callback) override { // request就是页面传过来的字符串请求 if (request GetDeviceList) { std::string jsonStr GetDeviceListFromMfc(); // 从MFC层拿数据 callback-Success(jsonStr); return true; } callback-Failure(404, unknown request); return true; } };初始化时创建消息路由器CefMessageRouterConfig config; config.js_query_function cefQuery; config.js_cancel_function cefQueryCancel; m_router CefMessageRouterBrowserSide::Create(config); // 注册自定义handler m_router-AddHandler(new CMyMessageHandler());在CefClient的OnProcessMessageReceived里把消息转发给router处理bool OnProcessMessageReceived(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefProcessId source_process, CefRefPtrCefProcessMessage message) override { return m_router-OnProcessMessageReceived(browser, frame, source_process, message); }页面里的JS调用就很简单了window.cefQuery({ request: GetDeviceList, onSuccess: function(response) { // response就是C回传的JSON字符串 let devices JSON.parse(response); renderDeviceTable(devices); }, onFailure: function(code, msg) { console.log(调用失败: code msg); } });这套模式的精髓是页面只发一个字符串请求C处理后回调Success/Failure结果自动送回renderer进程的JS。中间那些进程间消息的繁琐细节全部被框架消化掉了。我们项目里所有设备控制、数据库查询、配置文件读写都通过这层router暴露给前端。5.3 跨线程回调的注意事项CefMessageRouterBrowserSide::Handler::OnQuery默认在browser UI线程被调用这一点很舒服——意味着在这个方法里可以直接操作MFC的窗口和控件。但如果你把耗时操作直接放在OnQuery里执行UI线程会被卡住页面那边表现为转圈等待。正确姿势是OnQuery里先把任务丢到工作线程处理完再通过CefPostTask回到UI线程最后调用callback。注意callback不能跨线程直接调用必须回到UI线程。bool OnQuery(...) override { std::string requestStr request.ToString(); CallbackRefPtr callback_ptr callback; // 保存引用 std::thread([requestStr, callback_ptr]() { std::string result DoHeavyWork(requestStr); // 耗时操作 CefPostTask(TID_UI, base::BindOnce( [callback_ptr, result]() { callback_ptr-Success(result); })); }).detach(); return true; }这个模式我踩过一跤早期直接在OnQuery里查数据库遇到慢查询时页面整个卡住用户疯狂点按钮最后cefQuery回调堆积程序就假死了。改成工作线程后问题彻底消失。6. 进程生命周期管理别让程序退出时卡死或闪退6.1 为什么直接CefShutdown会崩溃很多人在开发阶段会这么写在对话框的OnDestroy里调用CefShutdown结果一退出就崩溃或者弹出0xC0000005 访问冲突。原因是CefShutdown要求所有浏览器窗口先关闭但此时CEF的browser对象还活着引用计数没归零你直接把整个CEF框架拆了等于把房子地基挖了而人还住在里面。CEF的官方文档写得很清楚CefShutdown必须在所有浏览器实例销毁之后调用。而这个销毁是异步过程CloseBrowser调用后要等OnBeforeClose回调确认。6.2 一个可靠的退出流程我在项目里封装了一个CEF生命周期管理器核心思路是维护浏览器计数关闭时先关浏览器等计数归零再shutdown。class CCefManager { public: static CCefManager Instance() { static CCefManager mgr; return mgr; } void OnBrowserCreated() { m_browserCount; } void OnBrowserDestroyed() { --m_browserCount; if (m_browserCount 0) { // 最后一个浏览器关闭了可以去shutdown ::SetEvent(m_hShutdownEvent); } } void Shutdown() { // 通知所有浏览器关闭 CefRefPtrCefBrowser browser GetMainBrowser(); if (browser.get()) { browser-GetHost()-CloseBrowser(false); } // 等待全部关闭超时兜底 ::WaitForSingleObject(m_hShutdownEvent, 5000); CefShutdown(); } };在CefLifeSpanHandler里维护计数void OnAfterCreated(CefRefPtrCefBrowser browser) override { g_browser browser; CCefManager::Instance().OnBrowserCreated(); } void OnBeforeClose(CefRefPtrCefBrowser browser) override { if (g_browser.get() g_browser-IsSame(browser)) { g_browser nullptr; } CCefManager::Instance().OnBrowserDestroyed(); }MFC主窗口关闭时流程void CMainDialog::OnClose() { CCefManager::Instance().Shutdown(); CDialogEx::OnClose(); }这里有个细节CloseBrowser(false)表示不强制立刻关闭让页面有机会处理beforeunload事件和JS清理。如果设为true页面直接被杀虽然快但可能丢失状态。对于内部工具false更稳妥。等待事件用超时兜底是为了防止某个浏览器迟迟不关整个退出卡住。6.3 强制杀进程的后遗症和预防排查prome cef 进程如何关掉这类问题时很多人的第一反应是任务管理器里手动结束进程。但我不推荐在MFC代码里用TerminateProcess杀CEF进程。因为CEF的多进程之间存在父子关系硬杀父进程会导致子进程变成孤儿进程残留。如果你的程序异常崩溃后任务管理器里留了一堆YourApp.exe子进程多半就是之前硬退导致子进程没被回收。预防方案有两个层面一是按上面的流程优雅关闭二是给CEF进程设置父进程失效自动退出机制。// 在CefSettings中开启 settings.chrome_runtime false; // 不使用chrome runtime实际上CEF默认在browser进程崩溃时会退出子进程但前提是browser进程是被CEF自己管理的。如果你用TerminateProcess结束主进程子进程没有收到关闭通知就会残留。我后来在项目里加了一个看门狗逻辑主进程启动时枚举并清理上次残留的CEF子进程用起来效果很好。另外一个实用技巧在程序启动时检测当前进程是不是被当作CEF子进程启动的如果是直接走CefExecuteProcess分支不要进入MFC的窗口创建逻辑。这个在前面初始化部分已强调但值得再重复一次——我看到不止一个团队因为子进程分支没写对导致软件每次启动都弹一堆窗口。7. 实测踩过的三个坑白屏、启动闪退、中文乱码7.1 白屏从日志到子进程的完整排查白屏是CEF集成里出现频率最高的问题现象是程序窗口正常出来了但嵌进去的页面区域一片空白。我自己遇到过三次根因各不相同。第一次是资源文件不全日志里报Failed to load CEF resources。解决方法是把前面列的那一堆.pak文件和locales目录全部拷过去。第二次是子进程路径配置错了。browser_subprocess_path如果指向不存在的exerenderer进程起不来页面渲染自然失败。我们当时路径里带了中文目录cef在解析时处理不当换成相对路径后就好。第三次最隐蔽是显卡驱动兼容问题。在客户的一台老办公电脑上CEF的GPU进程一启动就崩溃renderer进程反复重启页面一直白。通过CefApp::OnBeforeCommandLineProcessing给renderer和gpu进程追加--disable-gpu开关后问题迎刃而解。void CMyCefApp::OnBeforeCommandLineProcessing( const CefString process_type, CefRefPtrCefCommandLine command_line) { if (process_type.empty()) { // 仅对browser进程生效的命令行参数 command_line-AppendSwitch(disable-gpu); } else if (process_type renderer) { command_line-AppendSwitch(disable-gpu); } }排查白屏我建议按这个顺序来先开日志看CEF自己报了什么错再确认资源文件和子进程路径最后尝试关闭GPU加速。不要一开始就翻页面前端代码大概率不是页面问题。7.2 启动闪退初始化顺序和字符集的锅程序一启动就闪退而且是在CEF初始化之后。我在一次集成里遇到过表现是Debug版本正常Release版本闪退最后定位到是Release工程没改Unicode字符集CefString转换内存越界导致崩溃。还有一次是初始化顺序问题。我们开始在CWinApp的构造函数里调用CefInitialize结果程序启动到一半就崩。原因是MFC的构造函数阶段很多基础环境还没准备好CEF的某些模块依赖Windows的线程池和COM环境。正确做法是把CefInitialize放到InitInstance里最好是App类的InitInstance最早位置此时MFC环境已经就绪。另外要注意如果工程是VS2013但用较新版本的CEFCEF要求编译器至少支持C14部分特性需要把工程的语言标准调到合适的级别。我们后来为了省事把项目从VS2013升级到了VS2017CEF的兼容性明显好了很多。7.3 中文乱码与文本编码页面从C拿到中文数据显示成乱码这个问题几乎每个做CEFMFC的人都会遇到。根因在于编码不一致MFC里宽字符是UTF-16Web默认按UTF-8解析如果你用std::string存UTF-8又用CString转了一圈就乱了。我的处理模式是统一边界C侧和JS交互的数据一律用UTF-8编码传输的是JSON字符串MFC内部用CString或std::wstring只在发出去之前转成UTF-8。std::string WStringToUTF8(const std::wstring wstr) { int size WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), (int)wstr.size(), nullptr, 0, nullptr, nullptr); std::string str(size, 0); WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), (int)wstr.size(), str[0], size, nullptr, nullptr); return str; }反过来页面传上来的UTF-8字符串要先转成宽字符再进CString。这个转换函数要是写错了排查起来非常头疼因为表现是有的页面字是对的有的页面字是乱的。8. 打包部署和性能优化8.1 部署目录一个文件都不能少MFC项目在开发机上跑得好好拷到客户电脑上就白屏闪退大部分情况是部署目录少了CEF运行文件。我在2.3节列过最小文件清单这里再补充两个容易被忽略的项。locales目录里不需要全部语言包按需保留zh-CN.pak、en-US.pak几个就够能省几MB空间。Resources目录下的cef.pak和devtools_resources.pak不能删删了开发者工具和基础UI资源就进不去了。snapshot_blob.bin和v8_context_snapshot.bin是V8引擎的启动快照没有它JS引擎无法启动页面也跑不起来。如果客户机器没有安装VC运行库而你的MFC工程又依赖动态运行库打包时还需要把对应的msvcp140.dll、vcruntime140.dll带上或者用安装包工程把VC Redistributable一起装了。CEF本身的libcef.dll依赖的不是MSVC标准库它自带一套运行时这块倒不用额外处理。8.2 GPU加速开关和内存控制性能优化这块GPU加速是双刃剑。开了GPU页面CSS动画、Canvas绘图这些会流畅不少但在低配办公电脑上GPU进程反而可能成为稳定性的短板频繁崩溃还会拖累整个程序。我们的做法是做一个配置项默认开启GPU客户现场如果出现显示问题就通过配置项强制关闭。内存控制方面CEF每个标签页都会开一个renderer进程几十个标签页的内存占用非常可观。如果是单页应用尽量就开一个浏览器实例页面内部用前端路由切换。如果必须多开要留意废弃页面的释放把CefRefPtr 置空让引用计数归零。再补充一个经验初次加载页面时如果页面要请求大量资源首屏会白上一段时间。我们后来把初始页面改成加载本地打包好的静态资源而不是直接访问远程服务器首屏速度提升明显。本地点可以走file://协议或者自己注册一个自定义scheme比如http://app.local/由C端的scheme handler返回本地文件内容。自定义scheme的好处是还能做权限控制性能也比网络请求稳。我还试过用离屏渲染OSR模式把浏览器绘制到纹理里再贴到MFC窗口上为了实现一些异形窗口和透明效果。但这个模式性能开销比窗口嵌入模式大而且输入事件都要自己转发开发成本高不少。如果只是常规矩形窗口老老实实用窗口嵌入模式就够了。最后再说一点我自己的体会。CEFMFC的集成最难的不是把浏览器窗口放进去而是从初始化、消息循环到进程回收这一整套生命周期管理要设计周全。见过太多项目功能开发得飞快一到最后退出崩溃、进程残留就焦头烂额。把生命周期这件事在一开始就规划好当成基础设施来设计后面的开发会省心非常多。如果你准备动手做类似的集成建议先把CefLifeSpanHandler这份回调吃透再去看密密麻麻的功能API思路会清晰很多。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价