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

我在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(); for (int i = 0; i  { 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中使用它:

      

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

在此处输入图像描述

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

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

问题:

  1. 使用InvalidateVisual()更新MouseMove图形偏移量是否MouseMove ? 如果不好,什么是无效的正确技术?
  2. 冻结,其中有许多没有任何明显的影响。 我是否需要使用它们?
  3. 看起来完成渲染只需要5ms ,但主观移动需要更长的时间(200ms +)。 这是为什么?

主要问题当然是表现,为什么这么糟糕? 我可以在winform控件中绘制几十万行,直到它变得像马虎一样,因为我的wpf控件仅用1000 … =(


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

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

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

这将为您带来高性能。

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

这里是使用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); } } 

这很奇怪,这里没有人提到过,但是可以在wpf本地使用gdi draw(没有托管容器 )。

我首先找到了这个问题,它成为普通的基于渲染的图形(使用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对象(有时是不可能的):笔,画笔,点,矩形等。
  • 没有动画,图形本身可以动画(例如,转换),但绘图不是。