乘風破浪,遇見最美Windows 11之現代Windows桌面應用開發 - Microsoft Edge WebView2運行時


前言

對現代化Windows桌面應用而言,越來越多的應用程序采用Hybrid混合架構,即原生客戶端技術+Web網頁技術嵌入的混合模式提供應用服務,這樣既有原生技術先天的端能力優勢,又有來自Web技術的快速開發、靈活部署的優勢。

目前主流的方案肯定是"基於嵌入式Chromium框架(簡稱CEF)"解決方案,但是帶來的問題就是程序安裝包體積巨大,因為它必須要把整個Chromium內核完整的打包進去,而微軟原生控件WebView或者WebBrowser控件由於其技術或背后瀏覽器框架跟不上現代步伐,很難滿足實際場景需求,那么隨着Microsoft Edge積極采用Chromium內核,並被Windows 10/11內置,基於它誕生了WebView2這個控件,未來我們有希望可以直接通過使用WebView2來替代CEF,從而大幅降低混合架構開發模式下的安裝包體積,提高程序運行效率。

采用WebView2的核心優勢:1、縮小應用程序安裝包體積大小。2、降低應用程序磁盤空間占用。3、節約Hybrid架構開發實現成本。4、減少應用分發的CDN流量消耗。5、優化瀏覽器運行內核維護成本。

核心提要:1、從Windows 11開始的操作系統版本將直接內置WebView2運行時;2、Microsoft 365應用程序v2101版本已開始依賴WebView2運行時提供和Web無差體驗的新功能和特性;3、截止到目前,WebView2運行時已被超過2億台Windows設備部署。4、WebView2運行時自帶對H264編碼的支持,無需額外編譯配置。

什么是Microsoft Edge WebView2

https://docs.microsoft.com/zh-cn/microsoft-edge/webview2/

image

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運行時后,它不會顯示為用戶可見的瀏覽器應用。例如,用戶沒有瀏覽器桌面快捷方式或"開始"菜單中的條目。

image

有兩種不同的方法將"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)"后,不會更改用戶的默認瀏覽器選擇。

官方示例

https://github.com/MicrosoftEdge/WebView2Samples

勤學勤練

https://github.com/TaylorShi/HelloWebView2

創建解決方案及目錄

1. 新建名為"HelloWebView2"的解決方案

dotnet new sln -o HelloWebView2

image

2. 切換到"HelloWebView2"目錄

cd .\HelloWebView2\

image

創建.Net Core的Wpf項目

1. 創建名為"demoForWpfCore"的Wpf項目

dotnet new wpf -o demoForWpfCore -f net5.0

image

2. 添加"demoForWpfCore"到解決方案

dotnet sln add .\demoForWpfCore\demoForWpfCore.csproj

image

3. 切換到"demoForWpfCore"目錄

cd .\demoForWpfCore\

image

4. 運行"demoForWpfCore"項目

dotnet watch run

image

創建WinUI 3的桌面項目

1. 添加WinUI3的空白項目

在解決方案上右鍵,添加 => 新建項目,篩選C#語言,Windows平台,WinUI項目類型。

image

選擇"打包的空白應用(桌面版WinUI 3)(Blank App, Packaged(WinUI 3 in desktop))"項目類型,然后單擊"下一步"按鈕。

image

創建名為demoForWinUi3的項目。

image

創建成功之后,會發現多了兩個項目,一個是demoForWinUi3桌面項目,一個是demoForWinUi3 (Package)打包項目。

image

創建.Net Framework的Winforms項目

1. 創建名為"demoForWinFormFrame"的WinForms項目

image

這里需要將框架最低設置為:.Net Framework 4.5,這是目前WebView2的WinFroms包最低兼容版本。

image

image

2. 運行"demoForWinFormFrame"項目

image

.Net Framework WinForms項目添加並使用WebView2控件

.Net Framework WinForms項目安裝WebView2包

https://www.nuget.org/packages/Microsoft.Web.WebView2

demoForWinFormFrame項目右鍵進入"管理Nuget程序包"。

image

搜索關鍵詞WebView2即可找到Microsoft.Web.WebView2這個包,安裝即可。

image

image

初探嵌入WebView2控件

雙擊打開MainForm.cs文件,打開窗體設計視圖。

image

