拖动操作中的自动滚动






4.78/5 (9投票s)
实现拖动操作中自动滚动的方
介绍
在拖动操作中,当鼠标移动到边缘时,底层视图能够自动滚动以显示更多内容,这是非常理想的。本文将介绍一种在 WPF 控件内部的拖动操作中实现此类自动滚动的方法。
所提出的解决方案可以轻松地应用到任何 WPF 控件上(例如 Grid
、StackPanel
、WrapPanel
,甚至是 ItemsControl
),无论它们是放置在 ScrollViewer
内部,还是其模板中包含 ScrollViewer
(例如 TreeView
、ListBox
等)。然而,本解决方案并不涉及如何启用和处理拖放操作。
WPF 以其高度的灵活性而闻名。在大多数情况下,这正是 WPF 最强大的优势,但当你试图编写通用的实用代码时,你会很快发现,正是这种优势使得覆盖所有可能的情况变得相当困难。本文并非声称能在绝对所有场景下工作,但我仍然努力实现了这一目标,同时力求解决方案尽可能通用。我确信它将在绝大多数情况下无缝工作。
其他特性
可复用的
附加属性是 WPF 最强大的功能之一。
除了为 DependencyObject
关联额外的状态之外,它们还可以用来在保持 XAML 友好的同时添加新的行为。
我在解决方案中采用了这种方法,以便将其作为库进行重用和分发。
随附的演示应用程序包含一个名为 AutoDragScrollingProvider
的类,并提供了一个名为 EnableAutomaticScrolling
的附加属性,可以轻松地在任何为其附加的 UIElement
上启用自动滚动。
平滑滚动
您可能知道 WPF 支持两种滚动模式:- 基于像素的滚动
- 以及“逻辑”滚动,即滚动是按逻辑进行的,在大多数情况下意味着逐项滚动。例如,
ListBox
默认使用逐项滚动。
基于像素的滚动非常平滑,符合我们这些来自 WinForms 世界的人的期望。而逻辑滚动,尤其是逐项滚动时,对最终用户来说显得有些卡顿,因为整个项目会移出,新项目会移入。
要启用逻辑滚动,必须将 ScrollViewer.CanContentScroll
属性设置为 true,并且必须满足以下条件之一:
ScrollViewer.Content
实现IScrollInfo
接口- 或者
ScrollViewer
是模板的一部分,并且在其子视觉树中包含ItemsPresenter
,而ItemsPresenter
的子元素(即ItemsControl.ItemsPanel
模板中包含的面板)实现了IScrollInfo
接口。
VirtualizingStackPanel
和 StackPanel
都实现了 IScrollInfo
,如果满足上述场景,ScrollViewer
将按照这些面板的指示逻辑滚动。在拖动时,通常希望通过鼠标的“压力”来控制滚动速度,即鼠标越靠近边缘(给人一种“用力按压”的感觉),滚动速度越快。在本文(和随附演示)中,我使用“平滑滚动”一词来指代这种情况,而不是上面描述的基于像素的滚动。在这种情况下,“平滑滚动”不影响逐项滚动能力,只是滚动速度更能被用户控制,从而提供更愉悦的用户体验。
AutoDragScrollingProvider
确保基于像素的滚动始终是平滑的(如上所述),而逻辑滚动是否平滑取决于 IScrollInfo
的具体实现。要支持平滑滚动,IScrollInfo
实现必须支持滚动偏移量的分数更改。
例如,StackPanel
在与其方向相反的方向支持平滑滚动,而在其方向上则不支持。VirtualizingStackPanel
的情况比这更复杂一些。当它不被托管在 ItemsControl
中时(如 ItemsControl.ItemsPanel
所规定的),它的行为类似于 StackPanel
;否则,它支持平滑滚动,无论其方向如何。
但请记住,如果 CanContentScroll
属性为 false 或不满足任何必要条件,ScrollViewer
将默认使用 ScrollContentPresenter
(它本身实现了 IScrollInfo
)来提供基于像素的滚动。
另一个需要记住的点是,任何 IScrollInfo
实现都可以通过使用其行滚动方法来执行非平滑滚动。
AutoDragScrollingProvider
尝试尽可能利用“平滑滚动”,因为它能提供更愉悦的用户体验。它会自动处理因 StackPanel
和 VirtualizingStackPanel
在平滑滚动支持方面出现的各种情况,并提供一组附加属性来强制对那些不支持平滑滚动的 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.UseLineScrollingForHorizontalChange
或 AutoDragScrollingProvider.UseLineScrollingForVerticalChange
附加属性设置为 true,以强制在 respective 方向上使用“非平滑”行滚动。
看点
以通用方式处理 IScrollInfo
可能有些棘手。IScrollInfo
是一个接口,因此很多事情取决于其具体的实现。它的一些实现以像素为单位显示滚动属性(偏移量、视口尺寸等),一些以逻辑单位显示,还有一些在一个滚动方向上使用逻辑单位,而在另一个方向上使用像素。
AutoDragScrollingProvider
能够通过计算逻辑滚动情况下的平均每项像素宽度(或高度)(对于基于像素的滚动,平均值始终为 1,因此不影响后续计算),然后使用像素单位来确定所需的滚动量,从而以一致的方式处理此问题。
工作原理
- 当附加属性设置为 true 时,
AutoDragScrollingProvider
会订阅附加到的UIElement
上的PreviewDragEnter
和PreviewDragOver
事件。 - 在这些事件的处理程序中,它会查找当前生效的相关
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
用 LineSizeForHorizontalChange
和 LineSizeForVerticalChange
附加属性替换 UseLineScrollingForHorizontalChange
和 UseLineScrollingForVerticalChange
附加属性。 演示应用程序 V2 包含 AutoDragScrollingProvider
类的更新。这两个属性的类型均为 double,默认值为 double.NaN
。
当为任一属性提供有效值时,相应方向的滚动将像以前一样使用行滚动,但现在,就像默认的“平滑滚动”情况一样,它取决于计算出的增量,从而允许用户通过“鼠标压力”控制滚动速度。鼠标越靠近边缘,滚动越快。
对于 StackPanel
和 VirtualizingStackPanel
,这些属性会自动处理。对于那些与默认平滑滚动行为不佳的 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);
}