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.
It turns out it's the Layout that's changing and using so much CPU.
"Layout" is taking the most time to complete
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:
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>
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With