[英]Low Allocation Drawing in WPF
我在使用WPF并使用DrawingContext
时遇到了一些严重问题,或者特别是由于在元素上重写OnRender
或使用DrawingVisual.RenderOpen()
而VisualDrawingContext
。
问题是这分配了很多。 例如,似乎每次使用绘图上下文时都会分配一个byte[]
缓冲区。
有关如何使用绘图上下文的示例。
using (var drawingContext = m_drawingVisual.RenderOpen())
{
// Many different drawingContext.Draw calls
// E.g. DrawEllipse, DrawRectangle etc.
}
要么
override void OnRender(DrawingContext drawingContext)
{
// Many different drawingContext.Draw calls
// E.g. DrawEllipse, DrawRectangle etc.
}
这会导致大量分配,从而导致一些不必要的垃圾回收。 所以是的,我需要这个,请继续关注主题:)。
在托管堆分配为零或较少的情况下,在WPF中进行绘图的选项有哪些? 重用对象很好,但是我还没有找到一种方法来执行此操作...否则在DependencyProperty
及其周围/内部的分配没有问题。
我确实了解WritableBitmapEx
但希望找到一种解决方案,该解决方案不涉及栅格化到预定义的位图,而是例如可以缩放的适当“矢量”图形。
注意:CPU使用率是一个令人担忧的问题,但远少于由此引起的巨大垃圾压力。
更新:我正在寻找.NET Framework 4.5+的解决方案,如果更高版本(例如4.7)中的任何内容可能有助于回答此问题,那很好。 但这是针对桌面.NET Framework的。
更新2:简要介绍两个主要方案。 所有示例都已使用CLRProfiler
进行了CLRProfiler
,并且清楚地表明,由于此原因,发生了许多分配,这对我们的用例来说是个问题。 请注意,这是示例代码,旨在传达原理,而不是确切的代码。
答 :这种情况如下所示。 基本上,一个图像被显示和一些覆盖图形经由定制绘制DrawingVisualControl
,然后使用using (var drawingContext = m_drawingVisual.RenderOpen())
以得到一个绘图上下文,然后通过绘制。 绘制了大量的椭圆,矩形和文本。 此示例还显示了一些缩放内容,这仅用于缩放等。
<Viewbox x:Name="ImageViewbox" VerticalAlignment="Center" HorizontalAlignment="Center">
<Grid x:Name="ImageGrid" SnapsToDevicePixels="True" ClipToBounds="True">
<Grid.LayoutTransform>
<ScaleTransform x:Name="ImageTransform" CenterX="0" CenterY="0"
ScaleX="{Binding ElementName=ImageScaleSlider, Path=Value}"
ScaleY="{Binding ElementName=ImageScaleSlider, Path=Value}" />
</Grid.LayoutTransform>
<Image x:Name="ImageSource" RenderOptions.BitmapScalingMode="NearestNeighbor" SnapsToDevicePixels="True"
MouseMove="ImageSource_MouseMove" />
<v:DrawingVisualControl x:Name="DrawingVisualControl" Visual="{Binding DrawingVisual}"
SnapsToDevicePixels="True"
RenderOptions.BitmapScalingMode="NearestNeighbor"
IsHitTestVisible="False" />
</Grid>
</Viewbox>
`DrawingVisualControl定义为:
public class DrawingVisualControl : FrameworkElement
{
public DrawingVisual Visual
{
get { return GetValue(DrawingVisualProperty) as DrawingVisual; }
set { SetValue(DrawingVisualProperty, value); }
}
private void UpdateDrawingVisual(DrawingVisual visual)
{
var oldVisual = Visual;
if (oldVisual != null)
{
RemoveVisualChild(oldVisual);
RemoveLogicalChild(oldVisual);
}
AddVisualChild(visual);
AddLogicalChild(visual);
}
public static readonly DependencyProperty DrawingVisualProperty =
DependencyProperty.Register("Visual",
typeof(DrawingVisual),
typeof(DrawingVisualControl),
new FrameworkPropertyMetadata(OnDrawingVisualChanged));
private static void OnDrawingVisualChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var dcv = d as DrawingVisualControl;
if (dcv == null) { return; }
var visual = e.NewValue as DrawingVisual;
if (visual == null) { return; }
dcv.UpdateDrawingVisual(visual);
}
protected override int VisualChildrenCount
{
get { return (Visual != null) ? 1 : 0; }
}
protected override Visual GetVisualChild(int index)
{
return this.Visual;
}
}
B :第二种情况涉及绘制移动的数据“网格”,例如20行100列,其元素由边框和具有不同颜色的文本组成,以显示某些状态。 网格根据外部输入而移动,目前每秒仅更新5-10次。 30 fps会更好。 因此,这将更新与ListBox
绑定的ObservableCollection
2000个项目(其中VirtualizingPanel.IsVirtualizing="True"
),而ItemsPanel
是Canvas
。 我们甚至不能在正常使用情况下显示此情况,因为它分配的太多,导致GC暂停变得太长和太频繁了。
<ListBox x:Name="Items" Background="Black"
VirtualizingPanel.IsVirtualizing="True" SnapsToDevicePixels="True">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type vm:ElementViewModel}">
<Border Width="{Binding Width_mm}" Height="{Binding Height_mm}"
Background="{Binding BackgroundColor}"
BorderBrush="{Binding BorderColor}"
BorderThickness="3">
<TextBlock Foreground="{Binding DrawColor}" Padding="0" Margin="0"
Text="{Binding TextResult}" FontSize="{Binding FontSize_mm}"
TextAlignment="Center" VerticalAlignment="Center"
HorizontalAlignment="Center"/>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Canvas.Left" Value="{Binding X_mm}"/>
<Setter Property="Canvas.Top" Value="{Binding Y_mm}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True"
Width="{Binding CanvasWidth_mm}"
Height="{Binding CanvasHeight_mm}"
/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
这里有很多数据绑定,值类型的装箱确实会引起很多分配,但这不是这里的主要问题。 它是WPF完成的分配。
由于WindowsFormsHost
如何与WPF交互,WinForms Canvas
解决方案存在一些问题,尤其是所谓的“空域”问题。 为了简短起见,这意味着不能在主机顶部绘制WPF视觉效果。
这可以通过认识到解决方案来解决,因为无论如何我们都必须加倍缓冲,因此我们最好也可以缓冲到一个WriteableBitmap
,然后可以通过Image
控件照常绘制它。
这可以通过使用如下所示的实用程序类来简化:
using System;
using System.Drawing;
using System.Windows;
using SWM = System.Windows.Media;
using SWMI = System.Windows.Media.Imaging;
public class GdiGraphicsWriteableBitmap
{
readonly Action<Rectangle, Graphics> m_draw;
SWMI.WriteableBitmap m_wpfBitmap = null;
Bitmap m_gdiBitmap = null;
public GdiGraphicsWriteableBitmap(Action<Rectangle, Graphics> draw)
{
if (draw == null) { throw new ArgumentNullException(nameof(draw)); }
m_draw = draw;
}
public SWMI.WriteableBitmap WriteableBitmap => m_wpfBitmap;
public bool IfNewSizeResizeAndDraw(int width, int height)
{
if (m_wpfBitmap == null ||
m_wpfBitmap.PixelHeight != height ||
m_wpfBitmap.PixelWidth != width)
{
Reset();
// Can't dispose wpf
const double Dpi = 96;
m_wpfBitmap = new SWMI.WriteableBitmap(width, height, Dpi, Dpi,
SWM.PixelFormats.Bgr24, null);
var ptr = m_wpfBitmap.BackBuffer;
m_gdiBitmap = new Bitmap(width, height, m_wpfBitmap.BackBufferStride,
System.Drawing.Imaging.PixelFormat.Format24bppRgb, ptr);
Draw();
return true;
}
return false;
}
public void Draw()
{
if (m_wpfBitmap != null)
{
m_wpfBitmap.Lock();
int width = m_wpfBitmap.PixelWidth;
int height = m_wpfBitmap.PixelHeight;
{
using (var g = Graphics.FromImage(m_gdiBitmap))
{
m_draw(new Rectangle(0, 0, width, height), g);
}
}
m_wpfBitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
m_wpfBitmap.Unlock();
}
}
// If window containing this is not shown, one can Reset to stop draw or similar...
public void Reset()
{
m_gdiBitmap?.Dispose();
m_wpfBitmap = null;
}
}
然后将ImageSource
绑定到XAML中的Image
:
<Grid x:Name="ImageContainer" SnapsToDevicePixels="True">
<Image x:Name="ImageSource"
RenderOptions.BitmapScalingMode="HighQuality" SnapsToDevicePixels="True">
</Image>
</Grid>
并在网格上调整处理大小以使WriteableBitmap大小匹配,例如:
public partial class SomeView : UserControl
{
ISizeChangedViewModel m_viewModel = null;
public SomeView()
{
InitializeComponent();
this.DataContextChanged += OnDataContextChanged;
}
void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (m_viewModel != null)
{
this.ImageContainer.SizeChanged -= ImageSource_SizeChanged;
}
m_viewModel = e.NewValue as ISizeChangedViewModel;
if (m_viewModel != null)
{
this.ImageContainer.SizeChanged += ImageSource_SizeChanged;
}
}
private void ImageSource_SizeChanged(object sender, SizeChangedEventArgs e)
{
var newSize = e.NewSize;
var width = (int)Math.Round(newSize.Width);
var height = (int)Math.Round(newSize.Height);
m_viewModel?.SizeChanged(width, height);
}
}
这样,您就可以使用WinForms / GDI +进行零堆分配绘制,甚至可以根据需要使用WriteableBitmapEx
。 请注意,然后使用GDI + incl可以获得强大的DrawString
支持。 MeasureString
。
缺点是栅格化,有时可能会有一些插值问题。 因此,请确保还在父窗口/用户控件上设置UseLayoutRounding="True"
。
如演练:通过使用XAML和GDI +代替在WPF中托管Windows窗体控件中所述,使用WindowsFormsHost 。 这不是一个完美的解决方案,但到目前为止,我已经找到了最佳选择。
<Grid>
<WindowsFormsHost x:Name="WinFormsHost>
<custom:Canvas x:Name="Canvas" />
</WindowsFormsHost>
</Grid>
然后创建一个自定义控件并覆盖OnPaint
,类似于:
public partial class Canvas
: UserControl
{
// Implementing custom double buffered graphics, since this is a lot
// faster both when drawing and with respect to GC, since normal
// double buffered graphics leaks disposable objects that the GC needs to finalize
protected BufferedGraphicsContext m_bufferedGraphicsContext =
new BufferedGraphicsContext();
protected BufferedGraphics m_bufferedGraphics = null;
protected Rectangle m_currentClientRectangle = new Rectangle();
public Canvas()
{
InitializeComponent();
Setup();
}
private void Setup()
{
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint |
ControlStyles.Opaque | ControlStyles.ResizeRedraw, true);
DoubleBuffered = false;
this.Dock = DockStyle.Fill;
}
private void DisposeManagedResources()
{
m_bufferedGraphicsContext.Dispose();
if (m_bufferedGraphics != null)
{
m_bufferedGraphics.Dispose();
}
}
protected override void OnPaintBackground(PaintEventArgs e)
{
// Background paint is done in OnPaint
// This reduces the "leaks" of System.Windows.Forms.Internal.DeviceContext
// and the amount of "GC" handles created considerably
// as found by using CLR Profiler
}
protected override void OnPaint(PaintEventArgs e)
{
// Specifically not calling base here since we draw entire area ourselves
// base.OnPaint(e);
// Should this be disposed?
using (e)
using (var targetGraphics = e.Graphics)
{
ReallocBufferedGraphics(targetGraphics);
// Use buffered graphics object
var graphics = m_bufferedGraphics.Graphics;
// Raise paint event
PaintEvent?.Invoke(this.ClientRectangle, e.ClipRectangle, graphics);
// Render to target graphics i.e. paint event args graphics
m_bufferedGraphics.Render(targetGraphics);
}
}
protected virtual void ReallocBufferedGraphics(Graphics graphics)
{
Rectangle newClientRectangle = this.ClientRectangle;
// Realloc if new client rectangle is not contained within the current
// or if no buffered graphics exists
bool reallocBufferedGraphics = ShouldBufferBeReallocated(newClientRectangle);
if (reallocBufferedGraphics)
{
if (m_bufferedGraphics != null)
{
m_bufferedGraphics.Dispose();
}
m_bufferedGraphics = m_bufferedGraphicsContext.Allocate(
graphics, newClientRectangle);
m_currentClientRectangle = newClientRectangle;
}
}
protected virtual bool ShouldBufferBeReallocated(Rectangle newClientRectangle)
{
return !m_currentClientRectangle.Contains(newClientRectangle) ||
m_bufferedGraphics == null;
}
/// <summary>
/// PaintEvent with <c>clientRectangle, clipRectangle, graphics</c> for the canvas.
/// </summary>
public event Action<Rectangle, Rectangle, Graphics> PaintEvent;
}
更新:将Canvas控件更新为真正的零堆分配。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.