自定义绘制控件的可怕性能

时间:2014-08-22 15:53:19

标签: c# wpf rendering

我正在wpf进行简单的图表控制。我无法解释或解决性能问题:与winforms相比,它太慢了。也许我做错了什么。

我准备演示来演示问题。

这是测试控制:

public class Graph : FrameworkElement
{
    private Point _mouse;
    private Point _offset = new Point(500, 500);

    public Graph()
    {
        Loaded += Graph_Loaded;
    }

    private void Graph_Loaded(object sender, RoutedEventArgs e)
    {
        // use parent container with background to receive mouse events too
        var parent = VisualTreeHelper.GetParent(this) as FrameworkElement;
        if (parent != null)
            parent.MouseMove += (s, a) => OnMouseMove(a);
    }

    protected override void OnRender(DrawingContext context)
    {
        // designer bugfix
        if (DesignerProperties.GetIsInDesignMode(this))
            return;

        Stopwatch watch = new Stopwatch();
        watch.Start();

        // generate some big figure (try to vary that 2000!)
        var radius = 1.0;
        var figures = new List<LineSegment>();
        for (int i = 0; i < 2000; i++, radius += 0.1)
        {
            var segment = new LineSegment(new Point(radius * Math.Sin(i) + _offset.X, radius * Math.Cos(i) + _offset.Y), true);
            segment.Freeze();
            figures.Add(segment);
        }
        var geometry = new PathGeometry(new[] { new PathFigure(figures[0].Point, figures, false) });
        geometry.Freeze();
        var pen = new Pen(Brushes.Black, 5);
        pen.Freeze();
        context.DrawGeometry(null, pen, geometry);

        // measure time
        var time = watch.ElapsedMilliseconds;
        Dispatcher.InvokeAsync(() =>
        {
            Window.GetWindow(this).Title = string.Format("{0:000}ms; {1:000}ms", time, watch.ElapsedMilliseconds);
        }, DispatcherPriority.Loaded);
    }

    protected override void OnMouseMove(MouseEventArgs e)
    {
        base.OnMouseMove(e);
        var mouse = e.GetPosition(this);
        if (e.LeftButton == MouseButtonState.Pressed)
        {
            // change graph location
            _offset.X += mouse.X - _mouse.X;
            _offset.Y += mouse.Y - _mouse.Y;
            InvalidateVisual();
        }
        // remember last mouse position
        _mouse = mouse;
    }
}

以下是如何在xaml中使用它:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication1"
        Title="MainWindow" Height="350" Width="525" WindowState="Maximized">
    <Grid Background="White">
        <local:Graph/>
    </Grid>
</Window>

一些评论:控件将绘制图形,可以通过鼠标移动:

enter image description here

它将在标题中显示2个测量值:第一个是OnRender()完成所需的时间,第二个是实际渲染需要多长时间(渲染后首次调用)。

尝试改变2000:设置1000使移动舒适,3000就像是重新绘制数字之前的半秒延迟(在我的电脑上)

