前言 现在常用的方案
Duilib/(Duilib+CEF)只支持Windows的选择,优点是打包文件小(使用C++)(QQ、微信、有道精品课)。
Qt/(Qt+CEF) 支持跨平台,缺点是打包文件大(使用C++)这是很多客户端跨平台的首选,UI 库和各种功能的类库非常丰富,但是学习成本比较高。(WPS)。
WinForm/(WinForm+CEF) 微软第一代的 Desktop 版本,但是从开发体验角度来说自定义、美化控件会比较麻烦。不推荐。
WPF/(WPF+CEFSharp) 打包文件小,但是性能相比前两者弱,但比Electron强,内存占用高,只支持Windows。
Electron 打包文件大,但是性能弱,内存占用高,支持跨平台,开发效率最高。(迅雷)。
上面各种技术只要集成CEF的打包都大,所以上面所说的是在不集成CEF情况下对比的。
几种方案都各有利弊,可以根据团队的情况选用,都是相对可用的,其他的方案比如Flutter,Java就不太推荐。
目前因为C++的技术栈的原因,我们的团队主要用WPF或者是Electron来做桌面端的开发。
网易云信团队基于原 Duilib 魔改的 NIM Duilib库下载地址:https://github.com/netease-im/NIM_Duilib_Framework/
有些界面用WEB开发会更美观和快速,所以这里就来集成CEFSharp来加载WEB页面。
注意
添加CEF会大幅增加安装包大小。
为什么使用CEF
.NET 自带的 WebBrowser 是WEB 开发人员最讨厌的 IE,性能低下而且兼容性差
Webkit: 项目已经不再支持
Cef 是 Chrome 内核,性能和兼容性杠杠的。缺点就是带的 DLL 太多太大,一个发布版应该在150M左右,X86+X64一块就得快300M了。另外EXE加载速度也会稍慢。
CEF官方文档
https://github.com/cefsharp/CefSharp/wiki/CefSharp%E4%B8%AD%E6%96%87%E5%B8%AE%E5%8A%A9%E6%96%87%E6%A1%A3
https://github.com/cefsharp/CefSharp
http://cefsharp.github.io/
安装WPF版本依赖 通过Nuget安装,右击项目 -> 管理Nuget程序包 -> 在打开的界面中搜索CefSharp,依次安装 CefSharp.Common和 CefSharp.Wpf ,至于 cef.redist.x64和 cef.redist.x86会自动安装。
配置解决方案平台 因为CefSharp不支持Any CPU所以要配置x86、x64,点击菜单 生成 -> 配置管理器。
选择解决方案平台,点击编辑,先将x64和x86删掉,再重新新建,重新配置比较容易些。
Any CPU的支持 如果我们要支持Any CPU就要自己实现了。
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 57 58 59 60 61 62 63 using System.Windows;using System;using System.Runtime.CompilerServices;using CefSharp;using System.IO;using System.Reflection;using System.Windows.Threading;using CefSharpWpfDemo.Log;namespace CEFSharpTest { public partial class App : Application { public App () { AppDomain.CurrentDomain.AssemblyResolve += Resolver; InitializeCefSharp(); } [MethodImpl(MethodImplOptions.NoInlining) ] private static void InitializeCefSharp () { var settings = new CefSettings(); settings.BrowserSubprocessPath = Path.Combine( AppDomain.CurrentDomain.SetupInformation.ApplicationBase, Environment.Is64BitProcess ? "x64" : "x86" , "CefSharp.BrowserSubprocess.exe" ); Cef.Initialize(settings, performDependencyCheck: false , browserProcessHandler: null ); } private static Assembly Resolver (object sender, ResolveEventArgs args ) { if (args .Name.StartsWith("CefSharp" )) { string assemblyName = args .Name.Split(new [] { ',' }, 2 )[0 ] + ".dll" ; string archSpecificPath = Path.Combine( AppDomain.CurrentDomain.SetupInformation.ApplicationBase, Environment.Is64BitProcess ? "x64" : "x86" , assemblyName ); return File.Exists(archSpecificPath) ? Assembly.LoadFile(archSpecificPath) : null ; } return null ; } } }
使用 使用时可以直接在xaml文件中直接添加ChromiumWebBrowser控件,不过ChromiumWebBrowser控件特别消耗内存,所以代码里动态添加也是一种不错的选择。
Xaml中添加 xmal文件头部插入引用
1 xmlns:wpf="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf"
添加控件如下:
1 2 3 <Grid x:Name ="ctrlBrowerGrid" > <wpf:ChromiumWebBrowser x:Name ="Browser" /> </Grid >
cs文件中操作控件访问网址:
1 Browser.Load("https://www.psvmc.cn" );
代码中添加 添加浏览器类:
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 using CefSharp.Wpf;using System.ComponentModel;using System.Windows;namespace CEFSharpTest.view { internal sealed class CollapsableChromiumWebBrowser : ChromiumWebBrowser { public CollapsableChromiumWebBrowser () { Loaded += BrowserLoaded; } private void BrowserLoaded (object sender, RoutedEventArgs e ) { if (DesignerProperties.GetIsInDesignMode(this )) { return ; } ApplyTemplate(); } } }
动态添加和操作控件:
1 2 3 4 5 6 7 8 private CollapsableChromiumWebBrowser MyBrowser = null ;private void InitWebBrower () { MyBrowser = new CollapsableChromiumWebBrowser(); ctrlBrowerGrid.Children.Add(MyBrowser); MyBrowser.Address = "https://www.psvmc.cn" ; }
获取Cookie和Html 添加Cookie访问类
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 using CefSharp;using System;namespace CEFSharpTest.view { public class CookieVisitor : ICookieVisitor { private string Cookies = null ; public event Action<object > Action; public bool Visit (Cookie cookie, int count, int total, ref bool deleteCookie ) { if (count == 0 ) Cookies = null ; Cookies += cookie.Name + "=" + cookie.Value + ";" ; deleteCookie = false ; return true ; } public void Dispose () { if (Action != null ) Action(Cookies); return ; } } }
浏览器控件访问网址,并设置回调
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 private CollapsableChromiumWebBrowser MyBrowser = null ;private void InitWebBrower (){ MyBrowser = new CollapsableChromiumWebBrowser(); ctrlBrowerGrid.Children.Add(MyBrowser); MyBrowser.FrameLoadEnd += Browser_FrameLoadEnd; MyBrowser.Address = "https://www.psvmc.cn" ; } private async void Browser_FrameLoadEnd (object sender, FrameLoadEndEventArgs e ){ CookieVisitor visitor = new CookieVisitor(); string html = await MyBrowser.GetSourceAsync(); Console.WriteLine("html:" + html); visitor.Action += RecieveCookie; Cef.GetGlobalCookieManager().VisitAllCookies(visitor); return ; } public async void RecieveCookie (object data ){ string cookies = (string )data; Console.WriteLine("cookies:" + cookies); return ; }
加载本地页面和JS回调 添加HTML 项目下添加html路径html\index.html
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 <!DOCTYPE html > <html > <head > <title > </title > <meta charset ="utf-8" /> <script type ="text/javascript" > function callback ( ) { callbackObj.showMessage ('message from js' ); } function alert_msg (msg ) { alert (msg); } </script > </head > <body > <button onclick ="callback()" > Click</button > <style > * { margin : 0 ; padding : 0 ; } body { background-color : #f3f3f3 ; width : 100vw ; height : 100vh ; display :flex; align-items :center; justify-content :center; } </style > </body > </html >
复制页面 复制页面到目标目录
方式1 项目->属性->生成事件->生成前事件命令行
添加如下
1 xcopy /Y /i /e $(ProjectDir)\html $(TargetDir)\html
方式2 文件右键点击属性,设置复制到输出目录和生成操作。
如果文件较多建议用方式1 。
代码 全局初始化 Application中初始化默认配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 CefSettings cSettings = new CefSettings() { Locale = "zh-CN" , CachePath = Directory.GetCurrentDirectory() + @"\Cache" }; cSettings.MultiThreadedMessageLoop = true ; cSettings.CefCommandLineArgs.Add("proxy-auto-detect" , "0" ); cSettings.CefCommandLineArgs.Add("--disable-web-security" , "" ); cSettings.CefCommandLineArgs.Add("disable-gpu" ,"1" ); cSettings.CefCommandLineArgs.Add("disable-gpu-vsync" ); cSettings.CefCommandLineArgs.Add("enable-media-stream" , "1" ); Cef.Initialize(cSettings);
注意
上面的代码(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 private ChromiumWebBrowser MyBrowser = null ;private void InitWebBrower (){ string pagepath = string .Format(@"{0}html\index.html" , AppDomain.CurrentDomain.BaseDirectory); if (!File.Exists(pagepath)) { MessageBox.Show("HTML不存在: " + pagepath); return ; } MyBrowser = new ChromiumWebBrowser(); MyBrowser.MenuHandler = new MenuHandler(); MyBrowser.LifeSpanHandler = new LifeSpanHandler(); MyBrowser.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#f3f3f3" )); ctrlBrowerGrid.Children.Add(MyBrowser); MyBrowser.Address = pagepath; MyBrowser.JavascriptObjectRepository.Settings.LegacyBindingEnabled = true ; MyBrowser.JavascriptObjectRepository.Register( "callbackObj" , new CallbackObjectForJs(), isAsync: true , options: BindingOptions.DefaultBinder ); }
调用JS方法
1 2 3 4 private void Button_Click (object sender, RoutedEventArgs e ){ MyBrowser.ExecuteScriptAsync("alert_msg('123')" ); }
事件回调类
1 2 3 4 5 6 7 public class CallbackObjectForJs { public void showMessage (string msg ) { MessageBox.Show(msg); } }
禁用右键菜单的类
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 public class MenuHandler : IContextMenuHandler { public void OnBeforeContextMenu ( IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model ) { model.Clear(); } public bool OnContextMenuCommand ( IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags ) { return false ; } public void OnContextMenuDismissed (IWebBrowser browserControl, IBrowser browser, IFrame frame ) { } public bool RunContextMenu ( IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model, IRunContextMenuCallback callback ) { return false ; } }
原窗口打开链接的类
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 public class LifeSpanHandler : ILifeSpanHandler { public bool OnBeforePopup ( IWebBrowser webBrowser, IBrowser browser, IFrame frame, string targetUrl, string targetFrameName, WindowOpenDisposition targetDisposition, bool userGesture, IPopupFeatures popupFeatures, IWindowInfo windowInfo, IBrowserSettings browserSettings, ref bool noJavascriptAccess, out IWebBrowser newBrowser ) { newBrowser = null ; var chromiumWebBrowser = (ChromiumWebBrowser)webBrowser; chromiumWebBrowser.Load(targetUrl); return true ; } public void OnAfterCreated (IWebBrowser chromiumWebBrowser, IBrowser browser ) { } public bool DoClose (IWebBrowser chromiumWebBrowser, IBrowser browser ) { return true ; } public void OnBeforeClose (IWebBrowser chromiumWebBrowser, IBrowser browser ) { } }
注意项 API变更
1 2 3 4 5 6 MyBrowser.RegisterAsyncJsObject("callbackObj" , new CallbackObjectForJs(), options: BindingOptions.DefaultBinder); MyBrowser.JavascriptObjectRepository.Settings.LegacyBindingEnabled = true ; MyBrowser.JavascriptObjectRepository.Register("callbackObj" , new CallbackObjectForJs(), isAsync: true , options: BindingOptions.DefaultBinder);
本地文件路径
文件路径中不能包含特殊字符,否则不能加载,之前我的项目在C#目录下,就一直加载不了页面。
性能 在使用CEF的过程中,我发现了一个现象:
WPF版的CEF比Chrome性能要差:一些有动画的地方会掉帧(例如,CSS动画,全屏图片拖动等),视频播放的效果也没有Chrome流畅。
查了一下相关资料,发现CEFSharp.WPF不是直接渲染在控件上的,它的大概流程如下:
CEFSharp.WPF的ChromiumWebBrowser控件本质上是一个图片
而是通过离屏渲染的方式渲染在缓冲区里,
绘制完成后,然后将缓冲区的数据传递到InteropBitmap中去
将InteropBitmap作为ChromiumWebBrowser的图源更新
这个基本上是类似于WPF的视频播放器的做法:先离屏渲染出图片,在将图片更新到界面。这个做法由于是使用的WPF的原生渲染方案,可以说是WPF的原生控件的,本身是有不少好处的:
可以支持透明背景
可以在上面叠加其它WPF控件
可以支持WPF的变形,动画,裁剪等特效
简单一句话,是可以和WPF程序无缝集成的,如果将WEB界面作为控件嵌入式再方便不过的。
但是,它这个实现是有代价的:
离屏渲染本身需要多一层工序,
有切换上下文和内存拷贝的开销。
更要命的是,貌似目前GPU离线渲染视频效果还不是很好,因此默认还把gpu加速给关了,性能更下降了一截。
另外,InteropBitmap传递图片内存的效率本身就不高,不光吃cpu,还吃内存,网上也有人讨论过。
针对这些问题,有人建议使用WritableBitmap替换InteropBitmap,但貌似作者认为InteropBitmap的效率更好些。
我使用过WritableBitmap离屏渲染地图,应该是能做到比当前更好的性能的。
也有人建议使用效率更高的D3DImage(WPF原生支持这个,不过麻烦些),可能作者觉得目前它的这个性能问题不是首要解决的目标吧,也一直没有采纳。
最后说一下解决方法吧,虽然在大部分的情况下,当前的解决方案是能满足我们的需求的,不过如果遇到非要解决的情况下,可以使用下WinFrom版的CEFSharp,通过WinFormHost来集成到WPF程序中去。
WinFrom版的CEFSharp应该是直接渲染的,我试了一下,效率基本上接近Chrome,并且由于他们的基础库是公用的,在WPF程序中WinFrom版和WPF版的CEF是可以并存的,用起来还算方便。
WindowsFormsHost是置顶的,不能像WPF控件那样层叠实现。
WindowsFormsHost使用起来相当简单。 它有一个Child属性,你可以在其中定义一个WinForms控件,就像WPF Window只保存一个根控件一样。 如果在WindowsFormsHost中需要来自WinForms的更多控件,可以使用WinForms中的Panel 控件或任何其他容器控件。
使用方法:
首先,我们需要向项目中的引用(reference)中添加两个动态库dll
添加完两个动态dll以后,就可以在控件库中找到WindowsFormsHost这个控件;
将这个控件放入窗体,放置完以后在xmal代码中会自动生成相应代码:
1 2 3 <Grid > <WindowsFormsHost /> </Grid >
然后,需要在xmal的开始处添加两行代码 :
1 2 xmlns:WinFormHost="clr-namespace:System.Windows.Forms.Integration;assembly=WindowsFormsIntegration" xmlns:WinFormControls="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
这样就可以在WindowsFormsHost下放置需要的Windows Form控件了。
1 2 3 <WindowsFormsHost > <WinFormControls:Button Text ="WinformButton" /> </WindowsFormsHost >
WinForm版的CEF加载页面的显示效果比较流畅,这里就介绍怎么在WPF中加载WinForm版的CEF
添加如下依赖(圈中的两个)
添加头
1 xmlns:winforms="clr-namespace:CefSharp.WinForms;assembly=CefSharp.WinForms"
添加组件
1 2 3 4 5 <Grid > <WindowsFormsHost > <winforms:ChromiumWebBrowser x:Name ="MyBrowser" > </winforms:ChromiumWebBrowser > </WindowsFormsHost > </Grid >
加载页面
1 2 3 4 5 6 7 private async void initMyBrowser (){ MyBrowser.Visible = false ; MyBrowser.Load("https://www.psvmc.cn" ); await Task.Delay(200 ); MyBrowser.Visible = true ; }
注意
默认直接加载页面会闪一下黑色的背景,所以这里先隐藏,延迟200毫秒再显示就不会有黑屏的现象了。
注意如果程序有多个窗口,不要在窗口关闭时调用下面的方法,会导致其他窗口也不可用。
启动加速 在实际使用过程中,发现有的客户端会出现chrome加载网页过慢问题,定位后发现很多是因为设置系统代理所致,此时可以通过如下启动参数禁止系统代理。
1 2 {"proxy-auto-detect" , "0" }, {"no-proxy-server" , "1" },
另外一个小技巧是: 由于cef本身是一个独立的进程,我们不需要等待主窗口加载完成后再创建ChromiumWebBrowser,单独启动它也不影响主程序启动速度,
因此可以将ChromiumWebBrowser和主窗口一并启动。
1 2 3 4 5 6 7 8 public MainWindow (){ var setting = new CefSettings(); CefSharp.Cef.Initialize(setting); var chrome = new ChromiumWebBrowser() {Address = "http://www.baidu.com" }; InitializeComponent(); }
主窗口加载完成后,再将chrome放置到相应的控件上。
需要说明的是,ChromiumWebBrowser只有防止到窗口才开始渲染,要想预先渲染,可以先新建一个临时窗口,把这个临时窗口显示到屏幕外面去。要用ChromiumWebBrowser的时候再放置到我们的实际窗体中。
Flash支持 找到本地flash的dll(pepflashplayer.dll)
项目中新建plugins,添加pepflashplayer.dll,右击属性,改为始终复制
CEF初始化配置
1 2 3 4 5 CefSettings settings = new CefSharp.CefSettings() settings.CefCommandLineArgs["enable-system-flash" ] = "1" ; settings.CefCommandLineArgs.Add("ppapi-flash-version" , "32.0.0.171" ); settings.CefCommandLineArgs.Add("ppapi-flash-path" , @"plugins\pepflashplayer.dll" ); Cef.Initialize(settings);
Chrome插件支持 CEF只是集成了Chromium的Content API,对Chrome插件支持不完整,只支持部分功能。