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

目录

  • 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 对象用于转换字符串到 BrushBrushConverter converter = new BrushConverter();// 检查字符串是否可以转换为有效的 Brushif (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,// 则进行转换并将其应用于 ellipseBrushConverter 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;}
}
#endregionprivate 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);}}
  • 控件代码
<ResourceDictionaryxmlns="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}"><Borderx: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"><ContentPresenterx: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:InvokeCommandActionCommand="{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
<Windowx: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><TextBlockText="{Binding Value,StringFormat='String: {0}'}"TextWrapping="NoWrap" /></DataTemplate></local:DragBorderDataTemplateSelector.StringTemplate><local:DragBorderDataTemplateSelector.IntTemplate><DataTemplate><TextBlockWidth="200"Height="200"Text="{Binding Value,StringFormat='Int: {0}'}"TextWrapping="NoWrap" /></DataTemplate></local:DragBorderDataTemplateSelector.IntTemplate></local:DragBorderDataTemplateSelector></Window.Resources><StackPanelx:Name="Root"HorizontalAlignment="Center"VerticalAlignment="Center"Orientation="Horizontal"><local:DragBorderControlx:Name="DragBorder1"Background="Red"DragData="{Binding Data1}"IsRemoveAfterMerage="True"ParentControl="{Binding ElementName=Root}"><ListViewItemTemplateSelector="{StaticResource DataTemplateSelector}"ItemsSource="{Binding ElementName=DragBorder1,Path=DragData}" /></local:DragBorderControl><local:DragBorderControlx:Name="DragBorder2"Background="Green"DragData="{Binding Data2}"IsRemoveAfterMerage="True"ParentControl="{Binding ElementName=Root}"><ListViewItemTemplateSelector="{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.rhkb.cn/news/460413.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ubuntu 22.04网线连接无ip、网络设置无有线网界面(netplan修复)

目前遇到过树莓派和其他设备安装 ubuntu22.04&#xff0c; 使用有线网络一段时间&#xff08;可能有其他软件安装导致&#xff09;造成有线网络未启动无ip分配的问题。 1、动态分配 通过命令行启动dhcpclient实现 网络eth0存在异常&#xff0c;网口灯电源和信号灯均点亮&am…

开关灯问题(c语言)

样例&#xff1a;10 10 &#xff0c;输出&#xff1a;1&#xff0c;4&#xff0c;9 5 5 &#xff0c;输出&#xff1a;1&#xff0c;4 代码如下 #include<stdio.h> //引入bool值的概念 #include<stdbool.h> int main() {int n 0;//n为灯的数量int m 0;…

【Android14 ShellTransitions】(八)播放动画

书接上回&#xff0c;话说当WMCore部分走到了Transition.onTransactionReady&#xff0c;计算完参与动画的目标&#xff0c;构建出TransitionInfo后&#xff0c;接下来就把这个包含了动画参与者的TransitionInfo发给了WMShell&#xff0c;然后就该播放动画了&#xff0c;这部分…

git快速合并代码dev->master

需求&#xff1a; 日常开发都是在dev分支进行开发&#xff0c;但是dev代码开发测试完成后&#xff0c;需要将dev代码合到master主分支上 开始合并代码&#xff1a; 一、场景 一个代码仓库&#xff0c;包含两个分支&#xff0c;一个是master&#xff0c;另一个是dev&#xff1b…

gitblit 学习-hook功能

hook功能 hook是什么 git hooks是git提供的&#xff0c;在发生特定事件时&#xff0c;允许用户添加自定义代码&#xff08;或操作&#xff09;的方式。 就像Vue中组件的生命周期钩子&#xff0c;比如&#xff0c;你想在vue组件创建后输出一行log, 你可能会这么写 有什么用&a…

Android Input的流程和原理

Android Input事件机制 Android系统是由事件驱动的&#xff0c;而Input是最常见的事件之一&#xff0c;用户的点击、滑动、长按等操作&#xff0c;都属于Input事件驱动&#xff0c;其中的核心就是InputReader和InputDispatcher。InputReader和InputDispatcher是跑在system_serv…

Jmeter基础篇(19)JSR223预处理器

前言 JSR223预处理器是Apache JMeter中的一个组件&#xff0c;它允许用户使用任何支持Java Scripting API (JSR 223) 的脚本语言来执行预处理任务。这个功能非常强大&#xff0c;因为它让测试人员能够利用如Groovy、JavaScript&#xff08;Nashorn引擎&#xff09;、BeanShell…

轻松实现金蝶与旺店通数据无缝对接的完整解决方案

【金蝶】采购入库单对接【旺店通】委外入库单_合并 在企业信息化系统中&#xff0c;数据的高效流转和准确对接是确保业务顺畅运行的关键。本文将分享一个实际案例&#xff0c;展示如何通过轻易云数据集成平台实现金蝶云星空与旺店通企业奇门之间的数据无缝对接。具体方案为&am…

基于SpringBoot的汽车票网上预订系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

前端好用的网站分享——CSS(持续更新中)

1.CSS Scan 点击进入CSS Scan CSS盒子阴影大全 2.渐变背景 点击进入color.oulu 3.CSS简化压缩 点击进入toptal 4.CSS可视化 点击进入CSS可视化 这个强推&#xff0c;话不多说&#xff0c;看图! 5.Marko 点击进入Marko 有很多按钮样式 6.getwaves 点击进入getwaves 生…

【K8S系列】Kubernetes 中 Service IP 分配 问题及解决方案【已解决】

在 Kubernetes 中&#xff0c;LoadBalancer 类型的 Service 允许用户轻松地将应用暴露给外部流量。它自动创建一个云负载均衡器并分配一个外部 IP 地址。然而&#xff0c;在某些情况下&#xff0c;LoadBalancer 类型的 Service 可能未能成功分配 IP 地址&#xff0c;导致外部无…

Tomcat servlet response关于中文乱码的经验

前言 最近修改老项目项目&#xff0c;使用zuul网关返回的中文内容乱码了&#xff0c;如果使用GBK或者GB2312编码确正常显示&#xff0c;稍微实验了一下&#xff0c;发现里面很多细节&#xff0c;毕竟Springboot对我们做了很多事情&#xff0c;而且当我们使用不同的模式会出现很…

微服务之间调用,OpenFeign传递用户(RequestInterceptor接口)

场景&#xff1a;微服务之黑马商城项目-登录拦截器在网关完成用户的校验&#xff0c;并将用户信息&#xff08;用户id&#xff09;存入请求头&#xff0c;假设将购物车里面的商品进行结算就会生成订单并清空购物车&#xff0c;这里涉及到了交易服务模块远程调用购物车模块&…

Java中String的length与Oracle数据库中VARCHAR2实际存储长度不一致的问题

目录 一、根本原因 二、解决方案 一、根本原因 Oracle数据库新增数据的时候报如下错误&#xff1a; 先给大家看个小案例&#xff0c;这样更好去理解&#xff0c;下面是一段测试代码&#xff1a; 这里面我分别列举了三种字符串&#xff0c;中文&#xff0c;英文和数字以及两种…

探索 CrewAI:引领多智能体协作的未来

探索 CrewAI&#xff1a;引领多智能体协作的未来 在人工智能领域&#xff0c;如何让多个智能体协同工作以解决复杂问题一直是一个热门话题。CrewAI 作为一个前沿的框架&#xff0c;正是为了解决这一挑战而生。它通过角色扮演和自主智能体的协作&#xff0c;赋予了智能体无缝合…

ViSual studio如何安装 并使用GeographicLib

在C的 Boost.Geometry、GDAL/OGR 和 GeographicLib。这些库都可以用于计算两个经纬度点之间的地面距离。 . Boost.Geometry 描述&#xff1a;Boost库的一部分&#xff0c;提供了几何计算功能&#xff0c;包括计算两点之间的地面距离。 优势&#xff1a;轻量级、易于集成到C项…

网站攻击,XSS攻击的类型

XSS&#xff08;跨站脚本&#xff09;攻击是一种网络安全攻击方式&#xff0c;攻击者通过在网站页面中注入恶意脚本&#xff0c;使脚本在其他用户的浏览器中执行&#xff0c;从而窃取用户信息、篡改页面内容或操控用户账户。这类攻击通常利用网站对输入数据的过滤不严格&#x…

接口测试加密了怎么测试

各位小伙伴&#xff0c;大家好&#xff0c;今天给大家带来的是接口测试加密了我们该怎么测试。 首先我们来了解一下什么是接口&#xff0c;在我们生活中&#xff0c;水管接口&#xff0c;管道接口等等&#xff0c;在我们软件中通常说的是客户端和服务端之间数据传输的接口。我…

尽可能连续的基于挤压的表面模型制造

&#x1f98c;&#x1f98c;&#x1f98c;宝子们好&#xff0c;今天我们来看一个关于外壳打印的路径优化问题。同样的&#xff0c;红色的是术语、橙色的是方法、绿色的是算法过程&#xff0c;这次多加了紫色&#xff0c;是文章的创新点或目标等。废话不多说&#xff0c;上论文 …

linux驱动—在自己的总线目录下创建属性文件

在总线目录下创建属性文件以扩展其功能。 通过创建属性文件&#xff0c; 我们可以为总线添加额外的信息和控制选项&#xff0c; 以便与设备和驱动进行交互。 简单就是&#xff0c;属性文件&#xff0c;可以完成用户空间和内核空间的数据交互&#xff0c; 比如在应用层快速修改g…