MVVM WPF 中的拖放






4.97/5 (16投票s)
使用 MVVM 风格的架构在 WPF 中进行拖放。
引言
在本文中,我将展示如何在WPF中实现拖放。我们将使用自定义用户控件作为鼠标光标移动时的“幽灵”预览来向用户展示拖放操作,并使所有内容可绑定,以便我们可以使用MVVM。我们将提供一些事件,以便我们根据是否允许拖放操作来更改UI。除了使用MVVM Light来简化ViewModel之外,此示例将不依赖任何第三方库或包。
背景
我最近开始处理的一个应用程序没有任何用于执行逻辑操作的按钮。相反,所有操作都通过拖放完成。我最近被要求在WPF中实现这个核心UI功能的演示。
WPF原生支持拖放操作 - 即,当UI元素在屏幕上拖动并放置在另一个UI元素上时,您作为开发人员可以获取一些信息,但从用户的角度来看,没有任何反馈。如果我们想要某种视觉提示(鼠标光标更改、幽灵预览或移动元素本身),我们开发人员必须添加该功能。
我看到的一些其他文章和示例通过使用第三方库提供拖放预览操作。在企业环境中,我们并不总是有这样的奢侈。
因此,在本文中,我们将实现一个带有幽灵预览的拖放手势,将新用户添加到用户列表中。此外,我们将为幽灵图像添加一个视觉指示器动画,以表明是否允许拖放。我们将完全不依赖第三方库来支持我们的拖放功能。
代码
这篇文章比较长,所以我将其分为两个部分
使用库
我们需要做的第一件事是添加我们的预览用户控件 - 即,在拖放操作进行时显示的控件。
您的控件可以看起来任何您想要的,但对我而言,如果我可以添加用户,我希望有一个带有绿色加号的用户图标,如果不能,则是一个红色的减号。
让我们添加一个用户控件,并为其提供以下xaml
<Grid Name="grid">
<Grid.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="0"/>
<TranslateTransform X="0" Y="0"/>
</TransformGroup>
</Grid.RenderTransform>
<Image Name="imgIndicator" Source="user-icon.png" />
<Rectangle Name="horizontalBar" Height="50" Width="150" Margin="140,191,10,59">
<Rectangle.Fill>
<SolidColorBrush Color="Green" />
</Rectangle.Fill>
</Rectangle>
<Rectangle Name="verticalBar" Height="150" Width="50" Margin="190,140,60,10">
<Rectangle.Fill>
<SolidColorBrush Color="Green" />
</Rectangle.Fill>
</Rectangle>
</Grid>
在用户控件的资源中,我将添加两个动画
第一个在我们可以放置时将水平和垂直条设置为不透明和绿色
<Storyboard x:Key="canDropChanged" FillBehavior="HoldEnd">
<ColorAnimation To="Green" Storyboard.TargetName="horizontalBar" Storyboard.TargetProperty="(Rectangle.Fill).(SolidColorBrush.Color)" BeginTime="00:00:00" Duration="00:00:00.3" />
<ColorAnimation To="Green" Storyboard.TargetName="verticalBar" Storyboard.TargetProperty="(Rectangle.Fill).(SolidColorBrush.Color)" BeginTime="00:00:00" Duration="00:00:00.3" />
<DoubleAnimation BeginTime="00:00:00" Duration="00:00:00.25" AccelerationRatio=".1" DecelerationRatio=".9" To="1" Storyboard.TargetName="verticalBar" Storyboard.TargetProperty="(Rectangle.Opacity)" />
</Storyboard>
第二个在我们不能放置时将垂直条的不透明度设置为更改,并将水平条的颜色更改为红色
<Storyboard x:Key="cannotDropChanged" FillBehavior="HoldEnd">
<ColorAnimation To="Red" Storyboard.TargetName="horizontalBar" Storyboard.TargetProperty="(Rectangle.Fill).(SolidColorBrush.Color)" BeginTime="00:00:00" Duration="00:00:00.3" />
<ColorAnimation To="Red" Storyboard.TargetName="verticalBar" Storyboard.TargetProperty="(Rectangle.Fill).(SolidColorBrush.Color)" BeginTime="00:00:00" Duration="00:00:00.3" />
<DoubleAnimation BeginTime="00:00:00" Duration="00:00:00.25" AccelerationRatio=".1" DecelerationRatio=".9" To="0" Storyboard.TargetName="verticalBar" Storyboard.TargetProperty="(Rectangle.Opacity)" />
</Storyboard>
您应该注意到,我展示如何构建的库对这些故事板名称进行了硬编码,以便在“可以放置”和“不能放置”状态之间进行过渡。在未来的版本中,我可能会将其更改为数据触发器。现在,可以说,垂直条的颜色更改在技术上不是必需的,但是,在我的测试中,我认为当它不与绿色竞争时,水平条淡化为红色看起来更好。
现在我们已经有了将用作预览的控件,让我们前往主窗口。
MainWindow.xaml
让我们先布局窗口
<Window x:Class="DragDropExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dd="clr-namespace:DragDrop;assembly=DragDrop"
xmlns:controls="clr-namespace:DragDropExample"
DataContext="{Binding Source={StaticResource Locator}, Path=Main}"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<ResourceDictionary>
<controls:DragPreview x:Key="DragPreviewControl" />
</ResourceDictionary>
</Window.Resources>
<Grid>
<Canvas ZIndex="5" x:Name="OverlayCanvas" />
<Grid></Grid>
</Grid>
</Window>
在库中,我们的预览控件渲染在我们在绑定时指定的Canvas
上 - 覆盖画布将是该画布。我们使用XAML的布局来确保画布覆盖整个窗口,并设置z-index以确保画布上的任何内容都渲染在我们其他控件之上。
让我们滚动到<Border>
元素,看看我们如何设置拖放源
<Border BorderBrush="Black" BorderThickness="1" Margin="0 10 0 0"
dd:DragDrop.IsDragSource="True"
dd:DragDrop.DropTarget="{Binding ElementName=dropPanel}"
dd:DragDrop.DragDropPreviewControl="{StaticResource DragPreviewControl}"
dd:DragDrop.DragDropPreviewControlDataContext="{Binding Source={StaticResource Locator},Path=DragPreview}"
dd:DragDrop.DragDropContainer="{Binding ElementName=OverlayCanvas}"
dd:DragDrop.ItemDropped="{Binding AddUser}"
>
我们在DragDrop
类中声明的所有附加属性都设置在这里。将您设置为拖放源的项将是发起拖放的项。出于本文的目的,我将边框元素设置为拖放源,但您可以将其设置为任何控件。
DropTarget
是接收放置操作的控件 - 我们不需要对该控件做任何事情。DragDropPreviewControl
是我们用户控件,在拖放操作进行时会显示。DragDropPrewviewControlDataContext
是(可选的)数据上下文,它会在拖放操作进行时分配给预览控件以用于数据绑定。如果您省略此属性,则DragDropPreviewControl
会获取此控件的数据上下文作为其数据上下文。DragDropContainer
是在拖放操作期间渲染我们的预览控件的画布。ItemDropped
命令是确定我们是否可以放置以及作为放置项时的事件处理程序的命令。
现在,让我们来构建提供功能的库。
拖放库
让我们先设置一些基础设施。
DropState.cs
这是一个快速的枚举,用于告诉我们在放置操作方面我们可以做什么和不能做什么。
public enum DropState
{
CanDrop,
CannotDrop
}
DropState.CanDrop
表示拖放预览位于可放置区域内DropState.CannotDrop
表示拖放预览位于可放置区域外
DropEventArgs.cs
当我们启动放置操作时,我们需要一些事件参数传递给调用者。这只是一个简单的数据上下文包装器,以便我们可以将其作为事件参数对象传递。
/// <summary>
/// Event args for when a visual is dropped
/// </summary>
public class DragDropEventArgs : EventArgs
{
public Object DataContext;
public DragDropEventArgs() { }
public DragDropEventArgs(Object dataContext)
{
DataContext = dataContext;
}
}
DragDropPreviewBase.cs
DragDropPreviewBase
是我们将在拖放操作进行时向用户显示的类的基类。让我们先声明该类,并向渲染转换添加一些属性,以便在控件加载或处置时能够对其进行动画处理。
public class DragDropPreviewBase : UserControl
{
public DragDropPreviewBase()
{
ScaleTransform scale = new ScaleTransform(1f, 1f);
SkewTransform skew = new SkewTransform(0f, 0f);
RotateTransform rotate = new RotateTransform(0f);
TranslateTransform trans = new TranslateTransform(0f, 0f);
TransformGroup transGroup = new TransformGroup();
transGroup.Children.Add(scale);
transGroup.Children.Add(skew);
transGroup.Children.Add(rotate);
transGroup.Children.Add(trans);
this.RenderTransform = transGroup;
}
}
接下来,我们将在此控件中添加一个依赖项属性,该属性允许我们将此控件的DropState
绑定到数据上下文。
#region DropState Dependency Property
#region Binding Property
/// <summary>
/// Gets and sets drop state for this drag and drop preview control
/// </summary>
public DropState DropState
{
get { return (DropState)GetValue(DropStateProperty); }
set { SetValue(DropStateProperty, value); }
}
#endregion
#region Dependency Property
/// <summary>
/// The backing <see cref="DependencyProperty"/> which enables animation, styling, binding, etc. for <see cref="DropState"/>
/// </summary>
public static readonly DependencyProperty DropStateProperty =
DependencyProperty.Register("DropState", typeof(DropState), typeof(DragDropPreviewBase), new UIPropertyMetadata(DropStateChanged));
/// <summary>
/// Handles when drop state changes
/// </summary>
/// <param name="element">The <see cref="DependencyObject"/> that the attached <see cref="DependencyProperty"/>, <see cref="DropStateProperty"/>, is attched to.</param>
/// <param name="e"><see cref="DependencyPropertyChangedEventArgs"/> from the changed event</param>
public static void DropStateChanged(DependencyObject element, DependencyPropertyChangedEventArgs e)
{
var instance = (DragDropPreviewBase)element;
instance.StateChangedHandler(element, e);
}
public virtual void StateChangedHandler(DependencyObject d, DependencyPropertyChangedEventArgs e)
{ }
#endregion
#endregion
这是依赖项属性的标准样板代码,所以我将继续。
DragDrop.cs
这就是我们开始的地方。DragDrop
是我们用来指示对象可以成为拖放源,移动DragDropPreviewBase
,并在用户将鼠标释放到合格的放置目标上时调用ItemDropped
命令。
让我们先声明类,并添加两个我们稍后将使用的实用方法。
#region Utilities
/// <summary>
/// Walks the visual tree, and finds the ancestor of the <see cref="visual"/> which is an instance of <paramref name="ancestorType"/>
/// </summary>
/// <param name="ancestorType">The type of ancestor to look for in the visual tree</param>
/// <param name="visual">The object to start at in the visual tree</param>
/// <returns>The <see cref="FrameworkElement"/> which matches <paramref name="ancestorType"/></returns>
public static FrameworkElement FindAncestor(Type ancestorType, Visual visual)
{
while (visual != null && !ancestorType.IsInstanceOfType(visual))
{
visual = (Visual)VisualTreeHelper.GetParent(visual);
}
return visual as FrameworkElement;
}
/// <summary>
/// Determines if the delta between two points exceeds the minimum horizontal and vertical drag distance, as defined by the system
/// </summary>
/// <param name="initialMousePosition">The starting position</param>
/// <param name="currentPosition">The current position</param>
/// <returns>True, if the delta exceeds the minimum horizontal and vertical drag distance</returns>
public static Boolean IsMovementBigEnough(Point initialMousePosition, Point currentPosition)
{
return (Math.Abs(currentPosition.X - initialMousePosition.X) >= SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(currentPosition.Y - initialMousePosition.Y) >= SystemParameters.MinimumVerticalDragDistance);
}
#endregion
FindAncestor
的作用与您想象的差不多;它递归地遍历视觉树,并找到目标类型的祖先。IsMovementBigEnough
确定两点之间的差是否大于系统定义的最小值。我们稍后需要它来确定是否确实有必要继续进行拖放操作 - 如果用户只请求拖动2像素(例如),我们不想经历执行移动的麻烦。
接下来,我们将添加以下附加属性。由于这些都使用附加属性的标准代码片段实现,因此我将省略在文章中粘贴这些属性的代码。
DropTarget
- 将接收放置的元素DragDropPreviewControlDataContext
- 要与拖放操作关联的数据上下文DragDropPreviewControl
- 移动鼠标时将显示的DragDropPreviewBase
用户控件DragDropContainer
- 用于绝对定位预览控件的画布ItemDropped
- 控件放置时将执行的ICommand
。
我们需要再添加一个附加属性,但在此之前,我们需要设置实例变量。这些值将允许我们捕获附加属性的值,并将它们存储起来供以后在鼠标按下、鼠标移动和鼠标释放事件中使用。当我们移动鼠标时,我们希望所有与特定移动相关联的实例值都保持不变,并且在开始另一个拖放操作时不会被意外重复使用。
private Window _topWindow;
/// <summary>
/// The location where we first started the drag operation
/// </summary>
private Point _initialMousePosition;
/// <summary>
/// The outmost canvas which the user can drag the <see cref="_dragDropPreviewControl"/>
/// </summary>
private Panel _dragDropContainer;
/// <summary>
/// The control which will serve as the drop arget
/// </summary>
private UIElement _dropTarget;
/// <summary>
/// Determines if we're currently tracking the mouse
/// </summary>
private Boolean _mouseCaptured;
/// <summary>
/// The control that's displayed (and moving with the mouse) during a drag drop operation
/// </summary>
private DragDropPreviewBase _dragDropPreviewControl;
/// <summary>
/// The data context of the <see cref="_dragDropPreviewControl"/>
/// </summary>
private Object _dragDropPreviewControlDataContext;
/// <summary>
/// The command to execute when items are dropped
/// </summary>
private ICommand _itemDroppedCommand;
private Point _delta;
#region Instance
/// <summary>
/// Lazy loaded backing member variable for <see cref="Instance"/>
/// </summary>
private static readonly Lazy<DragDrop> _Instance = new Lazy<DragDrop>(() => new DragDrop());
/// <summary>
/// Gets a static instance of <see cref="DragDrop"/>
/// </summary>
private static DragDrop Instance
{
get { return _Instance.Value; }
}
#endregion
现在,让我们添加IsDragSource
附加属性。
首先,让我们添加依赖项属性,
#region IsDragSource Attached Property
#region Backing Dependency Property
/// <summary>
/// The backing <see cref="DependencyProperty"/> which enables animation, styling, binding, etc. for IsDragSource
/// </summary>
public static readonly DependencyProperty IsDragSourceProperty = DependencyProperty.RegisterAttached(
"IsDragSource", typeof(Boolean), typeof(DragDrop), new PropertyMetadata(false, IsDragSourceChanged));
#endregion
getter和setter
/// <summary>
/// Gets the attached value of IsDragSource
/// </summary>
/// <param name="element">The <see cref="DependencyObject"/> that the attached <see cref="DependencyProperty"/>, <see cref="IsDragSourceProperty"/>, is attched to.</param>
/// <returns>The attached value</returns>
public static Boolean GetIsDragSource(DependencyObject element)
{
return (Boolean)element.GetValue(IsDragSourceProperty);
}
/// <summary>
/// Sets the attached value of IsDragSource
/// </summary>
/// <param name="element">The <see cref="DependencyObject"/> that the attached <see cref="DependencyProperty"/>, <see cref="IsDragSourceProperty"/>, is attched to.</param>
/// <param name="value">the value to set</param>
public static void SetIsDragSource(DependencyObject element, Boolean value)
{
element.SetValue(IsDragSourceProperty, value);
}
以及changed处理程序
/// <summary>
/// Handles when <see cref="IsDragSourceProperty"/>'s value changes
/// </summary>
/// <param name="element">The <see cref="DependencyObject"/> that the attached <see cref="DependencyProperty"/>, <see cref="IsDragSourceProperty"/>, is attched to.</param>
/// <param name="e"><see cref="DependencyPropertyChangedEventArgs"/> from the changed event</param>
private static void IsDragSourceChanged(DependencyObject element, DependencyPropertyChangedEventArgs e)
{
var dragSource = element as UIElement;
if (dragSource == null)
{ return; }
if (Object.Equals(e.NewValue, true))
{
dragSource.PreviewMouseLeftButtonDown += Instance.DragSource_PreviewMouseLeftButtonDown;
dragSource.PreviewMouseLeftButtonUp += Instance.DragSource_PreviewMouseLeftButtonUp;
dragSource.PreviewMouseMove += Instance.DragSource_PreviewMouseMove;
}
else
{
dragSource.PreviewMouseLeftButtonDown -= Instance.DragSource_PreviewMouseLeftButtonDown;
dragSource.PreviewMouseLeftButtonUp -= Instance.DragSource_PreviewMouseLeftButtonUp;
dragSource.PreviewMouseMove -= Instance.DragSource_PreviewMouseMove;
}
}
基本上,如果依赖项属性更改为true,我们将连接事件处理程序,如果更改为false,我们将断开它们。
让我们先看看鼠标按下时会发生什么。
/// <summary>
/// Tunneled event handler for when the mouse left button is about to be depressed
/// </summary>
/// <param name="sender">The object which invokes this event</param>
/// <param name="e">event args from the sender</param>
private void DragSource_PreviewMouseLeftButtonDown(Object sender, MouseButtonEventArgs e)
{
try
{
var visual = e.OriginalSource as Visual;
_topWindow = (Window)DragDrop.FindAncestor(typeof(Window), visual);
_initialMousePosition = e.GetPosition(_topWindow);
//first, determine if the outer container property is bound
_dragDropContainer = DragDrop.GetDragDropContainer(sender as DependencyObject) as Canvas;
if (_dragDropContainer == null)
{
//set the container to the canvas ancestor of the bound visual
_dragDropContainer = (Canvas)DragDrop.FindAncestor(typeof(Canvas), visual);
}
_dropTarget = GetDropTarget(sender as DependencyObject);
//get the data context for the preview control
_dragDropPreviewControlDataContext = DragDrop.GetDragDropPreviewControlDataContext(sender as DependencyObject);
if (_dragDropPreviewControlDataContext == null)
{ _dragDropPreviewControlDataContext = (sender as FrameworkElement).DataContext; }
_itemDroppedCommand = DragDrop.GetItemDropped(sender as DependencyObject);
}
catch (Exception exc)
{
Console.WriteLine("Exception in DragDropHelper: " + exc.InnerException.ToString());
}
}
我们在这里所做的是获取我们使用依赖项属性引用的对象的引用,并将它们绑定到当前实例的成员变量。这不算什么,但它为我们准备好了鼠标移动事件。
/// <summary>
/// Tunneled event handler for when the mouse is moving
/// </summary>
/// <param name="sender">The object which invokes this event</param>
/// <param name="e">Event args from the sender</param>
private void DragSource_PreviewMouseMove(Object sender, MouseEventArgs e)
{
if (_mouseCaptured || _dragDropPreviewControlDataContext == null)
{
return; //we're already capturing the mouse, or we don't have a data context for the preview control
}
if (DragDrop.IsMovementBigEnough(_initialMousePosition, e.GetPosition(_topWindow)) == false)
{
return; //only drag when the user moved the mouse by a reasonable amount
}
_dragDropPreviewControl = (DragDropPreviewBase)GetDragDropPreviewControl(sender as DependencyObject);
_dragDropPreviewControl.DataContext = _dragDropPreviewControlDataContext;
_dragDropPreviewControl.Opacity = 0.7;
_dragDropContainer.Children.Add(_dragDropPreviewControl);
_mouseCaptured = Mouse.Capture(_dragDropPreviewControl); //have the preview control recieve and be able to handle mouse events
//offset it just a bit so it looks like it's underneath the mouse
Mouse.OverrideCursor = Cursors.Hand;
Canvas.SetLeft(_dragDropPreviewControl, _initialMousePosition.X - 20);
Canvas.SetTop(_dragDropPreviewControl, _initialMousePosition.Y - 15);
_dragDropContainer.PreviewMouseMove += DragDropContainer_PreviewMouseMove;
_dragDropContainer.PreviewMouseUp += DragDropContainer_PreviewMouseUp;
}
所以,这里很有意思 - 当我们将鼠标光标移动很大的距离时,我们将获取预览控件,为其附加所需的数据上下文以用于MVVM,将其不透明度设置为70%,使其看起来像是“幽灵”;但一旦完成,未来由拖放源生成的鼠标移动事件将因设计而提前退出。
即使鼠标移动事件仍然由拖放源引起,我们现在正在将幽灵预览移动到画布上;因此,画布处理预览鼠标移动事件而不是拖放源更合适。因此,我们将捕获该通道事件,在预览控件移动时调用Mouse.Capture
。我们还需要知道何时停止处理它,所以我们还将向预览鼠标释放事件添加一个处理程序。
DragDropContainer_PreviewMouseMove
事件中有很多事情发生,所以让我们一步步来看。
/// <summary>
/// Tunneled event handler for when the mouse is moving
/// </summary>
/// <param name="sender">The object which invokes this event</param>
/// <param name="e">Event args from the sender</param>
private void DragDropContainer_PreviewMouseMove(Object sender, MouseEventArgs e)
{
var currentPoint = e.GetPosition(_topWindow);
//offset it just a bit so it looks like it's underneath the mouse
Mouse.OverrideCursor = Cursors.Hand;
currentPoint.X = currentPoint.X - 20;
currentPoint.Y = currentPoint.Y - 15;
_delta = new Point(_initialMousePosition.X - currentPoint.X, _initialMousePosition.Y - currentPoint.Y);
var target = new Point(_initialMousePosition.X - _delta.X, _initialMousePosition.Y - _delta.Y);
Canvas.SetLeft(_dragDropPreviewControl, target.X);
Canvas.SetTop(_dragDropPreviewControl, target.Y);
_dragDropPreviewControl.DropState = DropState.CannotDrop;
if (_dropTarget == null)
{
AnimateDropState();
return;
}
所以我们首先获取鼠标当前位置的X和Y坐标,然后将其向上和向左稍微移动一点,以便左上角出现在鼠标下方。然后,通过使用Canvas.SetLeft
和Canvas.SetTop
附加属性,在画布上实际移动它。现在我们已经移动了它,如果配置没有为我们提供任何放置位置,则将放置状态动画设置为“不能放置”动画,然后返回。
接下来,我们将确定鼠标是否在放置目标上。
var transform = _dropTarget.TransformToVisual(_dragDropContainer);
var dropBoundingBox = transform.TransformBounds(new Rect(0, 0, _dropTarget.RenderSize.Width, _dropTarget.RenderSize.Height));
if (e.GetPosition(_dragDropContainer).X > dropBoundingBox.Left &&
e.GetPosition(_dragDropContainer).X < dropBoundingBox.Right &&
e.GetPosition(_dragDropContainer).Y > dropBoundingBox.Top &&
e.GetPosition(_dragDropContainer).Y < dropBoundingBox.Bottom)
{
_dragDropPreviewControl.DropState = DropState.CanDrop;
}
WPF的坐标系统对于给定控件的左、右、上、下是相对于容器控件的。由于放置目标可能(并且很可能不是)使用与我们的指针相同的相对坐标,因此我们需要将其标准化到相同的坐标系。我们通过调用TransformToVisual来做到这一点。
现在坐标系已标准化,我们绘制一个边界框,并确定指针当前是否位于框内。如果是,则将放置状态设置为CanDrop
。
最后一件事 - 即使我们现在将鼠标实际放在放置目标上,我们仍然可能不允许放置 - 这就是我们的命令发挥作用的地方。
//bounding box might allow us to drop, but now we need to check with the command
if (_itemDroppedCommand != null && _itemDroppedCommand.CanExecute(_dragDropPreviewControlDataContext) == false)
{
_dragDropPreviewControl.DropState = DropState.CannotDrop; //commanding trumps visual.
}
如果命令说我们不能放置,那么我们将放置状态设置为CannotDrop
。命令优先于视觉。
当我们释放鼠标时,有一些事情需要做
/// <summary>
/// Event handler for when the mouse button is released in the context of the drag and drop preview
/// </summary>
/// <param name="sender">The object which invokes this event</param>
/// <param name="e">event args from the sender</param>
private void DragDropContainer_PreviewMouseUp(Object sender, MouseEventArgs e)
{
switch (_dragDropPreviewControl.DropState)
{
case DropState.CanDrop:
try
{
//some animation code removed for article
var canDropSb = new Storyboard() { FillBehavior = FillBehavior.Stop };
canDropSb.Children.Add(scaleXAnim);
canDropSb.Children.Add(scaleYAnim);
canDropSb.Children.Add(opacityAnim);
canDropSb.Completed += (s, args) => { FinalizePreviewControlMouseUp(); };
canDropSb.Begin(_dragDropPreviewControl);
if (_itemDroppedCommand != null)
{ _itemDroppedCommand.Execute(_dragDropPreviewControlDataContext); }
}
catch (Exception ex)
{ }
break;
case DropState.CannotDrop:
try
{
//some animation code removed for article
var cannotDropSb = new Storyboard() { FillBehavior = FillBehavior.Stop };
cannotDropSb.Children.Add(translateXAnim);
cannotDropSb.Children.Add(translateYAnim);
cannotDropSb.Children.Add(opacityAnim);
cannotDropSb.Completed += (s, args) => { FinalizePreviewControlMouseUp(); };
cannotDropSb.Begin(_dragDropPreviewControl);
}
catch (Exception ex) { }
break;
}
_dragDropPreviewControlDataContext = null;
_mouseCaptured = false;
}
根据控件的放置状态,执行一些动画,然后调用FinalizePreviewControlMouseUp
,它将预览从画布中移除,断开处理程序,并将光标恢复正常。
还有最后一件事,那就是处理发起拖动的控件上的预览鼠标释放事件。
/// <summary>
/// Tunneled event handler for when the dragged item is released
/// </summary>
/// <param name="sender">The object which invokes this event</param>
/// <param name="e">Event args from the sender</param>
private void DragSource_PreviewMouseLeftButtonUp(Object sender, MouseButtonEventArgs e)
{
_dragDropPreviewControlDataContext = null;
_mouseCaptured = false;
if (_dragDropPreviewControl != null)
{ _dragDropPreviewControl.ReleaseMouseCapture(); }
}
我们需要从拖放预览控件中释放鼠标捕获,以便它不再信号鼠标事件。
贷方
Lee Roth提供了使用画布定位用户控件的想法
历史
2015-03-03:初始帖子