【WPF】使用RenderTargetBitmap截图的时候位置出现偏移的一些解决办法

简介

在WPF中,如果你需要把控件的渲染画面保存到图片,那么唯一的选择就是RenderTargetBitmap。
不过,RenderTargetBitmap是个比较难伺候的主,有时候你以为能工作,但实际上不能;你以为能够正常截图,但实际上截出来的图片是歪的。
所以,我总结一下自己项目中遇到的坑和解决办法吧!

保存的图片是黑色的

这种情况下,通常是WPF的RenderTargetBitmap无法正确获取视觉对象的渲染结果,我比较建议在需要截图的控件之上包装一层Border。

例如:

<ux:RarityRibbon Background="{Binding Background, Mode=OneWay, Converter={x:Static wf:Converters.StringToBrush}}"
                             Foreground="{Binding Foreground, Mode=OneWay, Converter={x:Static wf:Converters.StringToBrush}}"
                             RarityStarColor="{Binding Stroke, Mode=OneWay, Converter={x:Static wf:Converters.StringToBrush}}"
                             Title="{Binding Name, Mode=OneWay}"
                             Value="{Binding Value, Mode=OneWay}"
                             Level="{Binding Level, Mode=OneWay}"
                             Margin="0">
                <ux:RarityRibbon.MaskedBrush>
                    <ImageBrush ImageSource="{Binding Image, Mode=OneWay, Converter={x:Static wf:Converters.Icon}}"
                                Stretch="Uniform"
                                Viewbox="0.1 0.1 0.5 0.5"
                                Opacity="0.15" />
                </ux:RarityRibbon.MaskedBrush>
            </ux:RarityRibbon>

要截图的是ux:RarityRibbon控件,但保存的图片是纯黑的。
这时候,要做的就是在ux:RarityRibbon控件之上,包装一层Border,然后把截图对象改成Border即可。


  <Border x:Name="Container">
            <ux:RarityRibbon Background="{Binding Background, Mode=OneWay, Converter={x:Static wf:Converters.StringToBrush}}"
                             Foreground="{Binding Foreground, Mode=OneWay, Converter={x:Static wf:Converters.StringToBrush}}"
                             RarityStarColor="{Binding Stroke, Mode=OneWay, Converter={x:Static wf:Converters.StringToBrush}}"
                             Title="{Binding Name, Mode=OneWay}"
                             Value="{Binding Value, Mode=OneWay}"
                             Level="{Binding Level, Mode=OneWay}"
                             Margin="0">
                <ux:RarityRibbon.MaskedBrush>
                    <ImageBrush ImageSource="{Binding Image, Mode=OneWay, Converter={x:Static wf:Converters.Icon}}"
                                Stretch="Uniform"
                                Viewbox="0.1 0.1 0.5 0.5"
                                Opacity="0.15" />
                </ux:RarityRibbon.MaskedBrush>
            </ux:RarityRibbon>
        </Border>

控件位置发生了偏移

  1. 要截图的控件必须保证Margin属性没有赋值
  2. 要截图的控件必须保证父级元素没有使用Grid.ColumnDefnitions或者Grid.RowDefnitions属性,这个强制布局的属性会影响RenderTargetBitmap工作。
  3. 要截图的控件必须不适用HorizontalAlignment以及VerticalAlignment,这两个属性会影响RenderTargetBitmap工作。

控件大小无法对应

可以试试Snoop里面截图的帮助类,还挺有用的。


// (c) Copyright Cory Plotts.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.

namespace Snoop.Infrastructure;

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

public static class VisualCaptureUtil
{
    private const double BaseDpi = 96;

    public static void SaveVisual(Visual visual, int dpi, string filename)
    {
        // sometimes RenderTargetBitmap doesn't render the Visual or doesn't render the Visual properly
        // below i am using the trick that jamie rodriguez posted on his blog
        // where he wraps the Visual inside of a VisualBrush and then renders it.
        // http://blogs.msdn.com/b/jaimer/archive/2009/07/03/rendertargetbitmap-tips.aspx

        var visualBrush = CreateVisualBrushSafe(visual);

        if (visual is null
            || visualBrush is null)
        {
            return;
        }

        var renderTargetBitmap = RenderVisualWithHighQuality(visual, dpi, dpi);

        SaveAsPng(renderTargetBitmap, filename);
    }
    
    public static RenderTargetBitmap SaveVisual(Visual visual, int dpi)
    {
        // sometimes RenderTargetBitmap doesn't render the Visual or doesn't render the Visual properly
        // below i am using the trick that jamie rodriguez posted on his blog
        // where he wraps the Visual inside of a VisualBrush and then renders it.
        // http://blogs.msdn.com/b/jaimer/archive/2009/07/03/rendertargetbitmap-tips.aspx

        var visualBrush = CreateVisualBrushSafe(visual);

        if (visual is null || visualBrush is null)
        {
            return null;
        }

        return RenderVisualWithHighQuality(visual, dpi, dpi);
    }

    public static VisualBrush CreateVisualBrushSafe(Visual visual)
    {
        return IsSafeToVisualize(visual)
            ? new VisualBrush(visual)
            : null;
    }

