防止 WPF 控件在 MouseMove 事件上重叠

Luk*_*ler 5 c# wpf events event-handling mouseevent

我正在开发一个动态 C# WPF 应用程序(在 Windows 10 上),它使用全屏Grid. 控件在运行时动态添加到网格(在 a 中管理Dictionary<>),我最近添加了使用 a TranslateTransform(我现在怀疑其可行性)使用鼠标(也在运行时)沿网格移动控件的代码。

有没有办法在移动控件时防止控件在网格上重叠或“共享空间”?换句话说,添加某种碰撞检测。我会使用if语句来检查控制保证金范围之类的吗?我的移动事件如下所示:

主窗口.xaml.cs:

public partial class MainWindow : Window
{
     // Orientation variables:
     public bool _isInDrag = false;
     public Dictionary<object, TranslateTransform> PointDict = new Dictionary<object, TranslateTransform();
     public Point _anchorPoint;
     public Point _currentPoint;

     public MainWindow()
     {
          InitializeComponent();
     }

    public static void Control_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        if (_isInDrag)
        {
            var element = sender as FrameworkElement;
            element.ReleaseMouseCapture();
            _isInDrag = false;
            e.Handled = true;
        }           
    }

    public static void Control_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
         var element = sender as FrameworkElement;
         _anchorPoint = e.GetPosition(null);
         element.CaptureMouse();
         _isInDrag = true;
         e.Handled = true;
    }

    public static void Control_MouseMove(object sender, MouseEventArgs e)
    {
        if (_isInDrag)
        {
            _currentPoint = e.GetPosition(null);
            TranslateTransform tt = new TranslateTransform();
            bool isMoved = false;
            if (PointDict.ContainsKey(sender))
            {
                tt = PointDict[sender];
                isMoved = true;
            }
            tt.X += _currentPoint.X - _anchorPoint.X;
            tt.Y += (_currentPoint.Y - _anchorPoint.Y);
            (sender as UIElement).RenderTransform = tt;
            _anchorPoint = _currentPoint;
            if (isMoved)
            {
                PointDict.Remove(sender);
            }
            PointDict.Add(sender, tt);
        }
   }
}
Run Code Online (Sandbox Code Playgroud)

MainWindow.xaml(示例):

public partial class MainWindow : Window
{
     // Orientation variables:
     public bool _isInDrag = false;
     public Dictionary<object, TranslateTransform> PointDict = new Dictionary<object, TranslateTransform();
     public Point _anchorPoint;
     public Point _currentPoint;

     public MainWindow()
     {
          InitializeComponent();
     }

    public static void Control_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        if (_isInDrag)
        {
            var element = sender as FrameworkElement;
            element.ReleaseMouseCapture();
            _isInDrag = false;
            e.Handled = true;
        }           
    }

    public static void Control_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
         var element = sender as FrameworkElement;
         _anchorPoint = e.GetPosition(null);
         element.CaptureMouse();
         _isInDrag = true;
         e.Handled = true;
    }

    public static void Control_MouseMove(object sender, MouseEventArgs e)
    {
        if (_isInDrag)
        {
            _currentPoint = e.GetPosition(null);
            TranslateTransform tt = new TranslateTransform();
            bool isMoved = false;
            if (PointDict.ContainsKey(sender))
            {
                tt = PointDict[sender];
                isMoved = true;
            }
            tt.X += _currentPoint.X - _anchorPoint.X;
            tt.Y += (_currentPoint.Y - _anchorPoint.Y);
            (sender as UIElement).RenderTransform = tt;
            _anchorPoint = _currentPoint;
            if (isMoved)
            {
                PointDict.Remove(sender);
            }
            PointDict.Add(sender, tt);
        }
   }
}
Run Code Online (Sandbox Code Playgroud)

编辑:似乎用 a 移动控件TranslateTransform不会改变该控件的边距。不知道为什么。

编辑 2:没有太大的吸引力。如果有人需要澄清任何事情,请询问。

编辑 3:很确定我不能使用,TranslateTransform因为它不会改变给定控件的边距。有替代方案吗?

编辑 4:为那些想要复制和粘贴的人添加了一些“样板”代码。如果您对此有任何疑问,请告诉我。

Man*_*mer 3

TL;DR:本答案底部的演示

