前言

在Canvas中放置了一些元素,需要能够拖拉这些元素,在WPF Samples中的DragDropObjects项目中告诉了我们如何实现这种效果。

效果如下所示:

拖拉过程中的效果如下所示:

具体实现

xaml页面

我们先来看看xaml:

 <Canvas Name="MyCanvas"
PreviewMouseLeftButtonDown="MyCanvas_PreviewMouseLeftButtonDown"
PreviewMouseMove="MyCanvas_PreviewMouseMove"
PreviewMouseLeftButtonUp="MyCanvas_PreviewMouseLeftButtonUp">
<Rectangle Fill="Blue" Height="32" Width="32" Canvas.Top="8" Canvas.Left="8"/>
<TextBox Text="This is a TextBox. Drag and drop me" Canvas.Top="100" Canvas.Left="100"/>
</Canvas>

为了实现这个效果,在Canvas上使用了三个隧道事件(预览事件)PreviewMouseLeftButtonDownPreviewMouseMovePreviewMouseLeftButtonUp

而什么是隧道事件(预览事件)呢?

预览事件,也称为隧道事件,是从应用程序根元素向下遍历元素树到引发事件的元素的路由事件。

PreviewMouseLeftButtonDown当用户按下鼠标左键时触发。

PreviewMouseMove当用户移动鼠标时触发。

PreviewMouseLeftButtonUp当用户释放鼠标左键时触发。

再来看看cs:

 private bool _isDown;
private bool _isDragging;
private UIElement _originalElement;
private double _originalLeft;
private double _originalTop;
private SimpleCircleAdorner _overlayElement;
private Point _startPoint;

定义了这几个私有字段。

鼠标左键按下事件处理程序

鼠标左键按下事件处理程序:

 private void MyCanvas_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.Source == MyCanvas)
{
}
else
{
_isDown = true;
_startPoint = e.GetPosition(MyCanvas);
_originalElement = e.Source as UIElement;
MyCanvas.CaptureMouse();
e.Handled = true;
}
}

最开始引发这个事件的是MyCanvas元素,当事件源是Canvas的时候,不做处理,因为我们只想处理发生在MyCanvas子元素上的鼠标左键按下事件。

鼠标移动事件处理程序

现在来看看鼠标移动事件处理程序:

  private void MyCanvas_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (_isDown)
{
if ((_isDragging == false) &&
((Math.Abs(e.GetPosition(MyCanvas).X - _startPoint.X) >
SystemParameters.MinimumHorizontalDragDistance) ||
(Math.Abs(e.GetPosition(MyCanvas).Y - _startPoint.Y) >
SystemParameters.MinimumVerticalDragDistance)))
{
DragStarted();
}
if (_isDragging)
{
DragMoved();
}
}
}

鼠标左键已经按下了,但还没开始移动事,执行DragStarted方法。

创建装饰器

DragStarted方法如下:

 private void DragStarted()
{
_isDragging = true;
_originalLeft = Canvas.GetLeft(_originalElement);
_originalTop = Canvas.GetTop(_originalElement); _overlayElement = new SimpleCircleAdorner(_originalElement);
var layer = AdornerLayer.GetAdornerLayer(_originalElement);
layer.Add(_overlayElement);
}
_overlayElement = new SimpleCircleAdorner(_originalElement);

创建了一个新的装饰器(Adorner)并将其与一个特定的UI元素关联起来。

而WPF中装饰器是什么呢?

装饰器是一种特殊类型的 FrameworkElement,用于向用户提供视觉提示。 装饰器有很多用途,可用来向元素添加功能句柄,或者提供有关某个控件的状态信息。

Adorner 是绑定到 UIElement 的自定义 FrameworkElement。 装饰器在 AdornerLayer 中呈现,它是始终位于装饰元素或装饰元素集合之上的呈现表面。 装饰器的呈现独立于装饰器绑定到的 UIElement 的呈现。 装饰器通常使用位于装饰元素左上部的标准 2D 坐标原点,相对于其绑定到的元素进行定位。

装饰器的常见应用包括:

  • UIElement 添加功能句柄,使用户能够以某种方式操作元素(调整大小、旋转、重新定位等)。
  • 提供视觉反馈以指示各种状态,或者响应各种事件。
  • UIElement 上叠加视觉装饰。
  • 以视觉方式遮盖或覆盖 UIElement 的一部分或全部。

