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

使用 NP.Visuals 库进行 WPF 拖放

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2018年9月9日

CPOL

13分钟阅读

viewsIcon

15404

使用 NP.Visuals 包进行拖放

引言

本文内容

我正在构建几个可以帮助 C# 和 WPF 开发人员的库。这些库是完全开源的,可在 GITHUB 和 NuGet 包上找到。

在本文中,我将介绍 NP.Visuals 包中提供的拖放功能。

NP.Visuals 包含可以为 WPF 开发人员提供极大帮助的基本通用视觉工具、转换器、行为和控件。此库中没有任何内容暗示任何特定的业务逻辑 - 其所有类都可以用于任何 WPF 视觉项目。 NP.Visuals 依赖于另外两个库

  1. NP.Utilities - 一个包含一些基本非视觉实用程序和扩展类的库
  2. NP.Concepts - 一个包含更复杂类和行为的非视觉库

行为

本文描述的所有拖放逻辑都基于行为。因此,我提供了一个关于什么是行为的复习。

据我所知,“行为”一词由 Microsoft Blend SDK 的创建者创造。他们用这个术语称呼一些(不一定是视觉的)类,这些类可以附加到视觉控件上,通过修改它们的属性(在附加过程中)以及更重要的是——为它们的事件提供处理程序来改变它们的行为。

基于视图-视图模型的 WPF 和 XAML 实现模式 中,我提供了大量行为示例,包括视觉和非视觉的。

在某种意义上,行为提供了一种非侵入性地修改类的行为的方式,而无需修改类本身。

最近,我开始喜欢静态类形式的行为。这种行为是单例的,它们不需要创建多个行为对象就可以附加到不同的控件上。这种行为中定义的附加属性提供了一种非侵入性地扩展类的方式,这些类是附加了行为的。这种附加属性的值依赖于它所附加的对象,即使行为是单例的,因此每个对象都可以在同一个行为实例的上下文中拥有自己的附加属性值。

下面描述的 DragBehaviorDropBehavior 是这种静态行为的完美例证。

必备组件

本文假设读者对 WPF 有一些基本了解,包括附加属性和绑定,以及对行为的一些理解。

代码示例

代码位置

所有代码都在 Github 上,地址是 Drag And Drop Article Samples

NuGet 依赖和编译

上面提到的三个库,即

  1. NP.Visuals
  2. NP.Concepts
  3. NP.Utilities

将被 NuGet 到每个测试项目中。请记住,在第一次构建每个项目时,请确保您的互联网连接已打开——在这种情况下,NuGet 会自动从 NuGet.org 下载 DLL。

仅拖动示例

引言

在本节中,我们将展示在特定 WPF 面板内进行拖动操作的示例。

简单拖动、带边界的拖动示例

此示例位于 NP.Tests.SimpleDragTest VS2017 解决方案下。

此项目说明了在某个拖动容器内拖动元素的方法。

尝试运行此项目。您将看到一个粉色窗口,顶部左侧有一个蓝色矩形,中心有一个绿色圆圈。

矩形和圆都可以通过鼠标拖动。

现在让我们看一下代码。这个示例中没有任何非平凡的 C# 代码——唯一需要查看的文件是 MainWindow.xaml

<Grid x:Name="DragContainer" 
      Background="Pink">
    <Rectangle Width="20"
               Height="20"
               HorizontalAlignment="Left"
               VerticalAlignment="Top" 
               Fill="Blue"
               visuals:DragBehavior.DragContainerElement=
               "{Binding RelativeSource={RelativeSource AncestorType=Panel}}"
               visuals:DragBehavior.DraggedElement=
                      "{Binding RelativeSource={RelativeSource Mode=Self}}"
               visuals:ShiftBehavior.Position="{Binding Path=
               (visuals:DragBehavior.Shift), RelativeSource={RelativeSource Mode=Self}}"/>

    <Ellipse Width="20"
               Height="20"
               HorizontalAlignment="Center"
               VerticalAlignment="Center"
               Fill="Green"
               visuals:DragBehavior.DragContainerElement="
               {Binding RelativeSource={RelativeSource AncestorType=Panel}}"
               visuals:DragBehavior.DraggedElement=
                          "{Binding RelativeSource={RelativeSource Mode=Self}}"
               visuals:ShiftBehavior.Position="{Binding Path=
               (visuals:DragBehavior.Shift), RelativeSource={RelativeSource Mode=Self}}" />