在Visual Studio頂部菜單的"視圖" => "工具欄",這時候我們會看到頂部會多出來一個WebView2 Windows Forms Control組,里面有個控件叫WebView2控件。

image

我們把它拖到右側的窗體中,並且填充顯示,並且我們給他取名為WebViewForMain,設置其初始的Source值為https://www.bing.com

image

image

接下來,我們運行看看效果

image

給WebView添加導航功能

為了更好的展示WebView2的相關能力,我們當然需要給它插上導航的翅膀,為此我們需要構建一個可輸入的面板和導航按鈕。

1. 使用字體圖標來構建按鈕,准備字體資源

首先,我們還是需要引入SegoeFluentIcons.ttf這個字體圖標文件,我們把它放在根目錄的Fonts文件夾中,生成操作需設置成"內容",復制到輸出目錄設置為"始終復制"。

image

image

然后我們需要借助一個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的組合來實現一個字體圖標的按鈕效果。

image

從左側工具箱中拖取兩個控件組合成上訴截圖效果,然后在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
}

查看下運行效果:

image

效果還算讓人滿意。

這里有個技巧就是,我應該如何得到每個圖形對應的這個字符文本,這里我找到一個能夠預覽字體圖標的小網站IconFont Preview By Luckly,進入后,我們選擇解析本地的ttf文件。

image

然后選中前面的SegoeFluentIcons.ttf文件上傳並解析,然后它會把字體中所有圖標的Unicode編碼展示出來,這里我們以前進和后退兩個圖標為例,我們會看到他們的編碼都是以&#xe開頭和;結尾的,我們只需要提取剩下的字符,加上前綴\ue即可,比如前進按鈕編碼&#xe0ab;而言,最終的編碼為\ue0ab,依次類推即可。

image

  1. 為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,有了它,我們便可以構建一個圓角的面板。

image

基於它,我們結合LabelTextBox這兩個自帶控件,分別組建自定義控件LabelButtonCornerTextbox,都用這個CornerRadiusPanel做圓角的底盤。

image

控件的相對位置可能需要耐心的調整,為了更加精致一點,這里我們的LabelButton控件采用45x45的尺寸,CornerTextbox控件采用603x50的尺寸,其中內嵌的TextBox字體大小采用20pt。

image

.Net Core Wpf項目添加並使用WebView2控件

.Net Core的WPF項目安裝WebView2包

https://www.nuget.org/packages/Microsoft.Web.WebView2

a. 命令行安裝"Microsoft.Web.WebView2"

dotnet add package Microsoft.Web.WebView2

image

b. 或者項目右鍵Nuget包管理,通過可視化界面安裝"Microsoft.Web.WebView2"

image

c. 安裝之前,Bin目錄結構

image

d. 安裝之后,Bin目錄結構

image

發現,新增了Microsoft.Web.WebView2.Core.dllMicrosoft.Web.WebView2.WinForms.dllMicrosoft.Web.WebView2.Wpf.dll這三個文件。

e. 安裝之后,運行效果

image

f. 命令行打開項目位置

explorer.exe .

image

初探嵌入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"控件的實際效果

image

給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事件。

實際效果如下:

image

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"
    />

最終效果如下圖:

image

修改地址欄內容並回車

image

嘗試WPF上實現Windows 11的Mica風格

最近朋友分享關於一個在WPF上實現Windows 11的Mica風格的演示項目。

https://github.com/Difegue/Mica-WPF-Sample

它的文章發布在Apply Mica to a WPF app on Windows 11

a. 新建名為demoForWpfCoreModernUI的Wpf的.Net Core 5.0的項目

dotnet new wpf -o demoForWpfCoreModernUI -f net5.0

image

dotnet sln add .\demoForWpfCoreModernUI\demoForWpfCoreModernUI.csproj

image

b. 修改demoForWpfCoreModernUI項目的目標框架

這里你可能會問,為什么要改這個?嗯,我試過,如果TargetFrameworknet5.0-windows的時候,安裝ModernWpfUI這個組件會跑不起來。

無法引用ModernWpf.dll,因為它使用了對WinRT的內置支持,而.NET 5和更高版本中不再支持它。需要支持.NET 5的更新版本組件。更多信息查看Built-in support for WinRT is removed from .NET

但是我發現Mica-WPF-Sample項目是可以用的,最終發現它雖然也是使用.Net 5,但是指定了更具體的一個版本,也許是被微軟攔截之前的。

