65.9K
CodeProject 正在变化。 阅读更多。
Home

拖动操作中的自动滚动

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (9投票s)

2013 年 7 月 9 日

CPOL

8分钟阅读

viewsIcon

40746

downloadIcon

1784

实现拖动操作中自动滚动的方

介绍 

在拖动操作中,当鼠标移动到边缘时,底层视图能够自动滚动以显示更多内容,这是非常理想的。本文将介绍一种在 WPF 控件内部的拖动操作中实现此类自动滚动的方法。

所提出的解决方案可以轻松地应用到任何 WPF 控件上(例如 GridStackPanelWrapPanel,甚至是 ItemsControl),无论它们是放置在 ScrollViewer 内部,还是其模板中包含 ScrollViewer(例如 TreeViewListBox 等)。然而,本解决方案并不涉及如何启用和处理拖放操作。

WPF 以其高度的灵活性而闻名。在大多数情况下,这正是 WPF 最强大的优势,但当你试图编写通用的实用代码时,你会很快发现,正是这种优势使得覆盖所有可能的情况变得相当困难。本文并非声称能在绝对所有场景下工作,但我仍然努力实现了这一目标,同时力求解决方案尽可能通用。我确信它将在绝大多数情况下无缝工作。

其他特性

可复用的

附加属性是 WPF 最强大的功能之一。
除了为 DependencyObject 关联额外的状态之外,它们还可以用来在保持 XAML 友好的同时添加新的行为。
我在解决方案中采用了这种方法,以便将其作为库进行重用和分发。
随附的演示应用程序包含一个名为 AutoDragScrollingProvider 的类,并提供了一个名为 EnableAutomaticScrolling 的附加属性,可以轻松地在任何为其附加的 UIElement 上启用自动滚动。

平滑滚动

您可能知道 WPF 支持两种滚动模式:
  • 基于像素的滚动
  • 以及“逻辑”滚动,即滚动是按逻辑进行的,在大多数情况下意味着逐项滚动。例如,ListBox 默认使用逐项滚动。

基于像素的滚动非常平滑,符合我们这些来自 WinForms 世界的人的期望。而逻辑滚动,尤其是逐项滚动时,对最终用户来说显得有些卡顿,因为整个项目会移出,新项目会移入。

要启用逻辑滚动,必须将 ScrollViewer.CanContentScroll 属性设置为 true,并且必须满足以下条件之一:

  • ScrollViewer.Content 实现 IScrollInfo 接口
  • 或者 ScrollViewer 是模板的一部分,并且在其子视觉树中包含 ItemsPresenter,而 ItemsPresenter 的子元素(即 ItemsControl.ItemsPanel 模板中包含的面板)实现了 IScrollInfo 接口。
例如,VirtualizingStackPanelStackPanel 都实现了 IScrollInfo,如果满足上述场景,ScrollViewer 将按照这些面板的指示逻辑滚动。

在拖动时,通常希望通过鼠标的“压力”来控制滚动速度,即鼠标越靠近边缘(给人一种“用力按压”的感觉),滚动速度越快。在本文(和随附演示)中,我使用“平滑滚动”一词来指代这种情况,而不是上面描述的基于像素的滚动。在这种情况下,“平滑滚动”不影响逐项滚动能力,只是滚动速度更能被用户控制,从而提供更愉悦的用户体验。

AutoDragScrollingProvider 确保基于像素的滚动始终是平滑的(如上所述),而逻辑滚动是否平滑取决于 IScrollInfo 的具体实现。要支持平滑滚动,IScrollInfo 实现必须支持滚动偏移量的分数更改。
例如,StackPanel 在与其方向相反的方向支持平滑滚动,而在其方向上则不支持。
VirtualizingStackPanel 的情况比这更复杂一些。当它不被托管在 ItemsControl 中时(如 ItemsControl.ItemsPanel 所规定的),它的行为类似于 StackPanel;否则,它支持平滑滚动,无论其方向如何。
但请记住,如果 CanContentScroll 属性为 false 或不满足任何必要条件,ScrollViewer 将默认使用 ScrollContentPresenter(它本身实现了 IScrollInfo)来提供基于像素的滚动。
另一个需要记住的点是,任何 IScrollInfo 实现都可以通过使用其行滚动方法来执行非平滑滚动。

AutoDragScrollingProvider 尝试尽可能利用“平滑滚动”,因为它能提供更愉悦的用户体验。它会自动处理因 StackPanelVirtualizingStackPanel 在平滑滚动支持方面出现的各种情况,并提供一组附加属性来强制对那些不支持平滑滚动的 IScrollInfo 自定义实现执行“非平滑”行滚动。

与拖动操作的来源无关的滚动

在某些拖放操作中,通常需要拖动操作从一个控件开始,在另一个控件结束。例如,通过从一个列表拖动项目到另一个列表来移动项目。在其他情况下,拖放仅限于单个控件(例如,通过拖动重新排序行)。

AutoDragScrollingProvider 自动适用于任何将其 AllowDrop 属性设置为 true(这是启用拖动所必需的)并且当然将其 EnableAutomaticScrolling 设置为 true 的控件。它不取决于拖动是否源自底层控件。因此,它能够自动支持上述两种场景。

使用代码

