WPF拖拽交互全攻略及实现自定义拖拽控件及数据交换技巧解析

news2024/11/26 12:45:27

目录

  • 1. 基本概念
  • 2 . 实现拖拽功能
      • 概述
        • 需要要实现基本的拖放,完成以下任务:
        • 其他操作
      • 示例
        • 3.1 设置拖拽源,拖拽开始
        • 3.2 设置拖拽效果
          • DragDropEffects
        • 3.3 设置放置目标,处理拖拽数据
          • 拖拽输入DragEnter事件
          • DragOver事件
          • 拖拽离开DragLeave事件
          • 拖拽结束Drop事件
  • 3. 其他
    • 实际使用中遇到的问题
    • 实现拖拽交换数据的自定义控件
      • 效果
      • 思路解析
      • 具体实现
  • 参考

1. 基本概念

拖拽(Drag and Drop)是一种常见的用户交互方式、数据传输方法。允许用户通过拖动鼠标来移动或复制数据。
在WPF 中的拖拽操作主要涉及以下几个概念:

  • 拖动源(Drag Source):用户开始拖动的控件。
  • 拖动目标(Drop Target):用户释放拖动的控件。
  • 数据对象(Data Object):封装拖动过程中传递的数据。
  1. 可以通过拖放操作的对象的类型和数量是完全任意的。
    例如,文件、文件夹和内容选择是通过拖放操作操作的一些更常见的对象。
  2. 拖拽源和放置目标可以是同一应用程序或不同应用程序中的UI元素。
  3. 拖放支持在单个应用程序内或不同应用程序之间操作对象。
  4. 还完全支持 WPF 应用程序和其他 Windows 应用程序之间的拖放。
  5. 在 WPF 中,任何 UIElement 或 ContentElement 都可以参与拖放。
    UIElement 和 ContentElement 类包含 DragDrop 附加事件的别名,以便当 UIElement 或 ContentElement 作为基本元素继承时,这些事件会出现在类成员列表中。

2 . 实现拖拽功能

概述

需要要实现基本的拖放,完成以下任务:
  1. 确定将成为拖动源的元素。拖动源可以是 UIElement 或 ContentElement。
  2. 在将启动拖放操作的拖动源上创建一个事件处理程序。该事件通常是 MouseMove 事件。
  3. 在拖动源事件处理程序中,调用 DoDragDrop 方法来启动拖放操作。在 DoDragDrop 调用中,指定拖动源、要传输的数据以及允许的效果。
  4. 确定将成为放置目标的元素。放置目标可以是 UIElement 或 ContentElement。
  5. 在放置目标上,将AllowDrop 属性设置为 。
  6. 在放置目标中,创建一个 Drop 事件处理程序来处理放置的数据。
  7. 在 Drop 事件处理程序中,使用 GetDataPresent 和 GetData 方法从 DragEventArgs 中提取数据。
  8. 在 Drop 事件处理程序中,使用数据执行所需的拖放操作。
其他操作

要在拖动期间执行其他操作,请处理放置目标上的 DragEnter、DragOver 和 DragLeave 事件。

要更改鼠标指针的外观,请处理拖动源上的 GiveFeedback 事件。

要更改取消拖放操作的方式,请处理拖动源上的 QueryContinueDrag 事件。

示例

本节介绍如何实现椭圆元素的拖放。椭圆既是拖动源又是放置目标。传输的数据是椭圆的 Fill 属性的字符串表示形式。

<Ellipse Height="50" Width="50" Fill="Green"
     MouseMove="ellipse_MouseMove"
     GiveFeedback="ellipse_GiveFeedback"
     AllowDrop="True"
     DragEnter="ellipse_DragEnter" 
     DragLeave="ellipse_DragLeave"
     DragOver="ellipse_DragOver" 
     Drop="ellipse_Drop" />
3.1 设置拖拽源,拖拽开始

示例

private void ellipse_MouseMove(object sender, MouseEventArgs e)
{
    Ellipse ellipse = sender as Ellipse;
    if (ellipse != null && e.LeftButton == MouseButtonState.Pressed)
    {
        DragDrop.DoDragDrop(ellipse,
                            ellipse.Fill.ToString(),
                            DragDropEffects.Copy);
    }
}

在 MouseMove 事件处理程序内部,调用 DoDragDrop 方法来启动拖放操作。 DoDragDrop 方法采用三个参数:

  • dragSource – 对作为传输数据源的依赖对象的引用;这通常是 MouseMove 事件的来源。
  • data - - 包含传输数据的对象,包装在 DataObject 中。
    任何可序列化的对象都可以在参数中传递。如果数据尚未包装在 DataObject 中,它将自动包装在新的 DataObject 中。要传递多个数据项,您必须自己创建 DataObject,并将其传递给 DoDragDrop 方法。