image

所以,這里我們也將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包。

https://www.nuget.org/packages/ModernWpfUI

dotnet add package ModernWpfUI

image

image

image

注意,只有改了前面的TargetFrameworknet5.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_MODEDWMWA_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. 運行看看效果

image

效果還行,其實我驗證過,那個黑暗模式的下,效果出不來,具體為啥還沒弄清楚,總之就是沒透。

image

g. 結合前面的WebView2導航加持

image

WinUI項目添加並使用WebView2控件

添加WebView2控件

由於WinUI3中已經內置了WebView2控件了,所以我們不許額外安裝任何包就可以直接使用。

我們改造下HelloWinUI3桌面項目的MainWindow.xaml文件。

<WebView2
    x:Name="WebViewForMain"
    Source="https://www.bing.com"
    />

image

然后先編譯一次項目,隨后可以啟動部署試試,看看運行效果。

image

使用WebView2控件

為了更好的展示WebView2的能力,我們直接復制Demo4Window的已有能力好了。

image

目前WinUI控件提供的事件和能力還不夠完善,所以部分效果暫時屏蔽和替換了。

其中:

  • WebView2CoreWebView2InitializationCompleted事件需要替換成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}");
    }
}
  • BorderMouseDown事件需要替換成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
}
  • BorderMouseEnterMouseLeave需要替換成PointerEnteredPointerMoved
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);
}
  • WebView2Stop方法需要替換成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的打包項目中,我們可以選中一張高清圖作為圖標的素材來源,一鍵生成。

image

image

image

讓程序擁有自定義圖標

下載安裝圖標提取工具IconViewer

這里我們去提取一個來用,這里需要用到一個工具,叫IconViewer

https://www.botproductions.com/iconview/download.html

安裝地址:IconViewer3.02-Setup-x64.exe

安裝之后,啥動靜也沒有,但是實際已經有用了。

使用圖標提取工具IconViewer提取圖標

我們找到我們要提取的目標exe,嗯嗯,肯定是帶圖標的那個,我們就要提取他的圖標哈。

image

選中它,然后右鍵屬性。

image

如果安裝順利,這里會多出一個Icons的標簽,我們切過去,哈哈,驚喜來了,這里顯示了它的圖標,我們還可以選圖標的大小,毫無疑問,選最大的那個,點擊那個保存按鈕就可以了。

image

接下來,我們就順利得到一個超高清的Ico圖標了。

image

image

給應用程序掛載圖標

在項目上右鍵,打開項目"屬性",然后找到"圖標和清單"部分,瀏覽我們剛剛保存那個圖標即可。

image

運行一看,哈哈,已經生效了。

image

image

image

很香吧。

理解WebView2的導航事件

https://docs.microsoft.com/zh-cn/microsoft-edge/webview2/concepts/navigation-events

image

在網頁導航期間,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實例的源地址更新到地址輸入框中。

image

從首頁點擊頁面內的鏈接,跳轉到其他頁面之后,地址欄也會同步更新,顯示當前地址。

給頁面加載過程增加進度提示

有了前面的"導航開始(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操作中,我們同步控制GirdForProgressIsEnabled屬性、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. 運行演示效果

image

從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事件參數中IsSuccessTrue,說明瀏覽器核心初始化成功,如果為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代碼進行執行。

image

從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的動作,把當前網頁的地址發送給客戶端。

image

從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)",在風格上就完全參考之間的導航面板了。

image

b. 響應定制化消息發送

private void BorderForPost_MouseDown(object sender, MouseButtonEventArgs e)
{
    var messageContext = TextBoxForMessage.Text?.Trim();
    WebViewForMain.CoreWebView2.PostWebMessageAsString(messageContext);
}

在"發送按鈕(TextBlockForPost)"的響應事件BorderForPost_MouseDown中,通過CoreWebView2對象的PostWebMessageAsString方法,我們可以將界面上的定制化消息發送到網站,如果網站能接收到的話,那么根據前面的監聽機制,會彈出包含消息內容的警示彈窗,根據我們的設計,稍作注意是,需要重新加載新網頁才能響應。

image

從AddHostObjectToScript方法公開被Javascript調用的本機方法

https://docs.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2.addhostobjecttoscript?view=webview2-dotnet-1.0.992.28

為了更方便的實現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");

image

