在WPF中使用WebView2详解

Microsoft Edge WebView2

Microsoft Edge WebView2 控件允许在本机应用中嵌入 web 技术(HTML、CSS 以及 JavaScript)。 WebView2 控件使用 Microsoft Edge 作为绘制引擎,以在本机应用中显示 web 内容。

使用 WebView2 可以在本机应用的不同部分嵌入 Web 代码,或在单个 WebView2 实例中生成所有本机应用。

现在使用混合技术开发桌面客户端已经变得越来越常见了,如网易云音乐、QQ音乐等,因为可以嵌入现有的网页端页面,开发成本是比较低的,而且还可以实现跨平台。

使用混合技术开发的桌面客户端占用资源相对较多,反应速度也会慢点,如果需要对操作系统有较多的功能交互,推荐使用原生API开发。

WebView2已经出来好几年了,我一直没怎么使用过,前面一直使用的是CEFSharpCEF是基于Google Chromium项目的开源Web browser控件,和CEFSharp一样,WebView2也支持winformwpf

简单来说,WebView2就是一个浏览器控件,类似WPF里自带的WebBrowser,只是WebView2的内核ChromeWebBrowser的内核是IE,而且WebView2提供的可编程接口更多。

对于桌面开发开说,我们可以使用WebView2来实现以下功能

1、爬虫

对于动态网页,可以嵌入WebView2来进行抓取。

2、自制浏览器

使用WebView2可以自己开发简单的浏览器

3、嵌入本地网页

有时候要展示一些简单的网页内容,如报告、图表等,都可以使用WebView2进行加载显示 

4、开发混合应用

借助WebView2的本机和Web互操作功能,开发混合应用,例如:网易云音乐。

最近我们有一个需求是需要绘制大量的图表,使用WPF的免费控件都不能很好的满足需求,后面就选择使用Echarts(Echarts是一款基于JavaScript的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表),然后嵌入网页到WPF中来实现。

本来是准备使用CEF的,同事在前期使用了WebView2,所以就继续使用WebView2

这里我写篇文章做个记录,以便以后需要时查看。注意:本文仅介绍了WebView2中的常用功能,全部功能的使用和介绍可以访问WebView2的官方文档。

WebView2的优点

  • 本机功能。 访问完整的本机 API 集。

  • 代码共享。 向代码库添加 web 代码可以增加跨多个平台的重用。

  • Microsoft 支持。 Microsoft 在受支持的平台上提供支持并添加新功能请求。

  • 定期发布更新和安全修补的最新版 Chromium。

  • 已修复版本分布。 也可以在应用中打包特定版本的 Chromium 位。

  • 支持Windows10/11

WebView2支持的平台

编程语言

  • Win32 C/C++
  • .NET Framework 4.6.2 +
  • .NET Core 3.1 +
  • WinUI 2.0
  • WinUI 3.0

操作系统

  • Windows 11
  • Windows 10
  • Windows 10 IoT Enterprise LTSC x32 2019
  • Windows 10 IoT Enterprise LTSC x64 2019
  • Windows 10 IoT Enterprise 21h1 x64
  • Windows Server 2022
  • Windows Server 2019
  • Windows Server 2016

安装WebView2运行时

如果电脑中没有安装新版本Microsoft Edge(基于Chrome内核),就需要安装WebView2运行时。

分发应用时,最好带上WebView2运行时的安装包,因为不确定用户电脑上是否会有WebView2运行时。

下载地址:

https://go.microsoft.com/fwlink/p/?LinkId=2124703

WebView2 运行时中的进程

WebView2 进程组是 WebView2 运行时进程的集合。 WebView2 进程组包括以下内容:

  • 单个浏览器进程。
  • 一个或多个呈现器进程。
  • 其他帮助程序进程,例如 GPU 进程和音频服务进程。

WebView2 进程组中的进程数和状态可能会随着 WebView2 应用程序使用 WebView2 功能而更改。 (但是,WebView2 进程组中只有一个特定的浏览器进程。) 例如,从同 CoreWebView2Environment一个 创建新的 WebView2 实例,但属性中 Source 具有不同的域,通常会启动新的呈现器进程。

呈现器进程的数量可能因以下条件而异:

  • 在 WebView2 运行时中使用 站点隔离 功能。 

  • 在使用相同用户数据文件夹的 WebView2 实例中呈现的不同断开连接源的数目。

控制何时创建这些额外进程的逻辑取决于Chromium体系结构,并且超出了 WebView2 运行时的范围。

WebView2 运行时进程和用户数据文件夹

WebView2 运行时进程集合中的所有进程都与浏览器进程相关联,浏览器进程又与单个用户数据文件夹相关联。 如果应用程序使用多个用户数据文件夹,将为其中每个用户数据文件夹创建一组 WebView2 运行时进程。

用户数据文件夹可由多个应用程序共享,但请务必考虑对性能和管理的影响,如 管理用户数据文件夹中所述。

若要使用多个用户数据文件夹,WebView2 应用程序需要创建不同的 CoreWebView2Environment 对象。 WebView2通过配置的 CoreWebView2Environment 对象为给定的用户数据文件夹创建实例。 每个 CoreWebView2Environment 对象都需要配置不同的用户数据文件夹值。

