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

5
我正在开发一个动态的C# WPF应用程序(在Windows 10上),使用全屏的Grid。控件会在运行时动态添加到网格中(由Dictionary<>管理),我最近添加了代码,使用TranslateTransform使得这些控件能够随着鼠标在网格上移动(也是在运行时进行)(但现在我开始对其可行性感到怀疑)。
是否有一种方法可以防止控件在移动时重叠或“共享空间”在网格上?换句话说,是否可以添加某种冲突检测机制?我是否应该使用if语句来检查控件边距范围或其他内容?我的移动事件如下所示:
MainWindow.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);
        }
   }
}

MainWindow.xaml (example):

<Window x:Name="MW" x:Class="MyProgram.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:MyProgram"
    mc:Ignorable="d"
    Title="MyProgram" d:DesignHeight="1080" d:DesignWidth="1920" ResizeMode="NoResize" WindowState="Maximized" WindowStyle="None">

    <Grid x:Name="MyGrid" />
        <Image x:Name="Image1" Source="pic.png" Margin="880,862,0,0" Height="164" Width="162" HorizontalAlignment="Left" VerticalAlignment="Top" MouseLeftButtonDown="Control_MouseLeftButtonDown" MouseLeftButtonUp="Control_MouseLeftButtonUp" MouseMove="Control_MouseMove" />
        <TextBox x:Name="Textbox1" Margin="440,560,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" MouseLeftButtonDown="Control_MouseLeftButtonDown" MouseLeftButtonUp="Control_MouseLeftButtonUp" MouseMove="Control_MouseMove" />
</Window>
< p > < em > 编辑: < /em > 看起来使用 < code > TranslateTransform < /code > 移动控件不会改变该控件的边距。不确定为什么。< /p > < p > < em > 编辑2: < /em > 没有得到太多关注。如果有人需要任何澄清,请问。 < /p > < p > < em > 编辑3: < /em > 很确定我不能使用 < code > TranslateTransform < /code > ,因为它不会改变给定控件的边距。是否有其他替代方法?< /p > < p > < em > 编辑4: < /em > 为那些想要复制和粘贴的人添加了一些“样板”代码。如果您对此有任何疑问,请告诉我。 < /p >

我的建议是使用专门的面板来实现此功能。根据您实际布局控件的方式(您提到了Margin但使用RenderTransform),您可以使用TransformToAncestor确定相对于网格的实际位置。一旦您拥有控件占用的完整矩形,就可以使用这些矩形的Intersects函数检查重叠。顺便说一下:如果您想坚持使用变换,请改用LayoutTransform而不是RenderTransform - Manfred Radlwimmer
@ManfredRadlwimmer 不太确定您的意思,我在这个领域没有太多经验(而且这是我发现适用于网格的唯一“拖放”方法)。您是说在使用TranslateTransform时控件不再分配边距吗? - Luke Dinkler
@ManfredRadlwimmer,另外,如果您能提供一个示例,我将不胜感激。 - Luke Dinkler
2
最好提供完整的示例代码,这样帮助你的人就可以直接复制粘贴并立即看到问题所在。否则,他们必须花时间自己编写那些样板代码。 - Evk
2
是的,但我不能复制粘贴它并运行(它无法编译)。我不是说这是必需的,只是增加了你得到答案的机会(而且也是一个好的态度)。 - Evk
显示剩余3条评论
2个回答

3

总结:本答案底部提供演示

如果您想修改用户界面而不必为每个控件添加事件处理程序,那么使用Adorners就是正确的选择。 Adorners(如名称所示)是一种装饰另一个控件以添加其他可视化效果或功能的控件。 Adorners 存在于一个 AdornerLayer 中,您可以自己添加或使用每个 WPFWindow已有的一个。 AdornerLayer 位于所有其他控件之上。

由于您没有说明当用户松开鼠标按钮时应该发生什么,所以如果发生重叠,我会将控件重置为其原始位置。

此时,我通常会解释移动控件时要注意的事项,但由于您的原始示例甚至包含人们通常忘记的 CaptureMouse,我认为您可以在无需进一步解释的情况下理解代码 :)

您可能想添加/改进以下几点:

  • 吸附到网格特性(对于普通用户来说,像素精确移动可能有些压倒性)
  • 考虑RenderTransformLayoutTransform和非矩形形状(如果需要)在计算重叠时
  • 将编辑功能(启用、禁用等)移至单独的控件,并添加专用的 AdornerLayer
  • 在编辑模式下禁用交互式控件(ButtonsTextBoxesComboBoxes等等)
  • 用户按下Esc键时取消移动
  • 将移动限制为父容器的边界完成
  • active Adorner 移到 AdornerLayer的顶部
  • 允许用户同时移动多个控件(通常通过使用 Ctrl 选择它们)

之前未被回答的问题:

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

不用担心 - 您可以使用 Grid.RowGrid.ColumnMarginRenderTransform LayoutTransform 的组合,但是这将使确定控件实际显示的位置变得很困难。如果您坚持使用一种方法(例如,此处使用的是MarginLayoutTransform),那么处理和跟踪就容易得多。如果您发现自己需要同时使用多个选项的情况,则必须通过将控件的角转换为使用TransformToAncestor来确定其实际位置。相信我,你不想那样做 - 保持简单,坚持一种方法。
下面的代码并不是移动内容的"终极解决方案",但它应该可以让您了解如何执行此操作以及您可以执行哪些其他操作(调整大小、旋转、删除控件等)。布局仅基于控件的左边缘和顶部边缘的margin。如果您喜欢,完全可以将所有Margins替换为LayoutTransforms,只要保持一致即可。 移动装饰器
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);
    }
}