</Grid>  

两个可拖动项的代码几乎相同。事实上,我希望显示两个项而不是一个,是为了证明行为可以同时作用于多个项,尽管它们是静态的。

现在,我将逐一解释重要行。

  visuals:DragBehavior.DragContainerElement=
    "{Binding RelativeSource={RelativeSource AncestorType=Panel}}"

上面的行指定了拖动行为的容器,即元素可以在其内部拖动的面板。

visuals:DragBehavior.DraggedElement="{Binding RelativeSource={RelativeSource Mode=Self}}"  

DraggedElement 指定要拖动的元素。通常,它被设置为附加了该行为的对象的 'Self'。

  visuals:ShiftBehavior.Position="{Binding Path=(visuals:DragBehavior.Shift), 
                                           RelativeSource={RelativeSource Mode=Self}}" 

ShiftBehavior.Position 指定与初始位置的偏移量。ShiftBehavior 的工作方式是创建 TranslateTransform 并将其 XY 属性绑定到传递给它的点。因此,上面的行等同于

<FrameworkElement.TranslateTransfor>
 <TranslateTransform X="{Binding Path=(visuals:DragBehavior.Shift).X, 
                                    RelativeSource={RelativeSource Mode=Self}"
                        Y="{Binding Path=(visuals:DragBehavior.Shift).Y, 
                                    RelativeSource={RelativeSource Mode=Self}">
</FrameworkElement.TranslateTransfor>

如果您尝试拖动,您会发现您可以将矩形和圆都拖出应用程序的四个边界。接下来,我们将修改 XAML 代码,使其不允许圆超出顶部和左侧边界。这是需要添加到圆的 XAML 代码中的行

  visuals:DragBehavior.StartBoundaryPoint="0, 0"

以便圆的代码现在看起来像

<Ellipse Width="20"
           Height="20"
           HorizontalAlignment="Center"
           VerticalAlignment="Center"
           Fill="Green"
           visuals:DragBehavior.DragContainerElement=
           "{Binding RelativeSource={RelativeSource AncestorType=Panel}}"
           visuals:DragBehavior.DraggedElement="
           {Binding RelativeSource={RelativeSource Mode=Self}}"
           visuals:ShiftBehavior.Position="{Binding Path=
           (visuals:DragBehavior.Shift), RelativeSource={RelativeSource Mode=Self}}"  
           visuals:DragBehavior.StartBoundaryPoint="0, 0"/>

验证您无法将圆移出左侧和顶部边界(但仍可将其移出底部和右侧边界)。同时验证为圆设置边界并未影响矩形的边界——您仍然可以将其移出任意一个边界。

类似地,为了防止圆移出右侧和底部边界,可以将 visuals:DragBehavior.EndBoundaryPoint 设置为包含容器面板的 ActionWidthActualHeight。最好的方法是使用 MultiValue 转换器将这些值转换为一个点。

使用 DragBehavior 进行缩放

此示例包含在 NP.Tests.DragResizeTest 项目下。

运行项目时您会看到

一个粉色面板内的灰色矩形(或正方形)。矩形的右下角是红色的。右下角的红色方块是对缩放手柄的模仿。如果您拖动该手柄,灰色矩形的大小将发生变化,但您无法将其缩小到小于 20x20 或大于 100x100 的方块。

这是实现这一目标的 XAML 代码

<Grid x:Name="DragContainer"
      Background="Pink">
    <Grid x:Name="GlyphWithResizing"
          HorizontalAlignment="Left"
          VerticalAlignment="Top"
          Background="Gray"
          visuals:SizeSettingBehavior.InitialSize="50,50">
        <visuals:SizeSettingBehavior.RealSize>
            <MultiBinding Converter="{x:Static visuals:AddPointMultiConverter.Instance}">
                <Binding Path="(visuals:DragBehavior.TotalShiftWithRespectToContainer)"
                         ElementName="ResizingThumb" />
                <Binding Path="(visuals:ActualSizeBehavior.ActualSize)"
                         ElementName="ResizingThumb" />
            </MultiBinding>
        </visuals:SizeSettingBehavior.RealSize>
        <Rectangle x:Name="ResizingThumb"
                   HorizontalAlignment="Right"
                   VerticalAlignment="Bottom"
                   Width="10"
                   Height="10"
                   Fill="Red"
                   Cursor="SizeNWSE"
                   visuals:DragBehavior.StartBoundaryPoint="20,20"
                   visuals:DragBehavior.EndBoundaryPoint="100,100"
                   visuals:DragBehavior.DragContainerElement=
                   "{Binding ElementName=GlyphWithResizing}"
                   visuals:DragBehavior.DraggedElement=
                   "{Binding RelativeSource={RelativeSource Mode=Self}}" />
    </Grid>
