WPF中使用CEFSharp加载网页及交互

前言

现在常用的方案

  • 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.CommonCefSharp.Wpf ,至于 cef.redist.x64cef.redist.x86会自动安装。

image-20211029112909887

配置解决方案平台

因为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
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
public App()
{
// Add Custom assembly resolver
AppDomain.CurrentDomain.AssemblyResolve += Resolver;
//Any CefSharp references have to be in another method with NonInlining
// attribute so the assembly rolver has time to do it's thing.
InitializeCefSharp();
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static void InitializeCefSharp()
{
var settings = new CefSettings();

// Set BrowserSubProcessPath based on app bitness at runtime
settings.BrowserSubprocessPath = Path.Combine(
AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
Environment.Is64BitProcess ? "x64" : "x86",
"CefSharp.BrowserSubprocess.exe"
);

// Make sure you set performDependencyCheck false
Cef.Initialize(settings, performDependencyCheck: false, browserProcessHandler: null);
}

// Will attempt to load missing assembly from either x86 or x64 subdir
// Required by CefSharp to load the unmanaged dependencies when running using AnyCPU
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)
{
// Avoid loading CEF in designer
if (DesignerProperties.GetIsInDesignMode(this)) {
return;
}
// Avoid NRE in AbstractRenderHandler.OnPaint
ApplyTemplate();
}
}
}

动态添加和操作控件:

1
2
3
4
5
6
7
8
private CollapsableChromiumWebBrowser MyBrowser = null;
private void InitWebBrower() {
MyBrowser = new CollapsableChromiumWebBrowser();
//页面插入控件
ctrlBrowerGrid.Children.Add(MyBrowser);
//这里不能用Load()的方法,会报错。
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;
//这里不能用Load()的方法,会报错。
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 。

image-20211029152817660

代码

全局初始化

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", "");
//Disable GPU acceleration
cSettings.CefCommandLineArgs.Add("disable-gpu","1");
//Disable GPU vsync
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;
}

// Create a browser component
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
//Old Method
MyBrowser.RegisterAsyncJsObject("callbackObj", new CallbackObjectForJs(), options: BindingOptions.DefaultBinder);

//Replaced with
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不是直接渲染在控件上的,它的大概流程如下:

  1. CEFSharp.WPF的ChromiumWebBrowser控件本质上是一个图片
  2. 而是通过离屏渲染的方式渲染在缓冲区里,
  3. 绘制完成后,然后将缓冲区的数据传递到InteropBitmap中去
  4. 将InteropBitmap作为ChromiumWebBrowser的图源更新

这个基本上是类似于WPF的视频播放器的做法:先离屏渲染出图片,在将图片更新到界面。这个做法由于是使用的WPF的原生渲染方案,可以说是WPF的原生控件的,本身是有不少好处的:

  1. 可以支持透明背景
  2. 可以在上面叠加其它WPF控件
  3. 可以支持WPF的变形,动画,裁剪等特效

简单一句话,是可以和WPF程序无缝集成的,如果将WEB界面作为控件嵌入式再方便不过的。

但是,它这个实现是有代价的:

  1. 离屏渲染本身需要多一层工序,
  2. 有切换上下文和内存拷贝的开销。
  3. 更要命的是,貌似目前GPU离线渲染视频效果还不是很好,因此默认还把gpu加速给关了,性能更下降了一截。

另外,InteropBitmap传递图片内存的效率本身就不高,不光吃cpu,还吃内存,网上也有人讨论过。

针对这些问题,有人建议使用WritableBitmap替换InteropBitmap,但貌似作者认为InteropBitmap的效率更好些。

我使用过WritableBitmap离屏渲染地图,应该是能做到比当前更好的性能的。

也有人建议使用效率更高的D3DImage(WPF原生支持这个,不过麻烦些),可能作者觉得目前它的这个性能问题不是首要解决的目标吧,也一直没有采纳。

最后说一下解决方法吧,虽然在大部分的情况下,当前的解决方案是能满足我们的需求的,不过如果遇到非要解决的情况下,可以使用下WinFrom版的CEFSharp,通过WinFormHost来集成到WPF程序中去。

WinFrom版的CEFSharp应该是直接渲染的,我试了一下,效率基本上接近Chrome,并且由于他们的基础库是公用的,在WPF程序中WinFrom版和WPF版的CEF是可以并存的,用起来还算方便。

WPF使用WindowsFormsHost加载Winform控件

WindowsFormsHost是置顶的,不能像WPF控件那样层叠实现。

WindowsFormsHost使用起来相当简单。 它有一个Child属性,你可以在其中定义一个WinForms控件,就像WPF Window只保存一个根控件一样。 如果在WindowsFormsHost中需要来自WinForms的更多控件,可以使用WinForms中的Panel控件或任何其他容器控件。

使用方法:

首先,我们需要向项目中的引用(reference)中添加两个动态库dll

  • 一个是.NET库中的System.Windows.Forms

  • 另外一个是WindowsFormsIntegration

添加完两个动态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>

WPF中加载WinForm版的CEF

WinForm版的CEF加载页面的显示效果比较流畅,这里就介绍怎么在WPF中加载WinForm版的CEF

添加如下依赖(圈中的两个)

image-20211101142700009

添加头

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毫秒再显示就不会有黑屏的现象了。

注意如果程序有多个窗口,不要在窗口关闭时调用下面的方法,会导致其他窗口也不可用。

1
Cef.Shutdown();

启动加速

在实际使用过程中,发现有的客户端会出现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插件支持不完整,只支持部分功能。

20220511110557