为给定的用户数据文件夹创建第一 WebView2 个实例时,将启动与该用户数据文件夹关联的 WebView2 运行时进程集合的浏览器进程。 所有其他进程将由该浏览器进程的生存期管理。

多个环境对象

如果创建以相同方式配置的多个 CoreWebView2Environment 对象, (包括共享同一用户数据文件夹) ,则它们将表示相同的用户数据文件夹和相同的关联进程集合。 使用这些对象中的任何 CoreWebView2Environment 一个创建具有一个 CoreWebView2 共享用户数据文件夹和关联的进程集合的 。

如果尝试使用另CoreWebView2Environment一个已使用的用户数据文件夹创建 CoreWebView2Environment ,并且未将两个CoreWebView2Environment对象配置为相同(例如,如果它们使用不同的值创建CoreWebView2EnvironmentOptions.Language),则第二CoreWebView2Environment个对象将无法创建WebView2对象。 

在WPF中使用WebView2

这里以Visual Studio 2022.NET6进行演示

1、使用Visual Studio创建一个WPF工程

2、使用nuget引入包Microsoft.Web.WebView2

 3、在XAML中引入命名空间

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

4、放置一个WebView2控件

  <Window x:Class="WpfWebView2Demo.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:webview2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
          xmlns:local="clr-namespace:WpfWebView2Demo"
          mc:Ignorable="d"
          Title="MainWindow" Height="720" Width="1280">
      <Grid>
          <webview2:WebView2 x:Name="webview2" 
          Source="https://myfreetime.cn"></webview2:WebView2>
      </Grid>
  </Window>

5、运行效果

注意事项:

请将项目平台设置成x64而不是Any CPU。

以前在使用CEF时,如果使用Any CPU,在XAML设计器会无法实时预览CEF控件,WebView2虽然没有这个问题,但是也会也现IntelliSense无法正常工作的问题。

基本导航功能

1、通过Source属性,可以设置初始URI(支持网址、本地文件等)

2、导航到指定内容

调用WebView2.CoreWebView2.Navigate函数即可执行导航,如

webView2.CoreWebView2.Navigate("https://www.bing.com");
webView2.CoreWebView2.Navigate("C:\Users\xxx\Documents\说明书.pdf"); 


webView2.CoreWebView2.Navigate("file:///C:/Users/username/Documents/GitHub/Demos/demo-to-do/index.html");

 3、后退

调用WebView2.CoreWebView2.GoBack函数即可后退

webview2.CoreWebView2.GoBack();

4、前进

调用WebView2.CoreWebView2.GoForward函数即可前进

webview2.CoreWebView2.GoForward();

导航到本地内容

WebView2提供了几种方式,可以导航到本地内容。

通过不同的方式加载本地内容时会有一些区别 ,如下所示

Scenario导航到文件路径导航到HTML字符串使用虚拟主机名称映射使用WebResourceRequested
Origin-based DOM APIs✔️✔️✔️
DOM APIs requiring secure context✔️✔️
Dynamic content✔️✔️
Additional web resources✔️✔️✔️
Additional web resources resolved in WebView2 process✔️✔️

下面我们依次介绍各种方法

1、直接导航到本地文件

this.webview2.CoreWebView2.Navigate("D:\\demo.html");

通过文件路径方式加载的一些限制:

  • 指定文件URL时,应用程序会导航到磁盘上的文件,而不是网络上的域。因此,无法在生成的文档中使用跨源资源。
  • 从不同文件URL加载的不同文档不被视为来自同一来源,并且无法访问相同的存储数据。
  • 某些web API仅限于安全的HTTPS URL,不适用于文件URL加载的文档。
  • 不能使用相对路径,必须使用绝对路径
  • 要允许从文件URI引用其他本地文件,或显示应用了XSL转换的XML文件,可以设置--allow file access from files浏览器参数。

方法如下:

1  //允许从文件URI引用其他本地文件
2  var options = new CoreWebView2EnvironmentOptions("--allow-file-access-from-files");
3  var environment = await CoreWebView2Environment.CreateAsync(options: options);
4  await webview2.EnsureCoreWebView2Async(environment);
  • 当通过文件URL加载文档时,文档的内容来自磁盘上的静态文件。这意味着无法动态修改此本地内容。这与从web服务器加载文档不同,在web服务器中,每个响应都可以动态生成。
  • 通过文件URL加载的文档,可以引用其他web资源,如CSS、脚本或图像文件,这些文件也通过文件URL提供。

在Windows平台下,可以指定文件的绝对路径进行访问,如

D:\demo.html

如果想独立于各个平台,应该使用标准的方式指定文件路径,如

file:///D:/demo.html
file:///C:/Users/username/Documents/GitHub/Demos/demo-to-do/index.html

2、导航到HTML字符串

var html = System.IO.File.ReadAllText("D:\\demo.html");
 
this.webview2.CoreWebView2.NavigateToString(html);

