2

I have drawn a good deal of lines and texts on several canvases in WPF. I have used the most lightweight element possible in WPF: DrawingVisual

I have drawn lines on a different canvas and I have bound their thickness to the inverse of the zoom factor so that I can get a uniform line thickness while zooming. That means when I'm zooming I'm only redrawing the canvas with the lines. The canvas with the texts is only created at the start of the program.

Now I have encountered a rather odd problem. When I'm zooming I'm getting a slow performance. The performance gets worse when I use dashed line style. Slow like when you load a big text file in a word and you have problems scrolling it!

My first thought was that maybe the creation of the lines is taking too much. Since I'm creating those at each zoom (On mouse wheel), So I used a modest stopwatch to measure the time it takes between when I scroll mouse wheel till the creation of the lines ends. To my surprise it only takes 1 ms! So the creation of the lines cannot be the issue.

With further examination I figured out that I'm getting a rather slow performance while panning! Slow like when you a trying to pan in very very big image in windows!

So what could be the problem? I know you will want some code but since the code is very long I'll only show what goes on inside Mousewheel event:

private void OnMouseWheel(object sender, MouseWheelEventArgs e)
{
    var sw = new Stopwatch();
    sw.Start();

    var st = GetScaleTransform(Window);
    var tt = GetTranslateTransform(Window);

    var absoluteX = MousePos.Current.X * st.ScaleX + tt.X;
    var absoluteY = MousePos.Current.Y * st.ScaleY + tt.Y;

    const double zoomfactorforwheel = 1.3;
    if (e.Delta > 0)
    {
        st.ScaleX = Math.Min(st.ScaleX * zoomfactorforwheel, Scalemax);
        st.ScaleY = Math.Min(st.ScaleY * zoomfactorforwheel, Scalemax);
    }
    else
    {
        st.ScaleX = Math.Max(st.ScaleX / zoomfactorforwheel, Scalemin);
        st.ScaleY = Math.Max(st.ScaleY / zoomfactorforwheel, Scalemin);
    }
    tt.X = absoluteX - MousePos.Current.X * st.ScaleX;
    tt.Y = absoluteY - MousePos.Current.Y * st.ScaleY;

    Scale = st.ScaleX;

    // Inside this function I'm drawing the lines on drawingvisual
    // Then I'm adding the drawingvisual to a canvas
    DrawZeroWidthDrawing(Scale);

    sw.Stop();
    Console.WriteLine(sw.ElapsedMilliseconds);
}

I'm starting to think that maybe it has to do something with the way things get rendered in WPF, or maybe with how mouse buttons are getting handled in WPF? It seems like something is interfering with mouse event frequency. Any suggestions are very appreciated.

Update: I have tried to implement what GameAlchemist said in my code to my surprise it is still slow.

private MouseWheelEventArgs e;
private bool zoomNeeded;

CompositionTarget.Rendering += CompositionTargetOnRendering;

private void CompositionTargetOnRendering(object sender, EventArgs eventArgs)
{
    if (zoomNeeded)
    {
        zoomNeeded = false;
        DoZoom();

        DrawZeroWidthDrawing();              
    }
}

private void OnMouseWheel(object sender, MouseWheelEventArgs e)
{
    this.e = e;
    zoomNeeded = true;
}

private void DoZoom()
{
    var st = GetScaleTransform(Window);
    var tt = GetTranslateTransform(Window);

    var absoluteX = MousePos.Current.X * st.ScaleX + tt.X;
    var absoluteY = MousePos.Current.Y * st.ScaleY + tt.Y;

    const double zoomfactorforwheel = 1.3;
    if (e.Delta > 0)
    {
        st.ScaleX = Math.Min(st.ScaleX * zoomfactorforwheel, Scalemax);
        st.ScaleY = Math.Min(st.ScaleY * zoomfactorforwheel, Scalemax);
    }
    else
    {
        st.ScaleX = Math.Max(st.ScaleX / zoomfactorforwheel, Scalemin);
        st.ScaleY = Math.Max(st.ScaleY / zoomfactorforwheel, Scalemin);
    }
    tt.X = absoluteX - MousePos.Current.X * st.ScaleX;
    tt.Y = absoluteY - MousePos.Current.Y * st.ScaleY;

    Scale = st.ScaleX;            
}

private static TranslateTransform GetTranslateTransform(UIElement element)
{
    return (TranslateTransform)((TransformGroup)element.RenderTransform)
      .Children.First(tr => tr is TranslateTransform);
}

private static ScaleTransform GetScaleTransform(UIElement element)
{
    return (ScaleTransform)((TransformGroup)element.RenderTransform)
      .Children.First(tr => tr is ScaleTransform);
}

I figured out maybe redrawing the lines is the culprit but it seems not! I removed the call to the function that actually redraws them with the new thicknesses and only left the scaling part. The result didn't change! I'm still lacking performance.

private void CompositionTargetOnRendering(object sender, EventArgs eventArgs)
{
    if (zoomNeeded)
    {
        zoomNeeded = false;
        DoZoom();
    }
}

enter image description here

I tried to measure the FPS using WPF performance tool, it falls back to 16~24 during zoom!

Simon Sarris
  • 62,212
  • 13
  • 141
  • 171