// 创建一个新的 DataObject
DataObject dataObject = new DataObject();
// 添加多个数据项
dataObject.SetData("Text", "这是一个文本数据");
dataObject.SetData("Number", 12345);
dataObject.SetData("Date", DateTime.Now);
// 启动拖放操作
DragDropEffects effects = DragDrop.DoDragDrop((DependencyObject)sender, dataObject, DragDropEffects.Copy);
  • allowedEffects - DragDropEffects 枚举值之一,指定允许的拖放操作效果。
3.2 设置拖拽效果

向用户提供有关允许的操作(移动、复制、无)的反馈,并且可以基于附加的用户输入(例如在拖动期间按 ESC 键)取消拖放操作。可以选择处理拖动源上的 GiveFeedback 和 QueryContinueDrag 事件。

DragDropEffects

WPF 定义了一个 DragDropEffects 枚举用于指定拖放操作的效果。它支持按位组合其成员值,以便在拖放操作中表示不同的效果。

DragDropEffects 枚举包含以下成员:

  • None: 值为0,表示放置目标不接受数据。

  • Copy: 值为1,将拖动源中的数据复制到放置目标。

  • Move: 值为2,将拖动源中的数据移动到放置目标。

  • Link: 值为4,将拖动源中的数据链接到放置目标。

  • Scroll: 值为-2147483648,拖动时可以滚动目标,以定位在目标中当前不可见的某个放置位置。

  • All: 值为-2147483645,表示CopyMoveScroll效果的组合。

  • GiveFeedback

  1. GiveFeedback具有默认处理程序,通常可这些事件,除非有特定需要更改它们的默认行为。

  2. 拖动拖动源时,会连续引发 GiveFeedback 事件。

  3. 此事件的默认处理程序检查拖动源是否位于有效的放置目标上方。

    如果是,它会检查放置目标允许的效果。然后,它向最终用户提供有关允许的放置效果的反馈。

    例如:将鼠标光标更改为不可放置、复制或移动光标来完成的。

  4. 如果处理此事件,请务必将其标记为已处理,以便默认处理程序不会覆盖您的处理程序。

private void Element_GiveFeedback(object sender, GiveFeedbackEventArgs e)
{
    if (e.Effects == DragDropEffects.Copy)
    {
        // 更改鼠标光标为复制光标
        Mouse.SetCursor(Cursors.Cross);
    }
    else
    {
        // 更改鼠标光标为不可放置光标
        Mouse.SetCursor(Cursors.No);
    }

    // 标记事件为已处理
    e.Handled = true;
}

  • QueryContinueDrag

拖动拖动源时会连续引发 QueryContinueDrag 事件。

可以处理此事件,以根据 ESC、SHIFT、CTRL 和 ALT 键的状态以及鼠标按钮的状态确定结束拖放操作的操作。

如果按下 ESC 键,此事件的默认处理程序将取消拖放操作;

如果释放鼠标按钮,则删除数据。

这些事件在拖放操作期间不断引发。因此,您应该避免在事件处理程序中执行资源密集型任务。 例如,使用缓存的游标而不是每次引发 GiveFeedback 事件时创建新游标。

