本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目“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() ,在这里我们可以做两件事:

  1. 初始化 CEF
  2. 启动 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程序员 😂

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目“libcef3+MFC最简单的demo”展示了如何将Chromium Embedded Framework(CEF)与Microsoft Foundation Classes(MFC)集成,构建一个可嵌入Web内容的C++桌面应用程序。通过该Demo,开发者可学习CEF环境的配置、MFC界面搭建、浏览器组件嵌入以及JavaScript与C++之间的双向通信实现。项目涵盖CEF初始化、V8引擎交互、异步回调处理、页面事件监听等核心功能,适用于开发具备Web渲染能力的桌面软件,如嵌入式浏览器、混合型客户端等。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