Abstract
Keywords MFC  Cef  MFC  Cef 
Citation Yao Qing-sheng.MFC 集成 CEF3 窗口.FUTURE & CIVILIZATION Natural/Social Philosophy & Infomation Sciences,20240808. https://yaoqs.github.io/20240808/mfc-ji-cheng-cef3-chuang-kou/

转载自 MFC 集成 CEF3 窗口

前言

一般来讲我常规开发 windows 系统的程序的时候绝对会遇到一个问题,我们想要实现美观炫酷的界面效果但是 windows 无论是 QT 还是 MFC 这些老牌 C++ 应用框架还是 windows UFP 的.NET Winform 都很难去完整自定义你的样式。比如说 QT 里面的按钮你只能通过 C++ 或者 UI 文件对按钮生成项进行简单的设置,MFC 更加过分只有 30 不到的设置项,Winform 也差不多。如果你想完整的定义一个自己的按钮那就需要从绘制开始写了,这个要求就不是一点半点了。

但是我们在日常使用的时候发现很多程序实现了非常 NB 的界面样式,而且实现了非常多的动态效果,如果说这些效果全部是通过 C/C++ 重写绘制的话那太要命了。这里就举一个例子,网易云音乐应该是大家都在使用的音乐播放器,它里面的效果确实很漂亮美观。根据对网易云音乐的运行库进行分析我发现了一个神器,那就是 CEF。

CEF 库

CEF 是一个谷歌的半开源库,它提供原生 C++ 库实现了一个基于谷歌 V8 的浏览器创建,它采用多个子进程区分业务流程然后在各个子进程之中完成对应的回调与消息通知。这个库是我们可以完全脱离 QT 开发 Windows 应用程序,相当于实现了在 windows 上的 webview,重型或者底层的操作由前端 JS 告知 C++ 进行执行,服务端或者其他系统状态的消息通知由 C++ 通知 JS 执行,目前看来效果良好,除开 windows 基础窗口样式之外其他所有的东西都可以通过 html 进行定制。后期所有的三维可视化项目我们可以通过 C 完成实时的通讯,同时由 HTML/JS 实现页面的绘制,而且所有的全端资源采用 ZIP 加密为 PAK 包的方式供 C 调用绘制所以安全性大大高于之前的 QTwebengine。

流程

1、声明 APP 对象

这个东西一个浏览器的应用对象,它可以多态集成 CEF 的多个组件,一般来讲都是线程之类的东西,如渲染线程、异常线程等等,我们使用的时候为了完成 JS 调用 C++ 才会对它进行重写。具体声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#pragma once
#ifndef __CEF3SimpleSample__ClientHandler__
#define __CEF3SimpleSample__ClientHandler__

#include "include/cef_app.h"
#include "include/cef_client.h"

#include "HtmlEventHandler.h"

//创建CEF应用对象,同时继承渲染进程的消息回调接口
class ClientApp : public CefApp, public CefRenderProcessHandler {
public:
ClientApp();
//获取消息接口对象
CefRefPtr<CefRenderProcessHandler> GetRenderProcessHandler() OVERRIDE
{
return this;
}
//当html上下文加载完毕时回调的重写,用于注册C++函数到JS
virtual void OnContextCreated(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context) OVERRIDE;
//当html上下文被释放的回调的重写,用于释放声明的V8值对象
virtual void OnContextReleased(CefRefPtr< CefBrowser > browser, CefRefPtr< CefFrame > frame, CefRefPtr< CefV8Context > context) OVERRIDE;

private:
//注册JS函数
void RegisterFunction(CefRefPtr<CefV8Value> object);
//V8消息拦截,这里完成对注册函数的实现
CefRawPtr<CefV8Handler> functionhandler;
IMPLEMENT_REFCOUNTING(ClientApp);
};

#endif

2. 声明 Client 对象

Client 对象是一个独立的浏览器实例对象封装,也可以继承多个 CEF 组件,一般与上层的一些消息通知如请求处理、绘制处理等等,我们为了实现对请求资源的重映射、对底层浏览器对象的获取继承了多个东西具体声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#pragma once
#ifndef __CEFSimpleSample__ClientHandler__
#define __CEFSimpleSample__ClientHandler__

