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

可重新排序的 ListView

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.64/5 (11投票s)

2009年8月30日

CPOL

9分钟阅读

viewsIcon

65139

downloadIcon

1613

继承的 ListView,允许用户通过鼠标对多个项目进行排序,并在必要时自动滚动

Demo screenshot

引言

这项工作只是扩展了我在 CodeProject 上发现的另一项工作,主要受到文章 “手动重新排序 ListView 中的项目” 的启发。这个项目的新颖之处在于它紧凑(所有需要的功能都集成到继承的控件中),并且支持拖动多个项目和自动滚动,这是我根据 Foobar 2000 多媒体播放器的播放列表进行设计的。在这篇文章中,我不会详细描述所有内容,但我会描述所有我打算实现的功能扩展。

拖放结构

正如您可能已经发现的那样,一些编码人员,包括 MSDN 的作者,更喜欢使用 ListView 内置的拖放方法来实现“通过鼠标重新排序”功能。我发现这相当受限,并且没有任何优势。似乎我们还可以问,重新排序与剪贴板有什么关系。我想答案很清楚,直到您需要外部数据进入 ListView。所以我使用了一种与上述文章中非常相似的方法。我唯一改变的是 MouseDown 事件不用于启动拖动——初始化和进度都在 MouseMove 中完成。这也是计算所有源自鼠标位置的值的地方。在 MouseMove 中调用 Invalidate 时,使用重写 WndProc 来绘制插入线。最后,此事件处理程序会调用我们始终拖动的自动滚动初始化器。当然,MouseUp 事件用于完成重新排序或最终取消它。

滚动概念

Three images on scrolling principle

IsDraging 设置为 True,并且 MouseMove 发生在 ListView 顶部或底部的指定矩形中时,滚动开始(**图 1**)。我以 ItemCount 为单位定义了这个区域,滚动的速度是根据鼠标相对于这些项目序列的相对位置计算的。这意味着您可以将属性 MaxScrollAreaSize 设置为一些项目数量,并且只要您的列表足够高以包含此数量的两倍(**否则请参见图 2**),您可以确保当您将拖动光标放置在可见区域顶部或底部超出此数量的项目时,控件开始滚动。滚动本身由一个例程和一个计时器管理,该例程根据鼠标位置计算其参数。

Private Sub StartScroll()
  Dim hp# = Me.GetUserHurryPercentage
  Me.ScrollPerTick = cspt.Invoke(hp, Me.GetMaxItemVisibleCount)
  If Me.ScrollPerTick <> 0 Then
    Me.IsScrolling = True
    Me.ScrollTimer.Interval = csti.Invoke(hp)
    Me.ScrollTimer.Start()
  Else
    Me.ScrollTimer.Stop()
    Me.IsScrolling = False
  End If
End Sub