</Grid>  

灰色矩形由一个名为“GlyphWithResizing”的 Grid 表示,背景为灰色。在其右下角,它包含一个 10x10 的红色 Rectangle,名为“ResizingThumb”。这个 ResizingThumbDragBehavior 设置与上一节中的基本相同

visuals:DragBehavior.StartBoundaryPoint="20,20"
visuals:DragBehavior.EndBoundaryPoint="100,100"
visuals:DragBehavior.DragContainerElement="{Binding ElementName=GlyphWithResizing}"
visuals:DragBehavior.DraggedElement="{Binding RelativeSource={RelativeSource Mode=Self}}"  

DragContainerElement 设置为 GlyphWithResizing 网格。拖动操作的边界位于 (20, 20) 和 (100, 100) 之间,这可以防止拖动超出容器的这些边界。

代码中有趣的部分与 GlyphWithResizing 网格的属性有关,特别是 SizeSettingBehavior

Line...

visuals:SizeSettingBehavior.InitialSize="50,50"

...将调整大小的网格的初始大小设置为 50x50

以下行确保其大小等于拇指相对于容器的偏移量加上拇指的实际大小

<visuals:SizeSettingBehavior.RealSize>
    <MultiBinding Converter="{x:Static visuals:AddPointMultiConverter.Instance}">
        <Binding Path="(visuals:DragBehavior.TotalShiftWithRespectToContainer)"
                 ElementName="ResizingThumb" />
        <Binding Path="(visuals:ActualSizeBehavior.ActualSize)"
                 ElementName="ResizingThumb" />
    </MultiBinding>
</visuals:SizeSettingBehavior.RealSize>  

SizeSettingBehavior 负责控制其附加到的元素的大小。

拖放和缩放示例

下一个示例位于 NP.Tests.DragAndResizeTest 解决方案下。它演示了前面两个示例的组合——字形既可以在其容器内进行缩放,也可以进行拖动。

尝试运行示例。您将看到与上一个示例完全相同的布局,只是现在您不仅可以缩放灰色正方形,还可以将其在粉色面板内拖动。

这是该示例的代码

<Grid x:Name="DragContainer"
      Background="Pink">
    <Grid x:Name="GlyphWithResizing"
          HorizontalAlignment="Left"
          VerticalAlignment="Top"
          Background="Gray"
          visuals:DragBehavior.DragContainerElement=
          "{Binding ElementName=DragContainer}"
          visuals:DragBehavior.DraggedElement=
          "{Binding RelativeSource={RelativeSource Mode=Self}}"
          visuals:SizeSettingBehavior.InitialSize="50,50"
          visuals:ShiftBehavior.Position=
          "{Binding Path=(visuals:DragBehavior.Shift), 
                              RelativeSource={RelativeSource Mode=Self}}">
        <visuals:SizeSettingBehavior.RealSize>
            <MultiBinding Converter=
            "{x:Static visuals:AddPointMultiConverter.Instance}">
                <Binding Path=
                "(visuals:DragBehavior.TotalShiftWithRespectToContainer)"
                         ElementName="ResizingThumb" />
                <Binding Path="(visuals:ActualSizeBehavior.ActualSize)"
                         ElementName="ResizingThumb" />
            </MultiBinding>
        </visuals:SizeSettingBehavior.RealSize>
        <Grid x:Name="MouseDownEventConsumingGrid" 
              visuals:EventConsumingBehavior.EventToConsume=
              "{x:Static FrameworkElement.MouseDownEvent}">
            <Rectangle x:Name="ResizingThumb"
                       HorizontalAlignment="Right"
                       VerticalAlignment="Bottom"
                       Width="10"
                       Height="10"
                       Fill="Red"
                       Cursor="SizeNWSE"
                       visuals:DragBehavior.StartBoundaryPoint="20,20"
                       visuals:DragBehavior.EndBoundaryPoint="100,100"
                       visuals:DragBehavior.DragContainerElement=
                       "{Binding ElementName=GlyphWithResizing}"
                       visuals:DragBehavior.DraggedElement=
                       "{Binding RelativeSource={RelativeSource Mode=Self}}" />
        </Grid>
    </Grid>
