WPF/C# コントロールの要素をキャプチャする

# 概要

WPFとC#でウィンドウの中に配置したコントロールを画像として保存する必要があったので書いていく

# 書いた環境

この記事を書いた環境なので他の環境でも多分動く

|項目|値|
|:-:|:-:|
|OS|Windows 11 Pro (21H2)|
|IDE|Microsoft Visual Studio Community 2022 (64bit)|
|.NETバージョン|.NET 6|

# 手っ取り早く答えが欲しい人向けのコード

キャプチャしたい要素（一番親の要素）を引数に渡すとBitmapSourceで返すメソッド

```c#
//以下のusingを追加する
//using System.Windows;
//using System.Windows.Media;
//using System.Windows.Media.Imaging;

public static BitmapSource FrameworkElementToBitmapSource(FrameworkElement element)
{
	element.UpdateLayout();
	var width = element.ActualWidth;
	var height = element.ActualHeight;
	var dv = new DrawingVisual();
	using (var dc = dv.RenderOpen())
	{
		dc.DrawRectangle(new BitmapCacheBrush(element), null, new Rect(0, 0, width, height));
	}
	var rtb = new RenderTargetBitmap((int)width, (int)height, 96d, 96d, PixelFormats.Pbgra32);
	rtb.Render(dv);
	return rtb;
}
```

(おまけ)
上のメソッドで帰ってきたBitmapSourceをPNGファイルとして保存するメソッド。

```c#
//以下のusingを追加する
//using System.IO;

public static void BitmapSourceToPngFile(BitmapSource bitmapSource, string pngFilePath)
{
	using (var stream = new FileStream(pngFilePath, FileMode.Create, FileAccess.Write, FileShare.None))
	{
		var encoder = new PngBitmapEncoder();
		encoder.Frames.Add(BitmapFrame.Create(bitmapSource));
		encoder.Save(stream);
	}
}
```

# どうしてこのような処理が必要なのか

## 実は以下のようなコードでも一応コンパイルは通る

```c#
public static BitmapSource FrameworkElementToBitmapSource(FrameworkElement element)
{
	element.UpdateLayout();
	var rtb = new RenderTargetBitmap((int)element.ActualWidth, (int)element.ActualHeight, 96d, 96d, PixelFormats.Pbgra32);
	rtb.Render(element);
	return rtb;
}
```

なぜならFrameworkElementはRenderTargetBitmapのRenderメソッドの引数として渡すVisualクラスを継承しているため。

しかし、出力したい要素がStackPanelのChildrenプロパティから取得した要素だったり、ウィンドウの外に飛び出した要素だったりすると正しく動かない

## 第一の問題の解決法: DrawingVisualクラスを使う！

DrawingVisualクラスを使うと以下のようなコードになる

```c#
	var dv = new DrawingVisual();
	using (var dc = dv.RenderOpen())
	{
		//ここで「dc」に描画
	}
	var rtb = new RenderTargetBitmap((int)width, (int)height, 96d, 96d, PixelFormats.Pbgra32);
	rtb.Render(dv);
```

「dc」の持つDrawRectangleメソッドはBrushの派生クラスを使って要素を描画し、追加する項目はVisualクラスの派生クラスなので以下のようなコードが考えられる
実際ほとんどのブログでは以下のようなVisualBrushを使ったコードが掲載されている

```c#
	var dv = new DrawingVisual();
	using (var dc = dv.RenderOpen())
	{
		dc.DrawRectangle(new VisualBrush(element), null, new Rect(0, 0, element.ActualWidth, element.ActualHeight));
	}
	var rtb = new RenderTargetBitmap((int)width, (int)height, 96d, 96d, PixelFormats.Pbgra32);
	rtb.Render(dv);
```

しかしこのVisualBrushクラスには問題があり、大きいサイズの画像を作成しようとした場合サイズに比例して品質が悪くなる

実際にキャプチャした元のウィンドウとキャプチャした結果の画像を比較した画像が以下の通り

![スクリーンショット 2022-02-22 235417.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/607960/030b176f-1c07-3a9b-3bed-ca428abb2221.png)

折角出力した画像がこんなガビガビなのだったらガッカリなのでこれはなんとかしなければならない

## 第二の問題の解決法: VisualBrushクラスではなくBitmapCacheBrushを使う！

```c#
	var dv = new DrawingVisual();
	using (var dc = dv.RenderOpen())
	{
		dc.DrawRectangle(new BitmapCacheBrush(element), null, new Rect(0, 0, element.ActualWidth, element.ActualHeight)); //VisualBrush → BitmapCacheBrushに変更
	}
```

変更した結果の出力した比較が以下の通り

![スクリーンショット 2022-02-23 000431.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/607960/f78937ac-6851-70f7-26ec-98495921f620.png)

綺麗に出力できました！

# 結論

コントロールをキャプチャする時はDrawingVisualとBitmapCacheBrushを使おう！！

# サンプルソースコード

今回作成したサンプルを以下のGitHubへアップロードしておきますね

[Sakurai-Shinya/WPF_Element_Capture_Sample](https://github.com/Sakurai-Shinya/WPF_Element_Capture_Sample)