请注意,滚动参数有两个:每次滚动的项目数和滚动间隔。这非常重要,因为如果这些选项中的一个固定不变,滚动就不会非常友好(也不适合重新排序过程)。这两个参数都是根据 *HurryPercentage* 计算的,这只是光标在顶部或底部项目数量上方的百分比位置(**图 3**)。例如,如果我们将此滚动区域设置为每侧三个项目,并将鼠标放在其中第一个项目上,则 *HurryPercentage* 应约为 33%(= 0.33--)。GetHurryPercentage 函数返回零,如果滚动应该保持关闭(当我们已经在开始或结束位置,或者当我们实际上不在滚动区域时),对于向上滚动返回 <-1,0),对于向下滚动返回 (0,1>。另请注意,我使用委托函数将此值转换为 *ScrollTimer* 间隔和 *ScrollPerTick* <项目数> 值。除了为控件用户提供在需要时自定义滚动行为的机会之外,没有其他原因。

Public Delegate Function ScrollPerTickCalculator%_
	(ByVal UserHurryPercentage#, ByVal ItemsPerPage%)
Public Delegate Function ScrollTimerIntervalCalculator%(ByVal UserHurryPercentage#)
Private cspt As ScrollPerTickCalculator = _
	Function(a#, b%) Comparer(Of Double).Default.Compare(a, 0)
Private csti As ScrollTimerIntervalCalculator = _
	Function(a#) CInt(220 - 200 * Math.Abs(a))

这些是默认函数,在我看来,它们产生的滚动效果与 Foobar 中的非常相似。当您深入研究上面的 lambda 表达式时,您会看到计算每刻度项目数的函数只返回 {-1, 0, 1},这只是每个比较器的任务。这意味着在默认设置下,滚动的视觉组件始终是平滑的。所以只有第二个组件才能调节和调节滚动速度——您看,ScrollTimerInterval 始终在 220 毫秒到 20 毫秒之间。这意味着对于超过 500 个项目,从头到尾滚动会变得有点无聊。但尽管如此,这不是一个非常显著的延迟。如果您需要更多,您的函数可以降低最后 30% 急速滚动的 TimerInterval,并返回 ItemsPerPage 或其派生值作为 ScrollPerTick... 这将实现平滑滚动和快速滚动的选项。

外部拖放支持

第二个版本已经实现了这个选项。这样的实现为行为带来了一些新元素,当然也需要一些额外的属性。您可以研究一下,在 DragEnter 事件中,管理滚动和插入线绘制的事件处理程序不再响应。这很容易解决。我只是为 DragOverDragLeave 等事件添加了新的处理程序,这些处理程序通常只调用 MouseMove 或 MouseLeave。值得注意的一点是,当您需要从该控件拖动某些项目时,在按下指定鼠标按钮时,您会从 MouseLeave 调用 DoDragDrop,这样即使您将这些数据返回到控件中,您也无法以某种方式从拖放模式切换到 MouseMove 处理的重新排序例程。这通过内部 DragDrop 事件处理程序中的数据来源识别来解决。顺便说一句,通过一些即兴创作(您对 DragEventArgs 的了解不如对 MouseEventArgs 了解得多),可以基于标准鼠标事件和拖动事件提供所有功能。

请注意,每次外部拖放都必须在 DragEnter 事件中通过将 Effect 设置为非 None 值来确认。所有外部拖放都必须以 ListViewItem 数组或 Collection 的形式出现才能自动管理。如果无法实现,将引发 CantConvertDrop 事件,用户可以为控件转换它们,或者不处理该事件以取消拖放操作。如果数据无法转换或数据集合为空,则会引发 DropCancelled 事件。请注意 IsDragingInside 和 IsDragingOutside 属性,它们可以帮助您管理 DragEnter 或 MouseLeave 事件。

其他 ListView“视图”模式的行为扩展

第二个版本修改了内部事件处理的概念。如果您遵循事件处理程序的命名约定(在扩展基类时),该约定在顶层 XML 注释中描述,您只需将新行为添加到 Enum `BehaviorExtensionType`,并且所有用于切换事件处理程序以进行另一个扩展的工作都将为您完全自动化(请参阅代码区域 `Internal handler managing`)。如果您想通过继承来扩展控件,则必须满足于受保护属性 `AllowedBehaviorExtension`,通过该属性您可以在您的基类的行为扩展之间切换(或禁用它们)。

对事件进行如此深思熟虑的 Reflection 编码的原因是效率,但也方便了可扩展性。仅仅有两个影子事件,如果没有一些内置的辅助事件处理程序管理,似乎很难实现其他扩展。您可以简单地删除整个部分并保留该属性。如果您介意在 Select Case BehaviorExtension 中硬编码一堆 AddHandlerRemoveHandler 语句。对我来说,直接记住命名约定要容易得多。

重要提示

  • 如果您需要控件之间传输项目,请记住设置 AllowDrop
  • 请注意 shadows events DragEnterDragDrop。如果您处理 DragEnter,您可以通过将 Effect 设置为 None 来防止控件对跨越拖放数据的任何反应。如果您在 DragDrop 中这样做,则可以取消所有拖放的后果。
  • DragDrop 只接受 ListViewItem 数组或 Collection,因此请确保您只允许传入和传出这种类型的数据,或者您处理了转换事件 CantConvertDrop
  • 仅当其 View 属性设置为 Detail 时,控件行为才会扩展。
  • 在其他 View 模式下,BottomItemLastVisibleItem 属性可能返回无效结果(与标准 TopItem 相同)。

控件的公共属性

  • AllowReorder - 确定是否启用重新排序以及所有后续行为
  • AutoDropFocus - 确定控件在成功放置后是否自动聚焦
  • AutoDropSelect - 确定控件是否选择已放置的数据
  • DragMouseButton - 获取或设置用于启动重新排序或拖动的鼠标按钮
  • MaxScrollAreaSize - 获取或设置列表两端保持滚动的项目数,当可见项目数大于等于此数字时使用此数字。如果可见项目不足,则使用较小的数字。基本上,要求列表有两个项目高才能提供重新排序和自动滚动是毫无意义的。此类中的所有例程都会检查结果数字是否大于零,否则不执行任何操作。结果数字在每次 SizeChangedFontChanged 时计算。此属性对 DesignerDesignerSerialize 友好。
  • CalcScrollPerTick - 获取或设置一个函数,该函数根据用户请求的速度百分比计算滚动的平滑度,您也可以使用其第二个参数,该参数告诉您 listview 当前页面的长度。此属性不支持 PropertyEditor,也不会保存到设计器代码中。
  • CalcScrollTimerInterval - 获取或设置一个函数,该函数计算 ScrollTimer 的速度。此属性不支持 PropertyEditor,也不会保存到设计器代码中。
  • IsDragingInside - 确定控件当前是否处于重新排序或拖入状态
  • IsDragingOutside - 确定控件是否处于重新排序或拖入状态,但用户带着物品离开了其区域。请注意,此属性不反映当前拖放数据的来源。
  • LastVisibleItem - 获取 ListViewRO 的最后一个可见项目,此属性是 BottomItem 函数的扩展
  • SetReorderAutoScrollParameters - 这只是一个提供一次性设置所有滚动参数的方法。
  • BottomItem - 获取位于 DisplayRectangle 底部的项目
  • InsertionLineWidth - 获取或设置插入线的粗细。此属性对设计器友好,并存储在设计器代码中。
  • InsertionLineColor - 获取或设置插入线的颜色。此属性对设计器友好,并存储在设计器代码中。
  • InsertionLinePen - 获取或设置所有前两个属性。此属性对设计器不友好,也不会保存到设计器代码中。

其他重要的公开内容

  • ItemsReordered - public 事件,提供有关当前重新排序或拖放操作的一些重要信息。在外部放置时,此事件在聚焦之前发生(当 AutoDropFocus 启用时)
  • CantConvertDrop - 当允许传入的数据被拖放进来,但格式不被识别时(参见重要注意事项部分)会引发此事件。
  • CancelDrop - 当数据无法转换或为空时引发。
  • Reorder - 此例程将选定项目移动到指定索引,并在必要时引发 ItemsReordered 事件。

限制

该控件在当前状态下不支持外部拖放。 我无法想象对于尺寸极小的 ListView,最佳逻辑是什么,所以默认情况下它只会失去其 ReorderAutoscroll 功能。也许您可以通过将拖放效果设置为 Scroll+Move 来解决,欢迎任何想法。

讨论

如果您能找到不同的 lambda 表达式来计算滚动参数,我将不胜感激。然后我可以用一些 Enum 来更新此代码,它将在它们之间切换。我还好奇是否有人对在选择中有空格时移动多个项目有任何想法——正如您可能已经研究过的,我的代码在将所有选定项目移动到 newIndex 时会将它们合并成一个序列。

历史

  • 2009年8月30日:首次发布
  • 2009年12月14日:常规更新,主要基于 KPEBEDKO 的深入错误报告和功能请求。
可重新排序的 ListView - CodeProject - 代码之家
© . All rights reserved.