使用方法

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);
    }
}

演示 XAML

<Grid>
    <Button Content="Enable Editing" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="100" Click="BtnEnable_Click"/>
    <Button Content="Disable Editing" HorizontalAlignment="Left" Margin="115,10,0,0" VerticalAlignment="Top" Width="100" Click="BtnDisable_Click"/>

    <Grid Name="grid" Background="AliceBlue" Margin="10,37,10,10">
        <Button Content="Button" HorizontalAlignment="Left" Margin="83,44,0,0" VerticalAlignment="Top" Width="75"/>
        <Ellipse Fill="#FFF4F4F5" HorizontalAlignment="Left" Height="100" Margin="207,100,0,0" Stroke="Black" VerticalAlignment="Top" Width="100"/>
        <Rectangle Fill="#FFF4F4F5" HorizontalAlignment="Left" Height="100" Margin="33,134,0,0" Stroke="Black" VerticalAlignment="Top" Width="100"/>
    </Grid>
</Grid>
期望结果

当禁用编辑时,控件不可移动,可以点击/与交互式控件交互而无障碍。启用编辑模式后,每个控件都会叠加一个装饰器,可移动。如果目标位置与其他控件重叠,装饰器将变为红色,如果用户松开鼠标按钮,则边距将重置为初始位置。

快速演示


这个很好用。非常感谢。我想我还有很多要学习的... 有没有关于如何将控件限制在它们的容器中的提示呢(如果你不想回答也没关系)? - Luke Dinkler
@LukeDinkler 这很简单:在 UpdateMargin 方法中将值夹紧在 0(完全靠左/上)和 _parent.Width - _child.Width(当然还有 Width)之间。这样,您就无法将它们移动到边界之外了。 - Manfred Radlwimmer
1
@LukeDinkler 我已经更新了代码示例(只是对 UpdateMargin 进行了小改动)。 - Manfred Radlwimmer
再次感谢,我非常感激!! - Luke Dinkler
1
很棒。谢谢。问题的赞数比答案还多,这也有点奇怪。 - PauLEffect
显示剩余3条评论

0

除了检查您要移动到的位置是否存在控件外,没有其他方法。

由于您经常移动 UI 元素,最好使用画布而不是网格,在画布上可以使用 Top 和 Left 参数布置元素。

这里是修改后的代码

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

    public MainWindow()
    {
        InitializeComponent();
    }

    public void Control_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        if (_isInDrag)
        {
            var element = sender as FrameworkElement;
            element.ReleaseMouseCapture();
            Panel.SetZIndex(element, 0);
            _isInDrag = false;
            e.Handled = true;
        }
    }
    public void Control_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        var element = sender as FrameworkElement;
        _anchorPoint = e.GetPosition(null);
        element.CaptureMouse();
        Panel.SetZIndex(element, 10);
        _isInDrag = true;
        e.Handled = true;
    }
    public void Control_MouseMove(object sender, MouseEventArgs e)
    {
        if (_isInDrag)
        {
            _currentPoint = e.GetPosition(null);
            FrameworkElement fw = sender as FrameworkElement;
            if (fw != null)
            {
                FrameworkElement fwParent = fw.Parent as FrameworkElement;
                if (fwParent != null)
                {
                    Point p = new Point(_currentPoint.X - _anchorPoint.X + Canvas.GetLeft((sender as UIElement)), _currentPoint.Y - _anchorPoint.Y + Canvas.GetTop((sender as UIElement)));
                    List<HitTestResult> lst = new List<HitTestResult>()
                    {
                        VisualTreeHelper.HitTest(fwParent  , p),
                        VisualTreeHelper.HitTest(fwParent, new Point(p.X + fw.Width, p.Y)),
                        VisualTreeHelper.HitTest(fwParent, new Point(p.X, p.Y + fw.Height)),
                        VisualTreeHelper.HitTest(fwParent, new Point(p.X + fw.Width, p.Y +fw.Height)),
                    };
                    bool success = true;
                    foreach (var item in lst)
                    {
                        if (item != null)
                        {
                            if (item.VisualHit != sender && item.VisualHit != fwParent && fw.IsAncestorOf(item.VisualHit) == false)
                            {
                                success = false;
                                break;
                            }
                        }
                    }
                    if (success)
                    {
                        Canvas.SetTop((sender as UIElement), p.Y);
                        Canvas.SetLeft((sender as UIElement), p.X);
                        _anchorPoint = _currentPoint;
                    }
                }
            }
        }
    }
}

Xaml

<Window x:Class="ControlsOverlapWpf.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ControlsOverlapWpf"
        mc:Ignorable="d"
        Title="MyProgram" d:DesignHeight="500" d:DesignWidth="500" ResizeMode="NoResize" WindowState="Normal" WindowStyle="None">
    <Canvas  Background="Pink">
        <Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown"  PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
        <Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown"  PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
        <Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown"  PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
        <Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown"  PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
        <Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown"  PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
        <Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown"  PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />

    </Canvas>
</Window>

正如我之前所说,我需要在“网格”上实现。原因是:1.这个屏幕上会有很多用户交互;2.我需要能够动态对齐我的控件,而这在“画布”上无法实现。我相信在“网格”上实现可能更加困难,但我不认为这是不可能的。 - Luke Dinkler
@LukeDinkler 1->我不知道为什么用户交互会在画布上影响你,但对网格没有影响。 2->你所说的动态对齐是什么意思? 无论如何,您可以修改上面的代码以使用网格。而不是设置左和上,设置边距的左和上。我认为你不需要使用任何转换来做到这一点。 - Aman Seth

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接