当您想要修改 UI 而不向每个控件添加事件处理程序时,最好的方法是使用Adorners. 装饰器(顾名思义)是装饰另一个控件以添加额外视觉效果或根据您的情况提供功能的控件。装饰器驻留在其中,您可以自己添加或使用每个 WPF已有的AdornerLayer装饰器。Window它AdornerLayer位于所有其他控件之上。

您从未提到当控件重叠时用户松开鼠标按钮时会发生什么,因此如果发生这种情况,我只是将控件重置到其原始位置。

在这一点上,我通常会解释移动控件时要记住的内容,但由于您最初的示例甚至包含人们CaptureMouse通常忘记的内容,我认为您无需进一步解释即可理解代码:)

您可能想要添加/改进的一些内容:

  • 捕捉网格功能(像素精确移动对于普通用户来说可能有点难以承受)
  • 计算重叠时考虑 、 和非矩形形状(如果需要RenderTransform)LayoutTransform
  • 将编辑功能(启用、禁用等)移至单独的控件中并添加专用的AdornerLayer
  • 在编辑模式下禁用交互式控件(Buttons、TextBoxes、等)ComboBoxes
  • 当用户按下时取消移动Esc
  • 限制移动到父容器的边界完成
  • 将活动的装饰器移动到顶部AdornerLayer
  • 让用户一次移动多个控件(通常通过使用 选择它们Ctrl)

之前未回答的问题:

您是说使用 TranslateTransform 时不再为控件分配边距吗?

Grid.Row完全不 - 您可以使用、Grid.Column、 、的组合Margin,RenderTransform 但是 LayoutTransform确定控件实际显示的位置将是一场噩梦。如果您坚持使用一个(在本例中例如Margin或LayoutTransform),那么使用和跟踪会更容易。如果您发现自己处于同时需要多个控件的情况,则必须通过转换和确定控件的角点来找到实际位置。相信我,你不会想去那里 - 保持简单,坚持其中之一。(0, 0)(ActualWidth, ActualHeight)TransformToAncestor

下面的代码不是“如何移动物体的圣杯”,但它应该让您了解如何做到这一点以及您还可以用它做什么(调整大小、旋转、删除控件等)。布局纯粹基于控件的边距Left和边距。如果您愿意的话,只要保持一致,Top全部更换应该不难。MarginsLayoutTransforms

移动装饰器

using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;

public class MoveAdorner : Adorner
{
    // The parent of the adorned Control, in your case a Grid
    private readonly Panel _parent;
    // Same as "AdornedControl" but as a FrameworkElement
    private readonly FrameworkElement _child;

    // The visual overlay rectangle we can click and drag
    private readonly Rectangle _rect;
    // Our own collection of child elements, in this example only _rect
    private readonly UIElementCollection _visualChildren;

    private bool _down;
    private Point _downPos;
    private Thickness _downMargin;

    private List<Rect> _otherRects;

    protected override int VisualChildrenCount => _visualChildren.Count;

    protected override Visual GetVisualChild(int index)
    {
        return _visualChildren[index];
    }

    public MoveAdorner(FrameworkElement adornedElement) : base(adornedElement)
    {
        _child = adornedElement;
        _parent = adornedElement.Parent as Panel;
        _visualChildren = new UIElementCollection(this,this);
        _rect = new Rectangle
        {
            HorizontalAlignment = HorizontalAlignment.Stretch,
            VerticalAlignment = VerticalAlignment.Stretch,
            StrokeThickness = 1,
        };

        SetColor(Colors.LightGray);

        _rect.MouseLeftButtonDown += RectOnMouseLeftButtonDown;
        _rect.MouseLeftButtonUp += RectOnMouseLeftButtonUp;
        _rect.MouseMove += RectOnMouseMove;

        _visualChildren.Add(_rect);
    }

    private void SetColor(Color color)
    {
        _rect.Fill = new SolidColorBrush(color) {Opacity = 0.3};
        _rect.Stroke = new SolidColorBrush(color) {Opacity = 0.5};
    }

    private void RectOnMouseMove(object sender, MouseEventArgs args)
    {
        if (!_down) return;

        Point pos = args.GetPosition(_parent);
        UpdateMargin(pos);
    }