</Grid>

与上一个示例相比,这里发生了变化。首先,我们在 GlyphWithResizing 中添加了两行

visuals:DragBehavior.DragContainerElement="{Binding ElementName=DragContainer}"
visuals:DragBehavior.DraggedElement="{Binding RelativeSource={RelativeSource Mode=Self}}" 

这两行允许在粉色面板内拖动灰色矩形。

另外(非常重要的是),ResizingThumb 矩形不直接包含在 GlyphWithResizing 网格中。有一个 MouseDownEventConsumingGrid 包含它。这个 Grid 具有 EventConsumingBehavior.EventToConsume 附加属性,设置为 FrameworkElement.MouseDownEvent

visuals:EventConsumingBehavior.EventToConsume="{x:Static FrameworkElement.MouseDownEvent}"  

这一行确保 ResizingThumb 上的 MouseDown 事件不会冒泡到 GlyphWithResizing 网格,因此只会进行缩放,而不会移动整个 GlyphWithResizing 网格。

拖放示例

引言

在本节中,我将演示应用程序不同部分之间的实际拖放。

这里有几个与纯拖动示例的通用区别

  1. 纯拖动演示不需要任何 C# 代码,而拖放演示将操作视图模型,因此需要一些 C# 代码。
  2. 在纯拖动中,我们移动了对象本身,而拖放将有一个拖动提示,它将指示被拖动对象当前的位置,并且(可能)有一个放置提示,它将指示放置项将被插入的位置。

请注意,该项可能会跨越多个窗口被拖动,因此最佳的拖动提示是 Popup(因为它不绑定到单个窗口,具有允许其在屏幕上移动的属性,并且不需要仅仅为了提示而创建全新的 WPF 窗口)。

从列表拖动到面板放置测试

此示例位于 NP.Tests.DragDropFromListToPanel 项目中。它演示了将列表中的一个项(实际上不是一个列表,而是一个 ItemsControl)拖放到 Grid 中,以便项在 Grid 中的位置由放置位置决定。

尝试运行此示例。您将在左侧看到一列表浅绿色矩形,上面标有数字 1 到 5,右侧是一个粉色面板,两者之间由一个浅蓝色列隔开。

您可以从列表中拖动任何项并将其放到粉色面板中。一个带有以单词形式书写的数字的字形将出现在您放置它的位置。

请注意,拖动提示在鼠标直接悬停在可放置区域上方之前几乎是透明的。在此之后,拖动提示变得完全不透明。

让我们从视图模型开始讨论代码。有三个视图模型

public class NumberVmWithPostion : NumberVm, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
    }

    Point2D _position = new Point2D();
    public Point2D Position
    {
        get => _position;

        set
        {
            if (_position.Equals(value))
                return;

            _position = value;

            OnPropertyChanged(nameof(Position));
        }
    }

    public NumberVmWithPostion(int n, string str, Point2D position) : base(n, str)
    {
        Position = position;
    }
} 
  1. NumberVm - 列表项的视图模型,包含一个整数以及表示该数字的 string
    public class NumberVm
    {
        public int Number { get; }
        public string NumStr { get; }
    
        public NumberVm(int n, string str)
        {
            Number = n;
            NumStr = str;
        }
    }  
  2. NumberVmWithPosition - 放置到字形面板中的每个项的视图模型。它继承自 NumberVm 并为其添加了位置。
  3. TextVm - 完整的应用程序视图模型。它包含列表项视图模型的集合和字形视图模型的集合。它还将列表项初始化为数字 1 到 5 的视图模型。
    public class TestVm 
    {
        public ObservableCollection<NumberVm> ListItems { get; }
        public ObservableCollection<NumberVmWithPostion> Glyphs { get; }
    
        public TestVm()
        {
            ListItems = new ObservableCollection<NumberVm>();
    
            ListItems.Add(new NumberVm(1, "One"));
            ListItems.Add(new NumberVm(2, "Two"));
            ListItems.Add(new NumberVm(3, "Three"));
            ListItems.Add(new NumberVm(4, "Four"));
            ListItems.Add(new NumberVm(5, "Five"));
    
            Glyphs = new ObservableCollection<NumberVmWithPostion>();
        }
    } 