private void Element_QueryContinueDrag(object sender, QueryContinueDragEventArgs e)
{
    // 如果按下 ESC 键,则取消拖放操作
    if (e.EscapePressed)
    {
        e.Action = DragAction.Cancel;
    }
    // 如果释放鼠标按钮,则删除数据
    else if (e.KeyStates == DragDropKeyStates.None)
    {
        e.Action = DragAction.Drop;
    else
    {
        e.Action = DragAction.Continue;
    }
    Mouse.SetCursor(Cursors.Arrow);
}

3.3 设置放置目标,处理拖拽数据
  1. 指定它是有效的放置目标
    AllowDrop 属性设置为true
  2. 当拖动源拖过目标时响应拖动源
拖拽输入DragEnter事件

当数据被拖入放置目标的边界时;
不要在 DragEnter 事件中设置 DragEventArgs.Effects 属性,因为它会在 DragOver 事件中被覆盖。
示例

// 定义一个私有变量来保存椭圆的原始填充画笔
private Brush _previousFill = null;

// 椭圆的 DragEnter 事件处理程序
private void ellipse_DragEnter(object sender, DragEventArgs e)
{
    // 将发送者转换为 Ellipse 类型
    Ellipse ellipse = sender as Ellipse;
    // 如果转换成功,即发送者确实是一个椭圆
    if (ellipse != null)
    {
        // 保存当前的填充画笔以便在拖放操作结束后恢复
        _previousFill = ellipse.Fill;

        // 检查拖动的数据对象是否包含可转换为 Brush 的字符串数据
        if (e.Data.GetDataPresent(DataFormats.StringFormat))
        {
            // 提取拖动的数据对象中的字符串数据
            string dataString = (string)e.Data.GetData(DataFormats.StringFormat);

            // 创建一个 BrushConverter 对象用于转换字符串到 Brush
            BrushConverter converter = new BrushConverter();
            // 检查字符串是否可以转换为有效的 Brush
            if (converter.IsValid(dataString))
            {
                // 将字符串转换为 Brush 并应用于椭圆的填充
                Brush newFill = (Brush)converter.ConvertFromString(dataString);
                ellipse.Fill = newFill;
            }
        }
    }
}

GetDataPresent讲解
GetDataPresent 方法用于检查拖动的数据中是否包含指定格式的数据。
它有多个重载,常用的入参类型包括字符串和类型。
以下是对这些入参的详细介绍:

  1. 字符串格式
bool GetDataPresent(string format)
  • 参数: format 是一个字符串,表示要检查的数据格式。
  • 常用值: 可以使用 DataFormats 类中的预定义格式,如 DataFormats.TextDataFormats.BitmapDataFormats.FileDrop 等。
  • 示例:
if (e.Data.GetDataPresent(DataFormats.Text))
{
    // 数据中包含文本格式的数据
}
  1. 类型格式
bool GetDataPresent(Type format)
  • 参数: format 是一个 Type 对象,表示要检查的数据类型。
  • 常用值: 可以使用 typeof 关键字来指定类型,如 typeof(string)typeof(Button) 等。
  • 示例:
if (e.Data.GetDataPresent(typeof(Button)))
{
    // 数据中包含 Button 类型的数据
}
  1. 带有自动转换选项的字符串格式
bool GetDataPresent(string format, bool autoConvert)
  • 参数:
    • format: 一个字符串,表示要检查的数据格式。
    • autoConvert: 一个布尔值,指示是否允许自动转换数据格式。
  • 示例:
if (e.Data.GetDataPresent(DataFormats.Text, true))
{
    // 数据中包含文本格式的数据,允许自动转换
}
  • 总结
    GetDataPresent 方法通过检查拖动的数据中是否包含指定格式的数据,帮助确定拖放操作的有效性。根据不同的需求,可以使用字符串或类型作为参数,甚至可以指定是否允许自动转换数据格式。
DragOver事件
  1. 当数据被拖动到放置目标上时,DragOver 事件会连续发生。
  2. 此事件与拖动源上的 GiveFeedback 事件配对。
  3. 在 DragOver 事件处理程序中,通常使用 GetDataPresent 和 GetData 方法来检查传输的数据是否采用放置目标可以处理的格式。
  4. 可以检查是否按下了任何修饰键,这通常表明用户是否打算执行移动或复制操作。

示例

private void ellipse_DragOver(object sender, DragEventArgs e)
{
    // 默认不允许拖放操作
    e.Effects = DragDropEffects.None;

    // 如果 DataObject 包含字符串数据,则提取它
    if (e.Data.GetDataPresent(DataFormats.StringFormat))
    {
        string dataString = (string)e.Data.GetData(DataFormats.StringFormat);

        // 如果字符串可以转换为 Brush,则允许复制或移动操作
        BrushConverter converter = new BrushConverter();
        if (converter.IsValid(dataString))
        {
            e.Effects = DragDropEffects.Copy | DragDropEffects.Move;
        }
    }
}

  1. 拖拽离开或结束时,检查传输的数据是否采用其可以接收的格式,处理丢弃的数据。
拖拽离开DragLeave事件
private void ellipse_DragLeave(object sender, DragEventArgs e)
{
    // 将 sender 转换为 Ellipse 对象
    Ellipse ellipse = sender as Ellipse;
    
    // 如果转换成功
    if (ellipse != null)
    {
        // 将 Ellipse 的填充色恢复为之前的颜色
        ellipse.Fill = _previousFill;
    }
}

拖拽结束Drop事件
private void ellipse_Drop(object sender, DragEventArgs e)
{
    // 将 sender 转换为 Ellipse 对象
    Ellipse ellipse = sender as Ellipse;
    if (ellipse != null)
    {
        // 如果 DataObject 包含字符串数据,则提取它
        if (e.Data.GetDataPresent(DataFormats.StringFormat))
        {
            string dataString = (string)e.Data.GetData(DataFormats.StringFormat);

            // 如果字符串可以转换为 Brush,
            // 则进行转换并将其应用于 ellipse
            BrushConverter converter = new BrushConverter();
            if (converter.IsValid(dataString))
            {
                Brush newFill = (Brush)converter.ConvertFromString(dataString);
                ellipse.Fill = newFill;
            }
        }
    }
}

3. 其他

实际使用中遇到的问题

  1. DragDrop.DoDragDrop 后不触发鼠标事件

在使用拖住的过程中,也常常用到拖拽并移动控件位置。在我的尝试中,发现**DragDrop.DoDragDrop(_dragDropBorder, dataobject, DragDropEffects.Move);会阻碍_dragDropBorder**的其他鼠标事件。但是我并没找到直接明确的说法。
如以下代码,我平常事在PreviewouseLeftButtonDown中设置拖拽源,拖拽开始。
MouseMove中通过TranslateTransform移动控件位置,发现并不起效,因为没有触发MouseMove事件。

private Point _startPoint;
private TranslateTransform _translateTransform;
public Point _initialTransformPosition;
private void ellipse_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    _startPoint = e.GetPosition(Application.Current.MainWindow);
    if (_translateTransform != null)
    {
        _initialTransformPosition = new Point(_translateTransform.X, _translateTransform.Y);
    }
    else
    {
        _translateTransform = PART_Border?.RenderTransform as TranslateTransform;
        _initialTransformPosition = new Point(0, 0); // 如果没有Transform,则初始位置为(0,0)  
    }

    // 以下代码如果不注释,将影响ellipse_MouseMove执行
    DataObject dataObject = new DataObject();
    dataObject.SetData("Brush", PART_Ellipse.Fill);
    dataObject.SetData("Source", PART_Ellipse);
    DragDrop.DoDragDrop(PART_Ellipse, dataObject, DragDropEffects.All);
}
private void  ellipse_MouseMove(object sender, MouseEventArgs e)
{
    if (e.LeftButton != MouseButtonState.Pressed) return;
    if (_translateTransform == null) return;

    Point currentPoint = e.GetPosition(Application.Current.MainWindow);
    double offsetX = currentPoint.X - _startPoint.X;
    double offsetY = currentPoint.Y - _startPoint.Y;
    double newX = _initialTransformPosition.X + offsetX;
    double newY = _initialTransformPosition.Y + offsetY;

    if (newX == 0 && newY == 0) return;

    _translateTransform.X = newX;
    _translateTransform.Y = newY;
}