通过HTML字符串加载时,会有一些限制:

  • 使用NavigateToString方法加载的文档的位置设置为about:black,原点设置为null。这意味着不能使用依赖于所定义的源的web API,如localStorage或indexedDB。
  • 某些web API仅限于安全的HTTPS URL,并且不可用于通过NavigateToString方法加载的文档,因为它们的位置设置为about:black。
  • 通过NavigateToString方法加载本地内容时,直接将内容作为参数提供给该方法。这也就意味着可以动态显示内容。
  • 使用NavigateToString方法加载本地内容不会使生成的文档引用其他web资源,如CSS、图像或脚本文件。该方法只允许您指定HTML文档的字符串内容,可以在HTML文档中内联表示这些(或者使用下面的两种方法)。

3、使用虚拟主机映射 

1 //将demo映射到D:\demohtml\preview文件夹
2 this.webview2.CoreWebView2.SetVirtualHostNameToFolderMapping("demo", @"D:\demohtml\preview", CoreWebView2HostResourceAccessKind.DenyCors);
4 //可以通过https的方式访问,实际映射到D:\demohtml\preview\index.html
5 this.webview2.CoreWebView2.Navigate("https://demo/index.html");

使用虚拟主机映射时,会有一些限制:

  • 通过虚拟主机名映射加载的本地内容会生成一个具有HTTP或HTTPS URL和相应来源的文档。这意味着,需要localStorage或indexedDB等来源的web API将起作用,并且属于同一来源的其他文档将能够使用存储的数据。
  • 支持HTTPS的Web API
  • 通过虚拟主机名映射加载本地内容时,将虚拟主机名映像到磁盘上包含静态文件的本地文件夹。这意味着无法动态修改此本地内容。这与从web服务器加载文档不同,在web服务器中,每个响应都可以动态生成。
  • 通过虚拟主机名映射加载的本地内容具有支持相对URL解析的HTTP或HTTPS URL。这意味着加载的文档可以引用其他web资源,如CSS、脚本或图像文件,这些文件也通过虚拟主机名映射提供。

4、处理WebResourceRequested 事件

当WebView2尝试加载资源时,会触发WebResourceRequested 事件,我们可以使用此事件拦截请求并提供本地内容。在WebResourceRequested事件处理函数中,可以根据每个请求自定义本地内容的行为。

  private void btn_handleWebResourceRequested_Click(object sender, RoutedEventArgs e)
   {
       //为WebResourceRequested事件添加Uri和资源上下文过滤器
       this.webview2.CoreWebView2.AddWebResourceRequestedFilter("https://demo/*", CoreWebView2WebResourceContext.All);
       this.webview2.CoreWebView2.WebResourceRequested += CoreWebView2_WebResourceRequested;       
   }
  
   private void CoreWebView2_WebResourceRequested(object? sender, CoreWebView2WebResourceRequestedEventArgs e)
   {
       //获取本地文件路径
       //注意:资源文件要放在一个路径下
       string assetsFilePath = @"D:\demohtml\preview" + e.Request.Uri.Substring("https://demo/*".Length - 1);
       try
       {
           FileStream fs = File.OpenRead(assetsFilePath);
           ManagedStream ms = new ManagedStream(fs);
           string headers = "";
           if (assetsFilePath.EndsWith(".html"))
           {
               headers = "Content-Type: text/html";
           }
           else if (assetsFilePath.EndsWith(".jpg"))
           {
               headers = "Content-Type: image/jpeg";
           }
           else if (assetsFilePath.EndsWith(".png"))
           {
               headers = "Content-Type: image/png";
           }
           else if (assetsFilePath.EndsWith(".css"))
           {
               headers = "Content-Type: text/css";
           }
           else if (assetsFilePath.EndsWith(".js"))
           {
               headers = "Content-Type: application/javascript";
           }
  
           e.Response = this.webview2.CoreWebView2.Environment.CreateWebResourceResponse(ms, 200, "OK", headers);
       }
       catch (Exception)
       {
           e.Response = this.webview2.CoreWebView2.Environment.CreateWebResourceResponse(null, 404, "Not found", "");
       }
  
   }

ManagedStream.cs

 public class ManagedStream : Stream
  {
      public ManagedStream(Stream s)
      {
          steam = s;
      }
  
      public override bool CanRead => steam.CanRead;
  
      public override bool CanSeek => steam.CanSeek;
  
      public override bool CanWrite => steam.CanWrite;
  
      public override long Length => steam.Length;
  
      public override long Position { get => steam.Position; set => steam.Position = value; }
  
      public override void Flush()
      {
          
      }
  
      public override long Seek(long offset, SeekOrigin origin)
      {
          return steam.Seek(offset, origin);
      }
  
      public override void SetLength(long value)
      {
         
      }
  
      public override int Read(byte[] buffer, int offset, int count)
      {
          int read = 0;
          try
          {
              read = steam.Read(buffer, offset, count);
              if (read == 0)
              {
                  steam.Dispose();
              }
          }
          catch
          {
              steam.Dispose();
              throw;
          }
          return read;
      }
  
      public override void Write(byte[] buffer, int offset, int count)
      {
        
      }
  
      private Stream steam;
  }

