Slow pan and zoom in WPF

2019-02-15 18:10发布

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!

1条回答
祖国的老花朵
2楼-- · 2019-02-15 19:04

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.

查看更多
登录 后发表回答