【问题标题】:Determine the inner width of a ScrollViewer确定 ScrollViewer 的内部宽度
【发布时间】:2020-09-24 20:03:19
【问题描述】:

我在 .NET Framework 4.7 上的 WPF 应用程序中有一个 <ScrollViewer>。 ScrollViewer 内部是一个<Border>。该边界不一定占据它所获得的所有空间。对其应用ScaleTransform 以允许放大和缩小。

使用鼠标滚轮放大和缩小时,鼠标光标下的内容位置应保持不变,以进行新旧缩放。为此,我需要做一些数学运算来确定鼠标光标在新旧缩放中的位置。

我已经有了 ScrollViewer 的滚动位置,但是我找不到 ScrollViewer 的虚拟或可滚动或内容宽度/高度。如何确定 ScrollViewer 内容区域的总宽度或高度?这是不显示滚动条时的最小尺寸。我不能只使用内部边框的实际大小,因为它受到限制并且可能无法填充两个维度的可滚动区域。我需要直接从 ScrollViewer 或其滚动条(我无权访问)获取该信息。

这是 XAML 的一部分:

<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
              PreviewMouseWheel="ScrollViewer_PreviewMouseWheel">
    <Border HorizontalAlignment="Center" VerticalAlignment="Center" Margin="100">
        <Grid>
            <Grid.LayoutTransform>
                <ScaleTransform .../>
            </Grid.LayoutTransform>
            <dt:DrawingCanvas .../>
        </Grid>
    </Border>
</scrollViewer>

DrawingCanvas 是确定大小的对象。它在滚动查看器中水平和垂直居中。如果它超过一个或两个维度的可用屏幕空间,则会出现一个滚动条。

我正在使用此代码获取ScrollViewer_PreviewMouseWheel 事件处理程序中的当前位置:

private void ScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs args)
{
    // Determine the mouse cursor position relative to the content
    var sv = (ScrollViewer)sender;
    Point mousePoint = args.GetPosition(sv);
    double x = mousePoint.X + sv.HorizontalOffset;
    double y = mousePoint.Y + sv.VerticalOffset;
    double xFrac = x / sv.ContentWidth;
    double yFrac = y / sv.ContentHeight;

    // Updating scale factor and layout...

    // Move scrollbars to keep the same content under the mouse cursor
    sv.ScrollToHorizontalOffset(...);
    sv.ScrollToVerticalOffset(...);
}

具体来说,我正在从滚动查看器中寻找 ContentWidthContentHeight 不存在的属性。我怎样才能得到这些值?

【问题讨论】:

  • 您在寻找 sv.ExtentHeight 还是 sv.ExtentWidth
  • 我已经更新了我的答案。据我了解,您只是想在鼠标指针位置放大和缩小。我创建了一个附加行为来封装该功能。该行为适用于每个FrameworkElement,而不仅仅是Canvas。请阅读我的更新答案。

标签: .net wpf layout


【解决方案1】:

缩放应该以Canvas为目标。您可以通过设置ScaleTransform.CenterXScaleTransform.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;
  }
}

【讨论】:

  • 我环顾四周,似乎sv.ScrollableWidth + sv.ViewportWidth 是我需要的值。 sv.ExtentWidth 只覆盖内容,即使它小于可用空间。
  • 视情况而定。您要求提供可见区域,这是视口。所以ScrollViewer.ViewportWidth 返回纯可见区域。一些不适合的项目被剪裁,水平滚动条可能会显示(如果未禁用)。如果项目不适合可见区域,ScrollViewer.ScrollableWidth 将返回隐藏/剪辑的项目宽度,可以滚动到视口中。 ScrollViewer.ExtentWidth 将显示总宽度,相当于最大项目宽度。
  • 它是ScrollViewer.ExtentWidth = ScrollViewer.ViewportWidth + ScrollViewer.ScrollableWidth。这转换为 total_width = visible_width + clipped_max_widthScrollViewer.ActualWidth 将返回纯 ScrollViewer 的当前宽度,不包含隐藏/剪切的内容。
  • 好吧,我猜你误解了我的问题,但我不能怪你。我应该画一幅画,尤其是在术语不清楚的时候。你的等式并不总是正确的。 ExtentWidth 可以小于其他两个的总和,如果滚动查看器的内容小于可用的视口大小。这就是为什么我写我需要两者的总和,而不是ExtentWidth
  • 确实如此。当项目的宽度小于视口宽度时,ExtendWidth 等于项目的宽度。我们通常可以说 ÈxtendWidth 总是等于项目的宽度。项目的宽度分为可见部分 (ViewportWidth) 和隐藏部分 (ScrollableWidth)。如果我的回答不令人满意,我显然误解了你的问题。我再看看你的问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-10-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-17
  • 1970-01-01
  • 2015-03-03
相关资源
最近更新 更多