午後わてんのブログ

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

WPF、Paddingが0(既定値)のTextBlockを画像として保存すると、左端が半透明になることがある

けど、Paddingを1以上にしたら、期待通りのきれいな画像で保存できた

現象

TextBlock
これを画像として保存する処理が

//普通に要素のサイズで、BitmapCacheBrushで描画
private void Test2(FrameworkElement element, string fileName)
{
    Rect bounds = new(0, 0, element.ActualWidth, element.ActualHeight);

    var dv = new DrawingVisual();
    using (var context = dv.RenderOpen())
    {
        BitmapCacheBrush bru = new(element);
        context.DrawRectangle(bru, null, bounds);
    }
    RenderTargetBitmap bmp = new((int)Math.Ceiling(bounds.Width), (int)Math.Ceiling(bounds.Height), 96, 96, PixelFormats.Pbgra32);
    bmp.Render(dv);

    string? methodName = MethodBase.GetCurrentMethod()?.Name;
    Save(bmp, $"{fileName}{methodName}.png");
}

画像として保存されたのが

Padding0
問題なく見えるけど、拡大して

左端が半透明
背景の色を調べてみると、左端1ピクセルすべてが半透明になっているのがわかった

影響するのは背景色だけじゃない

アプリに表示されているTextBlock

アプリに表示されているTextBlock

これと保存された画像を比較してみると

4倍拡大
文字の太さというかClearTypeが適用されていないのは置いといて、
文字の横位置が違う。横幅も違うと思いきや

右端が半透明と完全透明
青の点線枠が実際の画像サイズで、これをみると、横幅はあっているけど、右端が半透明と完全透明になっているから、見た目上だと横幅が違って見えるだけなのがわかる…わけがわからん



Paddingを1以上にするといい

Paddingを1

Paddingを1にして保存した画像がこれ

Paddingを1

拡大して比較

Padding1の結果
完璧!

あとはフォントのClearType有効時の見た目を再現した画像にするには
BitmapCacheBrushのBitmapCacheに
EnableClearTypeをtrueにしたBitmapCacheを指定する

//BitmapCacheBrushのキャッシュモードのEnableClearTypeを有効にして、描画した画像を保存
//要素に文字列があるとき、ClearTypeが有効時の見た目で保存される
private void Test2WithClearType(FrameworkElement element, string fileName)
{
    Rect bounds = new(0, 0, element.ActualWidth, element.ActualHeight);

    var dv = new DrawingVisual();
    using (var context = dv.RenderOpen())
    {
        BitmapCacheBrush bru = new(element);
        BitmapCache bcMode = new() { EnableClearType = true };
        bru.BitmapCache = bcMode;
        context.DrawRectangle(bru, null, bounds);
    }

    RenderTargetBitmap bmp = new((int)Math.Ceiling(bounds.Width), (int)Math.Ceiling(bounds.Height), 96, 96, PixelFormats.Pbgra32);
    bmp.Render(dv);

    string? methodName = MethodBase.GetCurrentMethod()?.Name;
    Save(bmp, $"{fileName}{methodName}.png");
}

これで保存された画像が

ClearType
これで

拡大して比較してみると

ClearType
ClearTypeっぽくはなっているけど、なんか違うねえ、強すぎる感じなので調整したいけどできないみたい
でも、これはこれでいいね



試したこと

Paddingが0でもなんとかならないかと
描画の位置やサイズの調整をしてみたけど、どれもうまくいかなかった
DrawingVisualのOffset
DrawingRectangleでのRect調整
このあたり

子要素がすべて収まるRectを返してくれるメソッドがVisualTreeHelperクラスにあって
VisualTreeHelper.GetDescendantBounds(element);
これにPaddingが0のTextBlockを渡して返ってきたRectを見ると
左端の文字によっては、X座標が0じゃなくてマイナス0.4とかのものがあって、そういう場合は左端が半透明になっていた

GetDescendantBoundsのRectの比較

なので、このマイナス分をDrawingVisualや、そのDrawingRectangleとかでオフセットしたり、サイズ調整すればいいのかなと試したけど、どうにもならなかった
TextBlock要素からBitmapCacheBrushを作成した時点で、そのブラシ自体がズレているのかも




