简介
在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>
控件位置发生了偏移
- 要截图的控件必须保证Margin属性没有赋值
- 要截图的控件必须保证父级元素没有使用
Grid.ColumnDefnitions
或者Grid.RowDefnitions
属性,这个强制布局的属性会影响RenderTargetBitmap工作。 - 要截图的控件必须不适用
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.aspxvar 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.aspxvar 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 remainingif ((j + 1) * tileWidth > visualWidth){width = visualWidth - (j * tileWidth);}// Check if we would exceed the height of the visual and limit it by the remainingif ((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);}}}
}