Dizzy
Dizzy

Reputation: 23

WPF Canvas zoom and children position

I'm implementing a zoom behavior on my canvas. The canvas and the ScrollViewer react correctly but the children inside the canvas move weirdly, drifting from their original position. As a result, after a few of zoom in/out operation the children are in completely different position or in some cases even outside the canvas!

https://ibb.co/TwRW40Z https://ibb.co/16JKR5C

<ScrollViewer x:Name="ScrollViewerCanvas" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" CanContentScroll="True" >
    <MyCanvas Panel.ZIndex="0" x:Name="nodeGraph" Width="1200" Height="790" HorizontalAlignment="Center" />
</ScrollViewer>

protected override void OnPreviewMouseWheel(MouseWheelEventArgs e)
{
    if (Keyboard.Modifiers != ModifierKeys.Control)
        return;

    float scaleFactor = Zoomfactor;
    if (e.Delta < 0)
    {
        scaleFactor = 1f / scaleFactor;
    }

    Point mousePostion = e.GetPosition(this);

    Matrix scaleMatrix = _transform.Matrix;
    scaleMatrix.ScaleAt(scaleFactor, scaleFactor, mousePostion.X, mousePostion.Y);
    _transform.Matrix = scaleMatrix;

    foreach (UIElement child in Children)
    {
        Canvas.SetLeft(child, Canvas.GetLeft(child) * scaleFactor);
        Canvas.SetTop(child, Canvas.GetTop(child) * scaleFactor);

        child.RenderTransform = _transform;
    }

    this.LayoutTransform = _transform;
}

Upvotes: 2

Views: 3371

Answers (2)

Andy
Andy

Reputation: 12276

I'm not clear what you're trying to do but the following zooms around where the mouse position is:

         PreviewMouseWheel="nodeGraph_PreviewMouseWheel"
        >
    <Grid>
        <ScrollViewer x:Name="ScrollViewerCanvas" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" CanContentScroll="True" >
            <Canvas  x:Name="nodeGraph" Width="1200" Height="790" HorizontalAlignment="Center" 
                
                     >
                <Canvas.RenderTransform>
                    <ScaleTransform CenterX="0" CenterY="0" ScaleX="1" ScaleY="1"  x:Name="st"/>
                </Canvas.RenderTransform>

                <Rectangle Fill="Red" Width="100" Height="50"
                           Canvas.Left="200"
                           Canvas.Top="400"/>
                <Rectangle Fill="Blue" Width="100" Height="50"
                           Canvas.Left="500"
                           Canvas.Top="100"/>
            </Canvas>
        </ScrollViewer>
    </Grid>
    </Window>

and

    public MainWindow()
    {
        InitializeComponent();
    }

    float factor = 1;
    private void nodeGraph_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
    {
        if (e.Delta < 0)
        {
            factor -=.1f;
        }
        else
        {
            factor += .1f;
        }
        Point pt= Mouse.GetPosition(nodeGraph);
        st.CenterX = pt.X;
        st.CenterY = pt.Y;
        st.ScaleY = st.ScaleX = factor;
    }

Upvotes: 1

BionicCode
BionicCode

Reputation: 28968

I think you are getting into trouble because you are moving the objects across the canvas. I doubt if this is desirable. You rather should adjust the ScrollViewer position to keep the objects in position.

The following code encapsulates the zooming into an attached behavior.

To make it work properly you should always set ZoomBehavior.IsEnabled="True" and also bind the parent ScrollViewer to the ZoomBehavior.ScrollViewer attached property. The ZoomBehavior.ZoomFactor is optional and defaults to 0.1:

Usage

<ScrollViewer x:Name="MainScrollViewer"
              CanContentScroll="False"
              Width="500" Height="500"
              VerticalScrollBarVisibility="Auto"
              HorizontalScrollBarVisibility="Auto">
  <Canvas Width="300" Height="300"
          local:ZoomBehavior.IsEnabled="True"
          local:ZoomBehavior.ZoomFactor="0.1"
          local:ZoomBehavior.ScrollViewer="{Binding ElementName=MainScrollViewer}"
          Background="DarkGray">
    <Ellipse Fill="DarkOrange"
             Height="100" Width="100"
             Canvas.Top="100" Canvas.Left="100" />
  </Canvas>
</ScrollViewer>

ZoomBehavior.cs

public class ZoomBehavior : DependencyObject
{
  #region IsEnabled attached property

  // Required
  public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached(
    "IsEnabled", typeof(bool), typeof(ZoomBehavior), new PropertyMetadata(default(bool), ZoomBehavior.OnIsEnabledChanged));
    
  public static void SetIsEnabled(DependencyObject attachingElement, bool value) => attachingElement.SetValue(ZoomBehavior.IsEnabledProperty, value);

