The scaling should target the Canvas. You can scale the Canvas at mouse cursor position by setting the ScaleTransform.CenterX and
ScaleTransform.CenterY to the current mouse position. Scaling will trigger a layout pass, which will trigger the ScrollViewer to adjust to the changed content. This requires to reposition the content by scrolling to the new position.
The scaling factor applies to the complete coordinate system relative to the scaled element e.g., Canvas. This means when the mouse position is at P1(5;5) and a scaling factor of 2 is applied, the mouse position will move to P2(10;10). The new mouse position P2 describes the offset that must be added to the current ScrollViewer offsets:
Pscroll = Pmouse * scaleFactor + PcurrentScrollOffset
In order to scroll the the ScrollViewer by pixels (mouse position is returned in pixels), the ScrollViewer must be configured to use physical scrolling units instead of logical units. ScrollViewer.CanContentScroll must be set to false (which is the default).
Also the PreviewMouseWheel event handler needs to have access to the ScaleTransform of the Canvas (or FrameworkElement in general), either directly or via data binding. Same applies to the ScrollViewer.
For convenience I have created an attached behavior ZoomBehavior.
This behavior zooms in and out every element that is a FrameworElement on mouse wheel input.
To enable zoom, set the ZoomBehavior.IsEnabled to true.
Optionally set the zoom factor set ZoomBehavior.ZoomFactor (default is 0.1).
Optionally bind or set a ScrollViewer to ZoomBehavior.ScrollViewer, if you wish to adjust scroll position. Note that ScrollViewer.CanContentScroll should be set to false for proper behavior.
The following example enables zooming on a Canvas element:
ScrollViewer x:Name="ScrollViewer"
CanContentScroll="False"
Width="500" Height="500"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto">
<Canvas Width="300" Height="300"
main:ZoomBehavior.IsEnabled="True"
main:ZoomBehavior.ZoomFactor="0.1"
main:ZoomBehavior.ScrollViewer="{Binding ElementName=ScrollViewer}"
Background="DarkGray">
<Ellipse Fill="DarkOrange"
Height="100" Width="100"
Canvas.Top="100" Canvas.Left="100" />
</Canvas>
</ScrollViewer>
ZoomBehavior.cs
The code uses a switch expression, which is a feature of C# 8.0.
If your environment doesn't support this language version, you need to convert the expression to a classic switch statement (with two labels).
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), 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;
}
}
FrameworkElement, not only withCanvas. Please read my updated answer. - BionicCode