午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

WPF、Canvas全体やCanvasに配置した要素を画像(png)ファイルにする。回転や拡大変換要素にも対応版

コード

MakeBitmapに保存したい要素とその要素の親Canvasを渡してBitmapSourceを作成
SaveBitmapToPngにBitmapSourceと保存先フルパスを渡して完了

/// <summary>
/// Canvas上に配置された要素をBitmap画像に変換する。
/// 要素の座標が0,0以外、回転などの変換がされていても正しく変換する。
/// ただし、ActualWidthとActualHeightに数値が入っていない要素の場合はできない。
/// </summary>
/// <param name="element">対象要素</param>
/// <param name="parentCanvas">対象要素の親Canvas</param>
/// <returns></returns>
private BitmapSource MakeBitmap(FrameworkElement element, Canvas parentCanvas)
{
    //対象要素がピッタリ収まるRect取得
    Rect bounds = element.TransformToVisual(parentCanvas)
                         .TransformBounds(
        new Rect(0, 0, element.ActualWidth, element.ActualHeight));
    VisualBrush brush = new(element) { Stretch = Stretch.None };
    DrawingVisual dv = new();
    using (var context = dv.RenderOpen())
    {
        context.DrawRectangle(brush, null, new Rect(bounds.Size));
    }
    RenderTargetBitmap bitmap 
        = new((int)bounds.Width, (int)bounds.Height, 96, 96, PixelFormats.Pbgra32);
    bitmap.Render(dv);
    return bitmap;
}

/// <summary>
/// BitmapSourceをpngファイルにする
/// </summary>
/// <param name="bitmap">保存する画像</param>
/// <param name="filePath">保存先パス(拡張子も付けたフルパス)</param>
private void SaveBitmapToPng(BitmapSource bitmap, string filePath)
{
    PngBitmapEncoder encoder = new();
    encoder.Frames.Add(BitmapFrame.Create(bitmap));
    using (var stream = File.Open(filePath, FileMode.Create))
    {
        encoder.Save(stream);
    }
}




結果確認

デザイナー画面

配置構造

  • Canvas (MyCanvas、薄桃色)
    • Rectangle
    • Rectangle (MyRectangle2、回転した角丸四角形)
    • Canvas (MyCanvas2、トマト色)
      • Rectangle
      • Rectangle

保存する要素は3つ、MyCanvas、MyRectangle2、MyCanvas2

起動したところ

MyCanvas
スクロールバーで見えない部分も保存されている


MyCanvas2
Canvasの中のCanvasも正しく保存された


MyRectangle2
回転されている要素も余白部分は透明になって正しく保存された
実際のサイズは
回転要素の実際のサイズ
MyRectangle2自体のサイズは100x50だけど、回転しているのでそれがピッタリ収まるサイズに変更されて実際は110x78になっていた
回転以外も拡大表示された要素でもできるはず


コード全体

github.com



MainWindow.xaml

<Window x:Class="_20230101_ElementToPngFile.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:_20230101_ElementToPngFile"
        mc:Ignorable="d"
        Title="MainWindow" Height="250" Width="600">
  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition Width="200"/>
    </Grid.ColumnDefinitions>
    <ScrollViewer HorizontalScrollBarVisibility="Visible">
      <Canvas Name="MyCanvas" UseLayoutRounding="True" Width="400" Height="300"
            HorizontalAlignment="Left" VerticalAlignment="Top" Background="MistyRose">
        <Rectangle Name="MyRectangle1" Width="50" Height="50"
                   Fill="Gold" Stroke="tomato "
                   Canvas.Left="20" Canvas.Top="10"/>
        <Rectangle Name="MyRectangle2" Width="100" Height="50"
                   Fill="Gold" Stroke="Tomato"
                   Canvas.Left="100" Canvas.Top="100" RadiusX="10" RadiusY="10"
                   RenderTransformOrigin="0.5,0.5">
          <Rectangle.RenderTransform>
            <TransformGroup>
              <RotateTransform Angle="18"/>
            </TransformGroup>
          </Rectangle.RenderTransform>
        </Rectangle>
        <Canvas Name="MyCanvas2" Canvas.Left="100" Canvas.Top="10" Background="Tomato"
                Width="200" Height="50">
          <Rectangle Fill="Orange" Stroke="White" Width="100" Height="20"
                     Canvas.Left="0" Canvas.Top="0"/>
          <Rectangle Fill="Orange" Stroke="White" Width="100" Height="20"
                     Canvas.Left="100" Canvas.Top="30"/>
        </Canvas>
      </Canvas>
    </ScrollViewer>
    <StackPanel Grid.Column="1">
      <Button Content="save" Click="Button_Click"/>
      <Button Content="saveCanvas" Click="Button_Click_1"/>
      <Button Content="saveCanvas2" Click="Button_Click_2"/>

    </StackPanel>
  </Grid>
</Window>




MainWindow.xaml.cs

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.IO;

