缩放应该以Canvas为目标。您可以通过设置ScaleTransform.CenterX 和
ScaleTransform.CenterY 到当前鼠标位置。缩放将触发布局传递,这将触发ScrollViewer 调整以适应更改的内容。这需要通过滚动到新位置来重新定位内容。
缩放因子适用于相对于缩放元素的完整坐标系,例如Canvas。这意味着当鼠标位置在 P1(5;5) 并且应用比例因子 2 时,鼠标位置将移动到 P2(10;10)。新的鼠标位置 P2 描述了必须添加到当前ScrollViewer 偏移量的偏移量:
Pscroll = Pmouse * scaleFactor + PcurrentScrollOffset
为了按像素滚动ScrollViewer(鼠标位置以像素为单位返回),ScrollViewer 必须配置为使用物理滚动单位而不是逻辑单位。 ScrollViewer.CanContentScroll 必须设置为 false(这是默认设置)。
此外,PreviewMouseWheel 事件处理程序需要直接或通过数据绑定访问Canvas(或一般FrameworkElement)的ScaleTransform。同样适用于ScrollViewer。
为方便起见,我创建了一个附加行为ZoomBehavior。
此行为在鼠标滚轮输入上放大和缩小 FrameworElement 的每个元素。
要启用缩放,请将ZoomBehavior.IsEnabled 设置为true。
可选择设置缩放因子集ZoomBehavior.ZoomFactor(默认为 0.1)。
如果您想调整滚动位置,可以选择将ScrollViewer 绑定或设置为ZoomBehavior.ScrollViewer。请注意,ScrollViewer.CanContentScroll 应设置为 false 以获得正确的行为。
以下示例启用对 Canvas 元素的缩放:
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
代码使用switch expression,这是 C# 8.0 的一个特性。
如果您的环境不支持此语言版本,则需要将表达式转换为经典的switch 语句(带有两个标签)。
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;
}
}