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

WPF 中的拖放

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (42投票s)

2009年11月7日

BSD

5分钟阅读

viewsIcon

338670

downloadIcon

21918

本文介绍如何使用GongSolutions.Wpf.DragDrop库为WPF应用程序添加拖放功能。

引言

一段时间以来,我一直在抱怨WPF中缺乏像样的拖放支持。虽然WPF在桌面GUI编程方面比WinForms时代有了长足的进步,但拖放功能自我在Visual Basic 3.0开始用Windows编程以来一直没有改变。

特别是,人们必须在代码隐藏中充斥着拖放逻辑,这对于在MVVM时代任何有自尊的WPF开发人员来说都是不可接受的。

在忍受了这一切一段时间后,我偶然发现了一篇由Bea Stollnitz撰写的关于使用附加属性在XAML中为控件添加拖放逻辑的文章。然而,Bea的解决方案并没有达到我所需要的要求。我需要的是

  • MVVM支持:任何比基本拖放操作更复杂的功能都需要逻辑,而代码隐藏不适合存放这些逻辑。关于什么可以被拖动以及什么可以被放到哪里,这些决定应该委托给ViewModels。
  • 插入/拖入:有时当你将一个项目从一个地方拖到另一个地方时,你是在重新排序列表中的项目。有时,你是在将一个项目拖放到另一个项目上,就像在文件管理器中将文件拖到一个文件夹中一样。
  • TreeView支持
  • 多选
  • 自动滚动

环顾四周,我没有找到任何能满足我需求的东西,所以我决定自己写一个。你可以在Google Code项目找到最新的源代码和示例。它是在BSD许可证下发布的,所以你可以随心所欲地使用它。

Using the Code

为了演示拖放框架的使用,让我们使用一个简单的学校示例。在这个例子中,我们有三个ViewModels

  • MainViewModel:这仅包含学校的集合。
  • SchoolViewModel:一所学校有一个名字和学生集合。
  • PupilViewModel:一个学生有一个名字。

在我们的UI中,我们将显示两个列表框。第一个用于显示学校列表,第二个用于显示属于该学校的学生列表。我们的XAML如下所示

<Window.DataContext>
    <local:MainViewModel/>
</Window.DataContext>

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    
    <ListBox Grid.Column="0" 
       ItemsSource="{Binding Schools}" 
       DisplayMemberPath="Name" 
       IsSynchronizedWithCurrentItem="True"/>
    <ListBox Grid.Column="1" 
       ItemsSource="{Binding Schools.CurrentItem.Pupils}" 
       DisplayMemberPath="FullName"/>
</Grid>

以及由此产生的窗口如下所示

gong-wpf-dragdrop1.png

首先,我们希望能够重新排序第二个ListBox中的学生。这可以通过在ListBox上设置IsDragSourceIsDropTarget附加属性来完成

<ListBox Grid.Column="1" 
  ItemsSource="{Binding Schools.CurrentItem.Pupils}" 
  DisplayMemberPath="FullName"
  dd:DragDrop.IsDragSource="True" 
  dd:DragDrop.IsDropTarget="True"/>

你现在将能够拖放项目在ListBox内重新排序它们。现在,你可能会认为,如果你在第一个ListBox上设置了IsDropTarget属性,你就能将一个学生拖到一个学校里。然而,试试这样做,你不会被允许的。这是因为第一个ListBox绑定到SchoolViewModel对象的集合,而第二个ListBox绑定到PupilViewModels的集合。这里应该发生什么?框架不知道,所以我们必须告诉它。

进入DropHandlers

为了给ListBox添加一个拖放处理器,我们在第一个ListBox上设置DropHandler附加属性,并将其绑定到一个ViewModel。在这种情况下,我们将只把它绑定到MainViewModel,如下所示

<ListBox Grid.Column="0" 
  ItemsSource="{Binding Schools}" DisplayMemberPath="Name" 
  IsSynchronizedWithCurrentItem="True"
  dd:DragDrop.IsDropTarget="True" dd:DragDrop.DropHandler="{Binding}"/>

现在我们需要在MainViewModel中添加处理代码。要做到这一点,我们实现IDropTarget接口。这个接口定义了两个方法:DragOverDropDragOver在拖动过程中被调用,用于确定当前的拖放目标是否有效

void IDropTarget.DragOver(DropInfo dropInfo)
{
    if (dropInfo.Data is PupilViewModel && 
        dropInfo.TargetItem is SchoolViewModel)
    {
        dropInfo.DropTargetAdorner = DropTargetAdorners.Highlight;
        dropInfo.Effects = DragDropEffects.Move;
    }
}

在这里,我们正在检查被拖动的数据是否是PupilViewModel,并且当前是拖动目标的项是否是SchoolViewModel

dropInfo.Data属性包含被拖动的数据。如果拖动的源控件是一个绑定的控件(事实确实如此),它将是被拖动项绑定的对象。dropInfo.TargetItem属性包含鼠标光标当前下的项所绑定的对象。

如果数据类型正确,我们将继续设置拖放装饰器。因为将学生拖放到学校会导致学生被添加到学校,所以我们选择Highlight装饰器,它会高亮显示目标项。另一个可用的拖放目标装饰器是Insert装饰器,它会在列表中的插入点绘制一个插入符号,并且当你重新排序第二个ListBox中的学生时会看到它。

最后,我们将拖放效果设置为DragDropEffects.Move。这告诉框架被拖动的数据可以在这里放下,并显示一个移动鼠标指针。如果我们不设置这个属性,就会使用默认值DragDropEffects.None,这表示在该位置不允许放下。

接下来是Drop方法

void IDropTarget.Drop(DropInfo dropInfo)
{
    SchoolViewModel school = (SchoolViewModel)dropInfo.TargetItem;
    PupilViewModel pupil = (PupilViewModel)dropInfo.Data;
    school.Pupils.Add(pupil);
}

在这里,我们只是从DropInfo参数中获取涉及拖放的SchoolViewModelPupilViewModel,并将学生添加到学校。我们可以确定数据将是预期的类型,因为DragOver方法已经过滤掉了其他所有情况。

哦!但是等等,当我们把学生拖放到新学校时,我们并没有把他从原来的学校移走,也就是说,我们是在复制而不是移动。为了从之前的学校移走学生,我们需要能够获取拖动开始的集合。这由DropInfo.DragInfo对象提供。这个对象保存了与拖动源相关的信息。特别是,我们对SourceCollection属性感兴趣

((IList)dropInfo.DragInfo.SourceCollection).Remove(pupil);

本文到此结束

这完成了这篇入门文章。你可以在Google Code项目中找到更多示例。一些值得关注的功能

  • 拖放处理器
  • 拖放装饰器
  • 多选

如果这篇文章让你感兴趣,请加入我们!请发布到Google Groups,或为项目贡献代码,希望我们能做出一些能让拖放摆脱20世纪束缚的东西。

© . All rights reserved.