前言
對現代化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控件采用45x45的尺寸,CornerTextbox控件采用603x50的尺寸,其中內嵌的TextBox字體大小采用20pt。

.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
