前言

在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. 在Deepin 20.2系统中换源并全新图解安装MySQL数据库

    在Deepin 20.2系统中换源并全新图解安装MySQL数据库 https://www.ywnz.com/linuxysjk/9249.html ubuntu下apt-get彻底卸载mysql 删除 ...

  2. Docker使用docker compose部署zfile 实现在线浏览下载

    需要先安装docker-compose curl -L https://get.daocloud.io/docker/compose/releases/download/v2.2.3/docker-c ...

  3. Echarts设置饼状图保证你看的明明白白

    简单的饼状图 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UT ...

  4. 使用IIS部署WebDAV

    服务器开启WebDAV 在服务器安装IIS的同时 要启用Windows身份验证与WebDAV发布 如果不是服务器版本,参照下图 在IIS中新建WebDAV网站 配置好本地目录与端口 启用Windows ...

  5. PyQGIS二次开发指南

    当你的数据处理使用的是Python语言,而你的导师又让你开发界面,那么PyQGIS二次开发指南是你必读的圣经.QGIS支持Python语言进行二次开发,你将学会如何使用Qt Designer进行界面设 ...

  6. feign入门

    .net core: feign.net是一个spring cloud feign组件的c#移植版 https://github.com/daixinkai/feign.net 在.net core ...

  7. 一个免费、时尚、强大的 Windows GitHub 客户端

    前言 今天大姚给大家分享一个.NET开源(MIT License).免费.时尚.功能强大的 Windows GitHub 客户端:FluentHub. 工具功能 多任务标签页. 上下文菜单扩展. 对问 ...

  8. 透过 Go 语言探索 Linux 网络通信的本质

    前言 各种编程语言百花齐放.百家争鸣,但是 "万变不离其中".对于网络通信而言,每一种编程语言的实现方式都不一样:但其实,调用的底层逻辑都是一样的.linux 系统底层向上提供了统 ...

  9. Docker Build Cache 缓存清理

    Docker 18.09 引入了 BuildKit ,提升了构建过程的性能.安全.存储管理等能力. docker system df 命令,类似于 Linux上的 df 命令,用于查看 Docker ...

  10. mysql存储地理信息的方法

    MySQL 存储地理信息通常使用 GEOMETRY 数据类型或其子类型(如 POINT, LINESTRING, POLYGON 等).为了支持这些数据类型,MySQL 提供了 SPATIAL 索引, ...