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