Windows Presentation Foundation (WPF) 为装饰视觉元素提供了一个基本框架。

在这个Demo中装饰器就是移动过程中四个角上出现的小圆以及内部不断闪烁的颜色,如下所示:

这是如何实现的呢?

这个Demo中自定义了一个继承自Adorner的SimpleCircleAdorner,代码如下所示:

using System;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes; namespace DragDropObjects
{
public class SimpleCircleAdorner : Adorner
{
private readonly Rectangle _child;
private double _leftOffset;
private double _topOffset;
// Be sure to call the base class constructor.
public SimpleCircleAdorner(UIElement adornedElement)
: base(adornedElement)
{
var brush = new VisualBrush(adornedElement); _child = new Rectangle
{
Width = adornedElement.RenderSize.Width,
Height = adornedElement.RenderSize.Height
}; var animation = new DoubleAnimation(0.3, 1, new Duration(TimeSpan.FromSeconds(1)))
{
AutoReverse = true,
RepeatBehavior = RepeatBehavior.Forever
};
brush.BeginAnimation(Brush.OpacityProperty, animation); _child.Fill = brush;
} protected override int VisualChildrenCount => 1; public double LeftOffset
{
get { return _leftOffset; }
set
{
_leftOffset = value;
UpdatePosition();
}
} public double TopOffset
{
get { return _topOffset; }
set
{
_topOffset = value;
UpdatePosition();
}
} // A common way to implement an adorner's rendering behavior is to override the OnRender
// method, which is called by the layout subsystem as part of a rendering pass.
protected override void OnRender(DrawingContext drawingContext)
{
// Get a rectangle that represents the desired size of the rendered element
// after the rendering pass. This will be used to draw at the corners of the
// adorned element.
var adornedElementRect = new Rect(AdornedElement.DesiredSize); // Some arbitrary drawing implements.
var renderBrush = new SolidColorBrush(Colors.Green) {Opacity = 0.2};
var renderPen = new Pen(new SolidColorBrush(Colors.Navy), 1.5);
const double renderRadius = 5.0; // Just draw a circle at each corner.
drawingContext.DrawRectangle(renderBrush, renderPen, adornedElementRect);
drawingContext.DrawEllipse(renderBrush, renderPen, adornedElementRect.TopLeft, renderRadius, renderRadius);
drawingContext.DrawEllipse(renderBrush, renderPen, adornedElementRect.TopRight, renderRadius, renderRadius);
drawingContext.DrawEllipse(renderBrush, renderPen, adornedElementRect.BottomLeft, renderRadius, renderRadius);
drawingContext.DrawEllipse(renderBrush, renderPen, adornedElementRect.BottomRight, renderRadius,
renderRadius);
} protected override Size MeasureOverride(Size constraint)
{
_child.Measure(constraint);
return _child.DesiredSize;
} protected override Size ArrangeOverride(Size finalSize)
{
_child.Arrange(new Rect(finalSize));
return finalSize;
} protected override Visual GetVisualChild(int index) => _child; private void UpdatePosition()
{
var adornerLayer = Parent as AdornerLayer;
adornerLayer?.Update(AdornedElement);
} public override GeneralTransform GetDesiredTransform(GeneralTransform transform)
{
var result = new GeneralTransformGroup();
result.Children.Add(base.GetDesiredTransform(transform));
result.Children.Add(new TranslateTransform(_leftOffset, _topOffset));
return result;
}
}
}
  var animation = new DoubleAnimation(0.3, 1, new Duration(TimeSpan.FromSeconds(1)))
{
AutoReverse = true,
RepeatBehavior = RepeatBehavior.Forever
};
brush.BeginAnimation(Brush.OpacityProperty, animation);

这里在元素内部添加了动画。

 // Just draw a circle at each corner.
drawingContext.DrawRectangle(renderBrush, renderPen, adornedElementRect);
drawingContext.DrawEllipse(renderBrush, renderPen, adornedElementRect.TopLeft, renderRadius, renderRadius);
drawingContext.DrawEllipse(renderBrush, renderPen, adornedElementRect.TopRight, renderRadius, renderRadius);
drawingContext.DrawEllipse(renderBrush, renderPen, adornedElementRect.BottomLeft, renderRadius, renderRadius);
drawingContext.DrawEllipse(renderBrush, renderPen, adornedElementRect.BottomRight, renderRadius,
renderRadius);