我尝试在ellipse_QueryContinueDrag中处理位置移动,效果不理想——椭圆移动轨迹诡异。我暂时没有理清为什么。代码如下:

 private void ellipse_QueryContinueDrag(object sender, QueryContinueDragEventArgs e)
        {
            if (e.KeyStates != DragDropKeyStates.LeftMouseButton) return;
            if (_translateTransform == null) return;

            Point currentPoint = Mouse.GetPosition(Application.Current.MainWindow);
            double offsetX = currentPoint.X - _startPoint.X;
            double offsetY = currentPoint.Y - _startPoint.Y;
            double newX = _initialTransformPosition.X + offsetX;
            double newY = _initialTransformPosition.Y + offsetY;

            if (newX == 0 && newY == 0) return;

            _translateTransform.X = newX;
            _translateTransform.Y = newY;
        }

实现拖拽交换数据的自定义控件

鉴于在DragDrop中使用遇到的问题,我思考如果自己实现该如何编写?于是我自己简陋的实现了一个自定义控件DragBorder。只要用来实现一盒可拖拽移动控件位置,并且拖拽到另一个DragBorder的同时实现数据的合并,合并数据后将拖拽源从父控件中删除的功能。这里实现的比价简陋,只做抛砖引玉石。

在我的文章WPF 实现可拖拽调整顺序的ListView自定义控件_wpf 重写itemscontrol 可以手动拖动元素 任意位置停靠-CSDN博客中,提到了一个项目GongSolutions.WPF.DragDrop。我的实现思路也是来源于这个项目的启发。另外一个题外话是,我自己蛮喜欢的RPA软件影刀也是用了该项目的dll。

效果

在这里插入图片描述

思路解析

  1. 区分控件是拖拽源还是拖拽目标
  2. 当拖拽源拖拽到拖拽目标时,根据鼠标当前位置,获取到拖拽目标。
  3. 并在合理的时机触发拖拽源和拖拽目标的Drag事件、拖拽目标的DragLeave事件、DragOver事件

具体实现

使用MVVM模式,依赖CommunityToolkit.Mvvm、Microsoft.Xaml.Behaviors.Wpf

  • 实现自定义控件,在PreviewMouseLeftDown事件中确定该控件为拖拽源
