乘风破浪,遇见最美Windows 11之现代Windows桌面应用开发 - Microsoft Edge WebView2运行时
前言
对现代化Windows桌面应用而言,越来越多的应用程序采用Hybrid混合架构,即原生客户端技术+Web网页技术嵌入的混合模式提供应用服务,这样既有原生技术先天的端能力优势,又有来自Web技术的快速开发、灵活部署的优势。
目前主流的方案肯定是"基于嵌入式Chromium框架(简称CEF)"解决方案,但是带来的问题就是程序安装包体积巨大,因为它必须要把整个Chromium内核完整的打包进去,而微软原生控件WebView或者WebBrowser控件由于其技术或背后浏览器框架跟不上现代步伐,很难满足实际场景需求,那么随着Microsoft Edge积极采用Chromium内核,并被Windows 10/11内置,基于它诞生了WebView2这个控件,未来我们有希望可以直接通过使用WebView2来替代CEF,从而大幅降低混合架构开发模式下的安装包体积,提高程序运行效率。
采用WebView2的核心优势:1、缩小应用程序安装包体积大小。2、降低应用程序磁盘空间占用。3、节约Hybrid架构开发实现成本。4、减少应用分发的CDN流量消耗。5、优化浏览器运行内核维护成本。
核心提要:1、从Windows 11开始的操作系统版本将直接内置WebView2运行时;2、Microsoft 365应用程序v2101版本已开始依赖WebView2运行时提供和Web无差体验的新功能和特性;3、截止到目前,WebView2运行时已被超过2亿台Windows设备部署。4、WebView2运行时自带对H264编码的支持,无需额外编译配置。
什么是Microsoft Edge WebView2
Microsoft Edge WebView2
控件允许在本机应用中嵌入Web技术(HTML、CSS以及JavaScript)。WebView2
控件使用Microsoft Edge(Chromium)
作为绘制引擎,以在本机应用中显示Web内容。使用WebView2,可以在本机应用的不同部分嵌入Web代码,或在单个WebView实例中生成所有本机应用。
什么是WebView2运行时
https://developer.microsoft.com/zh-cn/microsoft-edge/webview2/#download-section
WebView2运行时简介
"WebView2运行时(Webview2 Runtime
)"是一个可再发行运行时,并充当WebView2(或)Web平台的基础组件。此概念类似于Visual C++
/.NET
应用的.NET运行时
。"WebView2运行时"包含经过修改的Microsoft Edge(Chromium)
二进制文件,这些二进制文件针对WebView2应用进行了微调和测试。安装WebView2运行时后,它不会显示为用户可见的浏览器应用。例如,用户没有浏览器桌面快捷方式或"开始"菜单中的条目。
有两种不同的方法将"WebView2运行时"分发和更新到客户端计算机:常青分发模式和离线分发模式。
常青分发模式(Evergreen Runtime
)
在“常青分发模式(Evergreen Runtime
)”下,WebView2运行时不与你的应用打包,但最初使用联机引导程序或脱机安装程序安装到客户端上。之后,WebView2运行时将在客户端计算机上自动更新。然后,你可以从最新的WebView2 SDK分发使用最新WebView2 API的WebView2应用更新。建议大多数开发人员使用常青分发模式。
-
优点:
- 基础Web平台(WebView2运行时)自动更新,无需你进行更多工作。
- 客户端系统上WebView2运行时所需的磁盘空间更少,因为WebView2运行时由客户端上的所有WebView2应用共享。
- 在符合条件的系统上,Microsoft Edge和Evergreen WebView2运行时的二进制文件在同一版本上时硬链接在一起。此链接为磁盘占用、内存和性能带来了好处。
-
缺点:
- WebView2应用不能指定需要WebView2运行时的特定版本。
离线分发模式(Offline Runtime
)
在“离线分发模式(Offline Runtime
)”下,下载特定版本的WebView2运行时,并随应用包中的WebView2应用一起打包它。随应用打包的WebView2运行时仅由WebView2应用使用,而客户端计算机上任何其他应用不会使用。
-
优点:
- 你可以更加控制WebView2运行时的版本控制。你知道哪些WebView2 API可用于你的应用,因为你控制哪个版本的WebView2运行时可用于你的应用。你的应用无需测试是否有最新的API。
-
缺点:
- 你需要自己管理WebView2运行时。WebView2运行时不会在客户端上自动更新,因此若要使用最新的WebView2 API,必须定期更新应用以及更新后的WebView2运行时。
- 如果安装了多个WebView2应用,则客户端上需要更多磁盘空间。
- 离线分发运行时无法通过使用安装程序进行安装。
宣告超过2亿设备的覆盖
https://blogs.windows.com/msedgedev/2021/08/31/webview2-windows-app-sdk-winui2-runtime-cdp-helper/
我们一直在努力提高WebView2运行时在Windows机器上的可用性。我们很高兴地宣布这项工作的两项更新。首先,WebView2运行时将在Windows11机器中内置。其次,我们看到许多应用程序,包括Microsoft Office,开始将WebView2 Runtime与其应用程序一起部署。迄今为止,WebView2 Runtime已安装在超过2亿台Windows设备上! WebView2 Runtime的日益普及将使以首选的Evergreen分发模式部署WebView2应用程序变得更加容易。
WebView2和Microsoft 365应用版
Microsoft 365
应用开始提供依赖"WebView2运行时(Webview2 Runtime
)"的新功能或改进功能。例如,Outlook中的会议室查找器和会议Insights功能。WebView2是Microsoft Edge使用的渲染引擎,在桌面应用程序中显示基于Web的功能。
通过使用"WebView2运行时(Webview2 Runtime
)",我们可以更轻松地为您的用户提供跨设备平台外观和感觉相同的Office功能。反过来,这种一致的体验可帮助您的用户学习和使用这些功能,而无需了解每个设备平台上Office的细微差别。
例如,通过使用"WebView2运行时(Webview2 Runtime
)",在运行Windows的设备上使用Outlook和在Web上使用Outlook时,房间查找器功能看起来相同。Office加载项也将开始依赖"WebView2运行时(Webview2 Runtime
)"。
WebView2要求在运行Office的设备上安装"WebView2运行时(Webview2 Runtime
)"。如果设备上未安装"WebView2运行时(Webview2 Runtime
)",您的用户将无法使用依赖于WebView2的Office功能。
因此,在2021年4月,我们开始在运行Windows且安装了Microsoft 365应用程序版本2101或更高版本的设备上安装"WebView2运行时(Webview2 Runtime
)"。
重要
- "WebView2运行时(
Webview2 Runtime
)"不会在设备上安装Microsoft Edge
(完整浏览器),并且不需要在设备上安装Microsoft Edge
。 - 在设备上安装"WebView2运行时(
Webview2 Runtime
)"后,不会更改用户的默认浏览器选择。
官方示例
勤学勤练
创建解决方案及目录
1. 新建名为"HelloWebView2"的解决方案
dotnet new sln -o HelloWebView2
2. 切换到"HelloWebView2"目录
cd .\HelloWebView2\
创建.Net Core的Wpf项目
1. 创建名为"demoForWpfCore"的Wpf项目
dotnet new wpf -o demoForWpfCore -f net5.0
2. 添加"demoForWpfCore"到解决方案
dotnet sln add .\demoForWpfCore\demoForWpfCore.csproj
3. 切换到"demoForWpfCore"目录
cd .\demoForWpfCore\
4. 运行"demoForWpfCore"项目
dotnet watch run
创建WinUI 3的桌面项目
1. 添加WinUI3的空白项目
在解决方案上右键,添加 => 新建项目,筛选C#
语言,Windows
平台,WinUI
项目类型。
选择"打包的空白应用(桌面版WinUI 3)(Blank App, Packaged(WinUI 3 in desktop)
)"项目类型,然后单击"下一步"按钮。
创建名为demoForWinUi3
的项目。
创建成功之后,会发现多了两个项目,一个是demoForWinUi3
桌面项目,一个是demoForWinUi3 (Package)
打包项目。
创建.Net Framework的Winforms项目
1. 创建名为"demoForWinFormFrame"的WinForms项目
这里需要将框架最低设置为:.Net Framework 4.5,这是目前WebView2的WinFroms包最低兼容版本。
2. 运行"demoForWinFormFrame"项目
.Net Framework WinForms项目添加并使用WebView2控件
.Net Framework WinForms项目安装WebView2包
在demoForWinFormFrame
项目右键进入"管理Nuget程序包"。
搜索关键词WebView2
即可找到Microsoft.Web.WebView2
这个包,安装即可。
初探嵌入WebView2控件
双击打开MainForm.cs
文件,打开窗体设计视图。
在Visual Studio顶部菜单的"视图" => "工具栏",这时候我们会看到顶部会多出来一个WebView2 Windows Forms Control
组,里面有个控件叫WebView2
控件。
我们把它拖到右侧的窗体中,并且填充显示,并且我们给他取名为WebViewForMain
,设置其初始的Source值为https://www.bing.com
。
接下来,我们运行看看效果
给WebView添加导航功能
为了更好的展示WebView2的相关能力,我们当然需要给它插上导航的翅膀,为此我们需要构建一个可输入的面板和导航按钮。
1. 使用字体图标来构建按钮,准备字体资源
首先,我们还是需要引入SegoeFluentIcons.ttf
这个字体图标文件,我们把它放在根目录的Fonts
文件夹中,生成操作需设置成"内容",复制到输出目录设置为"始终复制"。
然后我们需要借助一个IconfontHelper
的类来读取字体资源。
public class IconfontHelper
{
//提供一个字体系列集合,该集合是基于客户端应用程序提供的字体文件生成的。
private static System.Drawing.Text.PrivateFontCollection pfcc;
public static System.Drawing.Text.PrivateFontCollection PFCC
{
get { return pfcc ?? LoadFont(); }
}
public static System.Drawing.Text.PrivateFontCollection LoadFont()
{
pfcc = new System.Drawing.Text.PrivateFontCollection();
pfcc.AddFontFile(Environment.CurrentDirectory + "/Fonts/SegoeFluentIcons.ttf");
return pfcc;
}
}
2. 实验性的在WinForms上支持字体图标,并构建按钮
我们先尝试通过Panel
+ Label
的组合来实现一个字体图标的按钮效果。
从左侧工具箱中拖取两个控件组合成上诉截图效果,然后在MainForm
的Load函数中,我们需要给Label
挂载图标字体和指定图标。
public MainForm()
{
InitializeComponent();
Load += MainForm_Load;
}
private void MainForm_Load(object sender, EventArgs e)
{
InitButtonStyle();
}
具体初始化按钮样式的方法如下:
/// <summary>
/// 初始化按钮样式
/// </summary>
private void InitButtonStyle()
{
#region InitButtonStyle
// 后退按钮
TextBlockForNaviBack.Text = "\ue0a6";
TextBlockForNaviBack.Font = new Font(IconfontHelper.PFCC.Families[0], 24);
// 前进按钮
TextBlockForNaviForward.Text = "\ue0ab";
TextBlockForNaviForward.Font = new Font(IconfontHelper.PFCC.Families[0], 24);
// 停止按钮
TextBlockForNaviStop.Text = "\ue106";
TextBlockForNaviStop.Font = new Font(IconfontHelper.PFCC.Families[0], 26);
// 刷新按钮
TextBlockForNaviRefresh.Text = "\ue149";
TextBlockForNaviRefresh.Font = new Font(IconfontHelper.PFCC.Families[0], 24);
// 主页按钮
TextBlockForNaviHome.Text = "\ue10f";
TextBlockForNaviHome.Font = new Font(IconfontHelper.PFCC.Families[0], 24);
// 搜索按钮
TextBlockForNaviTarget.Text = "\uf78b";
TextBlockForNaviTarget.Font = new Font(IconfontHelper.PFCC.Families[0], 24);
#endregion
}
查看下运行效果:
效果还算让人满意。
这里有个技巧就是,我应该如何得到每个图形对应的这个字符文本,这里我找到一个能够预览字体图标的小网站IconFont Preview By Luckly,进入后,我们选择解析本地的ttf文件。
然后选中前面的SegoeFluentIcons.ttf
文件上传并解析,然后它会把字体中所有图标的Unicode编码展示出来,这里我们以前进和后退两个图标为例,我们会看到他们的编码都是以
开头和;
结尾的,我们只需要提取剩下的字符,加上前缀\ue
即可,比如前进按钮编码
而言,最终的编码为\ue0ab
,依次类推即可。
- 为Windows 11风格构建圆角按钮和圆角输入框控件,并自定义响应事件
我们知道,Win10是直角风格,但是Win11开始微软开始推行圆角,甚至默认窗体,你原来是直角的都会自动给你加成圆角。
那么我们也想办法来构建一组圆角的控件,查了一些资料,说实话没有找到特别满意的方案,最终找了个妥协的,那就是依靠绘制来做的一个圆角Panel
来构建控件的圆角,它还有个缺点就是不太方便去改变颜色了。
public class CornerRadiusPanel: Panel
{
protected override void OnPaint(PaintEventArgs e)
{
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.FillRoundedRectangle(new SolidBrush(Color.White), 10, 10, this.Width - 40, this.Height - 60, 10);
SolidBrush brush = new SolidBrush(
Color.Transparent
);
g.FillRoundedRectangle(brush, 12, 12, this.Width - 44, this.Height - 64, 10);
g.DrawRoundedRectangle(new Pen(ControlPaint.Light(Color.Transparent, 0.00f)), 12, 12, this.Width - 44, this.Height - 64, 10);
g.FillRoundedRectangle(new SolidBrush(Color.Transparent), 12, 12 + ((this.Height - 64) / 2), this.Width - 44, (this.Height - 64) / 2, 10);
}
}
首先我们新建一个名为CornerRadiusPanel
的自定义控件,让它继承自Panel
,通过重写OnPaint
这个事件来实现圆角的绘制,绘制的底色暂且先用白色Color.White
,这里还依赖一个全局静态帮助类GraphicsExtension,有了它,我们便可以构建一个圆角的面板。
基于它,我们结合Label
和TextBox
这两个自带控件,分别组建自定义控件LabelButton
和CornerTextbox
,都用这个CornerRadiusPanel
做圆角的底盘。
控件的相对位置可能需要耐心的调整,为了更加精致一点,这里我们的LabelButton
控件采用45
x45
的尺寸,CornerTextbox
控件采用603
x50
的尺寸,其中内嵌的TextBox
字体大小采用20
pt。
.Net Core Wpf项目添加并使用WebView2控件
.Net Core的WPF项目安装WebView2包
a. 命令行安装"Microsoft.Web.WebView2"
dotnet add package Microsoft.Web.WebView2
b. 或者项目右键Nuget包管理,通过可视化界面安装"Microsoft.Web.WebView2"
c. 安装之前,Bin目录结构
d. 安装之后,Bin目录结构
发现,新增了Microsoft.Web.WebView2.Core.dll
、Microsoft.Web.WebView2.WinForms.dll
、Microsoft.Web.WebView2.Wpf.dll
这三个文件。
e. 安装之后,运行效果
f. 命令行打开项目位置
explorer.exe .
初探嵌入WebView2控件
https://docs.microsoft.com/zh-cn/microsoft-edge/webview2/get-started/wpf
在demoForWpfCore
项目的MainWindow.xaml
文件中。
1. 新增引用"Microsoft.Web.WebView2.Wpf"的命名空间
xmlns:wpf="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
2. 添加"WebView2"控件即可,其中"Source"便是启动时加载的网址设定
<Window
x:Class="demoForWpfCore.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:demoForWpfCore"
xmlns:wpf="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
mc:Ignorable="d"
Title="MainWindow"
Height="450"
Width="800"
>
<Grid>
<wpf:WebView2 Source="https://www.bing.com"/>
</Grid>
</Window>
3. 运行着"WebView2"控件的实际效果
给WebView添加导航功能
为了更好的展示WebView2的相关能力,我们当然需要给它插上导航的翅膀,为此我们需要构建一个可输入的面板和导航按钮。
1. 添加Gird布局,将WebView和操作面板上下拆分
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid
Grid.Row="0"
x:Name="GirdForOperate"
>
</Grid>
<wpf:WebView2
x:Name="WebViewForMain"
Grid.Row="1"
Source="https://www.bing.com"
/>
</Grid>
2. 在操作面板添加TextBox地址输入框和导航按钮
这里我们为了美观一点,采用Border包起来,并且设置一定的圆角,而且采用Gird来做左右布局。
<Grid
Grid.Row="0"
x:Name="GirdForOperate"
Margin="8,4"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border
x:Name="BorderForSource"
CornerRadius="4"
Grid.Column="0"
Height="44"
BorderBrush="Gray"
BorderThickness="1"
Padding="4"
>
<TextBox
x:Name="TextBoxForSource"
BorderThickness="0"
TextAlignment="Left"
TextWrapping="NoWrap"
Padding="0,6,0,4"
Text=""
FontSize="18"
KeyDown="TextBoxForSource_KeyDown"
/>
</Border>
<Border
x:Name="BorderForNavi"
CornerRadius="4"
Grid.Column="2"
BorderBrush="#0780d8"
BorderThickness="1"
Background="#39baf4"
Padding="4"
MouseDown="BorderForNavi_MouseDown"
>
<TextBlock
x:Name="TextBlockForNavi"
Text="导航"
Width="100"
FontSize="18"
Background="Transparent"
Foreground="White"
TextAlignment="Center"
VerticalAlignment="Center"
/>
</Border>
</Grid>
这里我们给BorderForNavi
控件挂载一个BorderForNavi_MouseDown
事件,给TextBoxForSource
控件挂载一个TextBoxForSource_KeyDown
事件。
实际效果如下:
3. 程序启动的时候,自动把当前WebView的网址填写到网址输入框中
public MainWindow()
{
InitializeComponent();
Loaded += MainWindow_Loaded;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
TextBoxForSource.Text = WebViewForMain.Source?.ToString();
}
4. 响应导航按钮的BorderForNavi_MouseDown
点击事件
private void BorderForNavi_MouseDown(object sender, MouseButtonEventArgs e)
{
var sourceContext = TextBoxForSource.Text?.Trim();
WebViewForMain.CoreWebView2.Navigate(sourceContext);
}
这里需要用到WebView控件实例的CoreWebView2
对象的Navigate
方法。
5. 响应地址输入框的TextBoxForSource_KeyDown
回车事件
通常,根据用户的使用习惯,我们输入新的地址后会习惯性的回车,那么我们支持下这个习惯,增加对"地址输入框"的回车事件支持,这里运用控件"按键触发(KeyDown
)"事件来做,判断e.Key == Key.Enter
的情况即表示触发了回车事件。
private void TextBoxForSource_KeyDown(object sender, KeyEventArgs e)
{
if(e.Key == Key.Enter)
{
BorderForNavi_MouseDown(null, null);
}
}
6. 优化窗体启动位置、窗体大小和名称
<Window
...
Title="WebView2浏览器"
Height="800"
Width="1367"
WindowStartupLocation="CenterScreen"
WindowState="Normal"
/>
最终效果如下图:
修改地址栏内容并回车
尝试WPF上实现Windows 11的Mica风格
最近朋友分享关于一个在WPF上实现Windows 11的Mica风格的演示项目。
它的文章发布在Apply Mica to a WPF app on Windows 11
a. 新建名为demoForWpfCoreModernUI
的Wpf的.Net Core 5.0的项目
dotnet new wpf -o demoForWpfCoreModernUI -f net5.0
dotnet sln add .\demoForWpfCoreModernUI\demoForWpfCoreModernUI.csproj
b. 修改demoForWpfCoreModernUI
项目的目标框架
这里你可能会问,为什么要改这个?嗯,我试过,如果TargetFramework
是net5.0-windows
的时候,安装ModernWpfUI
这个组件会跑不起来。
无法引用ModernWpf.dll,因为它使用了对WinRT的内置支持,而.NET 5和更高版本中不再支持它。需要支持.NET 5的更新版本组件。更多信息查看Built-in support for WinRT is removed from .NET
但是我发现Mica-WPF-Sample项目是可以用的,最终发现它虽然也是使用.Net 5,但是指定了更具体的一个版本,也许是被微软拦截之前的。
所以,这里我们也将demoForWpfCoreModernUI
项目的目标框架修改为这个net5.0-windows10.0.18362.0
。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0-windows10.0.18362.0</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
</PropertyGroup>
</Project>
c. 安装ModernWpfUI
包,引入App资源
实际上,要在Wpf里面开启对Mica的支持是不需要用到它的,但是作者说,要实现对黑暗模式的响应,所以这里用到ModernWpfUI
包。
dotnet add package ModernWpfUI
注意,只有改了前面的TargetFramework
为net5.0-windows10.0.18362.0
,这里的依赖项才是干净的,否则你会看到ModernWpfUI
下面还有一个Microsoft.Windows.SDK.Contracts
,这也是WinRT
不被支持的根源。
接下来,我们需要在App.xaml
中引入ModernWpfUI
的样式资源。
<Application x:Class="demoForWpfCoreModernUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:demoForWpfCoreModernUI"
StartupUri="MainWindow.xaml"
xmlns:ui="http://schemas.modernwpf.com/2019">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ui:ThemeResources />
<ui:XamlControlsResources />
<!-- Other merged dictionaries here -->
</ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->
</ResourceDictionary>
</Application.Resources>
</Application>
d. 在窗体中引入Mica的Dwm支持
据说,微软偷偷的在DWMWINDOWATTRIBUTE
加了枚举值,这里面我们主要是利用DWMWA_USE_IMMERSIVE_DARK_MODE
和DWMWA_MICA_EFFECT
这两个来实现今天的Mica效果。
enum DWMWINDOWATTRIBUTE
{
DWMWA_NCRENDERING_ENABLED = 1, // [get] Is non-client rendering enabled/disabled
[...]
+ DWMWA_USE_HOSTBACKDROPBRUSH, // [set] BOOL, Allows the use of host backdrop brushes for the window.
+ DWMWA_USE_IMMERSIVE_DARK_MODE = 20, // [set] BOOL, Allows a window to either use the accent color, or dark, according to the user Color Mode preferences.
+ DWMWA_WINDOW_CORNER_PREFERENCE = 33, // [set] WINDOW_CORNER_PREFERENCE, Controls the policy that rounds top-level window corners
+ DWMWA_BORDER_COLOR, // [set] COLORREF, The color of the thin border around a top-level window
+ DWMWA_CAPTION_COLOR, // [set] COLORREF, The color of the caption
+ DWMWA_TEXT_COLOR, // [set] COLORREF, The color of the caption text
+ DWMWA_VISIBLE_FRAME_BORDER_THICKNESS, // [get] UINT, width of the visible border around a thick frame window
[...]
+ DWMWA_MICA_EFFECT = 1029, // [set] BOOL, undocumented??
DWMWA_LAST
};
前往MainWindow.xaml.cs
文件,新增如下部分:
namespace demoForWpfCoreModernUI
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Loaded += MainWindow_Loaded;
}
private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
// Get PresentationSource
PresentationSource presentationSource = PresentationSource.FromVisual((Visual)sender);
// Subscribe to PresentationSource's ContentRendered event
presentationSource.ContentRendered += Window_ContentRendered;
}
[DllImport("dwmapi.dll")]
public static extern int DwmSetWindowAttribute(IntPtr hwnd, DwmWindowAttribute dwAttribute, ref int pvAttribute, int cbAttribute);
[Flags]
public enum DwmWindowAttribute : uint
{
DWMWA_USE_IMMERSIVE_DARK_MODE = 20,
DWMWA_MICA_EFFECT = 1029
}
// Enable Mica on the given HWND.
public static void EnableMica(HwndSource source, bool darkThemeEnabled)
{
int trueValue = 0x01;
int falseValue = 0x00;
// Set dark mode before applying the material, otherwise you'll get an ugly flash when displaying the window.
if (darkThemeEnabled)
DwmSetWindowAttribute(source.Handle, DwmWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE, ref trueValue, Marshal.SizeOf(typeof(int)));
else
DwmSetWindowAttribute(source.Handle, DwmWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE, ref falseValue, Marshal.SizeOf(typeof(int)));
DwmSetWindowAttribute(source.Handle, DwmWindowAttribute.DWMWA_MICA_EFFECT, ref trueValue, Marshal.SizeOf(typeof(int)));
}
public static void UpdateStyleAttributes(HwndSource hwnd)
{
// You can avoid using ModernWpf here and just rely on Win32 APIs or registry parsing if you want to.
var darkThemeEnabled = ModernWpf.ThemeManager.Current.ActualApplicationTheme == ModernWpf.ApplicationTheme.Dark;
EnableMica(hwnd, darkThemeEnabled);
}
private void Window_ContentRendered(object sender, System.EventArgs e)
{
// Apply Mica brush and ImmersiveDarkMode if needed
UpdateStyleAttributes((HwndSource)sender);
// Hook to Windows theme change to reapply the brushes when needed
ModernWpf.ThemeManager.Current.ActualApplicationThemeChanged += (s, ev) => UpdateStyleAttributes((HwndSource)sender);
}
}
}
注意还要添加两个命名空间的引用:
using System.Runtime.InteropServices;
using System.Windows.Interop;
e. 重写Window窗体的WindowChrome
我们需要在MainWindow.xaml
文件中,新增对WindowChrome.WindowChrome
的重写。
<WindowChrome.WindowChrome>
<WindowChrome
CaptionHeight="20"
ResizeBorderThickness="8"
CornerRadius="0"
GlassFrameThickness="-1"
UseAeroCaptionButtons="True"
/>
</WindowChrome.WindowChrome>
另外为了达到最终效果,我们需要将Window的背景色设置成透明。
<Window
x:Class="demoForWpfCoreModernUI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:demoForWpfCoreModernUI"
mc:Ignorable="d"
Title="MainWindow"
Height="450"
Width="800"
Background="Transparent"
>
...
另外,为了让右侧的那些按钮处于正确的位置,我们还可以自定义
WindowChrome
中的NonClientFrameEdges
来修复Wpf的这个bug。
<WindowChrome.WindowChrome>
<WindowChrome
CaptionHeight="20"
ResizeBorderThickness="8"
CornerRadius="0"
GlassFrameThickness="-1"
UseAeroCaptionButtons="True"
NonClientFrameEdges="Bottom,Left,Right"
/>
</WindowChrome.WindowChrome>
f. 运行看看效果
效果还行,其实我验证过,那个黑暗模式的下,效果出不来,具体为啥还没弄清楚,总之就是没透。
g. 结合前面的WebView2导航加持
WinUI项目添加并使用WebView2控件
添加WebView2控件
由于WinUI3中已经内置了WebView2控件了,所以我们不许额外安装任何包就可以直接使用。
我们改造下HelloWinUI3
桌面项目的MainWindow.xaml
文件。
<WebView2
x:Name="WebViewForMain"
Source="https://www.bing.com"
/>
然后先编译一次项目,随后可以启动部署试试,看看运行效果。
使用WebView2控件
为了更好的展示WebView2的能力,我们直接复制Demo4Window
的已有能力好了。
目前WinUI控件提供的事件和能力还不够完善,所以部分效果暂时屏蔽和替换了。
其中:
WebView2
的CoreWebView2InitializationCompleted
事件需要替换成CoreWebView2Initialized
。
public MainWindow()
{
InitializeComponent();
WebViewForMain.NavigationStarting += WebViewForMain_NavigationStarting;
WebViewForMain.NavigationCompleted += WebViewForMain_NavigationCompleted;
//WebViewForMain.KeyDown += WebViewForMain_KeyDown;
WebViewForMain.CoreWebView2Initialized += WebViewForMain_CoreWebView2Initialized;
}
private void WebViewForMain_CoreWebView2Initialized(object? sender, CoreWebView2InitializedEventArgs e)
{
if (e.Exception!=null)
{
WebViewForMain.CoreWebView2.ProcessFailed += CoreWebView2_ProcessFailed;
}
else
{
//MessageBox.Show($"WebView2创建失败,发生异常 = {e.InitializationException}");
}
}
Border
的MouseDown
事件需要替换成Tapped
。
/// <summary>
/// 导航栏-后退按钮-点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void BorderForNaviBack_Tapped(object sender, TappedRoutedEventArgs e)
{
#region BorderForNaviBack_MouseDown
if (WebViewForMain.CanGoBack)
{
WebViewForMain.GoBack();
}
else
{
UpdateNaviButtonStatus();
}
#endregion
}
Border
的MouseEnter
和MouseLeave
需要替换成PointerEntered
、PointerMoved
。
private void BorderForButton_PointerEntered(object sender, PointerRoutedEventArgs e)
{
var border = sender as Border;
border.Background = new SolidColorBrush(Colors.White);
border.Focus(FocusState.Pointer);
}
private void BorderForButton_PointerMoved(object sender, PointerRoutedEventArgs e)
{
var border = sender as Border;
border.Background = new SolidColorBrush(Colors.Transparent);
border.Focus(FocusState.Pointer);
}
WebView2
的Stop
方法需要替换成Close
方法
/// <summary>
/// 导航栏-停止按钮-点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void BorderForNaviStop_Tapped(object sender, TappedRoutedEventArgs e)
{
#region BorderForNaviStop_MouseDown
WebViewForMain.Close();
UpdateNaviButtonStatus();
#endregion
}
设置部署图标
在WinUi的打包项目中,我们可以选中一张高清图作为图标的素材来源,一键生成。
让程序拥有自定义图标
下载安装图标提取工具IconViewer
这里我们去提取一个来用,这里需要用到一个工具,叫IconViewer
。
安装地址:IconViewer3.02-Setup-x64.exe
安装之后,啥动静也没有,但是实际已经有用了。
使用图标提取工具IconViewer提取图标
我们找到我们要提取的目标exe,嗯嗯,肯定是带图标的那个,我们就要提取他的图标哈。
选中它,然后右键属性。
如果安装顺利,这里会多出一个Icons的标签,我们切过去,哈哈,惊喜来了,这里显示了它的图标,我们还可以选图标的大小,毫无疑问,选最大的那个,点击那个保存按钮就可以了。
接下来,我们就顺利得到一个超高清的Ico图标了。
给应用程序挂载图标
在项目上右键,打开项目"属性",然后找到"图标和清单"部分,浏览我们刚刚保存那个图标即可。
运行一看,哈哈,已经生效了。
很香吧。
理解WebView2的导航事件
https://docs.microsoft.com/zh-cn/microsoft-edge/webview2/concepts/navigation-events
在网页导航期间,WebView2控件将引发事件。承载WebView2控件的应用侦听以下事件。
- NavigationStarting
- SourceChanged
- ContentLoading
- HistoryChanged
- NavigationCompleted
发生错误时,将引发以下事件,并可能依赖于导航到错误网页。
- SourceChanged
- ContentLoading
- HistoryChanged
如果发生HTTP重定向,则一行NavigationStarting中有多个事件。
从NavigationStarting事件切入强制HTTPS
public Demo2Window()
{
InitializeComponent();
WebViewForMain.NavigationStarting += WebViewForMain_NavigationStarting;
}
private void WebViewForMain_NavigationStarting(object? sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationStartingEventArgs e)
{
if (!e.Uri.ToLower().StartsWith("https://"))
{
e.Cancel = true;
}
}
在Demo2Window
窗体构造函数中注册WebViewForMain
控件的"导航开始(NavigationStarting
)"事件,在WebViewForMain_NavigationStarting
事件处理函数中,如果检测到Uri
不是以Https
开头的,直接取消掉当前导航动作,以达到强制HTTPS的目的。
从NavigationCompleted事件切入更新地址栏
public Demo2Window()
{
InitializeComponent();
WebViewForMain.NavigationCompleted += WebViewForMain_NavigationCompleted;
}
private void WebViewForMain_NavigationCompleted(object? sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationCompletedEventArgs e)
{
if (e.IsSuccess)
{
TextBoxForSource.Text = WebViewForMain.Source?.ToString();
}
}
在Demo2Window
窗体构造函数中注册WebViewForMain
控件的"导航完成(NavigationCompleted
)"事件,在WebViewForMain_NavigationCompleted
事件处理函数中,如果NavigationCompletedEventArgs
事件参数是成功状态,那么将当前WebView实例的源地址更新到地址输入框中。
从首页点击页面内的链接,跳转到其他页面之后,地址栏也会同步更新,显示当前地址。
给页面加载过程增加进度提示
有了前面的"导航开始(NavigationStarting
)"事件和"导航完成(NavigationCompleted
)"事件加持,我们便可以基于它们,提示用户正在加载了。
a. 添加一个进度指示器控件ProgressBar
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
...
<ProgressBar
Grid.Row="0"
x:Name="GirdForProgress"
Height="2"
VerticalAlignment="Bottom"
IsEnabled="False"
IsIndeterminate="False"
/>
...
</Grid>
b. 引入一个窗体变量控制进度指示器控件
private bool _isNavigationProgress;
public bool IsNavigationProgress
{
get
{
return _isNavigationProgress;
}
set
{
_isNavigationProgress = value;
GirdForProgress.IsEnabled = value;
GirdForProgress.IsIndeterminate = value;
GirdForProgress.Visibility = value ? Visibility.Visible : Visibility.Collapsed;
}
}
在IsNavigationProgress
变量的Set操作中,我们同步控制GirdForProgress
的IsEnabled
属性、IsIndeterminate
属性、Visibility
属性。
c. 基于事件控制IsNavigationProgress变量值
private void WebViewForMain_NavigationCompleted(object? sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationCompletedEventArgs e)
{
if (e.IsSuccess)
{
TextBoxForSource.Text = WebViewForMain.Source?.ToString();
}
IsNavigationProgress = false;
}
private void WebViewForMain_NavigationStarting(object? sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationStartingEventArgs e)
{
var uri = e.Uri;
if (!uri.ToLower().StartsWith("https://"))
{
WebViewForMain.CoreWebView2.ExecuteScriptAsync($"alert('{uri} 不安全,请使用HTTPS地址重新访问!')");
e.Cancel = true;
}
IsNavigationProgress = true;
}
在Demo4Window
窗体的WebViewForMain_NavigationCompleted
事件和WebViewForMain_NavigationStarting
事件中分别控制IsNavigationProgress
变量值,间接的实现对GirdForProgress
展示效果的控制。
d. 运行演示效果
从KeyDown事件切入支持组合快捷键
public Demo4Window()
{
InitializeComponent();
WebViewForMain.KeyDown += WebViewForMain_KeyDown;
}
private void WebViewForMain_KeyDown(object sender, KeyEventArgs e)
{
if (e.IsRepeat) return;
bool ctrl = e.KeyboardDevice.IsKeyDown(Key.LeftCtrl) || e.KeyboardDevice.IsKeyDown(Key.RightCtrl);
bool alt = e.KeyboardDevice.IsKeyDown(Key.LeftAlt) || e.KeyboardDevice.IsKeyDown(Key.RightAlt);
bool shift = e.KeyboardDevice.IsKeyDown(Key.LeftShift) || e.KeyboardDevice.IsKeyDown(Key.RightShift);
if (e.Key == Key.N && ctrl && !alt && !shift)
{
new MainWindow().Show();
e.Handled = true;
}
else if (e.Key == Key.W && ctrl && !alt && !shift)
{
Close();
e.Handled = true;
}
}
在Demo4Window
窗体构造函数中注册WebViewForMain
控件的"按键按下(KeyDown
)"事件,在WebViewForMain_KeyDown
事件处理函数中,如果KeyEventArgs
事件参数中是Ctrl+N
的组合,那么就新建一个窗口,如果是Ctrl+W
的组合,那么就关闭当前窗口,这个快捷键和目前Microsoft Edge是一致的。
从CoreWebView2InitializationCompleted事件切入知晓浏览器控件加载完毕
public Demo4Window()
{
InitializeComponent();
WebViewForMain.CoreWebView2InitializationCompleted += WebViewForMain_CoreWebView2InitializationCompleted;
}
private void WebViewForMain_CoreWebView2InitializationCompleted(object? sender, Microsoft.Web.WebView2.Core.CoreWebView2InitializationCompletedEventArgs e)
{
if (e.IsSuccess)
{
}
else
{
MessageBox.Show($"WebView2创建失败,发生异常 = {e.InitializationException}");
}
}
在Demo4Window
窗体构造函数中注册WebViewForMain
控件的"核心初始化完成(CoreWebView2InitializationCompleted
)"事件,在WebViewForMain_CoreWebView2InitializationCompleted
事件处理函数中,如果CoreWebView2InitializationCompletedEventArgs
事件参数中IsSuccess
为True
,说明浏览器核心初始化成功,如果为False
,则表示发生异常情况,那么可以弹出相关提示来告知用户,异常信息通过InitializationException
获取。
实践WebView2的双向通信
从ExecuteScriptAsync方法运行自定义Javascript代码
public Demo2Window()
{
InitializeComponent();
WebViewForMain.NavigationStarting += WebViewForMain_NavigationStarting;
}
private void WebViewForMain_NavigationStarting(object? sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationStartingEventArgs e)
{
if (!e.Uri.ToLower().StartsWith("https://"))
{
WebViewForMain.CoreWebView2.ExecuteScriptAsync($"alert('{uri} 不安全,请使用HTTPS地址重新访问!')");
e.Cancel = true;
}
}
在前面说到的WebViewForMain_NavigationStarting
事件处理函数中,我们给强制HTTPS增加一个提示,这里我们需要让WebView替代我们执行一段Javascript
代码的警告,以便给用户一个具体的提示,通过WebView实例的ExecuteScriptAsync
方法,可以传入自定义的Javascript
代码进行执行。
从EnsureCoreWebView2Async方法等待WebView2异步加载完成
public Demo3Window()
{
InitializeComponent();
InitializeAsync();
}
async void InitializeAsync()
{
// 确保WebView对象已经初始化完成
await WebViewForMain.EnsureCoreWebView2Async(null);
}
如果你曾尝试在Window窗体构建函数或者Windows的Loaded
函数去试图绑定WebView2
实例的CoreWebView2
对象相关的事件,你可能会遇到Null空值错误,原因是WebView2实例的CoreWebView2对象的初始化是异步加载的,如果我们要监听它的事件,那么需要等待它异步加载完成之后才行,所以这里我们在构造函数中,新增了可支持异步等待的InitializeAsync
方法,通过EnsureCoreWebView2Async
方法,我们可以确保这一句之后执行的代码是CoreWebView2
对象已经初始化成功之后的。
从WebMessageReceived方法监听来自WebView的消息
async void InitializeAsync()
{
// 确保WebView对象已经初始化完成
await WebViewForMain.EnsureCoreWebView2Async(null);
// 监听来自WebView的消息
WebViewForMain.CoreWebView2.WebMessageReceived += CoreWebView2_WebMessageReceived;
}
private void CoreWebView2_WebMessageReceived(object? sender, Microsoft.Web.WebView2.Core.CoreWebView2WebMessageReceivedEventArgs e)
{
// 试图以String的方式接收消息内容
var messageContent = e.TryGetWebMessageAsString();
// 以系统弹窗的方式展示消息内容
MessageBox.Show(messageContent);
}
在WebView2
实例的CoreWebView2
对象的EnsureCoreWebView2Async
方法之后,我们便可以安全的监听WebMessageReceived
事件,在CoreWebView2_WebMessageReceived
事件处理函数中,出于安全起见,我们试图以TryGetWebMessageAsString
的方法以字符串的格式接收消息内容,并且以系统弹窗MessageBox
的方式进行展示,这里只是我们临时的一种方案,用于演示哈。
从AddScriptToExecuteOnDocumentCreatedAsync方法模拟来自WebView的消息
async void InitializeAsync()
{
// 确保WebView对象已经初始化完成
await WebViewForMain.EnsureCoreWebView2Async(null);
// 监听来自WebView的消息
WebViewForMain.CoreWebView2.WebMessageReceived += CoreWebView2_WebMessageReceived;
// 模拟WebView的网站发送消息
await WebViewForMain.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("window.chrome.webview.postMessage(window.document.URL);");
}
有了前面步骤中对WebView2
实例的CoreWebView2
对象针对WebMessageReceived
事件的监听处理之后,我们可能需要模拟一下WebView网站内对客户端的消息动作,以便验证我们的监听处理是否符合预期,通过CoreWebView2
对象的AddScriptToExecuteOnDocumentCreatedAsync
方法,我们可以在新的网页内容被创建完成后追加一个PostMessage
的动作,把当前网页的地址发送给客户端。
从AddScriptToExecuteOnDocumentCreatedAsync方法模拟WebView网站监听消息
async void InitializeAsync()
{
// 确保WebView对象已经初始化完成
await WebViewForMain.EnsureCoreWebView2Async(null);
// 模拟WebView的网站监听消息
await WebViewForMain.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("window.chrome.webview.addEventListener(\'message\', event => alert(event.data));");
}
前面我们模拟了从WebView网站发送消息,那么反过来,我们也需要模拟下网站监听来自客户端的消息,以便后续响应我们从客户端发送消息给网站。
通过CoreWebView2
对象的AddScriptToExecuteOnDocumentCreatedAsync
方法,我们可以在新的网页内容被创建完成后追加一个AddEventListener
的动作,监听来自客户端的消息,并且以警告弹窗的形式把消息内容展示出来。
具体效果,稍后将进行验证。
从PostWebMessageAsString方法向WebView网站发送消息
a. 新增消息发送面板
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
...
<Grid
Grid.Row="1"
x:Name="GirdForMessage"
Margin="8,4"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border
x:Name="BorderForMessage"
CornerRadius="4"
Grid.Column="0"
Height="44"
BorderBrush="Gray"
BorderThickness="1"
Padding="4"
>
<TextBox
x:Name="TextBoxForMessage"
BorderThickness="0"
TextAlignment="Left"
TextWrapping="NoWrap"
Padding="0,6,0,4"
Text=""
FontSize="18"
KeyDown="TextBoxForMessage_KeyDown"
/>
</Border>
<Border
x:Name="BorderForPost"
CornerRadius="4"
Grid.Column="2"
BorderBrush="#0780d8"
BorderThickness="1"
Background="#39baf4"
Padding="4"
MouseDown="BorderForPost_MouseDown"
>
<TextBlock
x:Name="TextBlockForPost"
Text="发送"
Width="100"
FontSize="18"
Background="Transparent"
Foreground="White"
TextAlignment="Center"
VerticalAlignment="Center"
/>
</Border>
</Grid>
...
</Grid>
为了更加可视化的模拟向WebView网站发送消息,并定制消息内容,我们引入一个新的发送消息的面板,在原来的导航面板和WebView控件之间,采用Gird
布局,引入"消息输入框(TextBoxForMessage
)"和"发送按钮(TextBlockForPost
)",在风格上就完全参考之间的导航面板了。
b. 响应定制化消息发送
private void BorderForPost_MouseDown(object sender, MouseButtonEventArgs e)
{
var messageContext = TextBoxForMessage.Text?.Trim();
WebViewForMain.CoreWebView2.PostWebMessageAsString(messageContext);
}
在"发送按钮(TextBlockForPost
)"的响应事件BorderForPost_MouseDown
中,通过CoreWebView2
对象的PostWebMessageAsString
方法,我们可以将界面上的定制化消息发送到网站,如果网站能接收到的话,那么根据前面的监听机制,会弹出包含消息内容的警示弹窗,根据我们的设计,稍作注意是,需要重新加载新网页才能响应。
从AddHostObjectToScript方法公开被Javascript调用的本机方法
为了更方便的实现JS和本机之间的通信,我们还可以把本地方法通过AddHostObjectToScript
方法暴漏给Web来实现调用,这等同于传统WebBrower控件的ObjectForScripting
方法实现。
a. 对WebView2进行一些安全设置,允许使用注入本机方法
等待CoreWebView2核心初始化完毕之后,我们应该尽快完成一些安全设置,允许使用注入本机方法。
private async void Demo5Window_Loaded(object sender, RoutedEventArgs e)
{
await WebViewForMain.EnsureCoreWebView2Async();
WebViewForMain.CoreWebView2.Settings.AreHostObjectsAllowed = true;
WebViewForMain.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = true;
WebViewForMain.CoreWebView2.Settings.IsScriptEnabled = true;
WebViewForMain.CoreWebView2.Settings.IsWebMessageEnabled = true;
}
b. 定义公开的本机方法类
#pragma warning disable CS0618
[System.Runtime.InteropServices.ClassInterface(System.Runtime.InteropServices.ClassInterfaceType.AutoDual)]
#pragma warning restore CS0618
[System.Runtime.InteropServices.ComVisible(true)]
public class C2WHostObject
{
public void ClientFunction(string requestInfo)
{
Console.WriteLine(requestInfo);
}
public string ClientValueBack(string requestInfo)
{
return requestInfo;
}
}
这里对需要公开的本机方法类,需要通过System.Runtime.InteropServices.ComVisible(true)
和System.Runtime.InteropServices.ClassInterface(System.Runtime.InteropServices.ClassInterfaceType.AutoDual)
来公开它,否则将不可见。
这里由于
ClassInterfaceType.AutoDual
即将被废弃,暂时先通过#pragma warning disable CS0618
关闭警告,.NET host objects need to use deprecated AutoDual attribute
c. 等待CoreWebView2核心初始化完毕之后,注册本机公开方法
private async void Demo5Window_Loaded(object sender, RoutedEventArgs e)
{
await WebViewForMain.EnsureCoreWebView2Async();
WebViewForMain.CoreWebView2.AddHostObjectToScript("webView2Bridge", new C2WHostObject());
}
这里需要给这个公开方法对象取个名称,这里我们暂时叫它:webView2Bridge
。
d. 在WebView2中F12进入DevTool尝试调用
await chrome.webview.hostObjects.webView2Bridge.ClientFunction("somethings");
await chrome.webview.hostObjects.webView2Bridge.ClientValueBack("somethings");
处理WebView2的异常机制
从ProcessFailed事件切入监听浏览器异常
private void WebViewForMain_CoreWebView2InitializationCompleted(object? sender, Microsoft.Web.WebView2.Core.CoreWebView2InitializationCompletedEventArgs e)
{
if (e.IsSuccess)
{
WebViewForMain.CoreWebView2.ProcessFailed += CoreWebView2_ProcessFailed;
}
}
private void CoreWebView2_ProcessFailed(object? sender, Microsoft.Web.WebView2.Core.CoreWebView2ProcessFailedEventArgs e)
{
switch (e.ProcessFailedKind)
{
// 浏览器进程退出
case CoreWebView2ProcessFailedKind.BrowserProcessExited:
{
}
break;
// 浏览器渲染进程未响应
case CoreWebView2ProcessFailedKind.RenderProcessUnresponsive:
{
}
break;
// 浏览器渲染进程退出
case CoreWebView2ProcessFailedKind.RenderProcessExited:
{
}
break;
// 框架渲染进程退出
case CoreWebView2ProcessFailedKind.FrameRenderProcessExited:
{
}
break;
default:
{
// Show the process failure details. Apps can collect info for their logging purposes.
StringBuilder messageBuilder = new StringBuilder();
messageBuilder.AppendLine($"Process kind: {e.ProcessFailedKind}");
messageBuilder.AppendLine($"Reason: {e.Reason}");
messageBuilder.AppendLine($"Exit code: {e.ExitCode}");
messageBuilder.AppendLine($"Process description: {e.ProcessDescription}");
System.Threading.SynchronizationContext.Current.Post((_) =>
{
MessageBox.Show(messageBuilder.ToString(), "Child process failed", MessageBoxButton.OK);
}, null);
}
break;
}
}
在在Demo4Window
窗体的"核心初始化完成(CoreWebView2InitializationCompleted
)"事件响应中,通过注册CoreWebView2
对象的"进程失败(ProcessFailed
)"事件,在CoreWebView2_ProcessFailed
事件处理函数中,可通过e.ProcessFailedKind
来根据进程失败的种类分情况灵活处理。
优化WebView2导航控制
引入Segoe Fluent Icons字体图标
今天我们引入一个Windows 11最新版的图标字体Segoe Fluent Icons,如果想要查看字体内图标清单,可以浏览:https://linrstudio.github.io/win11/SEGOEICONS.html 查阅。
而要在WPF中引入字体,并且使用,我们先把下载好的字体丢进项目下Fonts
目录。
记得将字体文件设置成"始终复制"和生成操作为"内容"。
稍后在TextBlock中写FontFamily
使用/MiniEdge;component/Fonts/#Segoe Fluent Icons
,其中MiniEdge
是程序集的命名空间,Fonts
是字体文件的路径,而Segoe Fluent Icons
是字体名称。
字体名称建议你双击.ttf
打开看一下。
而在TextBlock中的Text需要采用&#
开头和;
结尾的编码,比如:
<TextBlock
x:Name="TextBlockForNaviStop"
FontFamily="/MiniEdge;component/Fonts/#Segoe Fluent Icons"
Text=""
FontSize="26"
VerticalAlignment="Center"
Foreground="Black"
/>
构建更丰富的导航控制面板
a. 引入后退、前进、刷新、停止、主页按钮布局
<Grid Grid.Column="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border
x:Name="BorderForNaviBack"
CornerRadius="4"
Grid.Column="1"
Padding="16,4"
MouseEnter="BorderForButton_MouseEnter"
MouseLeave="BorderForButton_MouseLeave"
MouseDown="BorderForNaviBack_MouseDown"
>
<TextBlock
x:Name="TextBlockForNaviBack"
FontFamily="/MiniEdge;component/Fonts/#Segoe Fluent Icons"
Text=""
FontSize="24"
VerticalAlignment="Center"
Foreground="Black"
/>
</Border>
<Border
x:Name="BorderForNaviForward"
CornerRadius="4"
Grid.Column="3"
Padding="16,4"
MouseEnter="BorderForButton_MouseEnter"
MouseLeave="BorderForButton_MouseLeave"
MouseDown="BorderForNaviForward_MouseDown"
>
<TextBlock
x:Name="TextBlockForNaviForward"
FontFamily="/MiniEdge;component/Fonts/#Segoe Fluent Icons"
Text=""
FontSize="24"
VerticalAlignment="Center"
Foreground="Black"
/>
</Border>
<Grid Grid.Column="5">
<Border
x:Name="BorderForNaviStop"
CornerRadius="4"
Grid.Column="5"
Padding="16,4"
MouseEnter="BorderForButton_MouseEnter"
MouseLeave="BorderForButton_MouseLeave"
MouseDown="BorderForNaviStop_MouseDown"
Visibility="Collapsed"
>
<TextBlock
x:Name="TextBlockForNaviStop"
FontFamily="/MiniEdge;component/Fonts/#Segoe Fluent Icons"
Text=""
FontSize="26"
VerticalAlignment="Center"
Foreground="Black"
/>
</Border>
<Border
x:Name="BorderForNaviRefresh"
CornerRadius="4"
Grid.Column="5"
Padding="16,4"
MouseEnter="BorderForButton_MouseEnter"
MouseLeave="BorderForButton_MouseLeave"
MouseDown="BorderForNaviRefresh_MouseDown"
>
<TextBlock
x:Name="TextBlockForNaviRefresh"
FontFamily="/MiniEdge;component/Fonts/#Segoe Fluent Icons"
Text=""
FontSize="24"
VerticalAlignment="Center"
Foreground="Black"
/>
</Border>
</Grid>
<Border
x:Name="BorderForNaviHome"
CornerRadius="4"
Grid.Column="7"
Padding="16,4"
MouseEnter="BorderForButton_MouseEnter"
MouseLeave="BorderForButton_MouseLeave"
MouseDown="BorderForNaviHome_MouseDown"
>
<TextBlock
x:Name="TextBlockForNaviHome"
FontFamily="/MiniEdge;component/Fonts/#Segoe Fluent Icons"
Text=""
FontSize="24"
VerticalAlignment="Center"
Foreground="Black"
/>
</Border>
</Grid>
b. 响应后退、前进、刷新、停止、主页按钮动作
#region NaviButton
/// <summary>
/// 导航栏-后退按钮-点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void BorderForNaviBack_MouseDown(object sender, MouseButtonEventArgs e)
{
#region BorderForNaviBack_MouseDown
if (WebViewForMain.CanGoBack)
{
WebViewForMain.GoBack();
}
else
{
UpdateNaviButtonStatus();
}
#endregion
}
/// <summary>
/// 导航栏-前进按钮-点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void BorderForNaviForward_MouseDown(object sender, MouseButtonEventArgs e)
{
#region BorderForNaviForward_MouseDown
if (WebViewForMain.CanGoForward)
{
WebViewForMain.GoForward();
}
else
{
UpdateNaviButtonStatus();
}
#endregion
}
/// <summary>
/// 导航栏-主页按钮-点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void BorderForNaviHome_MouseDown(object sender, MouseButtonEventArgs e)
{
#region BorderForNaviHome_MouseDown
WebViewForMain.CoreWebView2.Navigate("https://www.bing.com");
UpdateNaviButtonStatus();
#endregion
}
/// <summary>
/// 导航栏-刷新按钮-点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void BorderForNaviRefresh_MouseDown(object sender, MouseButtonEventArgs e)
{
#region BorderForNaviRefresh_MouseDown
WebViewForMain.Reload();
UpdateNaviButtonStatus();
#endregion
}
/// <summary>
/// 导航栏-停止按钮-点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void BorderForNaviStop_MouseDown(object sender, MouseButtonEventArgs e)
{
#region BorderForNaviStop_MouseDown
WebViewForMain.Stop();
UpdateNaviButtonStatus();
#endregion
}
#endregion
这里比较简单,主要是调用WebView2
实例的GoBack()
、GoForward()
、Reload()
、Stop()
函数来完成对应的功能。
/// <summary>
/// 更新导航栏-按钮-状态
/// </summary>
private void UpdateNaviButtonStatus()
{
#region UpdateNaviButtonStatus
var isCanGoBack = WebViewForMain.CanGoBack;
BorderForNaviBack.IsEnabled = isCanGoBack;
TextBlockForNaviBack.Foreground = isCanGoBack ? new SolidColorBrush(Colors.Black) : new SolidColorBrush(Colors.Gray);
var isCanGoForward = WebViewForMain.CanGoForward;
BorderForNaviForward.IsEnabled = isCanGoForward;
TextBlockForNaviForward.Foreground = isCanGoForward ? new SolidColorBrush(Colors.Black) : new SolidColorBrush(Colors.Gray);
#endregion
}
同时,为了当前后导航不可用的时候,能给用户一个明确提示,我们将其禁用并且颜色置灰。
回到主页按钮,暂时用CoreWebView2
对象的Navigate
方法来实现,其实我理解点击主页之后,应要清空前后导航的,但是还没找到对应的方法来做这件事。
c. 优化后退、前进、刷新、停止、主页按钮交互
private void BorderForButton_MouseEnter(object sender, MouseEventArgs e)
{
var border = sender as Border;
if (border.IsEnabled)
{
border.Background = new SolidColorBrush(Colors.White);
border.Focus();
}
}
private void BorderForButton_MouseLeave(object sender, MouseEventArgs e)
{
var border = sender as Border;
if (border.IsEnabled)
{
border.Background = new SolidColorBrush(Colors.Transparent);
}
}
实际上,我们给所有的图标按钮标配了一个效果,就是鼠标移上去就背景变白,移开后恢复,这样交互更加明确。
对于刷新和停止按钮,我们还需要根据是否正在加载来切换他们的显影,那么在之前的IsNavigationProgress
中处理就好了。
private bool _isNavigationProgress;
public bool IsNavigationProgress
{
get
{
return _isNavigationProgress;
}
set
{
_isNavigationProgress = value;
GirdForProgress.IsEnabled = value;
GirdForProgress.IsIndeterminate = value;
GirdForProgress.Visibility = value ? Visibility.Visible : Visibility.Collapsed;
BorderForNaviRefresh.IsEnabled = !value;
TextBlockForNaviRefresh.Foreground = !value ? new SolidColorBrush(Colors.Black) : new SolidColorBrush(Colors.Gray);
BorderForNaviStop.Visibility = value ? Visibility.Visible : Visibility.Collapsed;
BorderForNaviRefresh.Visibility = !value ? Visibility.Visible : Visibility.Collapsed;
}
}
d. 优化导航按钮为图标按钮,统一交互和视觉
<Grid Grid.Column="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="8"/>
</Grid.ColumnDefinitions>
<Border
x:Name="BorderForNaviTarget"
CornerRadius="4"
Grid.Column="0"
Padding="16,4"
MouseDown="BorderForNaviTarget_MouseDown"
MouseEnter="BorderForButton_MouseEnter"
MouseLeave="BorderForButton_MouseLeave"
>
<TextBlock
x:Name="TextBlockForNaviTarget"
FontFamily="/MiniEdge;component/Fonts/#Segoe Fluent Icons"
Text=""
FontSize="24"
VerticalAlignment="Center"
Foreground="Black"
/>
</Border>
</Grid>
/// <summary>
/// 导航栏-指定按钮-点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void BorderForNaviTarget_MouseDown(object sender, MouseButtonEventArgs e)
{
#region BorderForNaviTarget_MouseDown
#endregion
}
/// <summary>
/// 导航栏-地址输入框-快捷键(回车)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void TextBoxForNaviAddress_KeyDown(object sender, KeyEventArgs e)
{
#region TextBoxForNaviAddress_KeyDown
if (e.Key == Key.Enter)
{
BorderForNaviTarget_MouseDown(null, null);
}
#endregion
}
e. 优化地址输入框交互和视觉体验
<Grid Grid.Column="2">
<Border
x:Name="BorderForNaviAddress"
CornerRadius="4"
Grid.Column="2"
Height="44"
BorderBrush="Gray"
BorderThickness="1"
Padding="4"
Background="White"
>
</Border>
<TextBox
Margin="4"
x:Name="TextBoxForNaviAddress"
BorderThickness="0"
TextAlignment="Left"
TextWrapping="NoWrap"
Padding="0,6,0,4"
Text=""
FontSize="18"
KeyDown="TextBoxForNaviAddress_KeyDown"
MouseEnter="TextBoxForNaviAddress_MouseEnter"
MouseLeave="TextBoxForNaviAddress_MouseLeave"
Background="White"
Foreground="Black"
/>
</Grid>
我们做了一个布局调整,将Border和TextBox平行放在一个Gird里面,这样布局的好处就是当Border样式改变的时候,不会影响到TextBox。
private void TextBoxForNaviAddress_MouseEnter(object sender, MouseEventArgs e)
{
BorderForNaviAddress.BorderBrush = new SolidColorBrush(Color.FromRgb(143, 177, 229));
BorderForNaviAddress.BorderThickness = new Thickness(1.5);
}
private void TextBoxForNaviAddress_MouseLeave(object sender, MouseEventArgs e)
{
BorderForNaviAddress.BorderBrush = new SolidColorBrush(Colors.Gray);
BorderForNaviAddress.BorderThickness = new Thickness(1);
}
接下来,当然输入框被鼠标靠近的时候,我们让输入框背后的背景边框变个颜色,并且加粗边框,被鼠标移开的时候,效果还原。
f. 优化地址输入框直达和搜索体验
有时候用户可能输入的是一个网址链接,或者是一个不带HTTP头的链接,亦或只是联想的一些关键词,那么我们分开处理,以确保得到最佳体验。
/// <summary>
/// 导航栏-指定按钮-点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void BorderForNaviTarget_MouseDown(object sender, MouseButtonEventArgs e)
{
#region BorderForNaviTarget_MouseDown
var sourceContent = TextBoxForNaviAddress.Text?.Trim() ?? string.Empty;
if (!string.IsNullOrEmpty(sourceContent))
{
Uri? sourceUri;
// 如果当前地址是格式化合规的地址,那么直接使用
if (Uri.IsWellFormedUriString(sourceContent, UriKind.Absolute))
{
sourceUri = new Uri(sourceContent);
}
// 如果当前地址含.符号切不含空格,那么自动追加前缀
else if (!sourceContent.Contains(" ") && sourceContent.Contains("."))
{
sourceUri = new Uri("http://" + sourceContent);
}
// 如果当前地址不属于上诉情况,那么通过内置搜索引擎搜索
else
{
var searchKeywords = string.Join("+", Uri.EscapeDataString(sourceContent).Split(new string[] { "%20" }, StringSplitOptions.RemoveEmptyEntries));
var bingSearchAddress = $"https://bing.com/search?q={searchKeywords}";
sourceUri = new Uri(bingSearchAddress);
}
if (sourceUri != null)
{
WebViewForMain.CoreWebView2.Navigate(sourceUri.ToString());
}
}
#endregion
}
g. 不如取个更好的名字吧
之前我们管它叫WebView2浏览器
,该给它取个正式的名称了,不如就叫MiniEdge
吧,毕竟是借鉴了Edge的交互和视觉,还复用了它的渲染引擎。
需要注意的是,我们同时也把程序集名称一起改了。
这样最终exe就会改名字了。
注意也要把Demo4Window.xaml
的Title
改了。
WebView2的最佳部署指南
WebView2的最佳开发指南
每个开发团队在构建其应用程序时都遵循不同的做法。生成WebView2生产应用时,建议遵循这些建议和最佳做法。
使用EvergreenRuntime(推荐)
我们通常建议使用"Evergreen WebView2运行时"。固定版本运行时分发仅建议用于具有严格兼容性要求的应用。"Evergreen运行时"在客户端上自动更新,以便你的WebView2应用可以使用最新的功能和安全修补程序。与固定版本运行时相比,"Evergreen运行时"还需要更少的磁盘上的存储空间。
如果使用"Evergreen运行时",在运行WebView2应用之前,测试是否已在客户端上安装"Evergreen WebView2运行时"。
使用Evergreen运行时时定期运行兼容性测试
使用"Evergreen WebView2运行时"时,运行时会自动更新,因此必须定期运行兼容性测试。若要确保WebView2应用继续正常工作,请针对Microsoft Edge Insider(preview)Channels(Beta、Dev或Canary)在WebView2控件中测试Web内容。
本指南类似于我们向Web开发人员提供的指导。
测试安装的WebView2运行时是否支持较新的API
若要运行使用Webview2 SDK的特定版本开发的WebView2应用,客户端必须已安装WebView2运行时的兼容版本。由于API不断添加到WebView2,因此也发布了新版本的运行时以支持新的API。使用功能检测确保安装在客户端上的WebView2运行时支持WebView2应用使用的较新的API。
如果使用"Evergreen WebView2运行时",在某些情况下,客户端上的运行时尚未自动更新到最新版本。例如,如果客户端没有Internet访问权限,则运行时不会自动更新。此外,某些组策略会暂停运行时的更新。将更新推送到WebView2应用时,如果应用尝试调用客户端安装运行时中不可用的较新API,该应用可能无法运行。
若要解决此问题,在代码调用最近添加的WebView2 API之前,测试该API在客户端的安装运行时中是否可用。此较新功能测试与其他Web开发最佳实践类似,这些最佳实践在使用新的WebAPI之前检测支持的功能。若要测试已安装运行时中的API可用性,请使用:
QueryInterface
在C/C++中。try/catch
.NET或WinUI中的块。
更新固定版本运行时
如果使用固定版本的"WebView2运行时",请确保定期更新与应用打包的"WebView2运行时",以减少安全风险。在Webview2应用中使用第三方内容时,始终考虑不受信任的内容。
管理新版本的EvergreenRuntime
将新版本的"Evergreen WebView2运行时"下载到客户端后,正在运行的任何WebView2应用将继续使用早期版本的运行时,直到发布浏览器进程。此行为允许应用连续运行,并阻止删除以前的运行时。若要使用新版本的运行时,需要释放对以前的WebView2环境对象的所有引用,或重新启动应用。下次应用创建新的WebView2环境时,应用将使用新版本的运行时。
当新版本的运行时可用时,你的应用可以自动采取措施,例如通知用户重新启动该应用。若要检测新版本的运行时是否可用,可以在代码中使用add_NewBrowserVersionAvailable(Win32)
或CoreWebView2Environment.NewBrowserVersionAvailable(.NET)
事件。如果你的代码处理重新启动应用,请考虑在WebView2应用退出之前保存用户状态。
管理用户数据文件夹的生命周期
https://docs.microsoft.com/zh-cn/microsoft-edge/webview2/concepts/user-data-folder
WebView2应用创建用户数据文件夹来存储Cookie、凭据和权限等数据。创建文件夹后,应用负责管理用户数据文件夹的生命周期。例如,卸载应用时,你的应用必须执行清理操作。
处理运行时进程故障
WebView2应用应侦听和处理事件,以便该应用可以从支持WebView2应用进程的运行时进程故障ProcessFailed
中恢复。
与应用进程一起运行的运行时进程集合支持WebView2应用。这些支持运行时进程可能由于各种原因(如内存不足或用户终止)而失败。当支持运行时进程失败时,WebView2将通过引发ProcessFailed
事件通知应用。
遵循建议的WebView2安全性最佳做法
https://docs.microsoft.com/zh-cn/microsoft-edge/webview2/concepts/security
对于任何WebView2应用,请确保遵循我们建议的WebView2安全性最佳做法。
WebView2的最佳安全指南
https://docs.microsoft.com/zh-cn/microsoft-edge/webview2/concepts/security
WebView2控件允许开发人员在本机应用程序中承载Web内容。正确使用时,承载Web内容具有多项优势,例如使用基于Web的UI、访问Web平台的功能、跨平台共享代码等。
为了避免承载Web内容时可能出现的漏洞,请确保设计WebView2应用程序以密切监视Web内容和主机应用程序之间的交互:
- 将所有Web内容视为不安全。
- 使用每个参数之前验证Web消息和主机对象参数,因为Web消息和参数可能格式不正确(无意或恶意)并会导致应用意外运行。
- 始终检查在WebView2内运行的文档的来源,并评估内容可信度。
- 设计特定的Web消息和主机对象交互,而不是使用泛型代理。
- 设置以下选项,通过修改
ICoreWebView2Settings(Win32)
或CoreWebView2Settings(.NET)
来限制Web内容。
https://docs.microsoft.com/zh-cn/microsoft-edge/webview2/reference/win32/icorewebview2settings
https://docs.microsoft.com/zh-cn/dotnet/api/microsoft.web.webview2.core.corewebview2settings
- 如果您不期望Web内容访问主机对象,则设置
AreHostObjectsAllowed
为false
。 - 如果预计Web内容不会向本机应用程序发布Web消息,则设置
IsWebMessageEnabled
为false
。 - 如果您不期望Web内容运行脚本,则设置
IsScriptEnabled
为false
(例如,当显示静态html content)。 - 如果您预计Web内容不会显示或对话框
AreDefaultScriptDialogsEnabled
为false
。
- 在以下步骤中,使用
NavigationStarting
和FrameNavigationStarting
事件根据新页面的来源更新设置。
- 若要阻止应用程序导航到特定页面,请使用事件检查然后阻止页面或框架导航。
- 导航到新页面时,你可能需要调整
ICoreWebView2Settings(Win32)
或CoreWebView2Settings(.NET)
上的属性值,如前面所述。
- 导航到新文档时,使用
ContentLoading
事件删除公开的主机对象RemoveHostObjectFromScript
。
参考
- WebView2简单试用(七)—— WebMessage
- 基于 Chromium Edge ,微软发布 WebView2 四项更新:Windows App SDK、WinUI2(UWP)、Win11 内置 WebView2 Runtime
- Microsoft EdgeWebView2 和 Microsoft 365 应用版
- WebView2 Windows App SDK, WinUI2, Runtime, and CDP Helper Updates
- .Net桌面端开发使用WebView2,可以放弃CefSharp?
- WebView2 使用及现状
- Microsoft Edge WebView2初体验
- C#使用Microsoft Edge WebView2记录
- 【WPF实用教程1】WPF使用Iconfont图标字体
- https://github.com/MicrosoftEdge/WebView2Samples/blob/master/SampleApps/WebView2WpfBrowser/MainWindow.xaml.cs
- SEGOEICONS
- iconfont-preview
- WebView2 control is missing from Toolbox
- WebView2 doesn't appear in Toolbox; NavigationCompleted does not occur on 2nd Navigation
- iconfont 在线预览工具及其解析
- 使用iconfont图标的unicode编码动态赋值,发现只显示编码,不显示图片
- C# Winform 中使用字体图标
- C#使用Microsoft Edge WebView2记录-C#和JS互相调用
- Setting an object from .NET to JavaScript code through WebView2
- .NET host objects need to use deprecated AutoDual attribute
- Using new WebView2 control and hitting older server with window.external calls... is there any way to capture those?
- Is AddScriptToExecuteOnDocumentCreatedAsync meant to execute on cross origin iframes?
- Setting an object from .NET to JavaScript code through WebView2
- Two way communication between native and JS
- C# WEBBROWSER控件与JS互调
- c#和javascript函数的相互调用(ObjectForScripting 的类必须对 COM 可见。请确认该对象是公共的,或考虑向您的类添加 ComVisible 属性。)
- WebBrowser 类
- WebBrowser.ObjectForScripting 属性
- https://github.com/Difegue/Mica-WPF-Sample
- Apply Mica to a WPF app on Windows 11
- The WindowChrome class needs to be updated & fixed #3887
- ModernWPF UI Library
- Built-in support for WinRT is removed from .NET