UWPでCanvasに画像を置き、その上にPolygonなどの図形を描いた結果を「そのままWriteableBitmapに保存したい」のに、RenderTargetBitmapが0バイト(実質空)になって困ることがあります。原因はほぼパターン化しており、対策も決まっています。この記事では再現条件から、確実にキャプチャしてWriteableBitmap化・PNG保存まで行う実装をまとめます。
現象:Canvasの見た目を保存したいのに、RenderTargetBitmapが空になる
よくある流れは次のとおりです。
- WriteableBitmapに読み込んだ画像を
<Image>コントロールに設定する - そのImageを
<Canvas>に載せる <Polygon>や<Rectangle>で四角形(枠)を描画する- 最終的に「Canvasの見た目」を1枚の画像として保存したい
ところが、次のように“コードで新しく作ったCanvas”を対象に RenderTargetBitmap.RenderAsync を呼ぶと、ピクセルが取得できず0バイト相当の結果になることがあります。
var canvas2 = new Canvas();
canvas2.Children.Add(imageControl); // Imageと図形を追加
canvas2.Children.Add(polygon);
var rtb = new RenderTargetBitmap();
// canvas2.ActualWidth/ActualHeight が 0 のまま、またはそもそも描画されていない
await rtb.RenderAsync(canvas2, (int)canvas2.ActualWidth, (int)canvas2.ActualHeight);
var buffer = await rtb.GetPixelsAsync(); // 長さ0になってしまうことがある
「例外は出ないのに、結果だけ空」というのが厄介で、初見だとハマりやすいポイントです。
結論:RenderTargetBitmapは“画面上で実際にレンダリングされた要素”しかキャプチャできない
原因の本質はシンプルで、RenderTargetBitmapはXAMLのビジュアルツリー上で、実際にレンダリング(描画)されたUIElementしかキャプチャできないためです。
質問のコードでは canvas2 を生成して子要素を追加していますが、次のどちらか(または両方)が起きています。
| 起きていること | なぜ問題になるか | 結果 |
|---|---|---|
| canvas2 がビジュアルツリーに追加されていない | レイアウトも描画も走らず、レンダリング対象にならない | キャプチャできず、ピクセルが空になりやすい |
| canvas2 のサイズが確定していない(ActualWidth/ActualHeight が 0) | レンダリングするべき領域が 0×0 と判断される | 結果が空/意図しないサイズになる |
つまり、“画面に載っていないUI”をそのままRenderTargetBitmapで撮ることはできないのが基本方針です(例外的な回避策はありますが、安定性と保守性を考えるとおすすめしません)。
まず押さえるチェックポイント
0バイト問題の切り分けは、次のチェックでほぼ決着します。
| チェック項目 | 確認方法 | 目安 |
|---|---|---|
| canvas2 は画面に追加されているか | ルート要素(Gridなど)のChildrenに入れているか | 入っていない場合はまず追加 |
| canvas2.ActualWidth / ActualHeight は 0 ではないか | デバッグで値を確認する | 0ならレイアウト未完了 |
| 画像(Image.Source)は読み込み完了しているか | BitmapImageなら ImageOpened を待つ | 未完了だと真っ白になることがある |
| RenderAsync の呼び出しはUIスレッドか | Dispatcher上で実行されているか | 別スレッドだと例外 or 不定 |
基本の対処:Canvasを一度ビジュアルツリーに追加してからキャプチャする
最も安全で、再現性が高い対処は次の流れです。
- キャプチャ対象のCanvas(ImageやPolygonを含む)を構築する
- ページ上のルート要素(例:Grid)にCanvasを追加する
- レイアウト完了を待ってから RenderAsync を呼ぶ
- 必要なら、キャプチャ後にCanvasをツリーから外す
ポイントは「追加して表示状態にする」+「レイアウト完了後に呼ぶ」の2つです。
例:ルートGridに追加してからRenderAsyncする
まず、XAML側に“キャプチャ用の置き場”を用意しておくと実装が安定します。
<Grid x:Name="RenderedGrid">
<!-- 普段の画面 -->
<Grid x:Name="MainArea">
<!-- 既存UI -->
</Grid>
<!-- キャプチャ用の一時ホスト(必要に応じて最前面/背面に) -->
<Grid x:Name="CaptureHost" />
</Grid>
次にコードビハインドでCanvasを追加し、サイズが確定してからキャプチャします。
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.Graphics.Display;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Imaging;
public async Task<WriteableBitmap> CaptureCanvasToWriteableBitmapAsync(Canvas canvas2)
{
// 1) ビジュアルツリーに追加(ここが重要)
CaptureHost.Children.Add(canvas2);
// 2) レイアウト完了を待つ(ActualWidth/ActualHeight が確定するまで)
await WaitForNonZeroSizeAsync(canvas2);
// 3) RenderAsync(必要なら高解像度指定)
var rtb = new RenderTargetBitmap();
// RenderAsync(element) でもOKだが、解像度をコントロールしたい場合は幅/高さを渡す
var scale = DisplayInformation.GetForCurrentView().RawPixelsPerViewPixel;
int pixelWidth = (int)Math.Round(canvas2.ActualWidth * scale);
int pixelHeight = (int)Math.Round(canvas2.ActualHeight * scale);
await rtb.RenderAsync(canvas2, pixelWidth, pixelHeight);
// 4) ピクセル取得 → WriteableBitmap へコピー
var buffer = await rtb.GetPixelsAsync();
var pixels = buffer.ToArray();
var wb = new WriteableBitmap(rtb.PixelWidth, rtb.PixelHeight);
using (var dst = wb.PixelBuffer.AsStream())
{
await dst.WriteAsync(pixels, 0, pixels.Length);
}
// 5) もう不要ならツリーから外す(後片付け)
CaptureHost.Children.Remove(canvas2);
return wb;
}
private static Task WaitForNonZeroSizeAsync(FrameworkElement element)
{
// Loaded後でもActualWidth/Heightが0の瞬間があるため、「0でなくなる」まで待つ
if (element.ActualWidth > 0 && element.ActualHeight > 0)
{
return Task.CompletedTask;
}
var tcs = new TaskCompletionSource<object>();
void Handler(object s, SizeChangedEventArgs e)
{
if (element.ActualWidth > 0 && element.ActualHeight > 0)
{
element.SizeChanged -= Handler;
tcs.TrySetResult(null);
}
}
element.SizeChanged += Handler;
// 念のためLoadedも待つ(SizeChangedが来ないケースに備える)
RoutedEventHandler loadedHandler = null;
loadedHandler = (s, e) =>
{
element.Loaded -= loadedHandler;
if (element.ActualWidth > 0 && element.ActualHeight > 0)
{
element.SizeChanged -= Handler;
tcs.TrySetResult(null);
}
};
element.Loaded += loadedHandler;
return tcs.Task;
}
このパターンにすると、canvas2 が“ちゃんと画面の一部として扱われる”ため、RenderTargetBitmapが空になりにくくなります。
ActualWidth/ActualHeight が 0 になりやすいタイミングと回避策
「ビジュアルツリーに追加したのに、まだActualWidth/ActualHeightが0で失敗する」ケースもあります。これはレイアウトが確定していないだけなので、呼び出しタイミングを調整すれば解決できます。
| タイミング | 起こりやすいこと | おすすめの回避策 |
|---|---|---|
| コンストラクタ直後 | まだレイアウトが走っておらずサイズ0 | Loaded/SizeChangedを待ってから実行 |
| 子要素追加直後 | Measure/Arrange前でサイズ未確定 | UpdateLayout、または次フレーム待ち |
| 画像を差し替えた直後 | 画像デコードが間に合わず白くなる | ImageOpenedを待つ/WriteableBitmapなら更新後に1回UIを通す |
実務では「Loadedでトリガーし、サイズが0ならSizeChangedを待つ」という二段構えが最も安定します。上の WaitForNonZeroSizeAsync はそのための小さなユーティリティです。
解像度が思ったより荒いときは“DIP→ピクセル”変換を意識する
キャプチャが成功しても、「画像サイズが小さい」「高DPI端末でぼやける」といった別の悩みが出ることがあります。原因は、XAMLのサイズ(DIP)とビットマップのサイズ(物理ピクセル)が一致しないことです。
RenderTargetBitmapの RenderAsync(element, width, height) の width/height はピクセル数として扱われます。ActualWidth はDIPなので、次のようにスケールを掛けた値を渡すと期待どおりになりやすいです。
var scale = DisplayInformation.GetForCurrentView().RawPixelsPerViewPixel;
int pixelWidth = (int)Math.Round(canvas2.ActualWidth * scale);
int pixelHeight = (int)Math.Round(canvas2.ActualHeight * scale);
await rtb.RenderAsync(canvas2, pixelWidth, pixelHeight);
逆に「アプリ内での見た目と同じサイズで十分」なら、RenderAsync(canvas2)(サイズ指定なし)でも問題ありません。
WriteableBitmapにしたい理由別:最適な取り回し
「最終的にWriteableBitmapが欲しい」といっても、目的によって最適解が変わります。よくある目的とおすすめの経路を整理すると、選択ミスが減ります。
| やりたいこと | おすすめ | 理由 |
|---|---|---|
| 画面に結果を表示したい | RenderTargetBitmapをそのままImage.Sourceへ | 余計なコピーが減り高速 |
| PNG/JPEGとして保存したい | GetPixelsAsync → BitmapEncoderで保存 | WriteableBitmapに変換せず保存できる |
| ピクセル編集(塗りつぶし、解析など)をしたい | WriteableBitmapに変換 | PixelBufferに書き込み可能 |
「保存が目的」なら、実はWriteableBitmapに変換しなくてもOKです。次の章で“保存まで”の定番実装をまとめます。
キャプチャ結果をPNGで保存する実装例
RenderTargetBitmapで取得したピクセル(BGRA8)を、BitmapEncoderでPNGにします。FileSavePickerなどでユーザーに保存先を選ばせてもいいですし、アプリデータ領域に自動保存しても構いません。
public async Task SaveRenderTargetBitmapAsPngAsync(RenderTargetBitmap rtb, StorageFile file)
{
var buffer = await rtb.GetPixelsAsync();
var pixels = buffer.ToArray();
using (IRandomAccessStream stream = await file.OpenAsync(FileAccessMode.ReadWrite))
{
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream);
encoder.SetPixelData(
BitmapPixelFormat.Bgra8,
BitmapAlphaMode.Premultiplied,
(uint)rtb.PixelWidth,
(uint)rtb.PixelHeight,
96, 96, // DPI(必要に応じて調整)
pixels
);
await encoder.FlushAsync();
}
}
この方法だと、WriteableBitmapへのコピー工程が不要なのでメモリ効率が良く、保存だけならこちらが第一候補になります。
RenderTargetBitmap → WriteableBitmap 変換の注意点
WriteableBitmap化する場合は、ピクセルフォーマットとアルファモードを意識してください。UWPのRenderTargetBitmapは基本的にBGRA8 + Premultipliedとして扱うのが無難です。
- BGRA8:B,G,R,A の順に1バイトずつ(合計4バイト/ピクセル)
- Premultiplied:透明度を掛けた色が格納される(合成で自然に見える)
変換自体は前述のとおり “GetPixelsAsync → WriteableBitmap.PixelBufferへ書き込み” です。コピー時には次の点を押さえると事故が減ります。
| 注意点 | 具体例 | 対策 |
|---|---|---|
| サイズが大きいとメモリを食う | 4000×4000は約64MB(4バイト/ピクセル) | 必要最低限の解像度にする/不要になったら参照を外す |
| Premultipliedを忘れると色が変に見える | 半透明の縁が黒ずむ等 | 保存時はPremultiplied指定に寄せる |
| UI更新直後のキャプチャで古い見た目になる | Polygonを追加したのに反映されていない | レイアウト完了を待つ/必要なら次フレームで実行 |
画像が白い・図形が欠けるときの追加チェック
0バイト問題は解決したのに、出力が「白い」「一部だけ欠ける」場合は、次のパターンが多いです。
BitmapImage(URI読み込み)のデコード待ちが必要
Image.Source に BitmapImage(URIから読み込むタイプ)を使う場合、デコードが非同期で進むため、早すぎるキャプチャだと白くなります。確実にするなら ImageOpened を待ちます。
var tcs = new TaskCompletionSource<object>();
imageControl.ImageOpened += (s, e) => tcs.TrySetResult(null);
imageControl.ImageFailed += (s, e) => tcs.TrySetException(new Exception(e.ErrorMessage));
// Source設定
imageControl.Source = bitmapImage;
await tcs.Task; // 画像が開けたのを待つ
WriteableBitmapを直接Sourceにしている場合は比較的起きにくいですが、更新直後は念のため1回UIスレッドに制御を返してからキャプチャすると安定します(例:await Task.Yield();)。
Visibility=Collapsed はキャプチャ対象にならない
「画面に見せたくないから」といって Visibility.Collapsed にすると、そもそもレイアウトも描画もされないためキャプチャできません。キャプチャが必要な瞬間だけVisibleにする、または画面設計として“キャプチャ専用領域”を確保するのが現実的です。
Opacity=0 は“透明に描画される”ので結果も透明になる
Collapsedほど致命的ではありませんが、Opacity=0は「存在するが透明に描かれる」ため、取得結果も透明になります。ユーザーに見せたくない場合は、短時間だけ表示してすぐ外す、あるいはUI上の見えない場所に置く設計にします(ただし配置によってはクリップされるので注意してください)。
実務で使える“失敗しにくい”実装パターン
運用でトラブルが少ないのは、次の2パターンです。
パターンA:キャプチャ専用のホストに一時追加して撮る
- CaptureHost(Grid)を常設し、キャプチャ時だけCanvasを追加
- SizeChanged/Loadedでサイズ確定を待つ
- RenderAsync → 保存 → Remove
画面設計に依存しにくく、最もおすすめです。
パターンB:既存Canvasをそのまま撮る(画面に既にある場合)
キャプチャしたいCanvasが既に画面上に存在するなら、新しいCanvasを作らず、そのCanvas自体をRenderAsyncの対象にするのが簡単です。余計なコピーがなく、ズレも起きにくいです。
var rtb = new RenderTargetBitmap();
await rtb.RenderAsync(ExistingCanvas);
var buffer = await rtb.GetPixelsAsync();
// あとは保存やWriteableBitmap化
補足:それでも“画面に載せずに”描きたい場合の考え方
「どうしてもビジュアルツリーに追加したくない」「ユーザーに一瞬でも見せたくない」という要件がある場合、方向性としては次のどちらかになります。
- Win2Dなどの描画ライブラリでオフスクリーンにレンダリングして、CanvasRenderTarget等を保存する
- 最初からWriteableBitmapへ描画(画像合成・図形描画を自前実装)して保存する
前者は学習コストがある代わりに、UIとは独立して安定します。後者は自由度が高い反面、アンチエイリアスや線の描画など“見た目の品質”を揃えるのが難しくなります。まずは本記事の「一時的にビジュアルツリーへ追加して撮る」方式で要件を満たせるか確認するのが現実的です。
まとめ:0バイト問題は“描画されていないCanvasを撮ろうとしている”のが原因
- RenderTargetBitmapは、ビジュアルツリー上で実際に描画された要素しかキャプチャできない
- 新しく作ったCanvasは、ルートに追加してレイアウト完了後にRenderAsyncする
- ActualWidth/ActualHeight が 0 のまま呼ばない(Loaded/SizeChangedで待つ)
- 保存が目的なら、GetPixelsAsync → BitmapEncoderで直接PNG保存が効率的
この流れに揃えると「Canvasに描いた図形+画像」を、見た目どおりに安定して画像化できます。特に“ホストに一時追加して撮る”は、プロジェクトが大きくなっても壊れにくいので、最初からこの形に寄せておくのがおすすめです。

コメント