Vahid
  • 5,144
  • 13
  • 70
  • 146
  • Try Performance Profiling Tools for WPF. – Erti-Chris Eelmaa Dec 21 '14 at 00:05
  • How many times this is done per second ? Because if it's more than 60 times, this is a waste of time. You might consider only storing some data + set a 'dirty' flag in mouse handler, and have a draw method called like 30 times/s that redraws if dirty. – GameAlchemist Dec 21 '14 at 11:18
  • @GameAlchemist Thanks for giving attention to the question I have been stuck with this for months. Unfortunately I don't know how to measure the framerate of this. How can I get that? Can you help me with a little example as an answer. Also I asked a similar question some time ago, and one user gave me some hints about the mouse frequency but I couldn't understand it. You can see it here: http://stackoverflow.com/questions/24119311/the-reason-behind-slow-performance-in-wpf – Vahid Dec 21 '14 at 11:55
  • @GameAlchemist By the way, I'm also getting this when panning. – Vahid Dec 21 '14 at 12:19
  • The answer you quote is very interesting. Go for the solution i suggested by handling a dirty flag in mouseWheel handler and other mouse handlers , then hook a single draw method on CompositionTarget.Rendering. It should redraw only if dirty flag is set obviously, then set it to false after redraw. – GameAlchemist Dec 21 '14 at 12:21
  • I wonder if you're not redrawing on every mouse events. ==>> ?? If so in case you both move and zoom (quite likely), there will be far too many draw, even if each one is quite short you'll end up slow. – GameAlchemist Dec 21 '14 at 12:23
  • @GameAlchemist I'm only redrawing the lines on MouseWheel event which only takes less than 1ms to redraw! I'm not drawing anything on pan though. But somehow I'm getting this laggy zoom and pan. I couldn't understand how to set a dirty flag. And since I'm not drawing anything on pan, how this will help me in a smooth panning? On mousemove event which I used for the panning I'm only using RenderTransform nothing else. – Vahid Dec 21 '14 at 15:08
  • Possible alternative: do not redraw lines and bind their thickness to the inverse scaling factor of the Canvas' RenderTransform(?). Instead draw Paths with constant StrokeThickness and scale the Geometry in their Data property (by means of the Geometry.Transform property). – Clemens Jan 02 '15 at 20:54
  • @Clemens Thanks Clemens, actually the redrawing part doesn't take too much time. The problem is there when panning. In panning I'm not drawing anything just transforming. This makes me think that the problem is somewhere else. – Vahid Jan 02 '15 at 21:04

1 Answers1

3

Ok, Here's my advice : in all your events handler, do not draw or modify anything related to the render. Because the events might trigger more than once during a screen refresh (=16ms for 60Hz screen), so you might end up redrawing several times for nothing.
So handle some flags : in computer graphics, we often call them 'dirty' flags, but call them as you want. Here you might use two bools that might be private properties, let's call them linesNeedsRedraw and renderTransformNeedsUpdate (for instance).
Now in the mouseWheelHandler, for instance, just replace your call to DrawZeroWidthDrawing(Scale); by lineNeedsRedraw = true;. In the mouse move handler, replace your RenderTransform change by renderTransformNeedsUpdate=true;.

Second thing : hook a method on CompositionTarget.Rendering event. This method will be very simple :

void repaintIfRequired() {
    if (linesNeedRedraw) {
       DrawZeroWidthDrawing(Scale);
       linesNeedRedraw = false;
    }
    if (renderTransformNeedsUpdate) {
       // ... do your transform change
       renderTransformNeedsUpdate = false;
    }
}

This way you have a maximum of one update per frame.

I'll end by a question : why not also use the renderTransform to do the scaling ?

Good luck.

GameAlchemist
  • 18,995
  • 7
  • 36
  • 59
  • Thank you so much for the time you have put into this. I'v written most of my program and I have been struggling with this part for months. I'm actually using the renderTransform to do the scaling but since the thickness of the lines grows when getting scaled I had no choice but to redraw them with new thickness at each zoom. – Vahid Dec 21 '14 at 18:31
  • I tried this but unfortunately I didn't get any performance. It even seems slower. I'll update the question with what I have done so far in a minute. – Vahid Dec 21 '14 at 18:31
  • I haven't been able to solve this. Really frustrating. – Vahid Dec 21 '14 at 18:56
  • mmm... i've seen your update and i'm puzzled... I wonder if changing the transform doesn't make the window 'dirty', hence require a repaint on next frame... Question : does it get slower and slower, or is it always slow ? – GameAlchemist Dec 21 '14 at 18:59
  • It is always slow, Can I send the source code to you? – Vahid Dec 21 '14 at 19:02
  • No time i fear. Use a profiling tool, maybe the issue is coming from somewhere else... (free 30 days trial for this one : http://www.red-gate.com/products/dotnet-development/ants-performance-profiler/). Just another rq : the time taken to update the scene graph (== update the line definitions) has little to see with the time taken to draw those lines / update the whole render engine of WPF. I forgot to ask you about the line count, which might be too big. – GameAlchemist Dec 21 '14 at 19:53
  • Sorry I understand. Thank you so much for the time you put into this. The number of lines/text is under 1000. All drawn using DrawingVisuals. – Vahid Dec 21 '14 at 20:36
  • If you ever got the time please tell me via email, I'd be happy if you looked at it. It has been bugging me for months now :( I added a picture of the lines. – Vahid Dec 21 '14 at 20:47
  • i'll have to go to sleep soon but one thing i'd bet would be to slow down the pace of refresh. Like having a frameCount integer private property (increased on each CompositionTarget.Rendering) and update lines/transform only when (frameCount % 2==0) or even (frameCount%3=0) (otherwise return). – GameAlchemist Dec 21 '14 at 21:21
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/67432/discussion-between-vahid-and-gamealchemist). – Vahid Dec 21 '14 at 21:26