テストアプリのコード

2025WPF/20250424 at main · gogowaten/2025WPF
github.com


環境




MainWindow.xaml

<Window x:Class="_20250424.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:_20250424"
        mc:Ignorable="d"
        Title="MainWindow" Height="367" Width="654">
  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition Width="200"/>
    </Grid.ColumnDefinitions>
    <Canvas x:Name="MyCanvas" UseLayoutRounding="True">
      <TextBlock x:Name="MyUC" Canvas.Left="10" Canvas.Top="50"
                 FontSize="30" Background="MistyRose"
                 Text="TekitohBlock"
                 Padding="1"/>
      
    </Canvas>


    <StackPanel Grid.Column="1">
      <Button Content="test" Click="Button_Click"/>
    </StackPanel>
  </Grid>
</Window>




MainWindow.xaml.cs

using System.IO;
using System.Reflection;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
//要素を画像として保存
//TextBlock
//Paddingが0だとズレてぼやけることがある
//フォントや文字によっては、ずれないときもある
//対象は左端の文字、1文字目だけ
//左端の文字がTだとずれる、けどUだとずれない
//防ぐにはPaddingを1以上にするしかない?
//ずれるかどうかの判定は
//VisualTreeHelper.GetDescendantBounds(element);
//これで得られるRectのX座標がマイナスの値だとずれる、0ならずれないとおもったけど、
//0でもずれる(半透明になる)

