WPF/C#:如何實現拖拉元素

mingupupup發表於2024-06-27

前言

在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

相關文章