处理WebResourceRequested事件来加载本地内容时,也会有一些限制:

  • 通过WebResourceRequested加载的本地内容会生成一个具有HTTP或HTTPS URL和相应来源的文档。这意味着,需要localStorage或indexedDB等来源的web API将起作用,并且属于同一来源的其他文档将能够使用存储的数据。
  • 某些web API仅限于安全的HTTPS URL。使用WebResourceRequested可以将HTTPS URL web资源请求替换为我们自己的本地内容。
  • 通过WebResourceRequested加载本地内容时,我们在事件处理程序中指定要加载的本地内容,也就是可以动态生成内容。
  • WebResourceRequested修改通过支持相对URL解析的HTTP或HTTPS URL加载的内容。这意味着生成的文档可以引用其他web资源,如CSS、脚本或图像文件,这些文件也通过WebResourceRequested提供。
  • 通过文件URL或虚拟主机名映射加载内容时,解析发生在WebView2进程中。但是,WebResourceRequested事件是在宿主应用程序进程的WebView2 UI线程上引发的,这可能会导致结果文档的加载速度减慢。(如果加载的文件过多,过大,会出现界面假死的情况)

导航事件

在WebView2导航到指定URI期间,会按顺序引发以下事件

  • NavigationStarting
  • SourceChanged
  • ContentLoading
  • HistoryChanged
  • NavigationCompleted

如果导航失败,会按顺序引发以下事件

  • SourceChanged
  • ContentLoading
  • HistoryChanged

说明:

NavigationStarting事件中,可以通过设置args.Cancel=true来取消导航 

本机端和 Web 端代码的互操作

Microsoft Edge WebView2 控件允许将 Web 内容嵌入本机应用程序。 可以根据需要完成的任务,以不同的方式使用 WebView2。 

所以就需要本机与Web端进行互操作,例如:

  • 导航到其他网站后,更新本机主机窗口标题。
  • 从 Web 应用发送本机相机对象并使用其方法。
  • 在应用程序的 Web 端运行专用 JavaScript 文件。

WebView2支持以下几种互操作方式

执行JS脚本

这个功能对于混合开发很重要。以前我在CEFSharp中嵌入Echarts时,就是调用类似的函数将数据传到页面上。

WebView2提供了两种方式执行JS脚本,这两种方式都可以执行JS脚本,只是执行时机不一样

API说明
ExecuteScriptAsync在 WebView2 控件中运行 JavaScript。 在页面 文档对象模型 (DOM) 加载内容 或 完成导航后调用此方法。
AddScriptToExecuteOnDocumentCreatedAsync创建 DOM 时,在每个页面上运行。 在初始化 CoreWebView2 后调用此方法。

ExecuteScriptAsync

例如下面在浏览器弹框输出HelloWorld

1 await this.webview2.CoreWebView2.ExecuteScriptAsync("alert('HelloWorld')");

ExecuteScriptAsync会返回执行结果值的JSON串。

同时ExecuteScriptAsync也支持执行本地的js文件

例如我在运行路径下创建了一个changecolor.js

document.getElementsByTagName("body")[0].style.backgroundColor = "green";

然后加载执行:

var text = System.IO.File.ReadAllText(@"changecolor.js");
await webView2.CoreWebView2.ExecuteScriptAsync(text);

 可以看到网页背景变成了绿色

AddScriptToExecuteOnDocumentCreatedAsync

这个会增加一个在DOM加载但是未创建之前调用的脚本,所以可以进行一些初始始工作。

例如,禁用右键菜单

 1      private async  void webview2_Loaded(object sender, RoutedEventArgs e)
 2      {
 3          await webview2.EnsureCoreWebView2Async();
 4 
 5          if(this.webview2.IsLoaded)
 6          {
 7              //禁用右键菜单
 8              await this.webview2.CoreWebView2.ExecuteScriptAsync("window.addEventListener('contextmenu', window => {window.preventDefault();});");
 9          }
10      }

主机(应用程序)和Web页面通信

WebView2提供了两种方式和Web页面通信

  • 主机使用 CoreWebView2.PostWebMessageAsString 或 CoreWebView2.PostWebMessageAsJSON将消息发布到 WebView2 控件中的 Web 内容。 消息由添加到 window.chrome.webview.addEventListener的处理程序捕获。

  • WebView2 控件中的 Web 内容可以使用 将消息发布到主机 window.chrome.webview.postMessage。 主机使用主机上注册 WebMessageReceived 的任何内容处理消息。

使用PostWebMessageAsString

首先我们创建一个简单的网页(因为我不会前端 ,所以这真的是一个非常简单的页面)

这个页面会在收到字符串时进行打印

html

 <!DOCTYPE html>
 <html>
 <head>
     <title>ScenarioWebMessage</title>
     <script>
         window.chrome.webview.addEventListener('message', arg => {
             document.writeln(arg.data)
         });
     </script>
 </head>
 <body>
    
 </body>
 </html>

 cs

this.webview2.CoreWebView2.PostWebMessageAsString(inputWindow.Input);

然后调用PostWebMessageAsString函数发送字符串到Web端,页面会进行打印

PostWebMessageAsJson