namespace _20250424
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {

            //SavePng(MyTextBlock);
            string fileName = $"E:20250424_";
            Test2WithClearType(MyUC, fileName);
            Test2(MyUC, fileName);
            Test3(MyUC, fileName);
            Test4(MyUC, fileName);
            Test5(MyUC, fileName);
            Test6(MyUC, fileName);
        }


        //よくわからん
        private void Test6(FrameworkElement element, string fileName)
        {
            Rect bounds = VisualTreeHelper.GetDescendantBounds(element);

            var dv = new DrawingVisual();
            dv.Offset = new Vector(-bounds.X, -bounds.Y);
            bounds.Width += bounds.X;
            bounds.Height += bounds.Y;
            using (var context = dv.RenderOpen())
            {
                BitmapCacheBrush bru = new(element);
                context.DrawRectangle(bru, null, bounds);
            }
            RenderTargetBitmap bmp = new((int)Math.Ceiling(bounds.Width), (int)Math.Ceiling(bounds.Height), 96, 96, PixelFormats.Pbgra32);
            bmp.Render(dv);

            string? methodName = MethodBase.GetCurrentMethod()?.Name;
            Save(bmp, $"{fileName}{methodName}.png");
        }

        //GetDescendantBoundsのサイズと描画位置を0,0で保存
        private void Test5(FrameworkElement element, string fileName)
        {
            Rect bounds = VisualTreeHelper.GetDescendantBounds(element);
            bounds = new Rect(0, 0, bounds.Width, bounds.Height);

            var dv = new DrawingVisual();
            using (var context = dv.RenderOpen())
            {
                BitmapCacheBrush bru = new(element);
                context.DrawRectangle(bru, null, bounds);
            }
            RenderTargetBitmap bmp = new((int)Math.Ceiling(bounds.Width), (int)Math.Ceiling(bounds.Height), 96, 96, PixelFormats.Pbgra32);
            bmp.Render(dv);

            string? methodName = MethodBase.GetCurrentMethod()?.Name;
            Save(bmp, $"{fileName}{methodName}.png");
        }

        //GetDescendantBoundsのサイズとDrawingVisualをオフセットして保存
        private void Test4(FrameworkElement element, string fileName)
        {
            Rect bounds = VisualTreeHelper.GetDescendantBounds(element);

            var dv = new DrawingVisual();
            dv.Offset = new Vector(-bounds.X, -bounds.Y);
            using (var context = dv.RenderOpen())
            {
                BitmapCacheBrush bru = new(element);
                context.DrawRectangle(bru, null, bounds);
            }
            RenderTargetBitmap bmp = new((int)Math.Ceiling(bounds.Width), (int)Math.Ceiling(bounds.Height), 96, 96, PixelFormats.Pbgra32);
            bmp.Render(dv);

            string? methodName = MethodBase.GetCurrentMethod()?.Name;
            Save(bmp, $"{fileName}{methodName}.png");
        }

        //GetDescendantBoundsのサイズと位置で保存
        private void Test3(FrameworkElement element, string fileName)
        {
            Rect bounds = VisualTreeHelper.GetDescendantBounds(element);

            var dv = new DrawingVisual();
            using (var context = dv.RenderOpen())
            {
                BitmapCacheBrush bru = new(element);
                context.DrawRectangle(bru, null, bounds);
            }
            RenderTargetBitmap bmp = new((int)Math.Ceiling(bounds.Width), (int)Math.Ceiling(bounds.Height), 96, 96, PixelFormats.Pbgra32);
            bmp.Render(dv);

            string? methodName = MethodBase.GetCurrentMethod()?.Name;
            Save(bmp, $"{fileName}{methodName}.png");
        }

        //普通に要素のサイズで、BitmapCacheBrushで描画
        private void Test2(FrameworkElement element, string fileName)
        {
            Rect bounds = new(0, 0, element.ActualWidth, element.ActualHeight);

            var dv = new DrawingVisual();
            using (var context = dv.RenderOpen())
            {
                BitmapCacheBrush bru = new(element);
                context.DrawRectangle(bru, null, bounds);
            }
            RenderTargetBitmap bmp = new((int)Math.Ceiling(bounds.Width), (int)Math.Ceiling(bounds.Height), 96, 96, PixelFormats.Pbgra32);
            bmp.Render(dv);

            string? methodName = MethodBase.GetCurrentMethod()?.Name;
            Save(bmp, $"{fileName}{methodName}.png");
        }

        //BitmapCacheBrushのキャッシュモードのEnableClearTypeを有効にして、描画した画像を保存
        //要素に文字列があるとき、ClearTypeが有効時の見た目で保存される
        private void Test2WithClearType(FrameworkElement element, string fileName)
        {
            Rect bounds = new(0, 0, element.ActualWidth, element.ActualHeight);

            var dv = new DrawingVisual();
            using (var context = dv.RenderOpen())
            {
                BitmapCacheBrush bru = new(element);
                BitmapCache bcMode = new() { EnableClearType = true, SnapsToDevicePixels = true };
                bru.BitmapCache = bcMode;
                context.DrawRectangle(bru, null, bounds);
            }

            RenderTargetBitmap bmp = new((int)Math.Ceiling(bounds.Width), (int)Math.Ceiling(bounds.Height), 96, 96, PixelFormats.Pbgra32);
            bmp.Render(dv);

            string? methodName = MethodBase.GetCurrentMethod()?.Name;
            Save(bmp, $"{fileName}{methodName}.png");
        }

        private void Save(BitmapSource bmp, string fileName)
        {
            var enc = new PngBitmapEncoder();
            enc.Frames.Add(BitmapFrame.Create(bmp));
            using FileStream stream = new(fileName, FileMode.Create, FileAccess.Write);
            enc.Save(stream);
        }
    }
}

Eドライブに20250424_Test2.pngとかの名前で画像が保存される




感想

Paddingの既定値の0では解決できなかったけど、Padding0も1も見た目上は大差ないからこれでいい
でも、それを言うなら左端1ピクセルが半透明も大差ないのでは?




参照したところ

WPF/C# コントロールの要素をキャプチャする #C# - Qiita
qiita.com ここを見るまではVisualBrushを使っていたんだけど、低画質になるのは気づかなかった
なので今回はBitmapCacheBrushを使ったんだけど、今までのを書き直そうかと思ってgithubを検索したら50件以上あって諦めた


関連記事

次のWPF記事

C#で3点ABCからできる∠ABCの角度を求める関数をGitHub Copilotに作ってもらった - 午後わてんのブログ
gogowaten.hatenablog.com


前回のWPF記事
WPF、昨日のは失敗だった、変形時にPoint変化で全体が移動してしまう - 午後わてんのブログ
gogowaten.hatenablog.com