[RelayCommand]
private void DragStart(MouseEventArgs e)
{
    IsDragSource = true;
    
    //记录移动前的位置和变换数据
    mouseDownPosition = e.GetPosition(Application.Current.MainWindow);
    if (_translateTransform != null)
    {
        initialTransformPosition = new Point(_translateTransform.X, _translateTransform.Y);
    }
    else
    {
        _translateTransform = _source.PART_Border?.RenderTransform as TranslateTransform;
        initialTransformPosition = new Point(0, 0); // 如果没有Transform,则初始位置为(0,0)  
    }
    _source.CaptureMouse();//_source为View,初始化ViewModel时赋值
}
  • MouseMove事件中处理控件变换移动,通过命中测试(Hit Test)在视觉树中查找特定类型的控件(DragBorderControl)来确定拖拽目标,并触发拖拽目标的DragLeave事件、DragOver事件
 internal class HitTestHelper
 {
     public static DragBorderControl GetHitDropControls(Visual visual, Point elementPosition)
     {
         DragBorderControl hitControl = null;

         // 定义命中测试回调方法
         HitTestResultCallback resultCallback = new HitTestResultCallback(result =>
         {
             // 检查命中的控件是否为 DragBorderControl 类型
             if (result.VisualHit is FrameworkElement frameworkElement)
             {
                 var parent = frameworkElement;
                 while (parent != null)
                 {
                     if (parent is DragBorderControl hitItem && !hitItem.ViewModel.IsDragSource)
                     {
                         hitControl = hitItem;
                         return HitTestResultBehavior.Stop;
                     }
                     parent = VisualTreeHelper.GetParent(parent) as FrameworkElement;
                 }
             }
             return HitTestResultBehavior.Continue;
         });

         // 执行命中测试
         VisualTreeHelper.HitTest(visual, null, resultCallback, new PointHitTestParameters(elementPosition));

         return hitControl;
     }

 }
[RelayCommand]
 public void DragMove(MouseEventArgs e)
 {
     if (_source.IsMouseCaptured)
     {
         IsDragging = true;
         if (_translateTransform == null)
         {
             ReleaseMouseCapture(_source);
             return;
         }

         //计算偏移量,移动控件
         Point currentPoint = Mouse.GetPosition(Application.Current.MainWindow);
         double offsetX = currentPoint.X - mouseDownPosition.X;
         double offsetY = currentPoint.Y - mouseDownPosition.Y;
         double newX = initialTransformPosition.X + offsetX;
         double newY = initialTransformPosition.Y + offsetY;

         if (newX == 0 && newY == 0) return;

         _translateTransform.X = newX;
         _translateTransform.Y = newY;

         //是否进入目标区域
         var hitTarget = HitTestHelper.GetHitDropControls(this.ParentControl, Mouse.GetPosition(null));
         if (hitTarget != null && hitTarget != TargetControl)
         {
             //如果目标区域发生变化,则触发DragLeave事件
             if (TargetControl != null && TargetControl.ViewModel.IsDragEnter)
             {
                 TargetControl.ViewModel.IsDragEnter = false;
                 DragLeaveEvent?.Invoke(this, new DragEventArgs(DragData, _source, TargetControl));
             }
             //更新目标区域,触发DragEnter事件
             TargetControl = hitTarget;
             hitTarget.ViewModel.IsDragEnter = true;
             DragEnterEvent?.Invoke(this, new DragEventArgs(DragData,_source, TargetControl));
         }
         else if (hitTarget == null)
         {
             //如果没有进入目标区域,原目标区域触发DragLeave事件
             if (TargetControl != null)
             {
                 TargetControl.ViewModel.IsDragEnter = false;
                 TargetControl.ViewModel.DragLeaveEvent?.Invoke(this, new DragEventArgs(DragData, _source, TargetControl));
                 TargetControl = null;
             }
         }

     }
 }


  • PreviewMouseLeftButtonUp事件中处理拖拽源完成拖拽,处理数据合并、合并后源控件从父类中移除的功能。数据合并部分的功能我实现的比较粗糙,还是WPF的DragDrop实现的好。
[RelayCommand]
public void Drag(MouseButtonEventArgs e)
{
    if (TargetControl != null)
    {
        TargetControl.ViewModel.DragEvent?.Invoke(this, new DragEventArgs(DragData, _source, TargetControl));
    }

    if (_source.IsMouseCaptured)
    {
        ReleaseMouseCapture(_source);
    }
}