问题:

  1. 使用InvalidateVisual()更新MouseMove中的图表偏移量是否合适?如果不好,什么是无效的正确技术?
  2. 冻结,其中有许多没有任何明显的效果。我是否需要使用它们?
  3. 看起来只需5ms即可完成渲染,但主观移动需要更长的时间(200ms +)。那是为什么?
  4. 主要问题当然是表现,为什么这么糟糕?我可以在winform控件中绘制几十万行,直到它变得像马虎一样,因为我的wpf控件只用1000 ... =(


    我在最后一个问题上找到答案。使用鼠标移动时,测量渲染时间无法正常工作。但是如果窗口调整大小,那么第二次变为300ms(在我的PC上有2000个数字)。所以它不是错误的鼠标无效(第一个问题),但实际上渲染速度非常慢。

3 个答案:

答案 0 :(得分:3)

这是WPF不太擅长的任务。我的意思是一般的矢量图形。感谢保留模式。它适用于控件渲染,但不适用于您经常更新的繁忙图表。我在尝试在WPF地图上渲染GPS轨道时遇到了同样的问题。

我建议使用direct2d并在WPF中托管它。像这样的东西: http://www.codeproject.com/Articles/113991/Using-Direct-D-with-WPF

这将为您带来高性能。

PS不要误会我的意思。 WPF没什么不好的。它旨在解决具体问题。构建控件和构建令人印象深刻的UI非常容易。我们从自动布局系统中获得了很多理所当然的东西。但是在任何可能的情况下它都不会很聪明,微软在解释情况方面做得并不好,而且不是一个好的选择。让我给你举个例子。 IPad具有高性能,因为它具有固定的分辨率和绝对的布局。如果您修复WPF窗口大小并使用画布面板,您将获得相同的体验。

答案 1 :(得分:2)

这是使用StreamGeometry重写您的代码,这可以为您提供5%-10%的提升

    protected override void OnRender(DrawingContext context)
    {
        // designer bugfix
        if (DesignerProperties.GetIsInDesignMode(this))
            return;

        Stopwatch watch = new Stopwatch();
        watch.Start();

        // generate some big figure (try to vary that 2000!)
        var radius = 1.0;
        StreamGeometry geometry = new StreamGeometry();

        using (StreamGeometryContext ctx = geometry.Open())
        {
            Point start = new Point(radius * Math.Sin(0) + _offset.X, radius * Math.Cos(0) + _offset.Y);
            ctx.BeginFigure(start, false, false); 
            for (int i = 1; i < 2000; i++, radius += 0.1)
            {
                Point current = new Point(radius * Math.Sin(i) + _offset.X, radius * Math.Cos(i) + _offset.Y);
                ctx.LineTo(current, true, false);
            }
        }
        //var geometry = new PathGeometry(new[] { new PathFigure(figures[0].Point, figures, false) });
        geometry.Freeze();
        Pen pen = new Pen(Brushes.Black, 5);
        pen.Freeze();
        context.DrawGeometry(null, pen, geometry);

        // measure time
        var time = watch.ElapsedMilliseconds;
        Dispatcher.InvokeAsync(() =>
        {
            Window.GetWindow(this).Title = string.Format("{0:000}ms; {1:000}ms", time, watch.ElapsedMilliseconds);
        }, DispatcherPriority.Loaded);
    }

编辑2

这里是你的类的完全重写,这实现了缓存以避免重绘和转换转换以通过鼠标执行移动而不是重新绘制。还使用UIElement作为比轻量级的元素的基础,然后是FrameworkElement

public class Graph : UIElement
{
    TranslateTransform _transform = new TranslateTransform() { X = 500, Y = 500 };
    public Graph()
    {
        CacheMode = new BitmapCache(1.4); //decrease this number to improve performance on the cost of quality, increasing improves quality 
        this.RenderTransform = _transform;
        IsHitTestVisible = false;
    }

    protected override void OnVisualParentChanged(DependencyObject oldParent)
    {
        base.OnVisualParentChanged(oldParent);

        if (VisualParent != null)
            (VisualParent as FrameworkElement).MouseMove += (s, a) => OnMouseMoveHandler(a);
    }

    protected override void OnRender(DrawingContext context)
    {
        // designer bugfix
        if (DesignerProperties.GetIsInDesignMode(this))
            return;

        Stopwatch watch = new Stopwatch();
        watch.Start();

        // generate some big figure (try to vary that 2000!)
        var radius = 1.0;
        StreamGeometry geometry = new StreamGeometry();

        using (StreamGeometryContext ctx = geometry.Open())
        {
            Point start = new Point(radius * Math.Sin(0), radius * Math.Cos(0));
            ctx.BeginFigure(start, false, false);
            for (int i = 1; i < 5000; i++, radius += 0.1)
            {
                Point current = new Point(radius * Math.Sin(i), radius * Math.Cos(i));
                ctx.LineTo(current, true, false);
            }
        }
        //var geometry = new PathGeometry(new[] { new PathFigure(figures[0].Point, figures, false) });
        geometry.Freeze();
        Pen pen = new Pen(Brushes.Black, 5);
        pen.Freeze();
        context.DrawGeometry(null, pen, geometry);

        // measure time
        var time = watch.ElapsedMilliseconds;
        Dispatcher.InvokeAsync(() =>
        {
            Application.Current.MainWindow.Title = string.Format("{0:000}ms; {1:000}ms", time, watch.ElapsedMilliseconds);
        }, DispatcherPriority.Loaded);
    }

    protected void OnMouseMoveHandler(MouseEventArgs e)
    {
        var mouse = e.GetPosition(VisualParent as FrameworkElement);
        if (e.LeftButton == MouseButtonState.Pressed)
        {
            _transform.X = mouse.X;
            _transform.Y = mouse.Y;
        }
    }
}

在上面的示例中,我使用5000进行测试,我可以说它非常流畅。

由于这可以通过鼠标实现流体移动,但实际渲染可能需要更长的时间来创建缓存(仅限第一次)。我可以说通过鼠标移动对象可以提升1000%,渲染仍然非常接近我之前的方法,只需要很少的缓存开销。试试这个并分享你的感受


编辑3

这是一个使用DrawingVisual WPF中可用的最轻方法的示例

public class Graph : UIElement
{
    DrawingVisual drawing;
    VisualCollection _visuals;
    TranslateTransform _transform = new TranslateTransform() { X = 200, Y = 200 };
    public Graph()
    {
        _visuals = new VisualCollection(this);

        drawing = new DrawingVisual();
        drawing.Transform = _transform;
        drawing.CacheMode = new BitmapCache(1);
        _visuals.Add(drawing);
        Render();
    }

    protected void Render()
    {

        // designer bugfix
        if (DesignerProperties.GetIsInDesignMode(this))
            return;
        Stopwatch watch = new Stopwatch();
        watch.Start();

        using (DrawingContext context = drawing.RenderOpen())
        {

            // generate some big figure (try to vary that 2000!)
            var radius = 1.0;
            StreamGeometry geometry = new StreamGeometry();

            using (StreamGeometryContext ctx = geometry.Open())
            {
                Point start = new Point(radius * Math.Sin(0), radius * Math.Cos(0));
                ctx.BeginFigure(start, false, false);
                for (int i = 1; i < 2000; i++, radius += 0.1)
                {
                    Point current = new Point(radius * Math.Sin(i), radius * Math.Cos(i));
                    ctx.LineTo(current, true, false);
                }
            }
            geometry.Freeze();
            Pen pen = new Pen(Brushes.Black, 1);
            pen.Freeze();
            // measure time
            var time = watch.ElapsedMilliseconds;
            context.DrawGeometry(null, pen, geometry);

            Dispatcher.InvokeAsync(() =>
            {
                Application.Current.MainWindow.Title = string.Format("{0:000}ms; {1:000}ms", time, watch.ElapsedMilliseconds);
            }, DispatcherPriority.Normal);
        }

    }
    protected override Visual GetVisualChild(int index)
    {
        return drawing;
    }

    protected override int VisualChildrenCount
    {
        get
        {
            return 1;
        }
    }

    protected override void OnMouseMove(MouseEventArgs e)
    {
        if (e.LeftButton == MouseButtonState.Pressed)
        {
            var mouse = e.GetPosition(VisualParent as FrameworkElement);

            _transform.X = mouse.X;
            _transform.Y = mouse.Y;
        }
        base.OnMouseMove(e);
    }
}

答案 2 :(得分:1)

这很奇怪,这里没有人提到,但可以在wpf本地使用gdi draw(没有hosting container)。

我首先找到了this个问题,这个问题变成了普通的基于渲染的图表(使用InvalidateVisuals()来重绘)。

protected override void OnRender(DrawingContext context)
{
    using (var bitmap = new GDI.Bitmap((int)RenderSize.Width, (int)RenderSize.Height))
    {
        using (var graphics = GDI.Graphics.FromImage(bitmap))
        {
            // use gdi functions here, to ex.: graphics.DrawLine(...)
        }
        var hbitmap = bitmap.GetHbitmap();
        var size = bitmap.Width * bitmap.Height * 4;
        GC.AddMemoryPressure(size);
        var image = Imaging.CreateBitmapSourceFromHBitmap(hbitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
        image.Freeze();
        context.DrawImage(image, new Rect(RenderSize));
        DeleteObject(hbitmap);
        GC.RemoveMemoryPressure(size);
    }
}

这种方法能够绘制数十万行。非常敏感。

缺点:

  • 不像纯gdi图那样平滑,DrawImage发生一段时间之后,会有点闪烁。
  • 必须将所有wpf对象转换为gdi对象(有时是不可能的):笔,画笔,点,矩形等。
  • 没有动画,图表本身可以动画(例如,转换),但图纸不是。
相关问题