使用 WPF 中的 DragSource 类拖动元素






4.70/5 (8投票s)
在 WPF 中使用 DragSource 类拖动元素。
引言
实现拖放操作可以通过过程式代码实现,也可以通过一个称为附加行为的、不依赖于过程式代码的库来实现。由于设置属性比编写过程式代码更容易,因此后者更易于使用。这是另一个附加行为。DragSource
类为拖动操作提供了附加属性。该类的主要区别如下:
- 它提供了丰富的附加属性,让您可以更全面地控制拖动操作。
- 它提供了自动生成的拖动图像,称为视觉反馈,该图像基于被拖动的元素。
- 它使用一个窗口来显示视觉反馈。因此,它可以显示在宿主应用程序之外。
此类是 Nicenis 项目的一部分。您可以在 CodePlex 项目中找到最新信息:https://nicenis.codeplex.com。如果您只需要一个简短的教程,可以跳过本文,查看这个技巧。
基础
如果您编写过程式代码来使元素可拖动,您必须处理诸如 PreviewMouseDown
之类的事件。此外,您还必须跟踪鼠标移动以查看是否发生了拖动手势。如果发生了拖动手势,您还必须调用以下 DragDrop.DoDragDrop
方法。
public static DragDropEffects DoDragDrop
(
// A reference to the dependency object that is the source of the data being dragged.
DependencyObject dragSource,
// A data object that contains the data being dragged.
Object data,
// One of the DragDropEffects values that specifies permitted effects of the drag-and-drop operation.
DragDropEffects allowedEffects
)
在大多数附加行为中,这些代码都被打包到附加属性更改回调中。DragSource
为此目的提供了 AllowDrag
附加属性。当 AllowDrag
设置为 true 时,它会附加所需的事件处理程序。
static void AllowDragProperty_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UIElement target = d as UIElement;
// Removes the previous event handler if it exists.
target.PreviewMouseDown -= AllowDragProperty_PropertyHost_PreviewMouseDown;
if ((bool)e.NewValue)
target.PreviewMouseDown += AllowDragProperty_PropertyHost_PreviewMouseDown;
}
对于 DoDragDrop
方法的必需参数,DragSource
提供了 Data
和 AllowedEffects
附加属性。因此,您可以使用以下 XAML 使元素可拖动,而无需任何过程式代码。
<Window
x:Class="DragSourceSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:n="clr-namespace:Nicenis.Windows;assembly=Nicenis"
Title="MainWindow" Height="200" Width="200">
<!-- This border will be draggable.-->
<Border
n:DragSource.AllowDrag="True"
n:DragSource.Data="Test Data"
n:DragSource.AllowedEffects="Copy"
Margin="30"
Background="Green"
/>
</Window>
AllowedEffects
不是必需的属性,因此您可以省略它。拖动边框时,它看起来会像这张图片一样。
识别拖动手势
拖动操作是通过按下元素并移动它来开始的。按下元素可以通过各种方式实现,例如鼠标、触摸等。控制可以启动拖动操作的因素非常重要。DragSource
提供了 AllowedInitiators
附加属性,其类型为 DragInitiators
枚举。
[Flags]
public enum DragInitiators
{
MouseLeftButton = DragInitiator.MouseLeftButton,
MouseMiddleButton = DragInitiator.MouseMiddleButton,
MouseRightButton = DragInitiator.MouseRightButton,
MouseXButton1 = DragInitiator.MouseXButton1,
MouseXButton2 = DragInitiator.MouseXButton2,
/// <summary>
/// All mouse related drag initiators.
/// </summary>
Mouse = MouseLeftButton
| MouseMiddleButton
| MouseRightButton
| MouseXButton1
| MouseXButton2,
Default = MouseLeftButton,
All = Mouse,
}
目前,唯一支持的设备是鼠标。以下 XAML 显示了一个可以使用鼠标右键拖动的矩形。
<Window
x:Class="DragSourceSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:n="clr-namespace:Nicenis.Windows;assembly=Nicenis"
Title="MainWindow" Height="200" Width="200">
<!-- This border will be draggable.-->
<Border
n:DragSource.AllowDrag="True"
n:DragSource.Data="Test Data"
n:DragSource.AllowedEffects="Copy"
n:DragSource.AllowedInitiators="MouseRightButton"
Margin="30"
Background="Green"
/>
</Window>
当您开始移动元素时,它不会立即开始拖动操作。有一些移动阈值,这些阈值由以下附加属性设置。
- MinimumHorizontalDragDistance:围绕拖动点的矩形的宽度,允许指针有限移动后才开始拖动操作。
- MinimumVerticalDragDistance:围绕拖动点的矩形的高度,允许指针有限移动后才开始拖动操作。
下图显示了这些值如何应用。
- ContactPosition:您按下的第一个坐标。
- DraggedPosition:在开始拖动操作之前,您在矩形内拖动到的坐标。
这些值作为只读附加属性提供。ContactPosition
尤其有用。默认情况下,当被拖动的元素被拖动时,您单击它的位置不会被保留。您可以将 ContactPosition
设置为以下附加属性来解决此问题。
- VisualFeedbackOffset:由指向设备在视觉反馈中指向的偏移量。
以下 XAML 显示了一个矩形,在拖动时会保留单击的位置。
<Window
x:Class="DragSourceSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:n="clr-namespace:Nicenis.Windows;assembly=Nicenis"
Title="MainWindow" Height="200" Width="200">
<!-- This border will be draggable.-->
<Border
n:DragSource.AllowDrag="True"
n:DragSource.Data="Test Data"
n:DragSource.AllowedEffects="Copy"
n:DragSource.VisualFeedbackOffset="{Binding (n:DragSource.ContactPosition), RelativeSource={RelativeSource Self}}"
Margin="30"
Background="Green"
/>
</Window>
为了检查是否离开了虚线矩形,必须监视移动。在大多数情况下,处理移动事件(如 PreviewMouseMove
)就足够了。但如果您尝试在元素边缘附近拖动,则没有捕获设备将无法正常工作。
DragSource
不捕获任何设备。相反,它使用计时器定期检查移动。当鼠标离开元素时启用计时器,当鼠标进入元素时禁用计时器。static void AllowDragProperty_PropertyHost_MouseLeave(object sender, MouseEventArgs e)
{
// Since it is not rely on the capture, MouseMove event can not be used.
// It is required to check cursor position periodically.
GetSafeContext(sender as UIElement).ProcessMoveForDragSensingTimer.Start();
}
static void AllowDragProperty_PropertyHost_MouseEnter(object sender, MouseEventArgs e)
{
// Stops the process move timer.
GetSafeContext(sender as UIElement).ProcessMoveForDragSensingTimer.Stop();
}
以下是 DragSource
中使用的计时器。您可以看到它每 100 毫秒检查一次移动。
DispatcherTimer _processMoveForDragSensingTimer;
public DispatcherTimer ProcessMoveForDragSensingTimer
{
get
{
if (_processMoveForDragSensingTimer == null)
{
_processMoveForDragSensingTimer = new DispatcherTimer();
_processMoveForDragSensingTimer.Interval = TimeSpan.FromMilliseconds(100);
_processMoveForDragSensingTimer.Tick += (_, __) => ProcessDragSensing(_target, Mouse.GetPosition(_target));
}
return _processMoveForDragSensingTimer;
}
}
如果识别出拖动手势,则开始拖动操作,并且以下只读附加属性设置为 true。
- IsDragging:指示拖动是否正在进行。
例如,它可以用于指示已启动拖动操作:
<Window
x:Class="DragSourceSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:n="clr-namespace:Nicenis.Windows;assembly=Nicenis"
Title="MainWindow" Height="200" Width="200">
<!-- This border will be draggable.-->
<Border
n:DragSource.AllowDrag="True"
n:DragSource.Data="Test Data"
n:DragSource.AllowedEffects="Copy"
n:DragSource.VisualFeedbackOffset="{Binding (n:DragSource.ContactPosition), RelativeSource={RelativeSource Self}}"
Margin="30"
>
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<!-- If this border is dragging, sets its background to red. -->
<Trigger Property="n:DragSource.IsDragging" Value="True">
<Setter Property="Background" Value="Red" />
</Trigger>
</Style.Triggers>
<Setter Property="Background" Value="Green" />
</Style>
</Border.Style>
</Border>
</Window>
视觉反馈
默认情况下,DragSource
在您拖动元素时会显示生成的视觉反馈。生成的视觉反馈是通过使用 VisualBrush
创建的。以下是 DragSource
中使用的方法。它返回一个矩形,其 Fill
设置为被拖动元素的 VisualBrush
。
private static FrameworkElement CreateGeneratedContent(UIElement dragSource)
{
// If the drag source is null
if (dragSource == null)
return null;
// Creates a rectangle with a visual brush.
Rectangle rectangle = new Rectangle()
{
StrokeThickness = 0d,
Fill = new VisualBrush(dragSource),
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
};
// Sets the rectangle's width binding.
Binding widthBinding = new Binding("ActualWidth");
widthBinding.Source = dragSource;
rectangle.SetBinding(Rectangle.WidthProperty, widthBinding);
// Sets the rectangle's height binding.
Binding heightBinding = new Binding("ActualHeight");
heightBinding.Source = dragSource;
rectangle.SetBinding(Rectangle.HeightProperty, heightBinding);
// Returns the rectangle.
return rectangle;
}
上述方法中的两个绑定用于正确设置视觉反馈的大小。如果您需要更改视觉反馈的大小,可以使用以下附加属性。
- VisualFeedbackWidth:视觉反馈的宽度。
- VisualFeedbackHeight:视觉反馈的高度。
- VisualFeedbackMinWidth:视觉反馈的最小宽度。
- VisualFeedbackMinHeight:视觉反馈的最小高度。
- VisualFeedbackMaxWidth:视觉反馈的最大宽度。
- VisualFeedbackMaxHeight:视觉反馈的最大高度。
如果您想自定义视觉反馈,可以使用以下附加属性。
- VisualFeedback:一个对象,设置为视觉反馈的内容。
以下 XAML 使用椭圆作为自定义视觉反馈。
<Window
x:Class="DragSourceSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:n="clr-namespace:Nicenis.Windows;assembly=Nicenis"
Title="MainWindow" Height="200" Width="200"
>
<Border
n:DragSource.AllowDrag="True"
n:DragSource.Data="Test Data"
n:DragSource.VisualFeedbackOffset="70 70"
Margin="30"
Background="Green"
>
<!-- An ellipsis is set as a visual feedback.-->
<n:DragSource.VisualFeedback>
<Ellipse Fill="Red" Width="140" Height="140" />
</n:DragSource.VisualFeedback>
</Border>
</Window>
拖动边框时,它的外观会像这样。
- VisualFeedbackDataContext:一个对象,设置为视觉反馈的数据上下文。
此 XAML 使用自定义数据上下文作为视觉反馈。
<Window
x:Class="DragSourceSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:n="clr-namespace:Nicenis.Windows;assembly=Nicenis"
Title="MainWindow" Height="200" Width="200"
>
<Border
n:DragSource.AllowDrag="True"
n:DragSource.Data="Test Data"
n:DragSource.VisualFeedbackOffset="0 0"
n:DragSource.VisualFeedbackDataContext="Custom data context"
Margin="30"
Background="Green"
>
<!-- "Custom data context" is displayed as a visual feedback.-->
<n:DragSource.VisualFeedback>
<TextBlock Text="{Binding}" />
</n:DragSource.VisualFeedback>
</Border>
</Window>
可以使用以下附加属性将数据模板应用于视觉反馈。
- VisualFeedbackTemplate:视觉拖动反馈内容的模板。
- VisualFeedbackTemplateSelector:视觉拖动反馈内容的模板选择器。
此 XAML 使用自定义数据模板作为视觉反馈。
<Window
x:Class="DragSourceSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:n="clr-namespace:Nicenis.Windows;assembly=Nicenis"
Title="MainWindow" Height="200" Width="200"
>
<Border
n:DragSource.AllowDrag="True"
n:DragSource.Data="Test Data"
n:DragSource.VisualFeedbackOffset="0 0"
n:DragSource.VisualFeedback="Custom data template"
Margin="30"
Background="Green"
>
<!-- "Custom data template" is displayed as a visual feedback.-->
<n:DragSource.VisualFeedbackTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</n:DragSource.VisualFeedbackTemplate>
</Border>
</Window>
视觉反馈托管在一个窗口中。宿主窗口必须对任何输入透明。如果您尝试将 Window
类的 IsHitTestVisible
属性设置为 false,您可能会发现它不起作用。唯一的方法是使用 P/Invoke 中的 WS_EX_TRANSPARENT
扩展窗口样式调用 SetWindowLong
函数。这是从 DragSource
中提取的代码。它为视觉反馈创建了一个宿主窗口。
// Creates a host window for the visual drag feedback.
Window hostWindow = new Window()
{
ShowInTaskbar = false,
Topmost = true,
IsHitTestVisible = false,
AllowsTransparency = true,
WindowStyle = WindowStyle.None,
Background = Brushes.Transparent,
SizeToContent = SizeToContent.WidthAndHeight,
WindowStartupLocation = WindowStartupLocation.Manual,
};
hostWindow.SourceInitialized += (_, __) =>
{
// Gets the window handle.
IntPtr hWnd = new WindowInteropHelper(hostWindow).Handle;
// Gets the host window's long ptr.
IntPtr windowLongPtr = Win32.GetWindowLong(hWnd, Win32.GWL_EXSTYLE);
if (windowLongPtr == IntPtr.Zero)
{
Trace.WriteLine("DragSource: GetWindowLongPtr has failed. Error code " + Marshal.GetLastWin32Error());
return;
}
// Ors the WS_EX_TRANSPARENT.
if (IntPtr.Size == 4)
windowLongPtr = (IntPtr)(windowLongPtr.ToInt32() | Win32.WS_EX_TRANSPARENT);
else
windowLongPtr = (IntPtr)(windowLongPtr.ToInt64() | Win32.WS_EX_TRANSPARENT);
// Clears the last error for checking SetWindowLong error.
Win32.SetLastError(0);
// Sets the new long ptr.
if (Win32.SetWindowLong(hWnd, Win32.GWL_EXSTYLE, windowLongPtr) == IntPtr.Zero)
{
int lastWin32Error = Marshal.GetLastWin32Error();
if (lastWin32Error != 0)
{
Trace.WriteLine("DragSource: SetWindowLong has failed. Error code " + lastWin32Error);
return;
}
}
}; // hostWindow.SourceInitialized
将宿主窗口移动以跟随鼠标光标是 WPF 中的另一个问题。由于正在进行拖放操作,因此无法使用 Mouse.GetPosition
方法。根据 MSDN,必须通过 P/Invoke 使用 GetCursorPos
函数。
BOOL WINAPI GetCursorPos
(
// A pointer to a POINT structure that receives the screen coordinates of the cursor.
_Out_ LPPOINT lpPoint
);
它返回一个本机坐标,必须将其转换为设备无关坐标。PresentationSource
类提供了一个 Matrix
来实现此目的。以下是从 DragSource
中提取的方法。它返回一个 Matrix
用于转换。
private static Matrix GetTransformFromDevice(Window window)
{
Debug.Assert(window != null);
// Gets the host window's presentation source.
PresentationSource windowPresentationSource = PresentationSource.FromVisual(window);
if (windowPresentationSource == null)
{
Trace.WriteLine("PresentationSource.FromVisual has failed in DragSource.");
return Matrix.Identity;
}
// Returns the TransformFromDevice matrix.
return windowPresentationSource.CompositionTarget.TransformFromDevice;
}
这是 DragSource
中用于定位宿主窗口的方法。
private void UpdateHostWindowLocation(Point offset)
{
// Gets the current mouse positon.
Win32.POINT cursorPosition;
if (Win32.GetCursorPos(out cursorPosition) == 0)
{
Trace.WriteLine("DragSource: GetCursorPos has failed. Error code " + Marshal.GetLastWin32Error());
return;
}
// Gets the mouse position in device independent coordinate.
Point windowPosition = GetTransformFromDevice(HostWindow).Transform(new Point(cursorPosition.x, cursorPosition.y));
// Applies the offset.
windowPosition.X = windowPosition.X - offset.X;
windowPosition.Y = windowPosition.Y - offset.Y;
// Updates the host window's location.
HostWindow.Left = windowPosition.X;
HostWindow.Top = windowPosition.Y;
}
延迟数据对象创建
创建拖动操作的数据对象时,应考虑所有可能的使用者。在大多数情况下,支持各种数据格式的成本很高。如果您有许多可拖动的项,情况会更糟。因此,应避免在早期阶段创建它。为此提供了 IDataObjectProvider
接口。
namespace Nicenis.Windows
{
/// <summary>
/// Provides a way to get a data object that contains the data being dragged.
/// </summary>
public interface IDataObjectProvider
{
/// <summary>
/// Gets a data object that contains the data being dragged.
/// </summary>
/// <returns>A data object that contains the data being dragged.</returns>
object GetDataObject();
}
}
如果您实现了它并将实现设置到 Data
附加属性,则会在拖动操作开始前调用 GetDataObject
方法。这是一个实现 IDataObjectProvider
的示例数据上下文。
public class SampleDataContext : IDataObjectProvider
{
public object GetDataObject()
{
return "Test Data";
}
}
以下显示了如何将其绑定到 Data
附加属性。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// Initializes the DataContext.
DataContext = new SampleDataContext();
}
}
<Window
x:Class="DragSourceSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:n="clr-namespace:Nicenis.Windows;assembly=Nicenis"
Title="MainWindow" Height="200" Width="200">
<!-- The Data is bound to an implementation of the IDataObjectProvider interface.-->
<Border
n:DragSource.AllowDrag="True"
n:DragSource.Data="{Binding}"
n:DragSource.VisualFeedbackOffset="{Binding (n:DragSource.ContactPosition), RelativeSource={RelativeSource Self}}"
Margin="30"
Background="Green"
/>
</Window>
事件
DragSouce
在拖动操作期间会引发几个带有预览事件的路由事件。第一个是 DragSensing
事件。当用户开始可能触发拖动操作的某个操作时,会引发此事件。
- DragSensing:在拖动手势识别过程中引发的路由事件。
这是一个可取消的事件。如果将事件参数的 Cancel
属性设置为 true,则会阻止拖动操作。
private void Border_DragSensing(object sender, DragSourceDragSensingEventArgs e)
{
// Prevents starting drag operation.
e.Cancel = true;
}
当识别出拖动手势时,会在开始拖动操作之前引发 Dragging
事件。
- Dragging:即将开始拖动时引发的路由事件。
与 DragSensing
一样,它也是一个可取消的事件。事件参数提供了与 DragSource
的附加属性类似但不同的各种属性。因此,您可以使用它来覆盖 DragSource
附加属性设置的大多数值。它允许您在 Dragging
事件处理程序中而不是在 XAML 中创建数据对象。
private void Border_Dragging(object sender, DragSourceDraggingEventArgs e)
{
// Sets a data object that contains the data being dragged.
e.Data = "The Data";
}
如果开始拖动操作,则会定期引发 GiveFeedback
和 QueryContinueDrag
事件。
- GiveFeedback:在引发 DragDrop.GiveFeedback 事件时引发的路由事件。
- QueryContinueDrag:在引发 DragDrop.QueryContinueDrag 事件时引发的路由事件。
正如您在上述描述中看到的,这些事件只是包装事件。它仅提供与 DragSource
相关的其他属性。以下代码更新 VisualFeedback
以显示当前时间。
private void Border_GiveFeedback(object sender, DragSourceGiveFeedbackEventArgs e)
{
// Sets the visual feedback to the current time.
e.VisualFeedback = DateTime.Now.ToString();
}
当拖动操作结束时,会引发 Dragged
事件。
- Dragged:拖放操作完成时引发的路由事件。
您可以使用事件参数的 FinalEffects
属性来检查拖动操作是如何完成的。
private void Border_Dragged(object sender, DragSourceDraggedEventArgs e)
{
if (e.FinalEffects == DragDropEffects.Copy)
{
// The dragged data is copied.
}
if (e.FinalEffects == DragDropEffects.Move)
{
// The dragged data is moved.
}
}
历史
- 25th2015 年 8 月
- DragSource.Dropped 重命名为 DragSource.Dragged。
- DragSource.PreviewDropped 重命名为 DragSource.PreviewDragged。
- 15 日2013 年 9 月
- 初始发布。