private void ReleaseMouseCapture(UIElement element = null)
{
    IsDragging = false;
    IsDragSource = false;
    IsDragEnter = false;
    if (TargetControl != null && TargetControl.ViewModel.IsDragEnter)
    { 
        IsDragEnter = false;
    }
    TargetControl = null;
    if (element != null)
        element.ReleaseMouseCapture();
}
#region Event
private void Drag(object? sender, DragEventArgs e)
{
    if (e.Target != null && this._source ==  e.Target && e.Target.ViewModel.IsDragEnter)
    {
        this.DragData = MerageDragData(e);
        if (e.Source.ViewModel.IsRemoveAfterMerage)
        {
            RemoveAfterMerage(e.Source);
        }
        e.Target.ViewModel.IsDragEnter = false;
    }
}
#endregion
 private IEnumerable<DataItem> MerageDragData(DragEventArgs e)
 {
     var thisDragDataList = EnsureList(this.DragData).Cast<DataItem>().ToList();
     var sourceDragDataList = EnsureList(e.DragData).Cast<DataItem>().ToList();

     thisDragDataList.AddRange(sourceDragDataList);

     return thisDragDataList;
 }

 public List<DataItem> EnsureList(object dragData)
 {
     if (dragData is List<DataItem> existingList)
     {
         return existingList;
     }
     else if (dragData is DataItem singleItem)
     {
         return new List<DataItem> { singleItem };
     }
     else if (dragData is String singleString)
     {
         return new List<DataItem> { new DataItem { Value = singleString, Type = "String" } };
     }
     else if (dragData is List<string> singleStringList)
     {
         return singleStringList.Select(s => new DataItem { Value = s, Type = "String" }).ToList();
     }
     else if (dragData is null)
     {
         return new List<DataItem>();
     }
     else
     {
         // 处理其他类型的数据,可能需要转换  
         throw new ArgumentException("Unsupported drag data type");
     }
 }
 private void RemoveAfterMerage(DragBorderControl source)
 {
     if(source.ParentControl == null)
     {
         return;
     }
     if (source.ParentControl is Panel panel)
     {
         panel.Children.Remove(source);
     }
     else if (source.ParentControl is Grid grid)
     {
         grid.Children.Remove(source);
     }
     else if (source.ParentControl is Canvas canvas)
     {
         canvas.Children.Remove(source);
     }
 }
  • 控件代码
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
    xmlns:local="clr-namespace:DragBorder">
    <Style TargetType="{x:Type local:DragBorderControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:DragBorderControl}">
                    <Border
                        x:Name="PART_Border"
                        Grid.Column="1"
                        Height="Auto"
                        Padding="10"
                        HorizontalAlignment="Stretch"
                        VerticalAlignment="Center"
                        AllowDrop="False"
                        Background="{TemplateBinding Background}"
                        BorderThickness="0"
                        CornerRadius="5"
                        SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
                        <Grid HorizontalAlignment="Stretch">
                            <ContentPresenter
                                x:Name="contentPresenter"
                                Margin="{TemplateBinding Padding}"
                                Content="{TemplateBinding Content}"
                                ContentStringFormat="{TemplateBinding ContentStringFormat}"
                                ContentTemplate="{TemplateBinding ContentTemplate}"
                                ContentTemplateSelector="{TemplateBinding ContentTemplateSelector}" />
                        </Grid>
                        <Border.RenderTransform>
                            <TranslateTransform x:Name="PART_BorderTransform" />
                        </Border.RenderTransform>
                        <i:Interaction.Triggers>
                            <i:EventTrigger EventName="PreviewMouseLeftButtonDown">
                                <i:InvokeCommandAction
                                    Command="{Binding ViewModel.DragStartCommand,
                                                      RelativeSource={RelativeSource FindAncestor,
                                                                                     AncestorType={x:Type local:DragBorderControl}}}"
                                    PassEventArgsToCommand="True" />
                            </i:EventTrigger>
                        </i:Interaction.Triggers>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>                                          
  • 使用Demo
<Window
    x:Class="DragBorder.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:DragBorder"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Window.Resources>
        <local:DragBorderDataTemplateSelector x:Key="DataTemplateSelector">
            <local:DragBorderDataTemplateSelector.StringTemplate>
                <DataTemplate>
                    <TextBlock
                        Text="{Binding Value,
                                       StringFormat='String: {0}'}"
                        TextWrapping="NoWrap" />
                </DataTemplate>
            </local:DragBorderDataTemplateSelector.StringTemplate>
            <local:DragBorderDataTemplateSelector.IntTemplate>
                <DataTemplate>
                    <TextBlock
                        Width="200"
                        Height="200"
                        Text="{Binding Value,
                                       StringFormat='Int: {0}'}"
                        TextWrapping="NoWrap" />
                </DataTemplate>
            </local:DragBorderDataTemplateSelector.IntTemplate>
        </local:DragBorderDataTemplateSelector>
    </Window.Resources>

    <StackPanel
        x:Name="Root"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
        Orientation="Horizontal">
        <local:DragBorderControl
            x:Name="DragBorder1"
            Background="Red"
            DragData="{Binding Data1}"
            IsRemoveAfterMerage="True"
            ParentControl="{Binding ElementName=Root}">
            <ListView
                ItemTemplateSelector="{StaticResource DataTemplateSelector}"
                ItemsSource="{Binding ElementName=DragBorder1,
                                      Path=DragData}" />
        </local:DragBorderControl>
        <local:DragBorderControl
            x:Name="DragBorder2"
            Background="Green"
            DragData="{Binding Data2}"
            IsRemoveAfterMerage="True"
            ParentControl="{Binding ElementName=Root}">
            <ListView
                ItemTemplateSelector="{StaticResource DataTemplateSelector}"
                ItemsSource="{Binding ElementName=DragBorder2,
                                      Path=DragData}" />
        </local:DragBorderControl>
    </StackPanel>