这里在元素的四个角画了小圆形。

  var layer = AdornerLayer.GetAdornerLayer(_originalElement);
layer.Add(_overlayElement);

这段代码的作用是将之前创建的装饰器_overlayElement添加到与特定UI元素_originalElement相关联的装饰器层(AdornerLayer)中。一旦装饰器被添加到装饰器层中,它就会在_originalElement被渲染时显示出来。

AdornerLayer是一个特殊的层,用于在UI元素上绘制装饰器。每个UI元素都有一个与之关联的装饰器层,但并不是所有的UI元素都能直接看到这个层。

GetAdornerLayer方法会返回与_originalElement相关联的装饰器层。

装饰器层会负责管理装饰器的渲染和布局,确保装饰器正确地显示在UI元素上。

再来看看DragMoved方法:

 private void DragMoved()
{
var currentPosition = Mouse.GetPosition(MyCanvas); _overlayElement.LeftOffset = currentPosition.X - _startPoint.X;
_overlayElement.TopOffset = currentPosition.Y - _startPoint.Y;
}

计算元素的偏移。

鼠标左键松开事件处理程序

鼠标左键松开事件处理程序:

  private void MyCanvas_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (_isDown)
{
DragFinished();
e.Handled = true;
}
}

DragFinished方法如下:

 private void DragFinished(bool cancelled = false)
{
Mouse.Capture(null);
if (_isDragging)
{
AdornerLayer.GetAdornerLayer(_overlayElement.AdornedElement).Remove(_overlayElement); if (cancelled == false)
{
Canvas.SetTop(_originalElement, _originalTop + _overlayElement.TopOffset);
Canvas.SetLeft(_originalElement, _originalLeft + _overlayElement.LeftOffset);
}
_overlayElement = null;
}
_isDragging = false;
_isDown = false;
}
 AdornerLayer.GetAdornerLayer(_overlayElement.AdornedElement).Remove(_overlayElement);

从与_overlayElement所装饰的UI元素相关联的装饰器层中移除_overlayElement,从而使得装饰器不再显示在UI元素上。这样,当UI元素被渲染时,装饰器将不再影响其外观或行为。

代码来源

