转载自 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,谷歌是真的吊!!