</Window>

public partial class MainWindow : Window
{
    public MainWindow()
    {
        DataContext = this;
        InitializeComponent();
    }
    public List<string> Data1 { get; set; } = new List<string>() { "Data from DragBorder1" };
    public List<string> Data2 { get; set; } = new List<string>() { "DragBorder2" };
}

参考

  1. Drag and Drop Overview - WPF .NET Framework | Microsoft Learn
  2. (19) Setting Up Drag Drop - WPF DRAG DROP TUTORIAL #1 - YouTube
  3. 主 wpf-tutorials/DragDropDemo ·SingletonSean/wpf-tutorials — wpf-tutorials/DragDropDemo at master · SingletonSean/wpf-tutorials
  4. 本文自定义实现控件源码

备注

  • 链接2分了5期视频详细讲解了DragDrop并由提供源码,感兴趣的同学可以前去查看。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2228289.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【jvm】为什么Xms和Xmx的值通常设置为相同的?

目录 1. 说明2. 避免性能开销3. 提升稳定性4. 简化配置5. 优化垃圾收集6. 获取参数6.1 代码示例6.2 结果示例 1. 说明 1.-Xms 和 -Xmx 参数分别用于设置堆内存的初始大小&#xff08;最小值&#xff09;和最大大小。2.在开发环境中&#xff0c;开发人员可能希望快速启动应用程…

c++提示函数可能不安全This function or variable may be unsafe

现象 编译c代码时&#xff0c;经常会出现如下提示&#xff0c;大约有几种情况&#xff1a; 现象一 warning C4996: strcpy: This function or variable may be unsafe. Consider using strcpy_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online …

苹果ANE引擎以及使用编程方法介绍

苹果ANE引擎介绍 大多数新款iPhone和iPad都配备了一个神经网络引擎&#xff08;Neural Engine&#xff09;&#xff0c;这是一种特殊的处理器&#xff0c;能让机器学习模型的运行速度变得非常快。然而&#xff0c;关于这个处理器具体如何工作&#xff0c;公众所知的信息并不多…

Python | Leetcode Python题解之第516题最长回文子序列

题目&#xff1a; 题解&#xff1a; class Solution:def longestPalindromeSubseq(self, s: str) -> int:n len(s)dp [[0] * n for _ in range(n)]for i in range(n - 1, -1, -1):dp[i][i] 1for j in range(i 1, n):if s[i] s[j]:dp[i][j] dp[i 1][j - 1] 2else:dp…

网络编程 UDP编程 Linux环境 C语言实现

UDP编程 1. 一般UDP编程 UDP传输特点&#xff1a;非面向连接、不可靠的、无序的 报式传输 支持组播和广播 UDP应用数据最大长度建议&#xff1a;MTU(以太网分组数据的最大长度)1500 - 20(IP头) - 8(UDP头) 1472Bytes 客户端&#xff1a;支持两种形式的代码编写: 1. 不定向…

高压线路覆冰厚度测量,输电线路微波覆冰监测装置守护电网安全

随着北方地区正式步入冬季&#xff0c;气温急剧下降&#xff0c;高压线路覆冰现象日益严重&#xff0c;给电网安全带来了前所未有的挑战。近日&#xff0c;在东北某省&#xff0c;由于连续低温天气&#xff0c;多条高压线路遭受了严重的覆冰侵袭&#xff0c;这不仅极大地加重了…

Java面试经典 150 题.P88. 合并两个有序数组(001)

本题来自&#xff1a;力扣-面试经典 150 题 面试经典 150 题 - 学习计划 - 力扣&#xff08;LeetCode&#xff09;全球极客挚爱的技术成长平台https://leetcode.cn/studyplan/top-interview-150/ 题解 class Solution {public void merge(int[] nums1, int m, int[] nums2, …

基于安卓Android的健身系统APP(源码+文档+部署+讲解)

&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 选题不知道怎么选 不清楚自己适合做哪块内容 都可以免费来问我 会持续一直更新下去 有问必答 一键收藏关注不迷路 源码获取&#xff1a;https://pan.baidu.…

