转载自 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" class ClientApp : public CefApp, public CefRenderProcessHandler {public : ClientApp (); CefRefPtr<CefRenderProcessHandler> GetRenderProcessHandler () OVERRIDE { return this ; } virtual void OnContextCreated (CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context) OVERRIDE ; virtual void OnContextReleased (CefRefPtr< CefBrowser > browser, CefRefPtr< CefFrame > frame, CefRefPtr< CefV8Context > context) OVERRIDE ; private : void RegisterFunction (CefRefPtr<CefV8Value> object) ; 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 { 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 ; } 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" class HtmlEventHandler : public CefV8Handler{ public : HtmlEventHandler (CefRefPtr<CefBrowser> browser); 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 (); 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 ; 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; 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 ()) { CefRefPtr<CefFrame> frame = browser->GetMainFrame (); if (frame) { 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,谷歌是真的吊!!