    private void UpdateMargin(Point pos)
    {
        double deltaX = pos.X - _downPos.X;
        double deltaY = pos.Y - _downPos.Y;

        Thickness newThickness = new Thickness(_downMargin.Left + deltaX, _downMargin.Top + deltaY, 0, 0);

        //Restrict to parent's bounds
        double leftMax = _parent.ActualWidth - _child.ActualWidth;
        double topMax = _parent.ActualHeight - _child.ActualHeight;

        newThickness.Left = Math.Max(0, Math.Min(newThickness.Left, leftMax));
        newThickness.Top = Math.Max(0, Math.Min(newThickness.Top, topMax));

        _child.Margin = newThickness;

        bool overlaps = CheckForOverlap();

        SetColor(overlaps ? Colors.Red : Colors.Green);
    }

    // Check the current position for overlaps with all other controls
    private bool CheckForOverlap()
    {
        if (_otherRects == null || _otherRects.Count == 0)
            return false;

        Rect thisRect = GetRect(_child);
        foreach(Rect otherRect in _otherRects)
            if (thisRect.IntersectsWith(otherRect))
                return true;

        return false;
    }

    private Rect GetRect(FrameworkElement element)
    {
        return new Rect(new Point(element.Margin.Left, element.Margin.Top), new Size(element.ActualWidth, element.ActualHeight));
    }

    private void RectOnMouseLeftButtonUp(object sender, MouseButtonEventArgs args)
    {
        if (!_down) return;

        Point pos = args.GetPosition(_parent);

        UpdateMargin(pos);

        if (CheckForOverlap())
            ResetMargin();

        _down = false;
        _rect.ReleaseMouseCapture();
        SetColor(Colors.LightGray);
    }

    private void ResetMargin()
    {
        _child.Margin = _downMargin;
    }

    private void RectOnMouseLeftButtonDown(object sender, MouseButtonEventArgs args)
    {
        _down = true;
        _rect.CaptureMouse();
        _downPos = args.GetPosition(_parent);
        _downMargin = _child.Margin;

        // The current position of all other elements doesn't have to be updated
        // while we move this one so we only determine it once
        _otherRects = new List<Rect>();
        foreach (FrameworkElement child in _parent.Children)
        {
            if (ReferenceEquals(child, _child))
                continue;
            _otherRects.Add(GetRect(child));
        }
    }

    // Whenever the adorned control is resized or moved
    // Update the size of the overlay rectangle
    // (Not 100% necessary as long as you only move it)
    protected override Size MeasureOverride(Size constraint)
    {
        _rect.Measure(constraint);
        return base.MeasureOverride(constraint);
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        _rect.Arrange(new Rect(new Point(0,0), finalSize));
        return base.ArrangeOverride(finalSize);
    }
}
Run Code Online (Sandbox Code Playgroud)

用法

private void DisableEditing(Grid theGrid)
{
    // Remove all Adorners of all Controls
    foreach (FrameworkElement child in theGrid.Children)
    {
        var layer = AdornerLayer.GetAdornerLayer(child);
        var adorners = layer.GetAdorners(child);
        if (adorners == null)
            continue;
        foreach(var adorner in adorners)
            layer.Remove(adorner);
    }
}

private void EnableEditing(Grid theGrid)
{
    foreach (FrameworkElement child in theGrid.Children)
    {
        // Add a MoveAdorner for every single child
        Adorner adorner = new MoveAdorner(child);

        // Add the Adorner to the closest (hierarchically speaking) AdornerLayer
        AdornerLayer.GetAdornerLayer(child).Add(adorner);
    }
}
Run Code Online (Sandbox Code Playgroud)

演示XAML

using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;

public class MoveAdorner : Adorner
{
    // The parent of the adorned Control, in your case a Grid
    private readonly Panel _parent;
    // Same as "AdornedControl" but as a FrameworkElement
    private readonly FrameworkElement _child;

    // The visual overlay rectangle we can click and drag
    private readonly Rectangle _rect;
    // Our own collection of child elements, in this example only _rect
    private readonly UIElementCollection _visualChildren;

    private bool _down;
    private Point _downPos;
    private Thickness _downMargin;

    private List<Rect> _otherRects;

    protected override int VisualChildrenCount => _visualChildren.Count;

    protected override Visual GetVisualChild(int index)
    {
        return _visualChildren[index];
    }

