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

使用 WPF 中的 DragSource 类拖动元素

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.70/5 (8投票s)

2013 年 9 月 14 日

Ms-PL

8分钟阅读

viewsIcon

36441

downloadIcon

625

在 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 提供了 DataAllowedEffects 附加属性。因此,您可以使用以下 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)就足够了。但如果您尝试在元素边缘附近拖动,则没有捕获设备将无法正常工作。

捕获设备并不便宜。例如,一次只能有一个元素捕获鼠标。因此,如果捕获设备来监视移动,则很可能会破坏其他 UI 逻辑。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";
}

如果开始拖动操作,则会定期引发 GiveFeedbackQueryContinueDrag 事件。

  • 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 月
    • 初始发布。
© . All rights reserved.