Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF MouseMove InvalidateVisual OnRender update VERY SLOW

I haven't found anything useful either on Google or Stack Overflow or simply no answers (or maybe I just don't know what to search for) -- the closest question I can get to is this one: The reason behind slow performance in WPF

But I want to get to the bottom of this lag in this simple program, maybe I'm just not doing something right.

I'm rendering about 2000 points with lines between them in the OnRender() of a UI Element, essentially creating a line graph. That's okay, but I want to pan the graph with MouseMove. That works fine, but it is the LAG that is the problem. Whenever dragging with the mouse I'd expect a smooth update, I'd think that redrawing 2000 points with lines between them would be a walk in the park for an i5 CPU. But it is incredibly slow, even at low resolutions on my laptop at home. So I checked the Performance Profiler. The OnRender() function hardly uses any CPU.

MouseMove and OnRender hardly use much CPU

It turns out it's the Layout that's changing and using so much CPU.

The Layout is using most CPU

"Layout" is taking the most time to complete

Layout takes the most time It says that changes to the Visual tree were made -- but no changes were made - just InvalidateVisual was called

Now, I've heard the term Visual Tree kicking about , but there is hardly any visuals in this simple project. Just a UI Element on a Main Window. And it's using a drawing context, I'd have thought that the drawing context drew like a bitmap, or is it drawing UI elements with their own events/hit boxes etc? Because all I want is the UIElement to act like an image but also handle mouse events so I can drag the whole thing (or zoom with mousewheel).

So Questions:

  1. If Layout is causing the slowness/lag, how can I prevent this?
  2. I also Notice a lot of garbage collection which makes sense, but I don't want it to happen during Rendering. I'd rather do that while it's idle. but how?

Here is the source:

.cs file

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows;
using System.Windows.Media;

namespace SlowChart
{
    public class SlowChartClass : UIElement
    {
        List<Point> points = new List<Point>();

        double XAxis_Width = 2000;
        double XAxis_LeftMost = 0;

        double YAxis_Height = 300;
        double YAxis_Lowest = -150;

        Point mousePoint;
        double XAxis_LeftMostPan = 0;
        double YAxis_LowestPan = 0;

        public SlowChartClass()
        {
            for (int i = 0; i < 2000; i++)
            {
                double cos = (float)Math.Cos(((double)i / 100) * Math.PI * 2);
                cos *= 100;

                points.Add(new Point(i, cos));
            }

            MouseDown += SlowChartClass_MouseDown;
            MouseUp += SlowChartClass_MouseUp;
            MouseMove += SlowChartClass_MouseMove;
        }

        private void SlowChartClass_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
        {
            if (IsMouseCaptured)
            {
                XAxis_LeftMost = XAxis_LeftMostPan - (e.GetPosition(this).X - mousePoint.X);
                YAxis_Lowest = YAxis_LowestPan + (e.GetPosition(this).Y - mousePoint.Y);
                InvalidateVisual();
            }
        }

        private void SlowChartClass_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            ReleaseMouseCapture();
        }

        private void SlowChartClass_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            mousePoint = e.GetPosition(this);
            XAxis_LeftMostPan = XAxis_LeftMost;
            YAxis_LowestPan = YAxis_Lowest;
            CaptureMouse();
        }

        double translateYToScreen(double Y)
        {
            double y = RenderSize.Height - (RenderSize.Height * ((Y - YAxis_Lowest) / YAxis_Height));

            return y;
        }

        double translateXToScreen(double X)
        {
            double x = (RenderSize.Width * ((X - XAxis_LeftMost) / XAxis_Width));


            return x;
        }

        protected override void OnRender(DrawingContext drawingContext)
        {
            bool lastPointValid = false;
            Point lastPoint = new Point();
            Rect window = new Rect(RenderSize);
            Pen pen = new Pen(Brushes.Black, 1);

            // fill background
            drawingContext.DrawRectangle(Brushes.White, null, window);

            foreach (Point p in points)
            {
                Point screenPoint = new Point(translateXToScreen(p.X), translateYToScreen(p.Y));

                if (lastPointValid)
                {
                    // draw from last to  this one
                    drawingContext.DrawLine(pen, lastPoint, screenPoint);
                }

                lastPoint = screenPoint;
                lastPointValid = true;
            }

            // draw axis
            drawingContext.DrawText(new FormattedText(XAxis_LeftMost.ToString("0.0") + "," + YAxis_Lowest.ToString("0.0"),CultureInfo.InvariantCulture,FlowDirection.LeftToRight,new Typeface("Arial"),12,Brushes.Black),new Point(0,RenderSize.Height-12));

        }
    }
}

.XAML file

<Window x:Class="SlowChart.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:SlowChart"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <local:SlowChartClass/>
    </Grid>
</Window>
like image 523
pm101 Avatar asked Jun 02 '17 19:06

pm101


1 Answers

Don't call InvalidateVisual() for this. It triggers a full relayout of your UI, which is very slow.

The key to good performance in WPF is understanding that it's a retained drawing system. OnRender() should really be named AccumulateDrawingObjects(). It is only used at the end of the layout process, and the objects it is accumulating are actually live objects you can update after it's completed.


The efficient way to do what you are trying to do, is create a DrawingGroup "backingStore" for your chart. The only thing your OnRender() needs to do is add the backingStore to the DrawingContext. Then you can update it anytime you want by using backingStore.Open() and just drawing into it. WPF will automatically update your UI.

You'll find StreamGeometry is the fastest way to draw to a DrawingContext, as it optimizes for non-animated geometry.

You can also get some additional performance by using .Freeze() on your Pen, because it is not animated. Though I doubt you will notice when drawing only 2000 points.

It looks something like this:

DrawingGroup backingStore = new DrawingGroup();

protected override void OnRender(DrawingContext drawingContext) {      
    base.OnRender(drawingContext);            

    Render(); // put content into our backingStore
    drawingContext.DrawDrawing(backingStore);
}

// Call render anytime, to update visual
// without triggering layout or OnRender()
public void Render() {            
    var drawingContext = backingStore.Open();
    Render(drawingContext);
    drawingContext.Close();            
}

private void Render(DrawingContext drawingContext) {
    // move the code from your OnRender() here
} 

If you'd like to see more example code, take a look here:

https://github.com/jeske/SoundLevelMonitor/blob/master/SoundLevelMonitorWPF/SoundLevelMonitor/AudioLevelsUIElement.cs#L172


However, if the visual is relatively static, and all you want to do is pan and zoom around, there are other options. You can create a Canvas, and instantiate Shapes into it, and then during mouse move you mess with the Canvas transform to pan and zoom.

like image 179
David Jeske Avatar answered Nov 05 '22 06:11

David Jeske