这里发送一个简单的json串来指定页面的背景图片,依旧是一个非常简单的页面

html

  <!DOCTYPE html>
  <html>
  <head>
      <title>ScenarioWebMessage</title>
      <script>
          window.chrome.webview.addEventListener('message', arg => {
               document.body.style.backgroundImage = "url('" + arg.data.background + "')";
          });
      </script>
  </head>
  <body>
  
  </body>
  </html>

 cs

var json = "{\"background\":\"https://myfreetime.cn/usr/uploads/2024/4/%E6%B8%85%E5%B9%B3%E8%B0%83%C2%B7%E5%90%8D%E8%8A%B1%E5%80%BE%E5%9B%BD%E4%B8%A4%E7%9B%B8%E6%AC%A2/jk3.jpg\"}";

this.webview2.CoreWebView2.PostWebMessageAsJson(json);

执行以后可以看到网页的背景换了

将消息发送到主机

在页面中,调用window.chrome.webview.postMessage可以将消息发送到主机

首先我们创建一个html页面,在这个页面中增加一个按钮和文本框,当点击按钮时,将消息发送到主机

html

 <!DOCTYPE html>
 <html>
 <head>
     <title>ScenarioWebMessage</title>
     <script>
         function SetTitleText() {
             let titleText = document.getElementById("title-text");
             window.chrome.webview.postMessage(`${titleText.value}`);
         }
     </script>
 </head>
 <body>
     <div id="colorable">
         <h2>Receiving Messages</h2>
         <p>
             The host app can receive messages by registering an event handler
             with <code>ICoreWebView2::add_WebMessageReceived</code>. If you
             enter text and click "Send", this page will send a message to the
             host app which will change the text of the title bar.
         </p>
         <input type="text" id="title-text" />
         <button οnclick="SetTitleText()">Send</button>
     </div>
 </body>
 </html>

WebView2.CoreWebView2.WebMessageReceived事件中处理接收的消息

   private void CoreWebView2_WebMessageReceived(object? sender, CoreWebView2WebMessageReceivedEventArgs e)
   {
       this.lbl_status.Content = "从网页接收到:" + e.TryGetWebMessageAsString();
   }

 运行后在网页输入内容,在主机可以收到相应的内容

借助以上方法,可以做到消息的闭环,可以很好地将主机和Web交互起来。

处理与进程相关的事件

WebView2 使用多个进程来支持应用程序中的 WebView2 控件。 由于这些进程可以在使用过程中退出,因此WebView2增加了一些事件通知。

 通俗来说,当WebView2控件初始化完成后,WebView2控件将开始监视这些进程并报告以下事件:

  • 任何进程失败。 当 WebView2 运行时中的任何 进程 失败时,CoreWebView2 将引发 事件 ProcessFailed 

  • 主浏览器进程退出。 如果main浏览器进程出于任何原因退出,CoreWebView2Environment则会引发 事件BrowserProcessExited。 

  • 主浏览器进程崩溃。 当main浏览器进程崩溃时,它将同时生成ProcessFailed事件和BrowserProcessExited事件,因为main浏览器进程因失败而退出

这里我们做一个测试,

xaml

  <webview2:WebView2 x:Name="webview2" Source="https://myfreetime.cn"
                     Loaded="webview2_Loaded"></webview2:WebView2>

cs

   private async  void webview2_Loaded(object sender, RoutedEventArgs e)
   {
       await webview2.EnsureCoreWebView2Async();
  
       if(this.webview2.IsLoaded)
       {
           this.webview2.CoreWebView2.ProcessFailed += CoreWebView2_ProcessFailed;
           this.webview2.CoreWebView2.Environment.BrowserProcessExited += Environment_BrowserProcessExited;
       }
   }
  
   private void Environment_BrowserProcessExited(object? sender, CoreWebView2BrowserProcessExitedEventArgs e)
   {
       MessageBox.Show("Environment_BrowserProcessExited");
   }
  
   private void CoreWebView2_ProcessFailed(object? sender, Microsoft.Web.WebView2.Core.CoreWebView2ProcessFailedEventArgs e)
   {
       MessageBox.Show("CoreWebView2_ProcessFailed");   
   }

然后我们用任务管理器关闭WebView2的一个进程,可以看到会引发 ProcessFailed 事件

打印

WebView2提供了以下打印函数

ShowPrintUI打开“WebView2 打印预览 ”对话框或操作系统的“ 打印 ”对话框。 易于实现,对自定义的支持最少。
Print使用可选的以编程方式指定的打印设置将 WebView2 中的当前顶级文档打印到打印机。 可以使用此功能生成自己的“打印预览”对话框或打印体验。
PrintToPdf以无提示方式将 WebView2 中的当前顶级文档打印为 PDF 文件。 可以使用它生成自己的代码来打印 PDF 文件。
PrintToPdfStream以无提示方式将 WebView2 中的当前顶级文档打印到 PDF 流。 可以使用它来生成自己的代码来打印 PDF。

例如我们可以将当前网页打印为PDF

var filePath = Environment.CurrentDirectory + "\\output.pdf";
await this.webview2.CoreWebView2.PrintToPdfAsync(filePath);

