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