Blazor(Mobile Blazor Bindings)でWebViewの描画範囲だけスクショする方法 (Windows WPFの場合)

敗北から勝利までのメモ

# 何がしたかった
* アプリケーションのUI部分を除いたキャプチャがしたかった (タイトルバーとかのUIを除いたWebViewだけキャプチャしたかった)

* アプリケーション全体画像
![HoloViewer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74640/48f24c1e-07ec-0689-bd55-f0c53f0d1133.png)


# 宣伝
[C#とBlazorでホロライブファン向けの動画ビューワー『ホロビューワー』を開発しました](https://qiita.com/kawa0x0A/items/24ce1bda92d596f51926)

# 結論
Windowsでは WebView2 Coreクラスの[CapturePreview](https://docs.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2.capturepreviewasync?view=WebView2-dotnet-1.0.664.37#Microsoft_Web_WebView2_Core_CoreWebView2_CapturePreviewAsync_Microsoft_Web_WebView2_Core_CoreWebView2CapturePreviewImageFormat_System_IO_Stream_)メソッドを使用する。

以下PNGフォーマットで保存する場合のコード

* Sharedコード (UI)
  * [ループ内で@refを使うコードの参考資料](https://stackoverflow.com/questions/60064510/add-ref-to-a-blazor-component-in-a-loop)

``` C#
<ContentView>
    <StackLayout>

        // 省略

        <StackLayout Orientation="StackOrientation.Horizontal">
            <Button Text="Capture" OnClick="CaptureSingle" />
        </StackLayout>
        <Grid HorizontalOptions="LayoutOptions.FillAndExpand" VerticalOptions="LayoutOptions.FillAndExpand">
            <StackLayout>
                <BlazorWebView @ref="BlazorWebViews" VerticalOptions="LayoutOptions.FillAndExpand">
                    <HoloViewer.WebUI.App />
                </BlazorWebView>
            </StackLayout>
        </Grid>

        // 省略

    </StackLayout>
</ContentView>


@code
{
    private List<BlazorWebView> blazorWebViews = new List<BlazorWebView>();

    public BlazorWebView BlazorWebViews { set { blazorWebViews.Add(value); } }

    void CaptureSingle ()
    {
        DependencyService.Get<IScreenCapture>().CaptureSingle(blazorWebViews.First());
    }
}
```

* Sharedコード (DependencyService定義)

``` C#
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.MobileBlazorBindings.Elements;

namespace HoloViewer
{
    public interface IScreenCapture
    {
        protected const string CaptureFileNameFormat = "yyyyMMdd_HHmmss";

        protected const string CaptureFileExtension = ".png";

        void CaptureSingle (BlazorWebView blazorWebView);
   }
}
```

* Windows WPF側のコード

```C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.IO;
using Microsoft.MobileBlazorBindings.Elements;
using Xamarin.Forms.Platform.WPF;

[assembly: Xamarin.Forms.Dependency(typeof(HoloViewer.Windows.ScreenCapture))]
namespace HoloViewer.Windows
{
    class ScreenCapture : IScreenCapture
    {
        private async void Capture (BlazorWebView blazorWebView)
        {
            using (var fileStream = new FileStream(DateTime.Now.ToString(IScreenCapture.CaptureFileNameFormat) + IScreenCapture.CaptureFileExtension, FileMode.Create))
            {
                await WebView.CastWebView(blazorWebView).CoreWebView2.CapturePreviewAsync(Microsoft.Web.WebView2.Core.CoreWebView2CapturePreviewImageFormat.Png, fileStream);
            }
        }

        public void CaptureSingle (BlazorWebView blazorWebView)
        {
            Capture(blazorWebView);
        }
    }
}
```

* BlazorWebViewからWebView2への変換処理のコード
  * WebView2本体はアクセス範囲がpublicじゃないのでリフレクションでアクセスする。
  * この辺はデバッガで変数の中身をしらみつぶしにして探した。

``` C#
using System.Reflection;
using Microsoft.MobileBlazorBindings.Elements;
using Microsoft.MobileBlazorBindings.WebView.Elements;
using Microsoft.Web.WebView2.Wpf;

namespace HoloViewer.Windows
{
    class WebView
    {
        public static WebView2 CastWebView (BlazorWebView blazorWebView)
        {
            var content = ((Microsoft.MobileBlazorBindings.WebView.Elements.MobileBlazorBindingsBlazorWebView)blazorWebView.NativeControl).Content;
            var type = content.GetType();

            return (WebView2)type.GetProperty("RetainedNativeControl").GetValue(content);
        }
    }
}
```

* 成功例 (画像だけだと加工できるし、うまくいってるかわからんな これ)
![Sucucess.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74640/93f03f04-f189-166f-619f-165d2714479d.png)


# 勝利に至るまでの経過
* 敗北その1
  * WPFだしMainWindowをキャプチャして必要なところだけ切り出せばいいと思って試したら、そもそもWebViewのUIキャプチャできていない。

  * 敗北その1の結果
![Fail1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74640/ea8891ca-c994-2c81-ff93-95bb7e25d47e.png)


```C#:RenderTargetBitmapを使用するパターン
private void Capture ()
{
    var mainWindow = Application.Current.MainWindow;

    var renderTargetBitmap = new RenderTargetBitmap((int)mainWindow.Width, (int)mainWindow.Height, 96, 96, PixelFormats.Pbgra32);

    renderTargetBitmap.Render(mainWindow);

    var pngBitmapEncoder = new PngBitmapEncoder();

    pngBitmapEncoder.Frames.Add(BitmapFrame.Create(renderTargetBitmap));

    using (var fileStream = new FileStream(DateTime.Now.ToString(IScreenCapture.CaptureFileNameFormat) + IScreenCapture.CaptureFileExtension, FileMode.Create))
    {
        pngBitmapEncoder.Save(fileStream);
    }
}
```


* 敗北その1の原因 (推測)
  * タスクマネージャーでプロセスを見てみると別のプロセスでWebView2が動作している。
  * よってMainWindow(画像だと無名になっているプロセス)だけをキャプチャしてもWebViewの箇所が表示されない。 (と推測しています)
  * ~~WebView2のプロセスが複数ある理由は正直わかっていません。~~

![WebView2Process.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74640/227784bc-2ab9-6dfa-3eec-6daa4fc10d6f.png)


* 敗北その2
  * CapturePreviewAsyncメソッドを await しないと0バイトのPNG画像が生成される。
  * そもそもawaitしていなかったり、Wait関数で待っても0バイトのPNG画像が生成されてダメなので注意

``` C#:await忘れパターン
private void Capture (BlazorWebView blazorWebView)
{
    using (var fileStream = new FileStream(DateTime.Now.ToString(IScreenCapture.CaptureFileNameFormat) + IScreenCapture.CaptureFileExtension, FileMode.Create))
    {
        WebView.CastWebView(blazorWebView).CoreWebView2.CapturePreviewAsync(Microsoft.Web.WebView2.Core.CoreWebView2CapturePreviewImageFormat.Png, fileStream);
    }
}
```

* 敗北その2の結果
![Fail2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74640/a4e36534-ad80-e24a-4593-9c91be850955.png)



# まとめ
* Windows版はこの調子で機能を実装すれば勝てそう
* WebView2がMacには対応していないためMac版は別の地獄がまっているはず