写点什么

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

作者:EquatorCoco
  • 2024-06-28
    福建
  • 本文字数:5356 字

    阅读完需:约 18 分钟

前言


在 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 元素被渲染时,装饰器将不再影响其外观或行为。


文章转载自:mingupupup

原文链接:https://www.cnblogs.com/mingupupu/p/18270547

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

EquatorCoco

关注

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
WPF/C#:如何实现拖拉元素_C#_EquatorCoco_InfoQ写作社区