MainWindow.xaml.cs 文件中,创建了一个 TestVm 对象并将其分配给整个窗口的 DataContext

public MainWindow()
{
    InitializeComponent();

    DataContext = new TestVm();
} 

还有一个 C# 类 DropIntoGlypPanelOperation。此文件定义了放置期间发生的操作。

public class DropIntoGlyphPanelOperation : IDropOperation
{
    // drop method
    public void Drop
    (
        FrameworkElement draggedAndDroppedElement, 
        FrameworkElement dropContainer, 
        Point mousePositionWithRespectToContainer)
    {
        // get the dragged/dropped element's view model
        NumberVm droppedVm = 
            draggedAndDroppedElement.DataContext as NumberVm;

        // get the drop container's view model
        TestVm testVm = 
            dropContainer.DataContext as TestVm;

        // create the inserted glyph's view model
        // note that the last constructor argument
        // specifies the glyph's position within 
        // the drop container. 
        NumberVmWithPostion newGlyphVm = new NumberVmWithPostion
        (
            droppedVm.Number,
            droppedVm.NumStr,
            mousePositionWithRespectToContainer.ToPoint2D());

        // insert the new glyph into the Glyphs collection
        // of the view model 
        testVm.Glyphs.Add(newGlyphVm);
    }
} 

这个 DropIntoGlyphPanelOperation 将通过附加属性附加到放置行为,如 XAML 中所示。

现在让我们切换到 XAML 代码。此示例的 XAML 代码比上面描述的仅拖动示例更复杂,因此我将提供代码的高层概述,然后逐项描述。

在高层来看,XAML 代码由一个名为“TopLevelPanel”的 Grid 面板组成。该网格有 3 列——第一列用于项目列表,第二列用作垂直分隔符,第三列用于字形。

<Grid x:Name="TopLevelPanel">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    
    <!--Drag Cue-->
    <Popup x:Name="TheDragCue" .../>

    <!--Number Items (Drag Source)-->
    <ItemsControl x:Name="DragSourceList" .../>

    <!--Vertical Separator-->
    <Grid x:Name="Separator" .../>

    <!--Glyphs ItemsControl-->
    <ItemsControl x:Name="GlyphsItemsControl" .../>
</Grid> 

这是拖动提示弹出窗口的完整代码

<Popup x:Name="TheDragCue"
       AllowsTransparency="True"
       Placement="RelativePoint"
       PlacementTarget="{Binding ElementName=TopLevelPanel}"
       IsOpen="{Binding Path=(visuals:DragBehavior.IsDragOn), Mode=OneWay}"
       HorizontalOffset="{Binding Path=(visuals:DragBehavior.TotalShiftWithRespectToContainer).X}"
       VerticalOffset="{Binding Path=(visuals:DragBehavior.TotalShiftWithRespectToContainer).Y}"
       DataContext="{Binding Path=(visuals:DragBehavior.CurrentlyDraggedElement),
                             ElementName=TopLevelPanel}">
    <Popup.Resources>
        <visuals:BinaryToDoubleConverter x:Key="DragCueOpacityConverter"
                                         TrueValue="1"
                                         FalseValue="0.4" />
    </Popup.Resources>
    <visuals:LabelContainer x:Name="TheCueLabelContainer"
                            Background="LightGreen"
                            TheLabel="{Binding Path=DataContext.Number}"
                            Width="{Binding Path=ActualWidth}"
                            Height="{Binding Path=ActualHeight}"
                            Opacity="{Binding Path=(visuals:DropBehavior.IsDragAbove), 
                                              Converter={StaticResource DragCueOpacityConverter},
                                              ElementName=GlyphsItemsControl}" />
</Popup>

请注意,弹出窗口的 DataContext 已设置为拖动容器面板的 DragBehavior.CurrentlyDraggedElement