[WPF-Samples/Drag and Drop/DragDropObjects at main · microsoft/WPF-Samples (github.com)](https://github.com/microsoft/WPF-Samples/tree/main/Drag and Drop/DragDropObjects)

参考

1、预览事件 - WPF .NET | Microsoft Learn

2、装饰器概述 - WPF .NET Framework | Microsoft Learn

3、[Adorner 类 (System.Windows.Documents) | Microsoft Learn](

WPF/C#:如何实现拖拉元素的更多相关文章

  1. WPF笔记(1.3 属性元素)——Hello,WPF!

    原文:WPF笔记(1.3 属性元素)--Hello,WPF! 这一节中“属性元素”的概念可以用匪夷所思形容.1.WPF用标签元素实现对象建模,有两种:Control和Container,都用来装载内容 ...

  2. WPF学习系列之六 (元素绑定)

    元素绑定 简单地说,数据绑定是一种关系,该关系告诉WPF从一个源对象提取一些信息,并使用这些信息设置目标对象的属性.目标属性总是依赖属性,并且通常位于WPF元素中. 一.将元素绑定到一起 <Wi ...

  3. 每日分享!~ 使用js原生方式对拖拉元素(鼠标的事件)

    一个元素放置页面上.如何进行拖拉,实现想放哪里就放哪里的效果呢? 效果如下: 如果让你写这个效果,你会如何写呢? --- 思路分析:我首先想到的是,对这个元素先绑定一个事件.(什么事件? 那当然是鼠标 ...

  4. WPF中异步更新UI元素

    XAML 界面很简单,只有一个按钮和一个lable元素,要实现点击button时,lable的内容从0开始自动递增. <Grid> <Label Name="lable_p ...

  5. wpf,visibility属性的多元素绑定及值转换

    visibility实现多元素绑定. 实现多绑定转换 public class VisibilityConverter : IMultiValueConverter { public object C ...

  6. wpf 寻找TreeView的子元素,并对其进行操作

    //itemsControl 开始为指定的TreeView控件  item为TreeView子元素 private void PareItems(ItemsControl itemsControl, ...

  7. WPF 打印不显示的元素

    <Window x:Class="_097打印不显示的元素.MainWindow"        xmlns="http://schemas.microsoft.c ...

  8. WPF 获取元素(Visual)相对于屏幕设备的缩放比例,可用于清晰显示图片

    原文:WPF 获取元素(Visual)相对于屏幕设备的缩放比例,可用于清晰显示图片 我们知道,在 WPF 中的坐标单位不是屏幕像素单位,所以如果需要知道某个控件的像素尺寸,以便做一些与屏幕像素尺寸相关 ...

  9. WPF - 属性系统 (3 of 4)

    依赖项属性元数据 在前面的章节中,我们已经介绍了WPF依赖项属性元数据中的两个组成:CoerceValueCallback回调以及PropertyChangedCallback.而在本节中,我们将对其 ...

  10. 2000条你应知的WPF小姿势 基础篇<45-50 Visual Tree&Logic Tree 附带两个小工具>

    在正文开始之前需要介绍一个人:Sean Sexton. 来自明尼苏达双城的软件工程师.最为出色的是他维护了两个博客:2,000Things You Should Know About C# 和 2,0 ...

随机推荐

  1. LabView十六进制与字符串之间的转换

    一.准备工具 Labview开发软件 字符串与十六进制的转换工具(做测试使用) 在线转换工具:IEE754浮点数16进制转换 本地工具如下图所示: 二.LabView字符串转换为十六进制 在数值中选择 ...

  2. vue+vant实现省市联动(van-area)组件(包含比较全面的全国省市数组数据)

    组件库太香了,人家nb,自己写的都是** 效果: 1.安装vant库以及main.js的配置 2.一般结合van-popup组件 </template> <van-popup v-m ...

  3. golang中三种定时器的实现方式及周期定时

    一.定时器的创建 golang中定时器有三种实现方式,分别是time.sleep.time.after.time.Timer 其中time.after和time.Timer需要对通道进行释放才能达到定 ...

  4. Linux中的umask

    在Linux中,当创建一个文件或者目录的时候,系统会自动为这个文件或者目录赋予默认的权限,而umask命令就是用来控制这个默认权限的. 查看umask umask的查看有两种方式,一种不带选项-S,一 ...

  5. Windows 上安装 PostgreSQL详细图文教程

    转载于微信公众号:SQL数据库运维,如需转载请注明出处,谢谢! PostgreSQL 的 Slogan 是 "世界上最先进的开源关系型数据库". 这里使用 Enterprise D ...

  6. js图片懒加载,在不做分页的情况下的解决方案

    Intersection Observer API 1.注意点 一般都是后端返回数据, 用 this.$nextTick(() => { this.handleScroll(); }); 确保d ...

  7. Advanced .Net Debugging 8:线程同步

    一.介绍 这是我的<Advanced .Net Debugging>这个系列的第八篇文章.这篇文章的内容是原书的第二部分的[调试实战]的第六章[同步].我们经常写一些多线程的应用程序,写的 ...

  8. 【漏洞复现】用友NC-Cloud系统uploadChunk存在任意文件上传漏洞

    阅读须知 花果山的技术文章仅供参考,此文所提供的信息只为网络安全人员对自己所负责的网站.服务器等(包括但不限于)进行检测或维护参考,未经授权请勿利用文章中的技术资料对任何计算机系统进行入侵操作.利用此 ...

  9. redis 的下载与安装

    下载地址:https://github.com/MicrosoftArchive/redis/releases 选择免安装包: 解压到路径  D:\Redis-x64-3.0.504 用管理员权限打开 ...

  10. jsonp原理详解——终于弄明白了JSONP

    什么是JSONP? 其实网上关于JSONP的讲解有很多,但却千篇一律,而且云里雾里,对于很多刚接触的人来讲理解起来有些困难,着用自己的方式来阐释一下这个问题,看看是否有帮助. 1.一个众所周知的问题, ...