前言
现在常用的方案
- Duilib+CEF 只支持Windows的选择,优点是打包文件小(使用C++) QQ、微信、有道精品课。
- Qt+CEF 支持跨平台,缺点是打包文件大(使用C++)。
- WPF/(WPF+CEFSharp) 打包文件小,但是性能相比前两者弱,但比Electron强,内存占用高,只支持Windows。
- Electron 打包文件大,但是性能弱,内存占用高,支持跨平台。
几种方案都各有利弊,可以根据团队的情况选用,都是相对不错的,其他的方案比如Flutter,Java就不太推荐。
目前因为C++的技术栈的原因,我们的团队主要用WPF或者是Electron来做桌面端的开发。
有些界面用web开发会更好一点,所以这里就来集成CEFSharp来加载
注意
添加CEF会大幅增加安装包大小。
为什么使用CEF
- .NET 自带的 WebBrowser 是WEB 开发人员最讨厌的 IE,性能低下而且兼容性差
- Webkit: 项目已经不再支持
- Cef 是 Chrome 内核,性能和兼容性杠杠的。缺点就是带的 DLL 太多太大,一个发布版应该在150M左右,X86+X64一块就得快300M了。另外EXE加载速度也会稍慢。
安装依赖
通过Nuget安装,右击项目 -> 管理Nuget程序包 -> 在打开的界面中搜索CefSharp,依次安装 CefSharp.Common
和 CefSharp.Wpf
,至于 cef.redist.x64
和 cef.redist.x86
会自动安装。
配置解决方案平台
因为CefSharp不支持Any CPU
所以要配置x86、x64,点击菜单 生成
-> 配置管理器
。
选择解决方案平台,点击编辑,先将x64和x86删掉,再重新新建,重新配置比较容易些。
Any CPU的支持
如果我们要支持Any CPU
就要自己实现了。
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文件头部插入引用
xmlns:wpf="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf"
添加控件如下:
<Grid x:Name="ctrlBrowerGrid">
<wpf:ChromiumWebBrowser x:Name="Browser"/>
</Grid>
cs文件中操作控件访问网址:
Browser.Load("https://www.psvmc.cn");
代码添加浏览器
添加浏览器类:
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();
}
}
}
动态添加和操作控件:
private CollapsableChromiumWebBrowser MyBrowser = null;
private void InitWebBrower() {
MyBrowser = new CollapsableChromiumWebBrowser();
//页面插入控件
ctrlBrowerGrid.Children.Add(MyBrowser);
//这里不能用Load()的方法,会报错。
MyBrowser.Address = "https://www.psvmc.cn";
}
获取Cookie和Html
添加Cookie访问类
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;
}
}
}
浏览器控件访问网址,并设置回调
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
<!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
项目
->属性
->生成事件
->生成前事件命令行
添加如下
xcopy /Y /i /e $(ProjectDir)\html $(TargetDir)\html
方式2
文件右键点击属性,设置复制到输出目录和生成操作。
如果文件较多建议用方式1 。
代码
注册一个JS对象
private ChromiumWebBrowser MyBrowser = null;
private void InitWebBrower()
{
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");
//Disable GPU vsync
cSettings.CefCommandLineArgs.Add("disable-gpu-vsync");
//此配置可以允许摄像头打开摄像
cSettings.CefCommandLineArgs.Add("enable-media-stream", "1");
Cef.Initialize(cSettings);
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方法
private void Button_Click(object sender, RoutedEventArgs e)
{
MyBrowser.ExecuteScriptAsync("alert_msg('123')");
}
事件回调类
public class CallbackObjectForJs
{
public void showMessage(string msg)
{
MessageBox.Show(msg);
}
}
禁用右键菜单的类
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;
}
}
原窗口打开链接的类
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变更
//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#
目录下,就一直加载不了页面。