前言
對現代化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