自定义上下文菜单

WebView2提供了一个默认的上下文菜单,像下面这样

如果想自定义上下文菜单,WebView2提供了一个ContextMenuRequested 事件,在事件处理函数中可以自定义上下文菜单。

像下面这样

   private void btn_addcontextmenu_Click(object sender, RoutedEventArgs e)
   {
       this.webview2.CoreWebView2.ContextMenuRequested += CoreWebView2_ContextMenuRequested;
   }
  
   private void CoreWebView2_ContextMenuRequested(object? sender, CoreWebView2ContextMenuRequestedEventArgs e)
   {
  
       IList<CoreWebView2ContextMenuItem> menuList = e.MenuItems;
       CoreWebView2Deferral deferral = e.GetDeferral();
       e.Handled = true;
       ContextMenu cm = new ContextMenu();
       cm.Closed += (s, ex) => deferral.Complete();
  
       //添加上下文菜单
       AddContextMenu();
  
       cm.IsOpen = true;
   }

属性 AreDefaultContextMenusEnabled 控制是否可以打开任何上下文菜单。 如果 WebView2 AreDefaultContextMenusEnabled 设置设置为 False,则会禁用上下文菜单,并且 ContextMenuRequested 不会引发事件(例如用户右键单击时)。

AreDefaultContextMenusEnabled 属性为True时且当前网页允许显示上下文菜单时,WebView2 控件才会引发 ContextMenuRequested 事件。

 AreDefaultContextMenusEnabled 默认值为True。

在添加自定义上下文菜单之前,我们还需要了解两个类型:

1、System.Windows.Controls.ContextMenu类型

自定义上下文菜单时,需要用到ContextMenu类做为上下文菜单容器。ContextMenu是WPF自带的上下文菜单类,如果你还没有接触过,可以访问下面的链接

ContextMenu Class (System.Windows.Controls) | Microsoft Learn

2、Microsoft.Web.WebView2.Core.CoreWebView2ContextMenuItem类型

CoreWebView2ContextMenuItem是上下文菜单容器项,WebView2没有直接使用WPF的MenuItem类型,而是新增加了一个类型

 CoreWebView2ContextMenuItem定义的属性如下:

Children

当菜单项类型是子菜单时,获取菜单项的子菜单

CommandId

获取CoreWebView2ContextMenuItem的命令Id

Icon

获取图标,图标的来源是PNG, Bitmap 或SVG 格式的数据流(IStream)

IsChecked

获取或设置菜单项是否被选中。

IsEnabled

获取或设置菜单项是否启用(仅适用于自定义菜单).

Kind

获取菜单项类型( CoreWebView2ContextMenuItemKind.)

Label

获取菜单项的文本,支持快捷键(使用&开头)

Name

获取菜单项未本地化的名字

ShortcutKeyDescription

获取菜单项本地化键盘快捷键

CoreWebView2ContextMenuItem定义的事件如下

CustomItemSelected

当用户选择了CoreWebView2ContextMenuItem,会引发CustomItemSelected事件

菜单项类型

对于不同的元素,上下文菜单项的显示会不一样,例如,对于图片,显示的是

可以通过ContextMenuRequested 事件的参数CoreWebView2ContextMenuRequestedEventArgs.ContextMenuTarget.Kind来判断。

CoreWebView2ContextMenuRequestedEventArgs.ContextMenuTarget.Kind是一个CoreWebView2ContextMenuTargetKind枚举类型

取值如下:

Audio3

指示 这是为音频元素创建的上下文菜单

Image1

指示 这是为图像元素创建的上下文菜单

Page0

指示为页面创建的上下文菜单不包含任何其他内容。

SelectedText2

指示是为选择的文本创建的上下文菜单

Video4

指示是为视频 元素创建的上下文菜单

如何移除默认上下文菜单项

   private void btn_removeimagecontextmenu_Click(object sender, RoutedEventArgs e)
   {
       this.webview2.CoreWebView2.ContextMenuRequested += CoreWebView2_ContextMenuRequested1;
   }
  
   private void CoreWebView2_ContextMenuRequested1(object? sender, CoreWebView2ContextMenuRequestedEventArgs e)
   {
       CoreWebView2ContextMenuTargetKind context = e.ContextMenuTarget.Kind;
       if (context == CoreWebView2ContextMenuTargetKind.Image)
       {
           for (int index = 0; index < e.MenuItems.Count; index++)
           {
               if (e.MenuItems[index].Name == "saveImageAs")
               {
                   //移除另存为菜单项
                   e.MenuItems.RemoveAt(index);
                   break;
               }
           }
       }
   }

移除前

 移除后

如何插入菜单项到默认菜单中

