WPF中的低分配图

nie*_*ras 6 c# wpf performance garbage-collection

我在使用WPF时遇到了一些严重问题DrawingContext,或者特别VisualDrawingContext是来自覆盖OnRender元素或使用DrawingVisual.RenderOpen().

问题是这个分配很多.例如,byte[]每次使用绘图上下文时,似乎都在分配 缓冲区.

关于如何使用绘图上下文的示例.

using (var drawingContext = m_drawingVisual.RenderOpen())
{
    // Many different drawingContext.Draw calls
    // E.g. DrawEllipse, DrawRectangle etc.
}
Run Code Online (Sandbox Code Playgroud)

要么

override void OnRender(DrawingContext drawingContext)
{
    // Many different drawingContext.Draw calls
    // E.g. DrawEllipse, DrawRectangle etc.
}
Run Code Online (Sandbox Code Playgroud)

这会导致大量分配,从而导致一些不必要的垃圾收集.所以是的,我需要这个,请继续主题:).

在具有零或低托管堆分配数的WPF中绘制的选项有哪些?重用对象很好,但我还没有找到一种方法来做到这一点......或者DependencyProperty在它周围/内部没有问题和分配.

我知道WritableBitmapEx但希望有一个解决方案不涉及光栅化到预定位图,而是适当的"矢量"图形,仍然可以缩放例如.

注意:CPU使用率是一个问题,但远远低于由此引起的巨大垃圾压力.

更新:我正在寻找.NET Framework 4.5+的解决方案,如果在更高版本中有任何东西,例如4.7,可能有助于回答这个问题,那就没问题了.但它适用于桌面.NET Framework.

更新2:两个主要方案的简要说明.所有示例都已经过分析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>
Run Code Online (Sandbox Code Playgroud)

`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;
    }
}
Run Code Online (Sandbox Code Playgroud)

B:第二种情况涉及绘制数据的移动"网格",例如20行100列,其中元素由边框和具有不同颜色的文本组成以显示某些状态.网格根据外部输入移动,现在每秒仅更新5-10次.30 fps会更好.此,因此,更新的2000项ObservableCollection绑在ListBox(与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>
Run Code Online (Sandbox Code Playgroud)

这里有很多数据绑定,值类型的盒子确实会产生大量的分配,但这不是主要的问题.这是WPF完成的分配.

nie*_*ras 1

WinFormsCanvas解决方案存在一些问题,特别是由于与 WPF 交互方式而导致的所谓“空域”问题WindowsFormsHost。简而言之,这意味着无法在宿主之上绘制任何 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后将 绑定ImageSourceImageXAML 中的 an:

    <Grid x:Name="ImageContainer" SnapsToDevicePixels="True">
        <Image x:Name="ImageSource" 
               RenderOptions.BitmapScalingMode="HighQuality" SnapsToDevicePixels="True">
        </Image>
    </Grid>
Run Code Online (Sandbox Code Playgroud)

并在网格上调整大小以使 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这样,即使WriteableBitmapEx您愿意,您也可以使用 WinForms/GDI+ 进行零堆分配的绘图。请注意,您将获得DrawStringGDI+ 的大力支持,包括。MeasureString

缺点是这是光栅化的,有时可能会出现一些插值问题。因此,请务必UseLayoutRounding="True"在父窗口/用户控件上进行设置。