await chrome.webview.hostObjects.webView2Bridge.ClientValueBack("somethings");

image

處理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來根據進程失敗的種類分情況靈活處理。

進一步的處理細節參考:https://github.com/MicrosoftEdge/WebView2Samples/blob/c7d7c75184dec0c46634f27a8f4beba320b04618/SampleApps/WebView2WpfBrowser/MainWindow.xaml.cs#L223

優化WebView2導航控制

引入Segoe Fluent Icons字體圖標

今天我們引入一個Windows 11最新版的圖標字體Segoe Fluent Icons,如果想要查看字體內圖標清單,可以瀏覽:https://linrstudio.github.io/win11/SEGOEICONS.html 查閱。

image

而要在WPF中引入字體,並且使用,我們先把下載好的字體丟進項目下Fonts目錄。

image

記得將字體文件設置成"始終復制"和生成操作為"內容"。

image

稍后在TextBlock中寫FontFamily使用/MiniEdge;component/Fonts/#Segoe Fluent Icons,其中MiniEdge是程序集的命名空間,Fonts是字體文件的路徑,而Segoe Fluent Icons是字體名稱。

字體名稱建議你雙擊.ttf打開看一下。

image

而在TextBlock中的Text需要采用&#開頭和;結尾的編碼,比如:&#57606;

<TextBlock
    x:Name="TextBlockForNaviStop"
    FontFamily="/MiniEdge;component/Fonts/#Segoe Fluent Icons" 
    Text="&#57606;"
    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="&#57618;"
            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="&#57515;"
            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="&#57606;"
                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="&#57673;"
                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="&#57615;"
            FontSize="24"
            VerticalAlignment="Center"
            Foreground="Black"
            />
    </Border>

</Grid>

image

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
}

同時,為了當前后導航不可用的時候,能給用戶一個明確提示,我們將其禁用並且顏色置灰。

image

回到主頁按鈕,暫時用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);
    }
}

實際上,我們給所有的圖標按鈕標配了一個效果,就是鼠標移上去就背景變白,移開后恢復,這樣交互更加明確。

image

對於刷新和停止按鈕,我們還需要根據是否正在加載來切換他們的顯影,那么在之前的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;
    }
}

image

image

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="&#57763;"
            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
}

image

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);
}

接下來,當然輸入框被鼠標靠近的時候,我們讓輸入框背后的背景邊框變個顏色,並且加粗邊框,被鼠標移開的時候,效果還原。

image

image

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
}

image

g. 不如取個更好的名字吧

之前我們管它叫WebView2瀏覽器,該給它取個正式的名稱了,不如就叫MiniEdge吧,畢竟是借鑒了Edge的交互和視覺,還復用了它的渲染引擎。

需要注意的是,我們同時也把程序集名稱一起改了。

image

這樣最終exe就會改名字了。

image

注意也要把Demo4Window.xamlTitle改了。

image

WebView2的最佳部署指南

https://docs.microsoft.com/zh-cn/microsoft-edge/webview2/concepts/distribution#deploying-the-evergreen-webview2-runtime

WebView2的最佳開發指南

每個開發團隊在構建其應用程序時都遵循不同的做法。生成WebView2生產應用時,建議遵循這些建議和最佳做法。

使用EvergreenRuntime(推薦)

我們通常建議使用"Evergreen WebView2運行時"。固定版本運行時分發僅建議用於具有嚴格兼容性要求的應用。"Evergreen運行時"在客戶端上自動更新,以便你的WebView2應用可以使用最新的功能和安全修補程序。與固定版本運行時相比,"Evergreen運行時"還需要更少的磁盤上的存儲空間。

https://docs.microsoft.com/zh-cn/microsoft-edge/webview2/concepts/distribution#deploying-the-evergreen-webview2-runtime

如果使用"Evergreen運行時",在運行WebView2應用之前,測試是否已在客戶端上安裝"Evergreen WebView2運行時"。

使用Evergreen運行時時定期運行兼容性測試

https://www.microsoftedgeinsider.com/download

使用"Evergreen WebView2運行時"時,運行時會自動更新,因此必須定期運行兼容性測試。若要確保WebView2應用繼續正常工作,請針對Microsoft Edge Insider(preview)Channels(Beta、Dev或Canary)在WebView2控件中測試Web內容。

https://docs.microsoft.com/zh-cn/microsoft-edge/webview2/concepts/distribution#test-your-app-for-forward-compatibility

