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

wpf · 浏览次数 : 0

小编点评

本文介绍了在WPF中实现Canvas元素拖放功能的具体步骤和代码解析。 首先,通过XAML页面设置Canvas,并在其中添加了两个元素:一个蓝色矩形和一个文本框。然后,通过预览鼠标事件实现元素的拖拉功能。 在CS文件中,定义了几个私有变量,如_isDown、_isDragging、_originalElement等。鼠标左键按下事件处理程序中,判断事件源是否为Canvas,若不是则记录下相关信息并开始拖拽。鼠标移动事件处理程序中,根据_isDown值判断是否开始拖拽或结束拖拽,并调用相应的函数进行处理。 在DragStarted方法中,创建了一个装饰器_simpleCircleAdorner并将其添加到装饰器层中。在DragMoved方法中,计算元素的偏移量并更新装饰器位置。最后,在鼠标左键松开事件处理程序中,移除装饰器并结束拖拽过程。 通过这种方式,实现了在Canvas中拖拉元素的功能,并提供了拖拽过程中的视觉反馈。

正文

前言

在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](

与WPF/C#:如何实现拖拉元素相似的内容:

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

前言 在Canvas中放置了一些元素,需要能够拖拉这些元素,在WPF Samples中的DragDropObjects项目中告诉了我们如何实现这种效果。 效果如下所示: 拖拉过程中的效果如下所示: 具体实现 xaml页面 我们先来看看xaml:

WPF/C#:在WPF中如何实现依赖注入

本文先介绍依赖注入的概念,再解释为什么要进行依赖注入,最后通过 WPF Gallery 这个项目学习如何在WPF中使用依赖注入。

WPF/C#:数据绑定到方法

在WPF Samples中有一个关于数据绑定到方法的Demo,该Demo结构如下: 运行效果如下所示: 来看看是如何实现的。 先来看下MainWindow.xaml中的内容:

WPF/C#:在DataGrid中显示选择框

前言 在使用WPF的过程中可能会经常遇到在DataGrid的最前或者最后添加一列选择框的需求,今天跟大家分享一下,在自己的项目中是如何实现的。 整体实现效果如下: 如果对此感兴趣,可以接下来看具体实现部分。 实践 假设数据库中的模型如下: public class Person { public i

WPF/C#:实现导航功能

前言 在WPF中使用导航功能可以使用Frame控件,这是比较基础的一种方法。前几天分享了wpfui中NavigationView的基本用法,但是如果真正在项目中使用起来,基础的用法是无法满足的。今天通过wpfui中的mvvm例子来说明在wpfui中如何通过依赖注入与MVVM模式使用导航功能。实践起来

Azure DevOps(一)基于 Net6.0 的 WPF 程序如何进行持续集成、持续编译

一,引言 我们是否正在为如何快速的编译、部署客户端应用程序而烦恼?这也是博主最近遇到的问题。目前博主所在公司主要做项目级的定制化开发,多以 C/S 架构的 WPF 程序为主,每次到了协助开发团队给实施团队编译好的要测试程序包时,就会出现多人协助,编译、打包好的二进制程序包 pull 最新代码 ,以及

WPF/C#:如何将数据分组显示

WPF Samples中的示例 在WPF Samples中有一个关于Grouping的Demo。 该Demo结构如下: MainWindow.xaml如下:

WPF/C#:BusinessLayerValidation

BusinessLayerValidation介绍 BusinessLayerValidation,即业务层验证,是指在软件应用程序的业务逻辑层(Business Layer)中执行的验证过程。业务逻辑层是应用程序架构中的一个关键部分,负责处理与业务规则和逻辑相关的操作。业务层验证的主要目的是确保数

WPF/C#:显示分组数据的两种方式

前言 本文介绍自己在遇到WPF对数据进行分组显示的需求时,可以选择的两种方案。一种方案基于ICollectionView,另一种方案基于IGrouping。 基于ICollectionView实现 相关cs代码: [ObservableProperty] private ObservableColl

WPF/C#:程序关闭的三种模式

ShutdownMode枚举类型介绍 ShutdownMode是一个枚举类型,它定义了WPF应用程序的关闭方式。这个枚举类型有三个成员: OnLastWindowClose:当最后一个窗口关闭或者调用System.Windows.Application.Shutdown方法时,应用程序会关闭。 On