#include "include/cef_render_process_handler.h"
#include "include/cef_client.h"
#include "include/cef_v8.h"
#include "include/cef_browser.h"
#include "include/wrapper/cef_resource_manager.h"
//客户端的自定义类
namespace resource_manager {
//继承CefClient实现客户端功能,继承CefRequestHandler实现请求拦截功能,继承CefLifeSpanHandler实现关闭功能,继承CefDisplayHandler实现如全屏之类的功能
class ClientHandler : public CefClient, public CefLifeSpanHandler ,public CefDisplayHandler, public CefRequestHandler {
public:
ClientHandler();
//获得客户端的浏览器对象
CefRefPtr<CefBrowser> GetBrowser()
{
return m_Browser;
}
//得到浏览器窗口句柄
CefWindowHandle GetBrowserHwnd()
{
return m_BrowserHwnd;
}
//得到此客户端的周期控制
virtual CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() OVERRIDE
{
return this;
}
//触发关闭
virtual bool DoClose(CefRefPtr<CefBrowser> browser) OVERRIDE;
//创建前回调
virtual void OnAfterCreated(CefRefPtr<CefBrowser> browser) OVERRIDE;
//关闭之后回调
virtual void OnBeforeClose(CefRefPtr<CefBrowser> browser) OVERRIDE;
//请求处理对象
CefRefPtr<CefRequestHandler> GetRequestHandler() OVERRIDE { return this; }
//资源载入之前回调(重点功能拦截请求给到resource_manager)
cef_return_value_t OnBeforeResourceLoad(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefRequest> request, CefRefPtr<CefRequestCallback> callback) OVERRIDE;
//资源处理对象
CefRefPtr<CefResourceHandler> GetResourceHandler(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefRequest> request) OVERRIDE;

protected:
//浏览器对象
CefRefPtr<CefBrowser> m_Browser;
//资源管理对象(用于重映射资源文件)
CefRefPtr<CefResourceManager> resource_manager_;
//浏览器窗口句柄
CefWindowHandle m_BrowserHwnd;
//内部调用声明
IMPLEMENT_REFCOUNTING(ClientHandler);
DISALLOW_COPY_AND_ASSIGN(ClientHandler);
};
}
#endif

3.js 调用 C++ 处理

显示注册对应的 JS 函数,然后创建一个继承 CefV8Handler 的类进行对应注册函数的功能实现就可以了对应的代码比较基础就不谈了实现部分的声明大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#pragma once

#include "include/cef_v8.h"
#include "ClientApp.h"
//集成V8消息回调类
class HtmlEventHandler : public CefV8Handler
{
public:
//构造
HtmlEventHandler(CefRefPtr<CefBrowser> browser);
//注册函数最终实现的回调,使用name区分
virtual bool Execute(const CefString& name, CefRefPtr<CefV8Value> object, const CefV8ValueList& arguments, CefRefPtr<CefV8Value>& retval, CefString& exception) OVERRIDE;
private:
CefRefPtr<CefBrowser> browser;
IMPLEMENT_REFCOUNTING(HtmlEventHandler);
};

4. 资源重定向

在 App 对象里面新建一个资源管理对象,为这个对象添加一个协议,将一个请求地址进行拦截然后访问一个加密之后的 zip 文件对资源进行寻找然后返回给浏览器,具体实现部分大致为:

1
2
3
4
//初始化资源管理
resource_manager_ = new CefResourceManager();
//添加处理协议,拦截"http://data/",重映射到GetCurDir() + "/data.pak"文件,通过"......."为密码解压,执行顺序为0,身份校验位空
resource_manager_.get()->AddArchiveProvider("http://data/", GetCurDir() + "/data_1.pak", ".......", 0, std::string());

然后在 App 之中重写 OnBeforeResourceLoad 与 GetResourceHandler 就可以实现对资源的重定向。

5. 创建窗口

显示对 CEF 组件的初始化,这里需要使用 MFC 的对话框窗口句柄和实例进行对应的窗口初始化。我这里自己进行了封装大致是这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void CEFView::Init(HINSTANCE hInstance, HWND HWnd) {
//配置窗口句柄
CefMainArgs main_args(hInstance);
//赋值
this->hInstance = hInstance;
this->HWnd = HWnd;
//创建应用对象
CefRefPtr<ClientApp> app(new ClientApp);
//开启线程
int exit_code = CefExecuteProcess(main_args, app.get(), NULL);
if (exit_code >= 0) {
exit(exit_code);
}

//配置设置
CefSettings settings;
//初始化设置
CefSettingsTraits::init(&settings);
//启动多线程消息
settings.multi_threaded_message_loop = true;
//CEF组件初始化
CefInitialize(main_args, settings, app.get(), NULL);
}

