WPF/C#:如何实现拖拉元素

embedded/2024/10/21 7:30:17/

前言

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

效果如下所示:

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

image-20240627093348785

具体实现

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中装饰器就是移动过程中四个角上出现的小圆以及内部不断闪烁的颜色,如下所示:

image-20240627095912873

image-20240627095956121

这是如何实现的呢?

这个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


http://www.ppmy.cn/embedded/54018.html

相关文章

教程:Spring Boot中集成Elasticsearch的步骤

教程&#xff1a;Spring Boot中集成Elasticsearch的步骤 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 在当今大数据时代&#xff0c;搜索功能对于许多应用程…

第2章:Electron的安装与配置

2.1 环境准备 在开始使用 Electron 之前&#xff0c;需要准备开发环境。这包括安装必要的软件和工具。 2.1.1 操作系统要求 Electron 支持以下操作系统&#xff1a; Windows 7 及以上版本macOS 10.10 (Yosemite) 及以上版本Linux&#xff08;大多数现代发行版&#xff09; …

Grafana调整等待时间,避免Gateway timeout报错

使用Grafana的HTTP时&#xff0c;有些即时数据需要运算量与时间&#xff0c;而grafana的默认timeout是30秒&#xff0c;因此需要通过修改配置文件&#xff0c;避免grafana提前中断连接 修改原始配置文件: 删除;调整timeout30为timeout60 # This setting also applies to cor…

vue 实现 word/excel/ppt/pdf 等文件格式预览操作

效果图&#xff1a; 问题描述&#xff1a;一般情况下使用iframe标签就可以实现文件预览&#xff0c;但是这个标签只针对于ppt和pdf是有效的。对于doc文件就需要借助第三方插件&#xff08;vue-office/docx&#xff09;来实现预览了。下面介绍使用方法。 安装插件&#xff1a;n…

【linux】详解——库

目录 概述 库 库函数 静态库 动态库 制作动静态库 使用动静态库 如何让系统默认找到第三方库 lib和lib64的区别 /和/usr/和/usr/local下lib和lib64的区别 环境变量 配置相关文件 个人主页&#xff1a;东洛的克莱斯韦克-CSDN博客 简介&#xff1a;C站最萌博主 相关…

如何在 HTML 中实现响应式设计以适应不同设备的屏幕尺寸?

要在HTML中实现响应式设计以适应不同设备的屏幕尺寸&#xff0c;可以使用CSS媒体查询和流动布局。 以下是实现响应式设计的一些关键步骤&#xff1a; 使用CSS媒体查询&#xff1a;CSS媒体查询允许根据屏幕尺寸和设备特性应用不同的CSS样式。通过在CSS中使用media规则&#xf…

ubuntu22.04下编译安装dlib

为什么要自己编译&#xff0c;请自行摆渡。 #--------------------------------------------------------------------------- # compile and inistall dlib C library #--------------------------------------------------------------------------- cd /opt mkdir dlib-ro…

react 使用 valtio

安装 npm i valtio//好用的storage npm i good-storage使用 src目录下创建store文件夹&#xff0c;新增两个文件 index.js export * from ./useruser.js import { proxy,subscribe } from valtio import {useProxy} from valtio/utils import ss from good-storage const k…