    public MoveAdorner(FrameworkElement adornedElement) : base(adornedElement)
    {
        _child = adornedElement;
        _parent = adornedElement.Parent as Panel;
        _visualChildren = new UIElementCollection(this,this);
        _rect = new Rectangle
        {
            HorizontalAlignment = HorizontalAlignment.Stretch,
            VerticalAlignment = VerticalAlignment.Stretch,
            StrokeThickness = 1,
        };

        SetColor(Colors.LightGray);

        _rect.MouseLeftButtonDown += RectOnMouseLeftButtonDown;
        _rect.MouseLeftButtonUp += RectOnMouseLeftButtonUp;
        _rect.MouseMove += RectOnMouseMove;

        _visualChildren.Add(_rect);
    }

    private void SetColor(Color color)
    {
        _rect.Fill = new SolidColorBrush(color) {Opacity = 0.3};
        _rect.Stroke = new SolidColorBrush(color) {Opacity = 0.5};
    }

    private void RectOnMouseMove(object sender, MouseEventArgs args)
    {
        if (!_down) return;

        Point pos = args.GetPosition(_parent);
        UpdateMargin(pos);
    }

    private void UpdateMargin(Point pos)
    {
        double deltaX = pos.X - _downPos.X;
        double deltaY = pos.Y - _downPos.Y;

        Thickness newThickness = new Thickness(_downMargin.Left + deltaX, _downMargin.Top + deltaY, 0, 0);

        //Restrict to parent's bounds
        double leftMax = _parent.ActualWidth - _child.ActualWidth;
        double topMax = _parent.ActualHeight - _child.ActualHeight;

        newThickness.Left = Math.Max(0, Math.Min(newThickness.Left, leftMax));
        newThickness.Top = Math.Max(0, Math.Min(newThickness.Top, topMax));

        _child.Margin = newThickness;

        bool overlaps = CheckForOverlap();

        SetColor(overlaps ? Colors.Red : Colors.Green);
    }

    // Check the current position for overlaps with all other controls
    private bool CheckForOverlap()
    {
        if (_otherRects == null || _otherRects.Count == 0)
            return false;

        Rect thisRect = GetRect(_child);
        foreach(Rect otherRect in _otherRects)
            if (thisRect.IntersectsWith(otherRect))
                return true;

        return false;
    }

    private Rect GetRect(FrameworkElement element)
    {
        return new Rect(new Point(element.Margin.Left, element.Margin.Top), new Size(element.ActualWidth, element.ActualHeight));
    }

    private void RectOnMouseLeftButtonUp(object sender, MouseButtonEventArgs args)
    {
        if (!_down) return;

        Point pos = args.GetPosition(_parent);

        UpdateMargin(pos);

        if (CheckForOverlap())
            ResetMargin();

        _down = false;
        _rect.ReleaseMouseCapture();
        SetColor(Colors.LightGray);
    }

    private void ResetMargin()
    {
        _child.Margin = _downMargin;
    }

    private void RectOnMouseLeftButtonDown(object sender, MouseButtonEventArgs args)
    {
        _down = true;
        _rect.CaptureMouse();
        _downPos = args.GetPosition(_parent);
        _downMargin = _child.Margin;

        // The current position of all other elements doesn't have to be updated
        // while we move this one so we only determine it once
        _otherRects = new List<Rect>();
        foreach (FrameworkElement child in _parent.Children)
        {
            if (ReferenceEquals(child, _child))
                continue;
            _otherRects.Add(GetRect(child));
        }
    }

    // Whenever the adorned control is resized or moved
    // Update the size of the overlay rectangle
    // (Not 100% necessary as long as you only move it)
    protected override Size MeasureOverride(Size constraint)
    {
        _rect.Measure(constraint);
        return base.MeasureOverride(constraint);
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        _rect.Arrange(new Rect(new Point(0,0), finalSize));
        return base.ArrangeOverride(finalSize);
    }
}
Run Code Online (Sandbox Code Playgroud)

预期结果

当编辑被禁用时,控件无法移动,但可以毫无阻碍地单击/交互交互控件。启用编辑模式后,每个控件都会覆盖一个可以移动的装饰器。如果目标位置与另一个控件重叠,则装饰器将变成红色,并且如果用户松开鼠标按钮,边距将重置为初始位置。

快速演示