DataContext="{Binding Path=(visuals:DragBehavior.CurrentlyDraggedElement),
                      ElementName=TopLevelPanel}"  

因此,我们可以将 IsOpenHorizontalOffsetVerticalOffset 属性绑定到当前被拖动对象(弹出窗口的 DataContext)的相应 DragBehavior 属性。

IsOpen="{Binding Path=(visuals:DragBehavior.IsDragOn), Mode=OneWay}"
HorizontalOffset="{Binding Path=(visuals:DragBehavior.TotalShiftWithRespectToContainer).X}"
VerticalOffset="{Binding Path=(visuals:DragBehavior.TotalShiftWithRespectToContainer).Y}"  

LabelContainer 只是一个 Border 对象内的 TextBlock,用于显示拖动提示。请注意,其 WidthHeight 设置为与被拖动对象的 ActualWidthActualHeight 相同。

Width="{Binding Path=ActualWidth}"
Height="{Binding Path=ActualHeight}"  

另请注意其 Opacity 属性的绑定。

Opacity="{Binding Path=(visuals:DropBehavior.IsDragAbove), 
                  Converter={StaticResource DragCueOpacityConverter},
                  ElementName=GlyphsItemsControl}"  

它绑定到 GlyphsItemsControl 元素的 DropBehavior.IsDragAbove 附加属性,该属性指定是否可以将项放置到字形容器中。

这是数字项 List(拖放操作的源)的代码。

<!--Number Items (Drag Source)-->
<ItemsControl x:Name="DragSourceList"
              ItemsSource="{Binding Path=ListItems}"
              Width="100"
              ItemTemplate="{StaticResource NumberItemDataTemplate}">
    <ItemsControl.ItemContainerStyle>
        <Style TargetType="FrameworkElement">
            <Setter Property="visuals:DragBehavior.DragContainerElement"
                    Value="{Binding ElementName=TopLevelPanel}" />
            <Setter Property="visuals:DragBehavior.DraggedElement"
                    Value="{Binding RelativeSource={RelativeSource Mode=Self}}" />
            <Setter Property="visuals:DragBehavior.BounceBackAtDragEnd"
                    Value="True" />
            <Setter Property="Margin"
                    Value="10,2" />
        </Style>
    </ItemsControl.ItemContainerStyle>
</ItemsControl> 

我们使用 ItemContainerStyle 为列表中的每个项设置 DragBehavior 属性。DragContainerElementDraggedElement 属性您应该从前面的部分已经熟悉了。设置为 trueBoundBackAtDragEnd 属性将在拖动操作发生之前将 DragBehavior.Shift 属性恢复到原始值。

最后,这是 GlyphsItemsControl 的代码。

<!--Glyphs ItemsControl-->
<ItemsControl x:Name="GlyphsItemsControl"
              Grid.Column="2"
              Background="Pink"
              visuals:ActualSizeBehavior.IsSet="True"
              HorizontalAlignment="Stretch"
              VerticalAlignment="Stretch"
              ItemsSource="{Binding Path=Glyphs}"
              visuals:DropBehavior.ContainerElement=
              "{Binding RelativeSource={RelativeSource Mode=Self}}"
              visuals:DropBehavior.DraggedElement=
              "{Binding Path=(visuals:DragBehavior.CurrentlyDraggedElement), 
                                         ElementName=TopLevelPanel}"
              visuals:DropBehavior.TheDropOperation=
                     "{x:Static local:DropIntoGlyphPanelOperation.Instance}"
              ItemTemplate="{StaticResource NumberNameItemDataTemplate}">
    <ItemsControl.ItemContainerStyle>
        <Style TargetType="FrameworkElement">
            <Setter Property="HorizontalAlignment"
                    Value="Left" />
            <Setter Property="VerticalAlignment"
                    Value="Top" />
            <Setter Property="Width"
                    Value="50" />
            <Setter Property="Height"
                    Value="30" />
            <Setter Property="Margin"
                    Value="-25,-15,0,0" />
            <Setter Property="visuals:ShiftBehavior.Position"
                    Value="{Binding Path=Position, 
                                    Converter={x:Static visuals:ToVisualPointConverter.TheInstance}, 
                                    Mode=OneWay}" />
        </Style>
    </ItemsControl.ItemContainerStyle>
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <!-- Set item panel to grid (we want a container that takes the whole space 
                 of the items control-->
            <Grid />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl>  

请注意 DropBehavior 的附加属性。

<!--The drop container is set to the whole GlyphsItemsControl.-->
visuals:DropBehavior.ContainerElement="{Binding RelativeSource={RelativeSource Mode=Self}}"
<!--The <code>DraggedElement</code> is obtained from the drag container.-->
visuals:DropBehavior.DraggedElement="{Binding Path=(visuals:DragBehavior.CurrentlyDraggedElement), 
                                              ElementName=TopLevelPanel}"  