  public static bool GetIsEnabled(DependencyObject attachingElement) => (bool) attachingElement.GetValue(ZoomBehavior.IsEnabledProperty);

  #endregion

  #region ZoomFactor attached property

  // Optional
  public static readonly DependencyProperty ZoomFactorProperty = DependencyProperty.RegisterAttached(
    "ZoomFactor", typeof(double), typeof(ZoomBehavior), new PropertyMetadata(0.1));

  public static void SetZoomFactor(DependencyObject attachingElement, double value) => attachingElement.SetValue(ZoomBehavior.ZoomFactorProperty, value);

  public static double GetZoomFactor(DependencyObject attachingElement) => (double) attachingElement.GetValue(ZoomBehavior.ZoomFactorProperty);

  #endregion

  #region ScrollViewer attached property

  // Optional
  public static readonly DependencyProperty ScrollViewerProperty = DependencyProperty.RegisterAttached(
    "ScrollViewer", typeof(ScrollViewer), typeof(ZoomBehavior), new PropertyMetadata(default(ScrollViewer)));

  public static void SetScrollViewer(DependencyObject attachingElement, ScrollViewer value) => attachingElement.SetValue(ZoomBehavior.ScrollViewerProperty, value);

  public static ScrollViewer GetScrollViewer(DependencyObject attachingElement) => (ScrollViewer) attachingElement.GetValue(ZoomBehavior.ScrollViewerProperty);

  #endregion
  private static void OnIsEnabledChanged(DependencyObject attachingElement, DependencyPropertyChangedEventArgs e)
  {
    if (!(attachingElement is FrameworkElement frameworkElement))
    {
      throw new ArgumentException("Attaching element must be of type FrameworkElement");
    }

    bool isEnabled = (bool) e.NewValue;
    if (isEnabled)
    {
      frameworkElement.PreviewMouseWheel += ZoomBehavior.Zoom_OnMouseWheel;
      if (ZoomBehavior.TryGetScaleTransform(frameworkElement, out _))
      {
        return;
      }

      if (frameworkElement.LayoutTransform is TransformGroup transformGroup)
      {
        transformGroup.Children.Add(new ScaleTransform());
      }
      else
      {
        frameworkElement.LayoutTransform = new ScaleTransform();
      }
    }
    else
    {
      frameworkElement.PreviewMouseWheel -= ZoomBehavior.Zoom_OnMouseWheel;
    }
  }

  private static void Zoom_OnMouseWheel(object sender, MouseWheelEventArgs e)
  {
    var zoomTargetElement = sender as FrameworkElement;

    Point mouseCanvasPosition = e.GetPosition(zoomTargetElement);
    double scaleFactor = e.Delta > 0
      ? ZoomBehavior.GetZoomFactor(zoomTargetElement)
      : -1 * ZoomBehavior.GetZoomFactor(zoomTargetElement);
      
    ZoomBehavior.ApplyZoomToAttachedElement(mouseCanvasPosition, scaleFactor, zoomTargetElement);

    ZoomBehavior.AdjustScrollViewer(mouseCanvasPosition, scaleFactor, zoomTargetElement);
  }

  private static void ApplyZoomToAttachedElement(Point mouseCanvasPosition, double scaleFactor, FrameworkElement zoomTargetElement)
  {
    if (!ZoomBehavior.TryGetScaleTransform(zoomTargetElement, out ScaleTransform scaleTransform))
    {
      throw new InvalidOperationException("No ScaleTransform found");
    }

    scaleTransform.CenterX = mouseCanvasPosition.X;
    scaleTransform.CenterY = mouseCanvasPosition.Y;

    scaleTransform.ScaleX = Math.Max(0.1, scaleTransform.ScaleX + scaleFactor);
    scaleTransform.ScaleY = Math.Max(0.1, scaleTransform.ScaleY + scaleFactor);
  }

  private static void AdjustScrollViewer(Point mouseCanvasPosition, double scaleFactor, FrameworkElement zoomTargetElement)
  {
    ScrollViewer scrollViewer = ZoomBehavior.GetScrollViewer(zoomTargetElement);
    if (scrollViewer == null)
    {
      return;
    }

    scrollViewer.ScrollToHorizontalOffset(scrollViewer.HorizontalOffset + mouseCanvasPosition.X * scaleFactor);
    scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset + mouseCanvasPosition.Y * scaleFactor);
  }

  private static bool TryGetScaleTransform(FrameworkElement frameworkElement, out ScaleTransform scaleTransform)
  {
    // C# 8.0 Switch Expression
    scaleTransform = frameworkElement.LayoutTransform switch
    {
      TransformGroup transformGroup => transformGroup.Children.OfType<ScaleTransform>().FirstOrDefault(),
      ScaleTransform transform => transform,
      _ => null
    };

    return scaleTransform != null;
  }
}

Upvotes: 7

Related Questions