本文附带了一个演示应用程序。它展示了 AutoDragScrollingProvider 如何在不同场景下使用。

要启用自动滚动,只需将 AutoDragScrollingProvider.EnableAutomaticScrolling 附加属性设置为 true,并记住也将 AllowDrop 设置为 true。例如:

<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
  <WrapPanel AllowDrop="True" local:AutoDragScrollingProvider.EnableAutomaticScrolling="True">
    ...child elements...
  <WrapPanel>
<ScrollViewer>

如果您碰巧使用了 IScrollInfo 的自定义实现,并且知道它在某个方向上不支持平滑滚动(即分数滚动偏移量更改),您可以将 AutoDragScrollingProvider.UseLineScrollingForHorizontalChangeAutoDragScrollingProvider.UseLineScrollingForVerticalChange 附加属性设置为 true,以强制在 respective 方向上使用“非平滑”行滚动。

看点

以通用方式处理 IScrollInfo 可能有些棘手。IScrollInfo 是一个接口,因此很多事情取决于其具体的实现。它的一些实现以像素为单位显示滚动属性(偏移量、视口尺寸等),一些以逻辑单位显示,还有一些在一个滚动方向上使用逻辑单位,而在另一个方向上使用像素。

AutoDragScrollingProvider 能够通过计算逻辑滚动情况下的平均每项像素宽度(或高度)(对于基于像素的滚动,平均值始终为 1,因此不影响后续计算),然后使用像素单位来确定所需的滚动量,从而以一致的方式处理此问题。

工作原理 

  • 当附加属性设置为 true 时,AutoDragScrollingProvider 会订阅附加到的 UIElement 上的 PreviewDragEnterPreviewDragOver 事件。
  • 在这些事件的处理程序中,它会查找当前生效的相关 IScrollInfo(默认是 ScrollContentPresenter)并将其缓存以供后续使用。
  • 接下来,它获取鼠标在由缓存的 IScrollInfo 表示的控件内的位置。
    UIElement scrollable = _scrollInfo as UIElement;
    var mousePos = e.GetPosition(scrollable);
    
  • 现在它检查鼠标是否距离任何边缘 20 像素以内,如果是,则计算平均宽度或平均高度(视情况而定)。如果视口尺寸是像素,则此平均值始终为 1;否则,此值可被视为每像素的平均值。
    var avgWidth = scrollable.RenderSize.Width / _scrollInfo.ViewportWidth; //translate to pixels
    
  • 接下来,它计算需要更改的滚动偏移量,然后更改偏移量以引起滚动。鼠标越靠近边缘,增量越大。正如您可能猜到的,在大多数情况下,增量是分数。
    var delta = (mousePos.X - 20) / avgWidth; //translate back to original unit
    _scrollInfo.SetHorizontalOffset(_scrollInfo.HorizontalOffset + delta);
    
  • 对于使用其他两个附加属性强制执行行滚动的场景,它只会引起行滚动而不是基于增量的滚动(注意:请参阅更新部分)。例如:
    _scrollInfo.LineLeft();
    

更新

更新 1

LineSizeForHorizontalChangeLineSizeForVerticalChange 附加属性替换 UseLineScrollingForHorizontalChangeUseLineScrollingForVerticalChange 附加属性。 演示应用程序 V2 包含 AutoDragScrollingProvider 类的更新。这两个属性的类型均为 double,默认值为 double.NaN

当为任一属性提供有效值时,相应方向的滚动将像以前一样使用行滚动,但现在,就像默认的“平滑滚动”情况一样,它取决于计算出的增量,从而允许用户通过“鼠标压力”控制滚动速度。鼠标越靠近边缘,滚动越快。

对于 StackPanelVirtualizingStackPanel,这些属性会自动处理。对于那些与默认平滑滚动行为不佳的 IScrollInfo 自定义实现,可以设置这些属性来指示行滚动,同时又能提供平滑滚动的效果。如前所述,控制滚动速度的能力能提供更愉悦的用户体验。如果您尝试 演示应用程序 V1,您会发现行滚动的情况下,滚动速度太快而无法使用。与此对比 演示应用程序 V2,其中滚动速度是可以控制的。

如何实现

请回想一下之前的讨论,计算出的增量几乎总是分数。某些 IScrollInfo 实现不支持“平滑滚动”,因为它们不支持分数滚动偏移量更改。因此,解决方案是累积计算出的增量,直到它超过行滚动影响的“量”(或者换句话说,“行大小”),然后才执行行滚动。这样,即使需要行滚动,我们也可以模拟平滑滚动。但请记住,行大小本身必须是 IScrollInfo 实现能够理解的单位。例如,对于 StackPanel,它是 1.0,这是它能够支持的最小偏移量更改。

var delta = (mousePos.X - 20) / avgWidth; //translate back to original unit
var lineSize = GetLineSizeForHorizontalChange(scrollable);
if (!double.IsNaN(lineSize))
{
    _totalHorizontalDelta += delta;
    if ((0 - _totalHorizontalDelta) >= lineSize) //since delta is negative here
    {
        _scrollInfo.LineLeft();
        _totalHorizontalDelta = 0d;
    }
}
else
{
    _scrollInfo.SetHorizontalOffset(_scrollInfo.HorizontalOffset + delta);
}
© . All rights reserved.