本指南類似於我們向Web開發人員提供的指導。

測試安裝的WebView2運行時是否支持較新的API

若要運行使用Webview2 SDK的特定版本開發的WebView2應用,客戶端必須已安裝WebView2運行時的兼容版本。由於API不斷添加到WebView2,因此也發布了新版本的運行時以支持新的API。使用功能檢測確保安裝在客戶端上的WebView2運行時支持WebView2應用使用的較新的API。

如果使用"Evergreen WebView2運行時",在某些情況下,客戶端上的運行時尚未自動更新到最新版本。例如,如果客戶端沒有Internet訪問權限,則運行時不會自動更新。此外,某些組策略會暫停運行時的更新。將更新推送到WebView2應用時,如果應用嘗試調用客戶端安裝運行時中不可用的較新API,該應用可能無法運行。

https://docs.microsoft.com/zh-cn/microsoft-edge/webview2/concepts/versioning#feature-detecting-to-test-whether-the-installed-runtime-supports-recently-added-apis

若要解決此問題,在代碼調用最近添加的WebView2 API之前,測試該API在客戶端的安裝運行時中是否可用。此較新功能測試與其他Web開發最佳實踐類似,這些最佳實踐在使用新的WebAPI之前檢測支持的功能。若要測試已安裝運行時中的API可用性,請使用:

  • QueryInterface在C/C++中。
  • try/catch .NET或WinUI中的塊。

更新固定版本運行時

https://docs.microsoft.com/zh-cn/microsoft-edge/webview2/concepts/distribution#details-about-the-fixed-version-runtime-distribution-mode

如果使用固定版本的"WebView2運行時",請確保定期更新與應用打包的"WebView2運行時",以減少安全風險。在Webview2應用中使用第三方內容時,始終考慮不受信任的內容。

管理新版本的EvergreenRuntime

將新版本的"Evergreen WebView2運行時"下載到客戶端后,正在運行的任何WebView2應用將繼續使用早期版本的運行時,直到發布瀏覽器進程。此行為允許應用連續運行,並阻止刪除以前的運行時。若要使用新版本的運行時,需要釋放對以前的WebView2環境對象的所有引用,或重新啟動應用。下次應用創建新的WebView2環境時,應用將使用新版本的運行時。

https://docs.microsoft.com/zh-cn/microsoft-edge/webview2/reference/win32/icorewebview2environment#add_newbrowserversionavailable

https://docs.microsoft.com/zh-cn/dotnet/api/microsoft.web.webview2.core.corewebview2environment.newbrowserversionavailable

當新版本的運行時可用時,你的應用可以自動采取措施,例如通知用戶重新啟動該應用。若要檢測新版本的運行時是否可用,可以在代碼中使用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中恢復。

https://docs.microsoft.com/zh-cn/microsoft-edge/webview2/reference/win32/icorewebview2processfailedeventargs

與應用進程一起運行的運行時進程集合支持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內容和主機應用程序之間的交互:

  1. 將所有Web內容視為不安全。
  • 使用每個參數之前驗證Web消息和主機對象參數,因為Web消息和參數可能格式不正確(無意或惡意)並會導致應用意外運行。
  • 始終檢查在WebView2內運行的文檔的來源,並評估內容可信度。
  1. 設計特定的Web消息和主機對象交互,而不是使用泛型代理。
  2. 設置以下選項,通過修改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內容訪問主機對象,則設置AreHostObjectsAllowedfalse
  • 如果預計Web內容不會向本機應用程序發布Web消息,則設置IsWebMessageEnabledfalse
  • 如果您不期望Web內容運行腳本,則設置IsScriptEnabledfalse(例如,當顯示靜態html content)。
  • 如果您預計Web內容不會顯示或對話框AreDefaultScriptDialogsEnabledfalse
  1. 在以下步驟中,使用NavigationStartingFrameNavigationStarting事件根據新頁面的來源更新設置。
  • 若要阻止應用程序導航到特定頁面,請使用事件檢查然后阻止頁面或框架導航。
  • 導航到新頁面時,你可能需要調整ICoreWebView2Settings(Win32)CoreWebView2Settings(.NET)上的屬性值,如前面所述。
  1. 導航到新文檔時,使用ContentLoading事件刪除公開的主機對象RemoveHostObjectFromScript

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM