WPF开发-使用WebView2加载页面及页面交互(Edge内核)

WebView2

WebView2和CEF相比,在WPF中CEF相当于把渲染的界面生成图片再加载,而WebView2则没有这一步,性能有显著提升。

但是这种方式暂时没有找到支持Flash的方法。

这种方式可以支持Win7,XP尚未进行测试。

但是在安装的时候64位的Win7竟然无法安装32位的WebView2 运行时,所以建议64位的就安装64位的运行时。

官方教程 https://docs.microsoft.com/zh-cn/microsoft-edge/webview2/get-started/wpf

安装运行时

WebView2 实在诱人,最新的 Edge(Chromium) 性能强悍,而且所有使用 WebView2 的应用可以共用一个运行时(说人话就是一个安装了应用时,其他应用就不用装了)。

Windows 11 已经自带 WebView2 ,就连 Office 也会自动部署 WebView2 ,目前 WebView2 已经被部署到 2亿台电脑,并且还在继续增加 …… 未来是属于 WebView2 的。

重要的是 WebView2 仍然支持老旧的、即将被淘汰的 Windows 7 —— 拥有良好的兼容性。

WebView2是依赖于Edge chromium内核的,有如下三种方式可以获取:

  1. 安装开发版的Edge (Chromium),稳定版的Edge目前不支持WebView控件,不知道后续会不会开放。
  2. 安装独立的WebView2 Runtime,它可以独立下载和升级。
  3. 程序内嵌入Edge chromium内核

这三种方式运行效果基本一致,主要特点是:

  • 前两种方式和以前使用IE的浏览器控件非常类似,浏览器内核和程序是分离的,程序可以保持非常小的体积,浏览器内核可以单独升级。

  • 第一种方式目前还不支持Edge的稳定版,无法使用于生产环境

  • 第三种方式和以前的CEF比较类似,将chromium嵌入了程序,可以控制chromium的版本,减少依赖性,同时可以控制浏览器的版本,避免升级导致的不稳定。

    但是相应的程序包会特别大,配置也相对更麻烦。

所以这里我推荐第二种方式。

下载地址:

https://developer.microsoft.com/zh-cn/microsoft-edge/webview2/#download-section

image-20220320101156647

项目使用

使用WebView2

安装Microsoft.Web.WebView2程序包

1
Install-Package Microsoft.Web.WebView2

添加名字空间

1
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"

添加控件

1
<wv2:WebView2 Name="webView" Source="https://www.psvmc.cn"/>

代码设置URL

1
webView.Source = new Uri("https://www.baidu.com");

窗口关闭时要关闭WebView2,否则会报错。

1
2
3
4
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
webView.Dispose();
}

判断运行时是否安装

注意

建议专门一个页面进行检测,检测成功后再跳转到展示页面。

判断是否安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static bool IsInstallWebview2()
{
string result = "";
try
{
result = CoreWebView2Environment.GetAvailableBrowserVersionString();
}
catch (System.Exception)
{
}
if (result == "" || result == null)
{
return false;
}
return true;
}

安装并重启

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
64
/// <summary>
/// 安装并重启
/// </summary>
/// <returns></returns>
public static async Task InstallRuntimeAsync()
{
// 解决下载文件报错:请求被中止: 未能创建 SSL/TLS 安全通道
ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;
var webClient = new WebClient();
bool isDownload = false;
string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
string microsoftEdgeWebview2Setup = System.IO.Path.Combine(
desktopPath,
"MicrosoftEdgeWebview2Setup.exe"
);
try
{
webClient.DownloadFileCompleted +=
(
s,
e
) =>
{
if (e.Error != null)
{
Console.WriteLine(@"下载失败:" + e.Error.Message);
}
else if (e.Cancelled)
{
Console.WriteLine(@"下载取消");
}
else
{
isDownload = true;
}
};
await webClient.DownloadFileTaskAsync(
"https://go.microsoft.com/fwlink/p/?LinkId=2124703",
microsoftEdgeWebview2Setup
);
}
catch (Exception)
{
// ignored
}
if (isDownload)
{
await Task.Delay(300);
await Task.Run(
() =>
{
Process.Start(
microsoftEdgeWebview2Setup,
" /install"
)?.WaitForExit();
}
);
if (IsInstallWebview2())
{
Process.Start(Application.ResourceAssembly.Location);
Application.Current.Shutdown();
}
}
}

调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private async void Window_Loaded(object sender, RoutedEventArgs e)
{
webView.CoreWebView2InitializationCompleted += WebView_CoreWebView2InitializationCompleted;
sendBtn.Click += SendBtn_ClickAsync;
await webView.EnsureCoreWebView2Async();
// 判断Webview2是否安装
if (!ZWebView2Utils.IsInstallWebview2())
{
await ZWebView2Utils.InstallRuntimeAsync();
}
else
{
Console.WriteLine(@"Webview2已安装");
}
}

其中

1
ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;

是为了解决在Win7环境下载文件报错:

请求被中止: 未能创建 SSL/TLS 安全通道

其中重启应用

1
2
Process.Start(Application.ResourceAssembly.Location);
Application.Current.Shutdown();

工具类

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
using System.Diagnostics;
using Microsoft.Web.WebView2.Core;

namespace SchoolClient.Utils
{
using System;
using System.Net;
using System.Threading.Tasks;
using System.Windows;

public class ZWebView2Utils
{
/// <summary>
/// 是否安装Webview2
/// </summary>
/// <returns></returns>
public static bool IsInstallWebview2()
{
string result = "";
try
{
result = CoreWebView2Environment.GetAvailableBrowserVersionString();
}
catch (Exception)
{
// ignored
}
if (string.IsNullOrEmpty(result))
{
return false;
}
return true;
}

/// <summary>
/// 打开Webview2下载页面
/// </summary>
public static void OpenDownloadPage()
{
Process.Start("https://go.microsoft.com/fwlink/p/?LinkId=2124703");
}

/// <summary>
/// 安装并重启
/// </summary>
/// <returns></returns>
public static async Task InstallRuntimeAsync()
{
// 解决下载文件报错:请求被中止: 未能创建 SSL/TLS 安全通道
ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;
var webClient = new WebClient();
bool isDownload = false;
string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
string microsoftEdgeWebview2Setup = System.IO.Path.Combine(
desktopPath,
"MicrosoftEdgeWebview2Setup.exe"
);
try
{
webClient.DownloadFileCompleted +=
(
s,
e
) =>
{
if (e.Error != null)
{
Console.WriteLine(@"下载失败:" + e.Error.Message);
}
else if (e.Cancelled)
{
Console.WriteLine(@"下载取消");
}
else
{
isDownload = true;
}
};
await webClient.DownloadFileTaskAsync(
"https://go.microsoft.com/fwlink/p/?LinkId=2124703",
microsoftEdgeWebview2Setup
);
}
catch (Exception)
{
// ignored
}
if (isDownload)
{
await Task.Delay(300);
await Task.Run(
() =>
{
Process.Start(
microsoftEdgeWebview2Setup,
" /install"
)?.WaitForExit();
}
);
if (IsInstallWebview2())
{
Process.Start(Application.ResourceAssembly.Location);
Application.Current.Shutdown();
}
}
}
}
}

加载本地文件

你可以读取HTML文件,然后读取NavigateToString

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void Window_Loaded(object sender, RoutedEventArgs e)
{
webView.CoreWebView2InitializationCompleted += WebView_CoreWebView2InitializationCompleted;
webView.EnsureCoreWebView2Async();
}

private void WebView_CoreWebView2InitializationCompleted(object sender, Microsoft.Web.WebView2.Core.CoreWebView2InitializationCompletedEventArgs e)
{
string rootPath = Environment.CurrentDirectory;
string filepath = System.IO.Path.Combine(rootPath, "html", "index.html");
Console.WriteLine(filepath);
if (webView != null && webView.CoreWebView2 != null)
{
string text = System.IO.File.ReadAllText(filepath);
webView.CoreWebView2.NavigateToString(text);
}
}

或者

你也可以通过Navigate连接到本地文件:

1
2
3
string rootPath = Environment.CurrentDirectory;
string filepath = System.IO.Path.Combine(rootPath, "html", "index.html");
webView.Source = new Uri("file:///" + filepath);

互操作

C#向JS发消息

本机代码

1
2
3
4
if (webView != null && webView.CoreWebView2 != null)
{
webView.CoreWebView2.PostWebMessageAsString(inputTB.Text);
}

JS代码

1
2
3
4
5
<script type="text/javascript">
window.chrome.webview.addEventListener('message', arg => {
document.querySelector(".outer").innerHTML = arg.data;
});
</script>

或者发送JSON

1
2
3
4
if (webView != null && webView.CoreWebView2 != null)
{
webView.CoreWebView2.PostWebMessageAsJson("{\"color\":\"blue\"}");
}

JS接收

1
2
3
4
5
<script type="text/javascript">
window.chrome.webview.addEventListener('message', arg => {
document.querySelector(".outer").innerHTML = arg.data.color;
});
</script>

唯一的差别在于

接收的时候会自动转换为JSON对象。不过我还是建议传递字符串,转换的操作放在JS中处理。

C#调用JS代码

1
2
3
4
5
6
7
private async void SendBtn_ClickAsync(object sender, RoutedEventArgs e)
{
if (webView != null && webView.CoreWebView2 != null)
{
await webView.CoreWebView2.ExecuteScriptAsync("alert('123')");
}
}

或者调用JS方法

1
2
3
function showmsg(msg) {
alert(msg);
}

本机代码

1
2
3
4
5
6
7
private async void SendBtn_ClickAsync(object sender, RoutedEventArgs e)
{
if (webView != null && webView.CoreWebView2 != null)
{
await webView.CoreWebView2.ExecuteScriptAsync("showmsg('你好')");
}
}

JS调用C#代码

定义数据交互的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[ClassInterface(ClassInterfaceType.AutoDual)]
[ComVisible(true)]
public class ScriptCallbackObject
{
public string GetMessageInfo()
{
return "C#返回的信息";
}

public void ShowMessage(string arg)
{
Console.WriteLine(arg);
MessageBox.Show("【网页调用C#】:" + arg);
}
}

代码中注册事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void Window_Loaded(object sender, RoutedEventArgs e)
{
webView.CoreWebView2InitializationCompleted += WebView_CoreWebView2InitializationCompleted;
webView.EnsureCoreWebView2Async();
}

private void WebView_CoreWebView2InitializationCompleted(object sender, Microsoft.Web.WebView2.Core.CoreWebView2InitializationCompletedEventArgs e)
{
if (webView != null && webView.CoreWebView2 != null)
{
//注册csobj脚本c#互操作
webView.CoreWebView2.AddHostObjectToScript("csobj", new ScriptCallbackObject());
//注册全局变量csobj
webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("var csobj = window.chrome.webview.hostObjects.csobj;");
webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("var csobj_sync= window.chrome.webview.hostObjects.sync.csobj;");

//加载页面
string rootPath = Environment.CurrentDirectory;
string filepath = System.IO.Path.Combine(rootPath, "html", "index.html");
string text = System.IO.File.ReadAllText(filepath);
webView.CoreWebView2.NavigateToString(text);
}
}

HTML中

异步调用取值

1
2
3
4
5
6
7
8
9
10
<script type="text/javascript">
function myfunc() {
window.chrome.webview.hostObjects.csobj.GetMessageInfo()
.then(
msg => {
document.querySelector(".mytext").innerText = msg;
}
)
}
</script>

当然上面的代码也可以简写为

1
2
3
4
5
6
7
8
function myfunc() {
csobj.GetMessageInfo()
.then(
msg => {
document.querySelector(".mytext").innerText = msg;
}
)
}

这是因为我们已经在C#中创建了JS的对象

1
webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("var csobj = window.chrome.webview.hostObjects.csobj;");

同步调用取值

1
2
3
4
5
6
<script type="text/javascript">
function myfunc() {
var msg = window.chrome.webview.hostObjects.sync.csobj.GetMessageInfo();
document.querySelector(".mytext").innerText = msg;
}
</script>

同步调用

1
2
3
4
5
<script type="text/javascript">
function myfunc() {
window.chrome.webview.hostObjects.sync.csobj.ShowMessage("你好吗");
}
</script>

注意

window.chrome.webview.hostObjects.csobj是异步的,要想同步就要用window.chrome.webview.hostObjects.sync.csobj

打包

程序运行的时候会在根目录生成一个文件夹,类似于xhschool.exe.WebView2,也就是可执行程序名.WebView2

这个是运行过程中生成的,打包并不需要打包到安装包中,可以设置忽略。

Flash支持

很遗憾,现在还没找到WebView2支持Flash的方式。

目前要想支持Flash只有两种选择:

重新渲染

在个别电脑上出现奇葩的问题,网页加载后,页面不渲染。

这里只能通过InvalidateVisual重新触发渲染。

InvalidateVisualInvalidateArrangeInvalidateMeasure 是 WPF 中三个不同的方法,用于标记需要进行重新绘制、重新排列和重新测量的元素。

  1. InvalidateVisual: 该方法用于标记元素和其子元素需要重新绘制视觉呈现。

    当元素的外观或视觉效果发生改变时,通过调用该方法告诉 WPF 引擎需要更新元素的可视化。比如当一个元素的背景色、边框样式或其他可视化属性改变时,可以调用 InvalidateVisual 来触发重绘操作。

  2. InvalidateArrange: 该方法用于标记元素及其子元素需要进行重新排列。

    当元素的大小、位置或布局参数发生改变时,通过调用该方法告诉布局系统需要重新计算元素的布局。调用 InvalidateArrange 方法会触发元素的 ArrangeOverride 方法,从而实现重新排列。

  3. InvalidateMeasure: 该方法用于标记元素及其子元素需要进行重新测量。

    当元素的大小或布局参数发生改变时,通过调用该方法告诉布局系统需要重新计算元素的测量。调用 InvalidateMeasure 方法会触发元素的 MeasureOverride 方法,从而实现重新测量。

总结来说

InvalidateVisual 用于重新绘制元素的视觉呈现

InvalidateArrange 用于重新排列元素

InvalidateMeasure 用于重新测量元素

它们通常一起使用,以确保元素能够正确地进行绘制、排列和测量,以反映最新的属性或状态的变化。