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

2021-11-01 16:30:49 浏览数 (1)

前言

现在常用的方案

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

配置解决方案平台

因为CefSharp不支持Any CPU所以要配置x86、x64,点击菜单 生成 -> 配置管理器

选择解决方案平台,点击编辑,先将x64和x86删掉,再重新新建,重新配置比较容易些。

Any CPU的支持

如果我们要支持Any CPU就要自己实现了。

代码语言:javascript复制
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文件头部插入引用

代码语言:javascript复制
xmlns:wpf="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf"

添加控件如下:

代码语言:javascript复制
<Grid x:Name="ctrlBrowerGrid">
    <wpf:ChromiumWebBrowser x:Name="Browser"/>
</Grid>

cs文件中操作控件访问网址:

代码语言:javascript复制
Browser.Load("https://www.psvmc.cn");

代码添加浏览器

添加浏览器类:

代码语言:javascript复制
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();
        }
    }
}

动态添加和操作控件:

代码语言:javascript复制
private CollapsableChromiumWebBrowser MyBrowser = null;
private void InitWebBrower() {
    MyBrowser = new CollapsableChromiumWebBrowser();
    //页面插入控件
    ctrlBrowerGrid.Children.Add(MyBrowser);
    //这里不能用Load()的方法,会报错。
    MyBrowser.Address = "https://www.psvmc.cn";
}

获取Cookie和Html

添加Cookie访问类

代码语言:javascript复制
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;
        }
    }
}

浏览器控件访问网址,并设置回调

代码语言:javascript复制
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路径htmlindex.html

代码语言:javascript复制
<!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

项目->属性->生成事件->生成前事件命令行

添加如下

代码语言:javascript复制
xcopy /Y /i /e $(ProjectDir)html $(TargetDir)html

方式2

文件右键点击属性,设置复制到输出目录和生成操作。

如果文件较多建议用方式1 。

代码

注册一个JS对象

代码语言:javascript复制
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}htmlindex.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方法

代码语言:javascript复制
private void Button_Click(object sender, RoutedEventArgs e)
{
    MyBrowser.ExecuteScriptAsync("alert_msg('123')");
}

事件回调类

代码语言:javascript复制
public class CallbackObjectForJs
{
    public void showMessage(string msg)
    {
        MessageBox.Show(msg);
    }
}

禁用右键菜单的类

代码语言:javascript复制
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;
    }
}

原窗口打开链接的类

代码语言:javascript复制
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变更

代码语言:javascript复制
//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#目录下,就一直加载不了页面。

0 人点赞