WPF/C#:如何实现拖拉元素
前言
在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上使用了三个隧道事件(预览事件)PreviewMouseLeftButtonDown
、PreviewMouseMove
、PreviewMouseLeftButtonUp
。
而什么是隧道事件(预览事件)呢?
预览事件,也称为隧道事件,是从应用程序根元素向下遍历元素树到引发事件的元素的路由事件。
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#:如何实现拖拉元素的更多相关文章
- WPF笔记(1.3 属性元素)——Hello,WPF!
原文:WPF笔记(1.3 属性元素)--Hello,WPF! 这一节中“属性元素”的概念可以用匪夷所思形容.1.WPF用标签元素实现对象建模,有两种:Control和Container,都用来装载内容 ...
- WPF学习系列之六 (元素绑定)
元素绑定 简单地说,数据绑定是一种关系,该关系告诉WPF从一个源对象提取一些信息,并使用这些信息设置目标对象的属性.目标属性总是依赖属性,并且通常位于WPF元素中. 一.将元素绑定到一起 <Wi ...
- 每日分享!~ 使用js原生方式对拖拉元素(鼠标的事件)
一个元素放置页面上.如何进行拖拉,实现想放哪里就放哪里的效果呢? 效果如下: 如果让你写这个效果,你会如何写呢? --- 思路分析:我首先想到的是,对这个元素先绑定一个事件.(什么事件? 那当然是鼠标 ...
- WPF中异步更新UI元素
XAML 界面很简单,只有一个按钮和一个lable元素,要实现点击button时,lable的内容从0开始自动递增. <Grid> <Label Name="lable_p ...
- wpf,visibility属性的多元素绑定及值转换
visibility实现多元素绑定. 实现多绑定转换 public class VisibilityConverter : IMultiValueConverter { public object C ...
- wpf 寻找TreeView的子元素,并对其进行操作
//itemsControl 开始为指定的TreeView控件 item为TreeView子元素 private void PareItems(ItemsControl itemsControl, ...
- WPF 打印不显示的元素
<Window x:Class="_097打印不显示的元素.MainWindow" xmlns="http://schemas.microsoft.c ...
- WPF 获取元素(Visual)相对于屏幕设备的缩放比例,可用于清晰显示图片
原文:WPF 获取元素(Visual)相对于屏幕设备的缩放比例,可用于清晰显示图片 我们知道,在 WPF 中的坐标单位不是屏幕像素单位,所以如果需要知道某个控件的像素尺寸,以便做一些与屏幕像素尺寸相关 ...
- WPF - 属性系统 (3 of 4)
依赖项属性元数据 在前面的章节中,我们已经介绍了WPF依赖项属性元数据中的两个组成:CoerceValueCallback回调以及PropertyChangedCallback.而在本节中,我们将对其 ...
- 2000条你应知的WPF小姿势 基础篇<45-50 Visual Tree&Logic Tree 附带两个小工具>
在正文开始之前需要介绍一个人:Sean Sexton. 来自明尼苏达双城的软件工程师.最为出色的是他维护了两个博客:2,000Things You Should Know About C# 和 2,0 ...
随机推荐
- Solution Set - 点分治
A[POJ1741].给定一棵树,边有权,求长度不超过\(k\)的路径数目. B[HDU4871].给定一张图,边有权,求它的最短路径树上恰含\(k\)个点的路径中最长路径的长度及数目. C[HDU4 ...
- 在英特尔至强 CPU 上使用 🤗 Optimum Intel 实现超快 SetFit 推理
在缺少标注数据场景,SetFit 是解决的建模问题的一个有前途的解决方案,其由 Hugging Face 与 Intel 实验室 以及 UKP Lab 合作共同开发.作为一个高效的框架,SetFit ...
- Linux(二):Linux的灵魂
上次说Linux的前世今生的时候,提了一句,就像学习java一样,我们有一个核心的准则 "万物皆对象" ,学习Linux,同样有基本准则,这也是Linux的最基本的特点,那就是&q ...
- DOMRect对象
DOMRect 表示的盒子的类型由返回它的方法或属性指定.例如,WebVR API 的 VREyeParameters.renderRect (en-US) 指定了头戴式显示器的一只眼睛应该呈现的影像 ...
- 更智能!AIRIOT加速煤炭行业节能减排升级
"双碳政策"下,各个行业都在践行节能减排行动,依靠数字化.智能化手段开展节能减排工作. 煤炭行业是能源消耗大户,煤炭选洗是煤炭行业节能减排的重要环节之一,加强煤炭清洁高效利用工作, ...
- Haproxy+Nginx+Tomcat实现动静分离页面
一.Haproxy概述: 二.Haproxy原理实现: 三.Nginx.LVS.Haproxy对比: 四.Haproxy配置文件讲解: 五.案例:Haproxy+Nginx+Tomcat搭建高可用集群 ...
- Gin 框架是怎么使用 net http 包的(gin.go)
Gin 框架是基于 Go 语言的标准库 net/http 构建的,它使用 net/http 提供的基础功能来构建自己的高性能 Web 应用框架. 具体来说,Gin 使用 net/http 的以下方面: ...
- 线程同步 进程同步 EventWaitHandle
这个名字LLLLL取相同就能让同一台电脑上两个进程同步 主动控制程序 class Program { static EventWaitHandle eHandle = new EventWaitHan ...
- 牛逼!50.3K Star!一个自动将屏幕截图转换为代码的开源工具
1.背景 在当今快节奏的软件开发环境中,设计师与开发者之间的协同工作显得尤为重要.然而,理解并准确实现设计稿的意图常常需要耗费大量的时间和沟通成本.为此,开源社区中出现了一个引人注目的项目--scre ...
- WPF使用WPFMediaKit/AForge调用摄像头示例 .net core 8.0 也支持
调用摄像头使我们经常会用到的一个功能,可以根据摄像头捕捉到的图像进行分析处理来做很多的东西,比如电子档案.图像识别.AI分析等等. 本示例中主要介绍Nuget最常用的两个调用摄像头的轮子 WPFMed ...