<!--The <code>DropBehavior.TheDropOperation</code> 
is attached to the drop container to be invoked when
a dragged item is released into the drop container.-->
visuals:DropBehavior.TheDropOperation="{x:Static local:DropIntoGlyphPanelOperation.Instance}"

现在,查看 ItemContainerStyle——每个字形项的样式。这里有趣的行是

<Setter Property="visuals:ShiftBehavior.Position"
        Value="{Binding Path=Position, 
                        Converter={x:Static visuals:ToVisualPointConverter.TheInstance}, 
                        Mode=OneWay}" />

这一行通过上面描述的 ShiftBehavior 将字形的位置设置为 NumberVmWithPosition 视图模型对象的 Position 属性。

列表到列表的拖放示例

我们的最后一个示例演示了如何将一个项从源列表拖放到目标列表。您可以选择在目标列表的哪两个项之间添加被拖动的项。有一个放置提示,指示如果当前鼠标位置释放被拖动的项,它将被添加到哪里。

此示例位于 NP.Tests.DragDropFromListToList 解决方案下。

如果您运行它,您将在左侧看到数字列表(与上一个示例相同),在右侧看到空白区域。

您可以将项目从左侧面板拖放到右侧面板。

当您拖动一个项并且鼠标悬停在目标列表上方时,目标项之间的红线将指示如果在此鼠标位置释放被拖动的项,它将被放置在哪里(如上图所示)。这条红线表示放置提示。

该示例的代码与上一个示例非常相似。NumberVm 与上面完全相同。不需要 NumberVmWithPosition ——目标列表中项的位置由其顺序决定。TestVm 的外观如下。

public class TestVm 
{
    public ObservableCollection<numbervm> SourceList { get; }
    public ObservableCollection<numbervm> TargetList { get; }

    public TestVm()
    {
        SourceList = new ObservableCollection<numbervm>();

        SourceList.Add(new NumberVm(1, "One"));
        SourceList.Add(new NumberVm(2, "Two"));
        SourceList.Add(new NumberVm(3, "Three"));
        SourceList.Add(new NumberVm(4, "Four"));
        SourceList.Add(new NumberVm(5, "Five"));

        TargetList = new ObservableCollection<numbervm>();
    }
} 

现在,让我们看看 MainWindow.xaml 文件中的 XAML 代码。拖动提示和拖动源列表与上一个示例完全相同。这是目标区域(包括目标列表)的代码。

<!--Drop Target-->
<Grid Grid.Column="2">
    <Rectangle x:Name="DropCue"
               HorizontalAlignment="Stretch"
               VerticalAlignment="Top"
               Height="2"
               Margin="0,-1"
               Fill="Red"
               Visibility="{Binding Path=(visuals:DropBehavior.CanDrop), 
                                    Converter={x:Static visuals:BoolToVisConverter.TheInstance},
                                    ElementName=DropTargetList}">
        <Rectangle.RenderTransform>
            <TranslateTransform Y="{Binding Path=(visuals:DropBehavior.DropPosition).Y, 
                                               ElementName=DropTargetList}" />
        </Rectangle.RenderTransform>
    </Rectangle>
    <ItemsControl x:Name="DropTargetList"
                  HorizontalAlignment="Stretch"
                  ItemsSource="{Binding Path=TargetList}"
                  visuals:DropBehavior.ContainerElement=
                  "{Binding RelativeSource={RelativeSource Mode=Self}}"
                  visuals:DropBehavior.DraggedElement=
                  "{Binding Path=(visuals:DragBehavior.CurrentlyDraggedElement), 
                                                                ElementName=TopLevelPanel}"
                  visuals:DropBehavior.TheDropOperation=
                  "{x:Static local:DropIntoItemsPanelOperation.Instance}"
                  visuals:DropBehavior.TheDropPositionChooser=
                  "{x:Static local:VerticalItemsPanelPositionChooser.Instance}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel Margin="0,2" />
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <visuals:LabelContainer Background="LightGreen"
                                        TheLabel="{Binding NumStr}"
                                        Margin="0,2" />
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Grid>  