中电信翼康工程师:我在 Apache SeaTunnel 社区的贡献之旅

贡献者Github ID&#xff1a;luckyLJY 文章整理&#xff1a;曾辉 Apache SeaTunnel 作为一款强大的数据同步和转换工具&#xff0c;凭借其部署易用性、容错机制、数据源支持、性能优势、功能丰富性以及活跃的社区支持&#xff0c;成为了数据工程师们不可或缺的利器。 因其具有的…

offset Explorer连接云服务上的kafka连接不上

以上配置后报连接错误时&#xff0c;可能是因为kafka的server.properties配置文件没配置好&#xff1a; 加上面两条配置&#xff0c;再次测试连接&#xff0c;成功 listeners和advertised.listeners

使用Python来下一场雪

具体效果&#xff1a;&#xff08;大雪缓缓下落&#xff09; 完整代码&#xff1a; import pygame import random# 初始化 Pygame pygame.init()# 设置窗口 width, height 800, 600 screen pygame.display.set_mode((width, height)) pygame.display.set_caption("下雪…

k8s 1.28.2 集群部署 Thanos 对接 MinIO 实现 Prometheus 数据长期存储

文章目录 [toc]什么是 ThanosThanos 的主要功能Thanos 的架构组件Thanos 部署架构SidecarReceive架构选择 开始部署部署架构创建 namespacenode-exporter 部署kube-state-metrics 部署Prometheus Thanos-Sidecar 部署固定节点创建 label生成 secretMinIO 配置etcd 证书 启动 P…

13、基于AT89C52轮询方式扫描按键proteus仿真

一、仿真原理图: 二、仿真效果: 三、相关代码: 1、main主函数: void main(void) { unsigned char ll; VariableInit(); while(1) { value = keyScan(); display(); …

春季测试 2023 我的题解

T1 涂色游戏 这道题目还是比较简单的 容易发现&#xff0c;位于 ( x i , y i ) (x_i,y_i) (xi​,yi​) 的格子的颜色只取决于 ​ x i x_i xi​ 行与 y i y_i yi​ 列的颜色。 这时候可以想到开两个数组&#xff0c;分别存储列与行的绘画信息&#xff0c;然后发现前后的互相…

magic-api简单使用二:自定义返回结果

背景 在上一章 中我们学习了搭建项目和导入文件&#xff0c; 这二天稍微有点时间&#xff0c;研究下这个magic-api的写法。 后续如果需要维护或者更改&#xff0c;也能在项目中尽快上手。 今天我们主要学习自定义返回结果&#xff0c;当然也可以使用官方的。不需要任何更改。…

“启动智能制造引擎 推动新质生产力发展”高峰论坛在京成功举办

在智能制造技术的助力下,北京经开区的智能制造产业稳步发展。2024年10月25日,北京电子科技职业学院图书馆暨北京经济技术开发区公共图书馆举办一场以“启动智能制造引擎 推动新质生产力发展”为主题的高峰论坛于北京亦庄经开区盛元书院圆满落幕。此次论坛为北京经开区智能制造产…

Java 异常处理(6/30)

目录 Java 异常处理 1. 什么是异常 2. 异常处理的关键字 2.1 try-catch 语句 2.2 多个 catch 块 2.3 finally 块 2.4 throw 和 throws 3. 自定义异常 4. 异常处理的最佳实践 总结与后续 Java 异常处理 在软件开发中&#xff0c;异常处理&#xff08;Exception Handl…

ubuntu22.04安装向日葵

1、下载deb安装包 进入官网下载图形版本&#xff1a;https://sunlogin.oray.com/download/linux?typepersonal 2、命令行安装 sudo chmod x 文件名.deb sudo dpkg -i 文件名.deb 3、开始报错的看这里&#xff01; 首先展示一下安装成功的效果图&#xff1a; 接下来是我安…

小米手机投屏到Windows笔记本电脑的3个方法,随便选一个

方法一&#xff1a;Windows系统自带的投屏功能 Windows系统本身的功能很多&#xff0c;其中一项就是接收别的电脑或手机的投屏。 操作步骤一&#xff0c;在Windows电脑里进入【设置】&#xff0c;点击【系统】&#xff0c;往下翻页&#xff0c;找到【投影到此电脑】。 如果这…

漏洞挖掘 | 通过域混淆绕过实现账户接管

由于这是一个私有项目&#xff0c;我将使用 example.com 来代替。 很长一段时间以来&#xff0c;我一直想在漏洞赏金项目中找到一个账户接管&#xff08;ATO&#xff09;漏洞。于是&#xff0c;我开始探索项目范围内的 account.example.com。 我做的第一件事就是注册一个新账…