    public static bool IsSafeToVisualize(Visual visual)
    {
        if (visual is null)
        {
            return false;
        }

        if (visual is Window)
        {
            var source = PresentationSource.FromVisual(visual) as HwndSource;
            return source?.CompositionTarget is not null;
        }

        return true;
    }

    private static void SaveAsPng(RenderTargetBitmap bitmap, string filename)
    {
        var pngBitmapEncoder = new PngBitmapEncoder();
        pngBitmapEncoder.Frames.Add(BitmapFrame.Create(bitmap));

        using (var fileStream = File.Create(filename))
        {
            pngBitmapEncoder.Save(fileStream);
        }
    }

    /// <summary>
    /// Draws <paramref name="visual"/> in smaller tiles using multiple <see cref="VisualBrush"/>.
    /// </summary>
    /// <remarks>
    /// This way we workaround a limitation in <see cref="VisualBrush"/> which causes poor quality for larger visuals.
    /// </remarks>
    public static RenderTargetBitmap RenderVisualWithHighQuality(Visual visual, int dpiX, int dpiY, PixelFormat? pixelFormat = null, Viewport3D viewport3D = null)
    {
        var size = GetSize(visual);

        var drawingVisual = new DrawingVisual();
        using (var drawingContext = drawingVisual.RenderOpen())
        {
            DrawVisualInTiles(visual, drawingContext, size);
        }

        return RenderVisual(drawingVisual, size, dpiX, dpiY, pixelFormat, viewport3D);
    }

    public static RenderTargetBitmap RenderVisual(Visual visual, Size bounds, int dpiX, int dpiY, PixelFormat? pixelFormat = null, Viewport3D viewport3D = null)
    {
        var scaleX = dpiX / BaseDpi;
        var scaleY = dpiY / BaseDpi;

        pixelFormat ??= PixelFormats.Pbgra32;

        var renderTargetBitmap = new RenderTargetBitmap((int)Math.Ceiling(scaleX * bounds.Width), (int)Math.Ceiling(scaleY * bounds.Height), dpiX, dpiY, pixelFormat.Value);

        if (viewport3D is not null)
        {
            typeof(RenderTargetBitmap)
                .GetMethod("RenderForBitmapEffect", BindingFlags.Instance | BindingFlags.NonPublic)
                ?.Invoke(renderTargetBitmap, new object[] { visual, Matrix.Identity, Rect.Empty });
        }
        else
        {
            renderTargetBitmap.Render(visual);
        }

        return renderTargetBitmap;
    }

    private static Size GetSize(Visual visual)
    {
        if (visual is UIElement uiElement)
        {
            return uiElement.RenderSize;
        }

        var descendantBounds = VisualTreeHelper.GetDescendantBounds(visual);
        return new Size(descendantBounds.Width, descendantBounds.Height);
    }

    /// <summary>
    /// Draws <paramref name="visual"/> in smaller tiles using multiple <see cref="VisualBrush"/> to <paramref name="drawingContext"/>.
    /// This way we workaround a limitation in <see cref="VisualBrush"/> which causes poor quality for larger visuals.
    /// </summary>
    /// <param name="visual">The visual to be drawn.</param>
    /// <param name="drawingContext">The <see cref="DrawingContext"/> to use.</param>
    /// <param name="visualSize">The size of <paramref name="visual"/>.</param>
    /// <param name="tileWidth">The width of one tile.</param>
    /// <param name="tileHeight">The height of one tile.</param>
    /// <remarks>
    /// Original version of this method was copied from https://srndolha.wordpress.com/2012/10/16/exported-drawingvisual-quality-when-using-visualbrush/
    ///
    /// A tile size of 32x32 turned out deliver the best quality while not increasing computation time too much.
    /// </remarks>
    private static void DrawVisualInTiles(Visual visual, DrawingContext drawingContext, Size visualSize, double tileWidth = 32, double tileHeight = 32)
    {
        var visualWidth = visualSize.Width;
        var visualHeight = visualSize.Height;

        var verticalTileCount = visualHeight / tileHeight;
        var horizontalTileCount = visualWidth / tileWidth;

        for (var i = 0; i <= verticalTileCount; i++)
        {
            for (var j = 0; j <= horizontalTileCount; j++)
            {
                var width = tileWidth;
                var height = tileHeight;

                // Check if we would exceed the width of the visual and limit it by the remaining
                if ((j + 1) * tileWidth > visualWidth)
                {
                    width = visualWidth - (j * tileWidth);
                }

                // Check if we would exceed the height of the visual and limit it by the remaining
                if ((i + 1) * tileHeight > visualHeight)
                {
                    height = visualHeight - (i * tileHeight);
                }

                var x = j * tileWidth;
                var y = i * tileHeight;

                var rectangle = new Rect(x, y, width, height);

                var contentBrush = new VisualBrush(visual)
                {
                    Stretch = Stretch.None,
                    AlignmentX = AlignmentX.Left,
                    AlignmentY = AlignmentY.Top,
                    Viewbox = rectangle,
                    ViewboxUnits = BrushMappingMode.Absolute
                };

                drawingContext.DrawRectangle(contentBrush, null, rectangle);
            }
        }
    }
}
posted @   Acoris  阅读(18)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
点击右上角即可分享
微信分享提示