然后就是去创建这个窗口,流程就是创建一个浏览器然后对这个浏览器进行子窗口映射与大小设置就可以了,我的封装方式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void CEFView::CreatView(std::string url) {

//获取窗口坐标
RECT rect;
GetClientRect(HWnd, &rect);
//配置窗口信息
CefWindowInfo info;
//配置浏览器设置
CefBrowserSettings b_settings;
//创建客户对象
CefRefPtr<resource_manager::ClientHandler> client(new resource_manager::ClientHandler);
//赋值
CEF_Client = client;
//设置窗口为MFC窗口句柄的子窗口
info.SetAsChild(HWnd, rect);
//创建浏览器
CefBrowserHost::CreateBrowser(info, client.get(), url, b_settings, NULL);
}

6.C++ 调用 JS

这个比较简单直接使用浏览器对象执行一个上下文就是了,对应的东西都是 V8 搞好了的,封装就一点点,但是总的得说这种方式只能去执行顶层 frame 之中函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
void CEFView::RunJavaScript(std::string js) {
CefRefPtr<CefBrowser> browser = GetBrowser();
if (browser.get())
{
//得到web页面的顶层frame
CefRefPtr<CefFrame> frame = browser->GetMainFrame();
if (frame)
{
//执行JS函数
frame->ExecuteJavaScript(js, L"", 0);
}
}
}

7. 资源文件的加密

资源文件不能直接使用 winrar 或者 360 什么的,由于算法还是封装方式的问题,基本上全部都是卵的。最终的解决办法是使用 7-zip,没有就去下一个,加密算法选择 ZipCrypto 其他没有什么影响输出 zip 文件之后改为 pak 文件或者其他什么格式都是可以了,然后交给 Client 的资源管理对象就可以了。

8. 缩放自适应

MFC 的对话框有一个虚函数可以复写叫 OnSize 可以自己去找找,直接复写这个东西然后给匹配给浏览器的窗口句柄就可以了,执行起来也比较简单我封装在一个类之中的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void CEFView::ReSize() {
//获取窗口坐标
RECT rect;
GetClientRect(HWnd, &rect);
//获取浏览器对象
if (CEF_Client.get())
{
CefRefPtr<CefBrowser> browser = CEF_Client->GetBrowser();
if (browser)
{
//获取浏览器窗口句柄
CefWindowHandle hwnd = browser->GetHost()->GetWindowHandle();
//设置大小与位置(继承父级窗口)
::MoveWindow(hwnd, 0, 0, rect.right - rect.left, rect.bottom, true);
}
}
}

9. 库编译

首先这个东西是必须要 CMake 的,不然根本没得什么搞头。第二这个东西编译支持的最高版本为 2015 我是在 2015 之中编译好了拿给 17 用的,测试下来 X64 和 X86 都没有什么问题,release 和 debug 需要分开编译但是也没有什么问题,debug 里面使用资源管理对象进行重映射时加密文件不知为何打不开其他都是没有什么问题。

流程不算复杂还是比较好用的库了,首先不要去下载源码版,那个东西要编译死了一大堆依赖。最好下载二进制版本(下载地址可能有点慢),但是二进制版本之中的 libcef_dll_wrapper 还是需要自己去编译的,这个时候 CMake 一下就可以了,最好把 debug 和 release 版本都编译下来。这个库的头文件与其他在一起直接引用就可以了。重点是 CMake 的时候一定要 MT 版本,之后的运行依赖就好办很多,同时 MFC 也可以使用静态引用了。然后在 vs 新建一个像引用就是了。

10. 运行依赖

这个逼就很恶心了,它不仅仅有一堆动态库的依赖就是他自己的,还有一堆资源的依赖。首先如果编译 release 版本就将 release 文件夹里面的所有东西考到项目的根目录之中不然一大堆空指针中断。debug 就拷 debug 的。然后就是把 Resources 文件夹下的所有东西也要考到对应的项目的根目录之中无论是 release 还是 debug 都是一样的。

11. 总结

大概就是这些东西,最终效果还是非常不错的,比 QT 那个 webview 好了很多,我大概研究了一下不仅仅是网易云音乐还有 babylon 离线编辑器、迅雷等等全部都是使用 CEF 这个库实现的。而且百度云网盘也是无非就是吧 CEF 自己封装了一个 dll 实现的。

CEF 是真的 NB,谷歌是真的吊!!

References