C# 自定义绘制控件的糟糕性能

C# 自定义绘制控件的糟糕性能,c#,wpf,rendering,C#,Wpf,Rendering,我正在wpf中制作简单的图形控件。我无法解释也无法解决性能问题:与winforms相比,它太慢了。也许我做错了什么 我准备演示来演示这个问题 以下是测试控制: public class Graph : FrameworkElement { private Point _mouse; private Point _offset = new Point(500, 500); public Graph() { Loaded += Graph_Loade

我正在
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;
    }
}
公共类图:FrameworkElement
{
专用点鼠标;
专用点_偏移=新点(500500);
公共图()
{
加载+=图形_加载;
}
已加载私有无效图(对象发送器、路由目标)
{
//使用具有后台的父容器也可以接收鼠标事件
var parent=visualtreeheloper.GetParent(this)作为FrameworkElement;
如果(父项!=null)
parent.MouseMove+=(s,a)=>OnMouseMove(a);
}
受保护的覆盖void OnRender(DrawingContext上下文)
{
//设计器错误修复
if(DesignerProperties.GetIsInDesignMode(此))
返回;
秒表=新秒表();
watch.Start();
//生成一些大的数字(尝试改变2000!)
var半径=1.0;
var数字=新列表();
对于(int i=0;i<2000;i++,半径+=0.1)
{
var段=新线段(新点(半径*数学Sin(i)+_offset.X,半径*数学Cos(i)+_offset.Y),真);
段。冻结();
图.增加(部分);
}
var geometry=new PathGeometry(new[]{new PathFigure(图[0]。点,图,假)});
geometry.Freeze();
var笔=新笔(黑色,5);
笔。冻结();
DrawGeometry(空、笔、几何体);
//测量时间
var时间=watch.elapsedmillisons;
Dispatcher.InvokeAsync(()=>
{
GetWindow(this.Title=string.Format(“{0:000}ms;{1:000}ms”,time,watch.elapsedmillesons);
},DispatcherPriority.Loaded);
}
MouseMove上的受保护覆盖无效(MouseEventArgs e)
{
基地移动(e);
var mouse=e.GetPosition(这个);
如果(e.LeftButton==鼠标按钮状态。按下)
{
//更改图形位置
_offset.X+=mouse.X-_mouse.X;
_offset.Y+=mouse.Y-\u mouse.Y;
无效的(通常的);
}
//记得上次鼠标的位置吗
_鼠标=鼠标;
}
}
以下是如何在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>

备注:控件将绘制图形,可通过鼠标移动:

它将在标题中显示两个度量值:第一个是完成
OnRender()
所用的时间,第二个是实际渲染所用的时间(渲染后第一次调用)

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

问题:

  • MouseMove
    中使用
    InvalidateVisual()
    更新图形偏移是否合适?如果是坏的,什么是正确的技术使其无效
  • 冻结,有很多没有任何明显的效果。我是否需要使用它们
  • 看起来完成渲染只需
    5ms
    ,但主观上移动需要更长的时间(200ms+)。为什么呢
  • 主要的问题当然是性能,为什么这么糟糕?我可以在winform控件中绘制数十万条线,直到它变得像我的wpf控件那样草率,只需1000=(



    我找到了上一个问题的答案。使用鼠标移动时,渲染时间的测量无法正确工作。但如果调整窗口的大小,则第二次渲染时间将变为
    300ms
    (在我的电脑上显示
    2000
    数字)。因此,鼠标无效不是错误的(第一个问题),但渲染速度确实非常慢。

    这是WPF不擅长的一种任务。我指的是一般的矢量图形。由于保留模式。它对控件渲染很好,但对频繁更新的繁忙图形不太好。我尝试在WPF地图上渲染GPS轨迹时遇到了同样的问题

    我建议使用direct2d并将其托管在WPF中。类似于:

    这将给您带来高性能


    PS别误会我的意思。WPF没有什么不好的地方。它是为解决特定问题而设计的。它很容易组合控件和构建令人印象深刻的UI。我们对自动布局系统习以为常。但它不可能在所有可能的情况下都很聪明,微软也没有很好地解释这些情况,因为它不是这不是一个好的选择。让我给你一个例子。IPad之所以有性能,是因为它有固定的分辨率和绝对的布局。如果你固定WPF窗口大小并使用画布面板,你会得到同样的体验。

    下面是使用
    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);
        }
    

    这很奇怪,这里没有人提到过,但是可以在wpf中使用gdi绘图(没有)

    我首先发现了这个问题,它变成了普通的基于渲染的图形(使用
    InvalidateVisuals()
    重新绘制)

    这种方法能够画出数十万条线,响应速度非常快

    缺点:

    • 不像纯gdi one图形那样平滑,
      DrawImage
      出现几次之后,会闪烁一点
    • 需要将所有wpf对象转换为gdi对象(有时是不可能的):笔、画笔、点、矩形等
    • 没有动画,图形本身可以设置动画(例如,变换),但图形不能

    我使用WritablebitmapExtensions创建了一个实时图形。它具有更好的性能。使用StreamGeometry的WritablebitmapExtensions可以在一定程度上提高性能,可能是5%-10%。顺便说一句,如果您只是在鼠标移动时移动图形,而不是更新实际图形,那么缓存该图形并将其绘制为图片,我会这样做吗性能显著提高
    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;
            }
        }
    }
    
    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);
        }
    }
    
    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);
        }
    }