请注意,目标区域是一个 Grid,包含 DropCue 矩形和 DropTargetList ItemsControlDropCue 的可见性由以下行控制

Visibility="{Binding Path=(visuals:DropBehavior.CanDrop), 
                    Converter={x:Static visuals:BoolToVisConverter.TheInstance},
                    ElementName=DropTargetList}"    

DropBehavior.CanDrop 附加属性在放置目标上为 true 时,它会变得可见。

放置提示的垂直位置由 TranslateTransform 控制。

<Rectangle.RenderTransform>
    <TranslateTransform Y="{Binding Path=(visuals:DropBehavior.DropPosition).Y, 
                                       ElementName=DropTargetList}" />
</Rectangle.RenderTransform>  

DropTargetList 项目控件中的重要行是

visuals:DropBehavior.TheDropOperation="{x:Static local:DropIntoItemsPanelOperation.Instance}"
visuals:DropBehavior.TheDropPositionChooser=
"{x:Static local:VerticalItemsPanelPositionChooser.Instance}"

第一个将 DropBehavior.TheDropOperation 附加属性绑定到 DropIntoItemsPanelOperation 类型的对象,该对象选择放置发生时要执行的操作。第二个绑定 DropBehavior.TheDropPositionChooser 到选择放置提示位置的对象。

为了确定放置期间可能插入的垂直位置和索引,我采用了

public static class VerticalPositionHelper
{
    public static (double, int) GetVerticalOffsetAndInsertIdx
                  (this FrameworkElement dropContainer, double mouseVerticalOffset)
   {
      ...
   }
}

方法。该方法有些复杂,与我们这里讨论的内容没有直接关系,因此我将其留给读者自行研究(如果他们真的想的话)。该方法接受放置容器和容器内的当前鼠标垂直偏移量,并返回放置提示的垂直位置以及在这一点放置时被拖动项的插入索引。

上面提到的两个类使用了这个方法:VerticalItemsPanelPositionChooser(用于确定放置提示的位置)以及 DropIntoItemsPanelOperation(当被拖动的项被释放到目标时,实际上执行放置)。这是两个类的带注释的代码。

public class VerticalItemsPanelPositionChooser : IDropPositionChooser
{
    // used for easy XAML reference
    public static VerticalItemsPanelPositionChooser Instance { get; } = 
        new VerticalItemsPanelPositionChooser();

    public Point GetPositionWithinDropDontainer
    (
        FrameworkElement droppedElement, 
        FrameworkElement dropContainer, 
        Point mousePositionWithRespectToContainer)
    {
        // get the vertical offset
        (double verticalOffset, _) = dropContainer.GetVerticalOffsetAndInsertIdx(mousePositionWithRespectToContainer.Y);

        // return the position corresponding to the vertical offset.
        return new Point(0, verticalOffset);
    }
}    

public class DropIntoItemsPanelOperation : IDropOperation
{
    // used for easy XAML reference
    public static DropIntoItemsPanelOperation Instance { get; } = 
        new DropIntoItemsPanelOperation();

    public void Drop(FrameworkElement droppedElement, 
    FrameworkElement dropContainer, Point mousePositionWithRespectToContainer)
    {
        // get the view model for the items that's being dropped. 
        NumberVm droppedVm = droppedElement.DataContext as NumberVm;

        // get the view model into which we insert the dropped item.
        TestVm testVm = dropContainer.DataContext as TestVm;

        // get the insertion index
        (_, int insertIdx) = 
        dropContainer.GetVerticalOffsetAndInsertIdx(mousePositionWithRespectToContainer.Y);

        // insert the item into the target view model. 
        testVm.TargetList.Insert(insertIdx, droppedVm);
    }
}  

结论

在本文中,我描述了 NP.Visuals 包与拖放相关功能的一部分。如您所见,可以使用此功能执行几乎任何拖放操作。

© . All rights reserved.