基于libcef3与MFC的轻量级Web嵌入式应用Demo实战
简介:本项目“libcef3+MFC最简单的demo”展示了如何将Chromium Embedded Framework(CEF)与Microsoft Foundation Classes(MFC)集成,构建一个可嵌入Web内容的C++桌面应用程序。通过该Demo,开发者可学习CEF环境的配置、MFC界面搭建、浏览器组件嵌入以及JavaScript与C++之间的双向通信实现。项目涵盖CEF初始化、V8引擎交互、异步回调处理、页面事件监听等核心功能,适用于开发具备Web渲染能力的桌面软件,如嵌入式浏览器、混合型客户端等。
CEF + MFC 深度集成实战:从环境搭建到双向通信的全链路解析
在智能家居设备日益复杂的今天,确保无线连接的稳定性已成为一大设计挑战……等等,这好像不是我们要讲的内容 😅。抱歉走错片场了。
咱们今天聊点硬核的—— 如何在一个古老的MFC项目里,“塞”进一个现代浏览器?
是的,你没听错。想象一下:你的老板递过来一个20年前写的MFC工程,界面还是Win98风格,菜单栏上甚至还飘着“文件”、“编辑”、“查看”这种复古词汇。然后他说:“能不能让这个软件打开网页?” 你心里一万个拒绝涌上来,但手已经默默打开了Visual Studio……
这时候, Chromium Embedded Framework(简称 CEF) 就是你唯一的救赎。它能把整个Chrome内核嵌进你的原生C++程序,让你用HTML/CSS/JS打造现代化UI,同时还能调用本地API完成文件操作、注册表读写、串口通信等脏活累活。
而我们今天的主角,就是 libcef3 + MFC 的深度整合方案 。这不是简单的“加个WebBrowser控件”,而是真正把CEF跑起来,支持HTML5、WebGL、JavaScript交互、自定义协议、资源拦截……直到你能用 window.nativeApi.print("Hello") 弹出一个MFC对话框为止。
准备好了吗?让我们开始这场“时空穿越式”的开发之旅 🚀。
🧱 第一步:给老古董穿上新外衣 —— libcef3 环境配置与项目集成
先别急着写代码,咱们得先把“发动机”装好。
🔧 获取 CEF SDK
去 https://cef-builds.spotifycdn.com/index.html 下载最新版的 cef_binary_xxx_windows64.zip (建议选带 libcef_dll 的完整包)。解压后你会看到类似这样的目录结构:
cef/
├── include/ # 所有头文件
├── lib/ # libcef.lib 和 cef_sandbox.lib
├── Resources/ # 必需的资源文件
│ ├── icudtl.dat
│ ├── natives_blob.bin
│ └── snapshot_blob.bin
└── Release/
├── libcef.dll
├── cef.pak
├── cef_100_percent.pak
└── ...
这些 .pak 文件可是关键!少了它们,页面可能连字体都显示不出来。
⚙️ Visual Studio 配置三板斧
打开你的MFC项目,右键 → 属性,进入以下三个环节:
1. 包含目录(C/C++ → 常规)
添加:
$(SolutionDir)cef\include
$(SolutionDir)cef\include\internal
💡 提示:如果你用了
wrapper/cef_helpers.h这类辅助头文件,记得也加上cef/include/wrapper。
2. 库目录(链接器 → 常规)
添加:
$(SolutionDir)cef\lib
3. 依赖项(链接器 → 输入)
追加这两行:
libcef.lib
cef_sandbox.lib
⚠️ 注意顺序不能错! libcef.lib 要在前面。
🔄 编译模式必须匹配!
这是很多人踩坑的地方: CEF要求使用 /MD 或 /MDd 编译模式 (多线程DLL版本运行时库)。
如果你当前是 /MT 或 /MTd ,赶紧改!
👉 路径:C/C++ → 代码生成 → 运行库
- Debug:选 多线程调试 DLL (/MDd)
- Release:选 多线程 DLL (/MD)
否则你会遇到各种诡异的崩溃和断言失败。
📦 输出目录部署策略
光编译成功还不够,运行时还需要一堆DLL和资源文件。我们可以用 后期生成事件 自动复制:
xcopy "$(SolutionDir)cef\Resources\*.*" "$(OutDir)" /Y /D
设置位置:项目属性 → 生成事件 → 后期生成事件命令行。
这样每次编译完,所有必需资源都会被拷贝到 .exe 同级目录,再也不用手动拖拽了 ✅。
🏗️ 第二步:搭台唱戏 —— 在 MFC 中构建可承载浏览器的GUI框架
现在“发动机”装好了,接下来要造一辆车来装它。
🛠 使用 VS 向导创建 MFC SDI 项目
打开 Visual Studio,新建项目 → MFC 应用 → 名字叫 CefMfcApp 不妨。
向导中注意几个关键选项:
| 设置项 | 推荐值 | 为什么 |
|---|---|---|
| 应用程序类型 | 单个文档 | 最简单干净的结构 |
| 使用MFC | 在共享DLL中使用MFC | 动态链接更灵活,避免冲突 |
| 用户界面功能 | 工具栏+状态栏 | 看起来专业一点 😉 |
| 高级功能 | 可勾选ActiveX支持 | 为未来留扩展空间 |
点击完成,VS会自动生成一套标准的MFC文档/视图架构:
CefMfcApp.h/cpp // 主程序类
MainFrm.h/cpp // 主窗口
CefMfcAppView.h/cpp // 视图类 ← 我们的主战场
CefMfcAppDoc.h/cpp // 文档类(本次不用)
这个结构虽然古老,但它稳如老狗,特别适合长期维护的企业级应用。
🤝 MFC 核心类职责分工详解
让我们快速过一遍这三个核心类的作用,搞清楚谁该干啥:
CWinApp 子类:程序的大脑🧠
它是整个应用的入口,替代了传统的 WinMain 。最重要的函数是 InitInstance() ,在这里我们可以做两件事:
- 初始化 CEF
- 启动 MFC 框架
顺序很重要!必须先初始化 CEF 再创建窗口,否则子进程起不来。
来看一段经典代码:
BOOL CCefMfcApp::InitInstance()
{
// Step 1: 准备 CEF 初始化参数
CefMainArgs main_args(m_hInstance);
CefSettings settings;
// 指定子进程路径(重要!)
CefString(&settings.browser_subprocess_path).FromString(L"CefSubProcess.exe");
// 关键设置:启用多线程消息循环
settings.multi_threaded_message_loop = true;
// Step 2: 初始化 CEF
if (!CefInitialize(main_args, settings, new MyCefApp, nullptr))
return FALSE;
// Step 3: 继续 MFC 初始化流程
if (!CWinApp::InitInstance()) return FALSE;
// 创建文档模板...
CSingleDocTemplate* pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CCefMfcAppDoc),
RUNTIME_CLASS(CMainFrame),
RUNTIME_CLASS(CCefMfcAppView));
AddDocTemplate(pDocTemplate);
m_pMainWnd = pDocTemplate->CreateNewFrame(nullptr, nullptr);
m_pMainWnd->ShowWindow(SW_SHOW);
m_pMainWnd->UpdateWindow();
return TRUE;
}
看到了吗?我们在 CWinApp::InitInstance() 一开始就调用了 CefInitialize() ,这就是所谓的“早初始化”。
❗小贴士:
multi_threaded_message_loop = true是必须的!因为MFC有自己的消息泵,我们不能让它接管CEF的消息循环,所以要用内部线程池模式。
CFrameWnd 子类:主窗口管理者🖼️
CMainFrame 控制着整个窗口的外观:菜单栏、工具栏、状态栏、客户区大小等。它本身不直接参与浏览器嵌入,但它提供的“画布”区域(即客户区)会被 CView 使用。
你可以在这里添加前进/后退按钮、地址栏输入框等UI元素,实现一个真正的“迷你浏览器”。
CView 子类:内容展示中心🎨
这才是我们的主战场!默认情况下, CView 通过重写 OnDraw() 来绘制GDI图形。但我们不需要画画,我们要的是一个能显示网页的“容器”。
怎么做?很简单—— 创建一个子窗口作为宿主HWND 。
class CCefMfcAppView : public CView
{
DECLARE_DYNCREATE(CCefMfcAppView)
public:
HWND m_hBrowserWnd; // 浏览器宿主句柄
protected:
virtual void OnInitialUpdate();
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
DECLARE_MESSAGE_MAP()
};
int CCefMfcAppView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
m_hBrowserWnd = ::CreateWindowEx(
0,
_T("STATIC"), // 用静态控件当容器
_T(""), // 无文字
WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
0, 0, 100, 100, // 初始尺寸
GetSafeHwnd(), // 父窗口是当前视图
nullptr,
AfxGetInstanceHandle(),
nullptr);
return 0;
}
为什么选 STATIC ?因为它轻量、透明、不会抢焦点,完美适合作为“画布”。
而且加上 WS_CLIPCHILDREN | WS_CLIPSIBLINGS 可以防止渲染撕裂,提升性能。
🔌 第三步:点燃引擎 —— CEF 运行时初始化全流程
现在硬件有了,舞台搭好了,该点火了 🔥。
📦 链接 libcef.lib 并配置运行时环境
前面已经配好了头文件和库路径,这里再强调几点容易忽略的细节:
- 确保
libcef.lib和cef_sandbox.lib都链接了 - Release 和 Debug 版本不要混用
- CEF 的 lib 是基于 MSVC 2019+ 编译的,旧版VS可能不兼容
🧬 自定义 CefApp 实现全局逻辑控制
CefApp 是 CEF 的“全局应用对象”,相当于 Chrome 的“Browser Process”。我们需要继承它来定制行为。
class MyCefApp : public CefApp,
public CefBrowserProcessHandler
{
private:
CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler() override {
return this;
}
void OnContextInitialized() override {
// 全局上下文已准备好,可以创建浏览器实例了
LOG(INFO) << "CEF context initialized.";
}
};
然后在 InitInstance() 中传进去:
if (!CefInitialize(main_args, settings, new MyCefApp, nullptr)) {
AfxMessageBox(_T("Failed to initialize CEF."));
return FALSE;
}
OnContextInitialized() 是最关键的回调之一,表示 CEF 内部已完成初始化,此时才能安全地调用 CreateBrowser() 。
🚀 调用 CefInitialize() 启动运行时
完整的初始化代码如下:
CefMainArgs args(m_hInstance);
CefSettings settings;
// 日志输出(调试神器)
CefString(&settings.log_file).FromString(L"cef_log.txt");
settings.log_severity = LOGSEVERITY_INFO;
// 多线程消息循环(必须开启)
settings.multi_threaded_message_loop = true;
// 指定子进程路径(非常重要!)
CefString(&settings.browser_subprocess_path).FromString(L"CefSubProcess.exe");
// 初始化
if (!CefInitialize(args, settings, new MyCefApp, nullptr)) {
AfxMessageBox(_T("CEF initialization failed!"));
return FALSE;
}
最后别忘了在退出时关闭:
int CCefMfcApp::ExitInstance()
{
CefShutdown(); // 必须调用!释放资源
return CWinApp::ExitInstance();
}
🛑 如果漏掉
CefShutdown(),可能会导致内存泄漏或子进程残留。
🖼️ 第四步:把浏览器“种”进MFC窗口
终于到了激动人心的时刻——让网页出现在你的MFC界面上!
📍 方法一:使用资源控件预留宿主区域(推荐)
比起代码创建 STATIC 控件,更优雅的方式是在资源编辑器里画一个占位符。
打开 .rc 文件或资源视图,在 CFormView 上放一个 Static Text 控件,ID设为 IDC_BROWSER_HOST ,文本清空,样式设为“Visible”。
然后在视图类中获取它的句柄:
void CBrowserView::OnInitialUpdate()
{
CFormView::OnInitialUpdate();
m_hBrowserHost = GetDlgItem(IDC_BROWSER_HOST)->GetSafeHwnd();
if (!m_hBrowserHost) {
AfxMessageBox(_T("找不到浏览器宿主控件!"));
return;
}
// 修改样式增强兼容性
::SetWindowLong(m_hBrowserHost, GWL_STYLE,
WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS);
CreateCEFBrowser(); // 开始创建浏览器
}
这种方式的好处是布局直观,可以用对话框编辑器调整位置和大小,甚至响应 DPI 缩放。
🪟 构造 CefWindowInfo 设置父窗口
有了宿主HWND,下一步就是告诉CEF:“我要把浏览器嵌在这里”。
CefWindowInfo window_info;
window_info.SetAsChild(
m_hBrowserHost, // 父窗口句柄
CefRect(0, 0, 800, 600) // 相对位置和初始尺寸
);
window_info.window_name = L"Embedded Browser";
window_info.style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
SetAsChild() 是个便捷方法,它会自动设置正确的窗口风格和坐标系统。
注意: 不要开启 windowless_rendering_enabled ,除非你想自己处理像素绘制。普通场景下用 WS_CHILD 模式最稳定。
🌐 异步创建浏览器实例
调用 CefBrowserHost::CreateBrowser() 发起请求:
CefRefPtr<CefClient> client = new CustomCefClient();
CefBrowserSettings browser_settings;
browser_settings.web_security = STATE_DISABLED; // 可选:禁用同源策略
BOOL success = CefBrowserHost::CreateBrowser(
window_info,
client.get(),
L"https://www.google.com",
browser_settings,
nullptr, nullptr);
由于CEF是异步架构,这个函数只是“提交请求”,真正的创建会在IO线程完成。
成功与否看返回值:
- true :请求已接收,稍后会回调 OnAfterCreated
- false :参数错误或未初始化
真正的浏览器实例会在 CefLifeSpanHandler::OnAfterCreated() 中拿到:
class CustomLifeSpanHandler : public CefLifeSpanHandler
{
public:
void OnAfterCreated(CefRefPtr<CefBrowser> browser) override
{
AutoLock lock_scope(lock_);
browser_list_.push_back(browser);
// 保存引用用于后续操作
}
};
🔄 第五步:生命周期管理与事件回调体系
CEF 的强大之处在于其模块化的事件处理机制。每个关注点都被拆分成独立接口,按需组合即可。
🎮 实现 CefClient 接管各类事件
CefClient 是事件分发中枢,你需要继承它并聚合多个处理器:
class CustomCefClient : public CefClient,
public CefDisplayHandler,
public CefLifeSpanHandler,
public CefLoadHandler
{
public:
virtual CefRefPtr<CefDisplayHandler> GetDisplayHandler() override { return this; }
virtual CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
virtual CefRefPtr<CefLoadHandler> GetLoadHandler() override { return this; }
IMPLEMENT_REFCOUNTING(CustomCefClient);
};
常见处理器用途一览表:
| 接口 | 关键回调 | 用途 |
|---|---|---|
CefDisplayHandler | OnTitleChange | 更新窗口标题 |
CefLoadHandler | OnLoadStart/End/Error | 显示加载动画、错误提示 |
CefLifeSpanHandler | OnBeforePopup , DoClose | 控制弹窗和关闭逻辑 |
CefRequestHandler | OnBeforeBrowse | 拦截导航请求 |
CefKeyboardHandler | OnPreKeyEvent | 处理快捷键 |
举个例子,想让MFC主窗口标题随网页变化?
void OnTitleChange(CefRefPtr<CefBrowser> browser, const CefString& title) override
{
CString strTitle(title.c_str());
AfxGetMainWnd()->SetWindowText(strTitle);
}
就这么简单!
🚪 管理弹窗与关闭策略
默认情况下, window.open() 会打开新窗口。但在MFC应用中,我们通常希望统一管理标签页或直接阻止。
通过重写 OnBeforePopup 实现控制:
bool OnBeforePopup(...) override
{
// 方案1:完全禁止弹窗
return true;
// 方案2:在当前页面跳转
parent_browser->GetMainFrame()->LoadURL(target_url);
return true;
// 方案3:允许但记录日志
return false;
}
至于窗口关闭,有两个阶段:
bool DoClose(CefRefPtr<CefBrowser> browser) override {
// 返回false表示接受关闭
return false;
}
void OnBeforeClose(CefRefPtr<CefBrowser> browser) override {
// 最后的清理工作
RemoveBrowserFromList(browser);
}
📶 监听加载状态变化
想知道页面是否加载完毕?监听 CefLoadHandler :
void OnLoadStart(...) override {
PostMessageToUI(WM_LOADING_START);
}
void OnLoadEnd(...) override {
PostMessageToUI(WM_LOADING_END);
}
void OnLoadError(...) override {
AfxMessageBox(L"页面加载失败!");
}
还可以同步前进后退按钮状态:
void UpdateNavButtons(CefRefPtr<CefBrowser> browser)
{
CWnd* pBack = GetDlgItem(IDC_BACK);
CWnd* pForward = GetDlgItem(IDC_FORWARD);
pBack->EnableWindow(browser->CanGoBack() ? TRUE : FALSE);
pForward->EnableWindow(browser->CanGoForward() ? TRUE : FALSE);
}
stateDiagram-v2
[*] --> Idle
Idle --> Loading: OnLoadStart()
Loading --> Success: OnLoadEnd(httpCode=200)
Loading --> Failure: OnLoadError()
Success --> Idle
Failure --> Idle
🔄 第六步:双向通信 —— 让 JS 调用 C++ 函数
这才是最炫的部分: 让前端 JavaScript 直接调用本地 C++ 代码!
🧩 注册 CefV8Handler 暴露原生方法
原理很简单:在 V8 上下文中注入一个全局对象,比如 window.nativeApi ,然后绑定方法。
首先定义处理器:
class MyV8Handler : public CefV8Handler
{
public:
bool Execute(const CefString& name,
CefRefPtr<CefV8Value> object,
const CefV8ValueList& arguments,
CefRefPtr<CefV8Value>& retval,
CefString& exception) override
{
if (name == "print") {
std::string msg = arguments[0]->GetStringValue();
AfxMessageBox(CString(msg.c_str()));
retval = CefV8Value::CreateBool(true);
return true;
}
return false;
}
IMPLEMENT_REFCOUNTING(MyV8Handler);
};
然后在 OnContextCreated 中挂载:
void OnContextCreated(...)
{
CefRefPtr<CefV8Value> window = context->GetGlobal();
CefRefPtr<CefV8Value> handler = CefV8Value::CreateObject(nullptr, nullptr);
handler->SetValue("print",
CefV8Value::CreateFunction("print", new MyV8Handler()),
V8_PROPERTY_ATTRIBUTE_NONE);
window->SetValue("nativeApi", handler, V8_PROPERTY_ATTRIBUTE_NONE);
}
现在前端就可以写了:
nativeApi.print("Hello from JS!");
Boom!一个MFC消息框就弹出来了 💥
📦 支持复杂数据类型传递
除了字符串,我们还希望能传对象、数组。
nativeApi.submitUser({ id: 1001, name: "Alice", roles: ["admin"] });
对应的C++处理:
if (name == "submitUser" && arguments[0]->IsObject()) {
CefRefPtr<CefV8Value> obj = arguments[0];
int id = obj->GetValue("id")->GetIntValue();
std::string name = obj->GetValue("name")->GetStringValue();
CefRefPtr<CefV8Value> rolesArr = obj->GetValue("roles");
std::vector<std::string> roles;
for (int i = 0; i < rolesArr->GetArrayLength(); ++i) {
roles.push_back(rolesArr->GetValue(i)->GetStringValue());
}
ProcessUserData(id, name, roles);
}
对于更复杂的结构,建议使用 JSON 序列化:
#include "include/wrapper/cef_json.h"
std::string V8ToJSON(CefRefPtr<CefV8Value> v8Obj) {
CefRefPtr<CefDictionaryValue> dict = CefDictionaryValue::Create();
// 手动遍历属性填充dict...
CefRefPtr<CefValue> val = CefValue::Create();
val->SetDictionary(dict);
return CefJSONWriter::Write(val, JSON_WRITER_DEFAULT)->ToString();
}
sequenceDiagram
participant JS as JavaScript
participant V8 as CEF V8 Context
participant Handler as CefV8Handler
participant Cpp as C++ Logic
JS->>V8: nativeApi.call(data)
V8->>Handler: Execute("call", args)
Handler->>Cpp: 解析args并调用业务逻辑
Cpp-->>Handler: 返回结果
Handler-->>V8: 设置retval
V8-->>JS: 接收返回值
🧯 第七步:避坑指南与常见问题排查
最后送上一份“血泪总结”清单:
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| LNK2019: unresolved symbol | 未链接 libcef.lib | 检查附加依赖项 |
| ASSERT in CefExecuteProcess | 子进程路径错误 | 设置 browser_subprocess_path |
| 页面白屏 | 缺少 .pak 文件 | 确保资源文件部署到位 |
| 字体乱码 | icudtl.dat 缺失 | 检查Resources目录 |
| 初始化失败 | multi_threaded_message_loop 未开启 | 设为 true |
| JS无法调用C++ | OnContextCreated 未执行 | 检查是否派生了 CefRenderProcessHandler |
还有一个隐藏巨坑: 防病毒软件可能会拦截 CefSubProcess.exe !记得加白名单或者换个名字打包。
🎉 结语:老树开新花,MFC也能很Modern
看到这里,你应该已经掌握了如何将一个现代化的浏览器引擎嵌入到传统的MFC项目中。从环境配置、窗口嵌入、生命周期管理到JS-C++双向通信,整套流程现在已经清晰可见。
这种“混合架构”不仅能让老旧系统焕发新生,还能大幅降低UI迭代成本——以后改界面只需要前端工程师改HTML,不用再动辄修改MFC资源脚本了。
更重要的是, 你已经拥有了一个强大的跨平台能力扩展基座 。无论是对接硬件设备、访问本地数据库,还是实现离线优先的应用逻辑,CEF + MFC 的组合都能游刃有余。
所以,下次当你面对那个“祖传代码”时,不要再想着重写了。试试把它变成一个“能跑Chrome的MFC程序”吧!
毕竟, 最好的重构,不是推倒重来,而是让它活得更久一点 🫶 。
“Old code never dies, it just gets embedded.”
—— 一位不愿透露姓名的MFC程序员 😂
简介:本项目“libcef3+MFC最简单的demo”展示了如何将Chromium Embedded Framework(CEF)与Microsoft Foundation Classes(MFC)集成,构建一个可嵌入Web内容的C++桌面应用程序。通过该Demo,开发者可学习CEF环境的配置、MFC界面搭建、浏览器组件嵌入以及JavaScript与C++之间的双向通信实现。项目涵盖CEF初始化、V8引擎交互、异步回调处理、页面事件监听等核心功能,适用于开发具备Web渲染能力的桌面软件,如嵌入式浏览器、混合型客户端等。
更多推荐



所有评论(0)