namespace _20230101_ElementToPngFile
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        /// <summary>
        /// 要素をBitmap画像に変換する。だたし、
        /// 要素の座標が0,0以外だった場合はできない。
        /// 回転などの変換がされていた場合もできない。
        /// ActualWidthとActualHeightに数値が入っていない場合もできない。
        /// スクロールバーなどで全体が表示されていないときもでいない。
        /// </summary>
        /// <param name="element"></param>
        /// <returns></returns>
        private BitmapSource MakeBitmap(FrameworkElement element)
        {
            RenderTargetBitmap bitmap = new((int)element.ActualWidth,
                                            (int)element.ActualHeight,
                                            96,
                                            96,
                                            PixelFormats.Pbgra32);
            bitmap.Render(element);
            return bitmap;
        }

        /// <summary>
        /// Canvas上に配置された要素をBitmap画像に変換する。
        /// 要素の座標が0,0以外、回転などの変換がされていても正しく変換する。
        /// ただし、ActualWidthとActualHeightに数値が入っていない要素の場合はできない。
        /// </summary>
        /// <param name="element">対象要素</param>
        /// <param name="parentCanvas">対象要素の親Canvas</param>
        /// <returns></returns>
        private BitmapSource MakeBitmap(FrameworkElement element, Canvas parentCanvas)
        {
            //対象要素がピッタリ収まるRect取得
            Rect bounds = element.TransformToVisual(parentCanvas)
                                 .TransformBounds(
                new Rect(0, 0, element.ActualWidth, element.ActualHeight));
            VisualBrush brush = new(element) { Stretch = Stretch.None };
            DrawingVisual dv = new();
            using (var context = dv.RenderOpen())
            {
                context.DrawRectangle(brush, null, new Rect(bounds.Size));
            }
            RenderTargetBitmap bitmap 
                = new((int)bounds.Width, (int)bounds.Height, 96, 96, PixelFormats.Pbgra32);
            bitmap.Render(dv);
            return bitmap;
        }

        /// <summary>
        /// BitmapSourceをpngファイルにする
        /// </summary>
        /// <param name="bitmap">保存する画像</param>
        /// <param name="filePath">保存先パス(拡張子も付けたフルパス)</param>
        private void SaveBitmapToPng(BitmapSource bitmap, string filePath)
        {
            PngBitmapEncoder encoder = new();
            encoder.Frames.Add(BitmapFrame.Create(bitmap));
            using (var stream = File.Open(filePath, FileMode.Create))
            {
                encoder.Save(stream);
            }
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {   
            SaveBitmapToPng(MakeBitmap(MyRectangle2, MyCanvas), "E:element.png");
            //SaveBitmapToPng(MakeBitmap(MyRectangle1, MyCanvas), "E:elementImage.png");
        }

        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            SaveBitmapToPng(MakeBitmap(MyCanvas,MyCanvas), "E:Canvas.png");
            //SaveBitmapToPng(MakeBitmap(MyCanvas), "E:Canvas.png");
        }

        private void Button_Click_2(object sender, RoutedEventArgs e)
        {
            SaveBitmapToPng(MakeBitmap(MyCanvas2,MyCanvas), "E:Canvas2.png");
        }
    }
}


注意したいのはActualWidthとActualHeightに数値が入っていないと正しく保存されないってところ
CanvasはWidthとHeightを指定しないとActualWidthとActualHeightはNaNのままなので、Canvas自体を保存したいときは、あらかじめWidthとHeightを指定しておく必要がある



今回は使っていない簡易バージョン

/// <summary>
/// 要素をBitmap画像に変換する。だたし、
/// 要素の座標が0,0以外だった場合はできない。
/// 回転などの変換がされていた場合もできない。
/// ActualWidthとActualHeightに数値が入っていない場合もできない。
/// スクロールバーなどで全体が表示されていないときもでいない。
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
private BitmapSource MakeBitmap(FrameworkElement element)
{
    RenderTargetBitmap bitmap = new((int)element.ActualWidth,
                                    (int)element.ActualHeight,
                                    96,
                                    96,
                                    PixelFormats.Pbgra32);
    bitmap.Render(element);
    return bitmap;
}


条件が合えばこれでも画像として保存できるけど今回の場合だと期待外れな結果になる
スクロールバーが一番上&一番左じゃない場合

スクロールバー
この状態で保存すると
結果
んー、違うんだよなあ
簡易じゃない今回の方法ならスクロールバーが中途半端な位置でも正しく保存できる

簡易バージョンでMyCanvas2を保存

簡易バージョンでMyCanvas2保存結果
サイズは正しいけど座標がずれているから左半分しか見えていない
確認
本来の座標はleft=100,top=10なのに0,0で保存された結果左側が透明になっている(マウスカーソル部分)


テスト環境

関連記事

2ヶ月後、もっといいやつ
gogowaten.hatenablog.com

6年前
gogowaten.hatenablog.com 今回の記事はこのときVBだったのをC#に書き直しただけな気がする