这里我们插入一个保存网页内容为PDF的菜单项

    private void CoreWebView2_ContextMenuRequested(object? sender, CoreWebView2ContextMenuRequestedEventArgs e)
    {
        iconStream = new MemoryStream(Properties.Resources.pdf);
        //创建新上下文菜单项
        CoreWebView2ContextMenuItem newItem = this.webview2.CoreWebView2.Environment.CreateContextMenuItem(
                  "保存网页为PDF", iconStream, CoreWebView2ContextMenuItemKind.Command);
        //菜单项选中事件
        newItem.CustomItemSelected += NewItem_CustomItemSelected;
        //插入到当前菜单项最后
        e.MenuItems.Insert(e.MenuItems.Count, newItem);
    }
  
    private void NewItem_CustomItemSelected(object? sender, object e)
    {
        var path = Environment.CurrentDirectory + "\\output.pdf";
        this.webview2.CoreWebView2.PrintToPdfAsync(path);
        System.Diagnostics.Process.Start("explorer.exe", $"/select, {path}");
    }

运行效果

添加自定义上下文菜单

通过下面的方式可以替换默认的上下文菜单,并创建自己的上下文菜单。

通过这种方式创建的上下文菜单没有WebView2默认上下文菜单的外观样式,需要自己创建MenuItem的样式。

  private void CoreWebView2_ContextMenuRequested2(object? sender, CoreWebView2ContextMenuRequestedEventArgs e)
  {
      IList<CoreWebView2ContextMenuItem> menuList = e.MenuItems;
      CoreWebView2Deferral deferral = e.GetDeferral();
      e.Handled = true;
      ContextMenu cm = new ContextMenu();
      cm.Closed += (s, ex) => deferral.Complete();
      ReplaceContextMenu(e, menuList, cm);
      cm.IsOpen = true;
  }
 
  void ReplaceContextMenu(CoreWebView2ContextMenuRequestedEventArgs args, IList<CoreWebView2ContextMenuItem> menuList, ItemsControl cm)
  {
      MenuItem menuItem = new MenuItem();
      menuItem.Header = "HelloWorld";
      menuItem.IsEnabled = true;
      menuItem.Click += MenuItem_Click;
      //自定义样式
      menuItem.Style = this.FindResource("StyleMenuItem") as Style;
      cm.Items.Add(menuItem);
  }
  
  private void MenuItem_Click(object sender, RoutedEventArgs e)
  {
      MessageBox.Show("HelloWorld");
  }

显示效果如下:

示例代码

点击下载

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/869845.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【网络流】——初识(最大流)

网络流-最大流 基础信息引入一些概念基本性质 最大流定义 Ford–Fulkerson 增广Edmons−Karp算法Dinic 算法参考文献 基础信息 引入 假定现在有一个无限放水的自来水厂和一个无限收水的小区&#xff0c;他们之间有多条水管和一些节点构成。 每一条水管有三个属性&#xff1a…

【算法】单链表面试题

1.求单链表中有效节点的个数 //方法&#xff1a;获取到单链表的节点的个数(如果是带头节点的链表&#xff0c;不统计头节点)/**** param head 链表的头节点* return 返回有效节点的个数*/public static int getLength(HeroNode head) {if (head.next null) {return 0;}int le…

面试场景题系列--(2)短 URL 生成器设计:百亿短 URL 怎样做到无冲突?--xunznux

文章目录 面试场景题&#xff1a;短 URL 生成器设计&#xff1a;百亿短 URL 怎样做到无冲突&#xff1f;1. 需求分析2. 短链接生成算法2.1 自增法2.2 散列函数法2.3 预生成法 3. 部署模型3.1 其他部署方案 4. 设计4.1 重定向响应码4.2 短 URL 预生成文件及预加载4.3 用户自定义…

抖音直播弹幕数据逆向:websocket和JS注入

&#x1f50d; 思路与步骤详解 &#x1f575;️‍♂️ 思路介绍 首先&#xff0c;我们通过抓包工具进入的直播间&#xff0c;捕获其网络通信数据&#xff0c;重点关注WebSocket连接。发现直播弹幕数据通过WebSocket传输&#xff0c;这种方式比传统的HTTP更适合实时数据的传输。…

【LLM】-07-提示工程-聊天机器人

目录 1、给定身份 1.1、基础代码 1.2、聊天机器人 2、构建上下文 3、订餐机器人 3.1、窗口可视化 3.2、构建机器人 3.3、创建JSON摘要 利用会话形式&#xff0c;与具有个性化特性&#xff08;或专门为特定任务或行为设计&#xff09;的聊天机器人进行深度对话。 在 Ch…

聊聊基于Alink库的主成分分析(PCA)

概述 主成分分析&#xff08;Principal Component Analysis&#xff0c;PCA&#xff09;是一种常用的数据降维和特征提取技术&#xff0c;用于将高维数据转换为低维的特征空间。其目标是通过线性变换将原始特征转化为一组新的互相无关的变量&#xff0c;这些新变量称为主成分&…

基于opencv[python]的人脸检测

1 图片爬虫 这里的代码转载自&#xff1a;http://t.csdnimg.cn/T4R4F # 获取图片数据 import os.path import fake_useragent import requests from lxml import etree# UA伪装 head {"User-Agent": fake_useragent.UserAgent().random}pic_name 0 def request_pic…

idea springBoot启动时覆盖apollo配置中心的参数

vm options -Dorder.stat.corn“0/1 * * * * ?” 只有vm options, -D参数才能覆盖apollo参数 program arguments –key01val01 --key02val02 environment varibales envFAT;key02val02;key03val03

