在 WPF ListView 中拖放项目。






4.91/5 (89投票s)
讨论 WPF ListView 中的自动拖放功能。
引言
本文介绍了一个名为 ListViewDragDropManager
的类,它会自动处理 WPF ListView
中的拖放操作。它允许用户在 ListView
中拖放项目,或者将项目从一个 ListView
拖放到另一个 ListView
。该类足够智能,可以确定用户希望将项目放置在 ListView
的哪个位置,并自动在该位置插入项目。该类还公开了几个属性和一个事件,使开发人员能够自定义拖放操作的行为方式。
背景
拖放功能在现代用户界面中无处不在。大多数用户都期望在他们频繁使用的任何应用程序中都能找到拖放带来的简洁性和交互性。它使生活对用户来说更容易。
WPF 在其框架中内置了对拖放的支持,但仍然需要大量繁琐的工作才能使开箱即用的功能平滑运行。我决定创建一个类来处理这些繁琐的工作,这样我编写的任何需要 ListView
中拖放功能的应用程序都可以“免费”获得它。
我曾以为实现这样一个类会是轻而易举的事情。我错了。基本功能很容易封装到一个辅助类中,但核心功能到位后,出现了许多棘手的问题和“万一”的情况。我决定不应该再让其他人经历这种挫败感的泥沼,所以我把完成的产品发布在这里 CodeProject。现在,您只需一行代码即可在 WPF ListView
中实现拖放!
它是什么
WPF 非常灵活,这使得创建一个提供简单服务的通用辅助类变得困难。ListViewDragDropManager
并没有解决 ListView
中拖放的所有可能问题,但它应该足以应对大多数场景。让我们花点时间回顾一下它提供了什么。
- 在同一个
ListView
内的项目实现自动拖放操作。 - 支持两个
ListView
之间的拖放操作。 - 可选的拖动修饰符 - 被拖动
ListViewItem
的视觉表示,它会跟随鼠标光标(参见上面的截图)。 - 能够修改拖动修饰符的透明度。
- 一种提示
ListViewItem
正在被拖动的方式,可用于样式设置。 - 一种提示
ListViewItem
鼠标光标在其上方的方式,可用于样式设置。 - 一个在项目被拖放到目标位置时触发的事件,允许您运行自定义逻辑来重新定位被拖放的项目。
- 与
ListViewItem
中的控件进行鼠标交互,例如能够正常工作的CheckBox
(这个“不容易”弄好!)。
它不是什么
如前所述,ListViewDragDropManager
并没有涵盖所有情况。以下是我(至少目前为止)未包含的一些功能。
- 不支持同时拖放多个
ListViewItem
。 - 如果将
ListView
的View
设置为自定义视图实现,则不保证它能正常工作。我只在使用标准的GridView
作为ListView
的View
时测试过代码。 - 尝试放置不同类型的项目到
ListView
中时,不支持类型转换。 ListView
的ItemsSource
必须引用一个ObservableCollection<ItemType>
,其中ItemType
对应于ListViewDragManager<ItemType>
的类型参数。- 拖动修饰符不会离开它被创建的
ListView
。 - 由于该类具有泛型类型参数,因此无法在 XAML 中轻松使用它。
- 它不能在未获得完全信任权限的应用程序中使用,因为它会
PInvoke
调用User32
来获取鼠标光标位置。这意味着该类可能不适用于 Web 浏览器应用程序(XBAP),除非明确授予了完全信任权限。 - 对
ListView
的ItemsSource
集合中的 null 值支持有限。ObservableCollection
在尝试移动或删除 null 值时会抛出异常(我不知道为什么,但它确实会)。我怀疑这对很多人来说都不是问题,因为 null 项目会显示为空白,并且在ListView
中不太有用。
使用代码
正如之前承诺的,ListViewDragDropManager
允许您只需一行代码即可在 ListView
中实现功能齐全的拖放。这是那一行代码:
new ListViewDragDropManager<Foo>( this.listView );
关于这一行代码有几点需要指出。您可能想将其放在 Window
的 Loaded
事件处理方法中,这样当 Window
打开时,ListView
就会具有拖放支持。类型参数 'Foo
' 表示 ListView
显示的对象类型。ListView
的 ItemsSource
属性必须引用一个 ObservableCollection<Foo>
。或者,ItemsSource
属性可以绑定到 DataContext
属性,后者可以引用一个 ObservableCollection<Foo>
。
在进一步讨论如何使用 ListViewDragDropManager
之前,让我们先看一下它的公共属性。
DragAdornerOpacity
- 获取/设置拖动修饰符的不透明度。如果ShowDragAdorner
为false
,则此属性无效。默认值为0.7
。IsDragInProgress
- 如果当前正在进行拖动操作,则返回true
。ListView
- 获取/设置由其拖动进行管理的ListView
。此属性可以设置为null
,以阻止拖动管理发生。如果ListView
的AllowDrop
属性为false
,则将其设置为true
。ShowDragAdorner
- 获取/设置在拖动操作期间,被拖动的ListViewItem
的视觉表示是否跟随鼠标光标。默认值为true
。
还有一个暴露的事件:
ProcessDrop
- 在发生拖放时触发。默认情况下,被拖放的项目将被移动到目标索引。如果重新定位被拖放的项目需要自定义行为,请处理此事件。请注意,如果处理此事件,则不会发生默认的项目拖放逻辑。
样式设置项目
当 ListViewItem
被拖动时,您可能希望对其进行不同的样式设置。您还可能希望为鼠标光标下方的 ListViewItem
设置样式 - 不一定是正在拖动的项目,而是光标当前所在的任何项目。要做到这一点,您可以使用静态 ListViewItemDragState
类公开的附加属性。下面是一个 Style
的简短示例,该 Style
由 ListView
的 ItemContainerStyle
使用,它利用上述附加属性来适当地设置 ListViewItem
的样式。
<Style x:Key="ItemContStyle" TargetType="ListViewItem">
<!-- These triggers react to changes in the attached properties set
during a managed drag-drop operation. -->
<Style.Triggers>
<Trigger Property="jas:ListViewItemDragState.IsBeingDragged" Value="True">
<Setter Property="FontWeight" Value="DemiBold" />
</Trigger>
<Trigger Property="jas:ListViewItemDragState.IsUnderDragCursor" Value="True">
<Setter Property="Background" Value="Blue" />
</Trigger>
</Style.Triggers>
</Style>
该 Style
将使 ListViewItem
在被拖动时具有半粗体文本,或者在鼠标光标在其上方时具有蓝色背景。
自定义拖放逻辑
默认情况下,当项目被拖放到由 ListViewDragDropManager
管理的 ListView
中时,该项目将从其当前索引移动到与鼠标光标位置对应的索引。某些应用程序可能需要不同的项目重新定位逻辑。为了适应这些情况,ListViewDragDropManager
在发生拖放时会引发 ProcessDrop
事件。如果您处理该事件,则必须将拖放的项目移动到其新位置,否则默认的重新定位逻辑将不会执行。
自定义拖放逻辑的一个例子可能是“交换”被拖放的项目与占用目标索引的项目。例如,假设 ListView
的三个项目按此顺序排列:'A'、'B'、'C'。再假设用户拖动项目 'A' 并将其拖放到项目 'C' 上。默认的拖放逻辑将导致项目按此顺序排列:'B'、'C'、'A'。然而,有了“交换”逻辑,我们期望拖放完成后项目按此顺序排列:'C'、'B'、'A'。
下面是“交换”逻辑的实现。
void OnProcessDrop( object sender, ProcessDropEventArgs<Foo> e )
{
// This shows how to customize the behavior of a drop.
// Here we perform a swap, instead of just moving the dropped item.
int higherIdx = Math.Max( e.OldIndex, e.NewIndex );
int lowerIdx = Math.Min( e.OldIndex, e.NewIndex );
e.ItemsSource.Move( lowerIdx, higherIdx );
e.ItemsSource.Move( higherIdx - 1, lowerIdx );
e.Effects = DragDropEffects.Move;
}
本文顶部提供的源代码下载中包含一个演示应用程序,它展示了如何使用上面看到的所有功能。它还演示了如何在两个 ListView
之间实现拖放。
技巧和窍门
我将不费力气去展示代码是如何工作的,因为它相对复杂,需要数十页的代码和解释才能传达大意。相反,我们将检查一些我花了很长时间才弄清楚并弄好的代码。
光标位置
我面临的最大问题是在拖放操作期间获取鼠标光标坐标。WPF 获取光标位置的机制在拖放过程中会失效。为了规避这个问题,我调用了非托管代码来获取光标位置。Dan Crevier,微软 WPF 团队的一员,似乎也遇到了同样的问题,并在此处发布了一个解决方法。一旦我开始使用他的 MouseUtilities
类,我所有的光标烦恼就消失了。
ListViewItem 索引
另一个棘手的问题是确定鼠标光标下的 ListViewItem
的索引。ListViewDragDropManager
需要此信息才能知道用户正在尝试拖动哪个项目,将拖放的项目移动到哪里,以及让 ListViewItemDragState
类知道何时指示光标位于 ListViewItem
上方。我的实现如下所示。
int IndexUnderDragCursor
{
get
{
int index = -1;
for( int i = 0; i < this.listView.Items.Count; ++i )
{
ListViewItem item = this.GetListViewItem( i );
if( this.IsMouseOver( item ) )
{
index = i;
break;
}
}
return index;
}
}
ListViewItem GetListViewItem( int index )
{
if( this.listView.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated )
return null;
return this.listView.ItemContainerGenerator.ContainerFromIndex( index ) as ListViewItem;
}
bool IsMouseOver( Visual target )
{
// We need to use MouseUtilities to figure out the cursor
// coordinates because, during a drag-drop operation, the WPF
// mechanisms for getting the coordinates behave strangely.
Rect bounds = VisualTreeHelper.GetDescendantBounds( target );
Point mousePos = MouseUtilities.GetMousePosition( target );
return bounds.Contains( mousePos );
}
拖动距离阈值
我将在这里展示的最后一段棘手的代码确定何时开始拖动操作。在 Windows 中,有一个“拖动距离阈值”的概念,它指定在按下左鼠标按钮后,鼠标必须移动多远才能开始拖放操作。
ListViewDragDropManager
尝试遵循此阈值,但在某些情况下必须减小垂直阈值。这是因为如果按下左鼠标按钮时,光标非常靠近 ListViewItem
的顶部或底部边缘,当光标移动到它上面时,ListView
会选择相邻的 ListViewItem
。为了防止这种情况发生,如果光标非常靠近要拖动的 ListViewItem
的顶部或底部边缘,它将减小垂直阈值。这是它的工作原理。
bool HasCursorLeftDragThreshold
{
get
{
if( this.indexToSelect < 0 )
return false;
ListViewItem item = this.GetListViewItem( this.indexToSelect );
Rect bounds = VisualTreeHelper.GetDescendantBounds( item );
Point ptInItem = this.listView.TranslatePoint( this.ptMouseDown, item );
// In case the cursor is at the very top or bottom of the ListViewItem
// we want to make the vertical threshold very small so that dragging
// over an adjacent item does not select it.
double topOffset = Math.Abs( ptInItem.Y );
double btmOffset = Math.Abs( bounds.Height - ptInItem.Y );
double vertOffset = Math.Min( topOffset, btmOffset );
double width = SystemParameters.MinimumHorizontalDragDistance * 2;
double height = Math.Min(
SystemParameters.MinimumVerticalDragDistance, vertOffset ) * 2;
Size szThreshold = new Size( width, height );
Rect rect = new Rect( this.ptMouseDown, szThreshold );
rect.Offset( szThreshold.Width / -2, szThreshold.Height / -2 );
Point ptInListView = MouseUtilities.GetMousePosition( this.listView );
return !rect.Contains( ptInListView );
}
}
修订历史
- 2007 年 2 月 1 日 - 修复了两个错误。其中一个由 micblues 指出,涉及拖动
ListView
的滚动条时会不恰当地开始拖放操作。另一个错误与ListView
滚动到右侧时拖动修饰符位置不正确有关。已更新源代码。 - 2007 年 2 月 25 日 - 修复了
MouseUtilities
类中的一个 bug,该 bug 仅在屏幕分辨率高于标准 96 DPI 的机器上发生。此 bug 由 William J. Roberts (又名 Billr17) 通过本文的消息板报告并解决。已更新源代码。 - 2007 年 4 月 13 日 - 修复了拖动修饰符位置的一个小问题。修饰符过去会“卡入”位置,使得拖动开始时鼠标光标的顶部与修饰符的顶部相交。我将其修复为使鼠标光标的顶部在修饰符内的位置保持不变(相对于鼠标光标在被拖动的
ListViewItem
内的位置)。已更新源代码。