BGP选路之Preferred value

原理概述 当一台BGP路由器中存在多条去往同一目标网络的BGP路由时&#xff0c;BGP协议会对这些BGP路由的属性进行比较&#xff0c;以确定去往该目标网络的最优BGP路由&#xff0c;然后将该最优BGP路由与去往同一目标网络的其他协议路由进行比较&#xff0c;从而决定是否将该最优…

在 VM 虚拟机中安装 openEuler + 桌面

在 VM 虚拟机中安装 openEuler 1 介绍2 步骤语言Root 账户安装位置网络和主机名自动检索到【推荐】手动配置网络 软件选择安装完成登录测试网络curl ip / ping ipip link show / ip a如网络不通&#xff0c;可检查网卡状态和dns配置 安装命令设置以图形界面的方式启动【dde】第…

【屏显MCU】多媒体接口总结

本文主要介绍【屏显MCU】的基本概念&#xff0c;用于开发过程中的理解 以下是图层叠加示例 【屏显MCU】多媒体接口总结 0. 个人简介 && 授权须知1. 三大引擎1.1 【显示引擎】Display Engine1.1.1 【UI】 图层的概念1.1.2 【Video】 图层的概念1.1.3 图层的 Blending 的…

Linux——管理本地用户和组(详细介绍了Linux中用户和组的概念及用法)

目录 一、用户和组概念 &#xff08;一&#xff09;、用户的概念 &#xff08;二&#xff09;、组的概念 补充组 主要组 二、获取超级用户访问权限 &#xff08;一&#xff09;、su 命令和su -命令 &#xff08; 二&#xff09;、sudo命令 三、管理本地用户账户 &…

【OpenCV C++20 学习笔记】图片处理基础

OpenCV C20 图片处理基础 VS 2022 C20 标准库导入的问题头文件包含以及命名空间声明main函数读取图片读取检查显式图片写入图片 完整代码bug VS 2022 C20 标准库导入的问题 VS还没有完全兼容C20。C20的import语句不一定能正确导入标准库&#xff0c;所以必须要新建一个头文件专…

实时同步:使用 Canal 和 Kafka 解决 MySQL 与缓存的数据一致性问题

目录 1. 准备工作 2. 将需要缓存的数据存储 Redis 3. 监听 canal 存储在 Kafka Topic 中数据 1. 准备工作 1. 开启并配置MySQL的 BinLog&#xff08;MySQL 8.0 默认开启&#xff09; 修改配置&#xff1a;C:\ProgramData\MySQL\MySQL Server 8.0\my.ini log-bin"HELO…

Github个人网站搭建详细教程【Github+Jekyll模板】

文章目录 前言一、介绍1 Github Pages是什么2 静态网站生成工具3 Jekyll简介Jekyll 和 GitHub 的关系 4 Mac系统Jekyll的安装及使用安装Jekyll的简单使用 二、快速搭建第一个Github Pages网站三、静态网站模板——Chirpy1 个人定制 四、WordPress迁移到Github参考资料 前言 23…

机器学习笔记——决策树

定义 决策树是一种可以用来解决回归和分类的问题的算法 决策树使用树形结构&#xff0c;通过叶子节点上的条件层层推理&#xff0c;得到最终的结果 例如&#xff1a;通过上面的简单决策&#xff0c;我们可以通过形状这一条件决策出水果属于哪一类。 决策树的学习结果和取什么规…

在Windows安装、部署Tomcat的方法

本文介绍在Windows操作系统中&#xff0c;下载、配置Tomcat的方法。 Tomcat是一个开源的Servlet容器&#xff0c;由Apache软件基金会的Jakarta项目开发和维护&#xff1b;其提供了执行Servlet和Java Server Pages&#xff08;JSP&#xff09;所需的所有功能。其中&#xff0c;S…

ROS配置并同时驱动多个UVC相机(含功能包)

配置并同时驱动多个UVC相机&#xff0c;并将数据保存为ROS话题形式的bag文件。 ROS可以同时驱动多个UVC相机。要实现这个目标并将数据保存成ROS话题的形式&#xff0c;再保存为bag文件&#xff0c;可以按照以下步骤操作&#xff1a; 1. 安装必要的包 sudo apt-get update sud…

环境搭建-Docker搭建ClickHouse

Docker搭建ClickHouse 一、前言二、ClickHouse安装2.1 拉取镜像运行ClickHouse服务 三、测试安装3.1 进入clickhouse容器3.2 命令补充说明 四、测试连接五、设置CK的用户名密码 一、前言 本文使用的Docker使用Windows搭建&#xff0c;Linux版本的搭建方式一样。 Windows系统搭…

【笔记:3D航路规划算法】二、RRT*

目录 RRT*于RRT的不同之处1、路径优化&#xff1a;2、成本计算&#xff1a;3、重连线步骤&#xff1a; 图解1、初始化2、路径搜索3、效果展示 总结 3D路径规划是在三维空间中寻找从起点到终点的最短或最优路径的一种技术。它广泛应用于无人机导航、机器人运动规划、虚拟现实等领…