Как перетащить UserControl внутри холста

Я пишу свое первое приложение WPF. У меня есть Canvas, в котором пользователь может добавлять подклассы UserControl, содержащие форму. Пользователь должен иметь возможность перетаскивать эти UserControl вокруг холста. Какая наилучшая практика для WPF? Спасибо.

Ответ 1

Это выполняется в silverlight, а не в WPF, но оно должно работать одинаково.

Создайте два частных свойства элемента управления:

protected bool isDragging;  
private Point clickPosition;

Затем установите некоторые обработчики событий в конструкторе элемента управления:

this.MouseLeftButtonDown += new MouseButtonEventHandler(Control_MouseLeftButtonDown);
this.MouseLeftButtonUp += new MouseButtonEventHandler(Control_MouseLeftButtonUp);
this.MouseMove += new MouseEventHandler(Control_MouseMove);

Теперь создайте эти методы:

private void Control_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    isDragging = true;
    var draggableControl = sender as UserControl;
    clickPosition = e.GetPosition(this);
    draggableControl.CaptureMouse();
}

private void Control_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    isDragging = false;
    var draggable = sender as UserControl;
    draggable.ReleaseMouseCapture();
}

private void Control_MouseMove(object sender, MouseEventArgs e)
{
    var draggableControl = sender as UserControl;

    if (isDragging && draggableControl != null)
    {
        Point currentPosition = e.GetPosition(this.Parent as UIElement);

        var transform = draggableControl.RenderTransform as TranslateTransform;
        if (transform == null)
        {
            transform = new TranslateTransform();
            draggableControl.RenderTransform = transform;
        }

        transform.X = currentPosition.X - clickPosition.X;
        transform.Y = currentPosition.Y - clickPosition.Y;
    }
}

Несколько вещей, чтобы отметить здесь:
1. Это не должно быть в холсте. Он также может быть в стеке или сетке.
2. Это делает весь элемент управления перетаскиваемым, это означает, что если вы щелкните в любом месте элемента управления и перетащите его, он перетащит весь элемент управления. Не уверен, что это именно то, что вы хотите.

Edit-
Расширяя некоторые особенности вашего вопроса: Лучший способ реализовать это - создать класс, который наследуется от UserControl, может быть назван DraggableControl, который построен с этим кодом, тогда все перетаскиваемые элементы управления должны расширить DraggableControl.

Редактировать 2 - В этом элементе есть небольшая проблема, когда у вас есть datagrid. Если вы сортируете столбец в datagrid, событие MouseLeftButtonUp никогда не срабатывает. Я обновил код, так что isDragging защищен. Я нашел, что лучшим решением является привязка этого анонимного метода к событию LostMouseCapture для datagrid:

this.MyDataGrid.LostMouseCapture += (sender, e) => { this.isDragging = false; };

Ответ 2

Ответ кори в основном правильный, но в нем отсутствует один важный элемент: память о том, что было последним преобразованием. В противном случае, когда вы перемещаете элемент, отпускаете кнопку мыши и затем снова щелкаете этот элемент, преобразование сбрасывается в (0,0), и элемент управления возвращается в исходное положение.

Вот немного измененная версия, которая работает для меня:

public partial class DragItem : UserControl
{
    protected Boolean isDragging;
    private Point mousePosition;
    private Double prevX, prevY;

    public DragItem()
    {
        InitializeComponent();
    }

    private void UserControl_MouseLeftButtonDown(Object sender, MouseButtonEventArgs e)
    {
        isDragging = true;
        var draggableControl = (sender as UserControl);
        mousePosition = e.GetPosition(Parent as UIElement);
        draggableControl.CaptureMouse();
    }

    private void UserControl_MouseLeftButtonUp(Object sender, MouseButtonEventArgs e)
    {
        isDragging = false;
        var draggable = (sender as UserControl);
        var transform = (draggable.RenderTransform as TranslateTransform);
        if (transform != null)
        {
            prevX = transform.X;
            prevY = transform.Y;
        }
        draggable.ReleaseMouseCapture();
    }

    private void UserControl_MouseMove(Object sender, MouseEventArgs e)
    {
        var draggableControl = (sender as UserControl);
        if (isDragging && draggableControl != null)
        {
            var currentPosition = e.GetPosition(Parent as UIElement);
            var transform = (draggableControl.RenderTransform as TranslateTransform);
            if (transform == null)
            {
                transform = new TranslateTransform();
                draggableControl.RenderTransform = transform;
            }
            transform.X = (currentPosition.X - mousePosition.X);
            transform.Y = (currentPosition.Y - mousePosition.Y);
            if (prevX > 0)
            {
                transform.X += prevX;
                transform.Y += prevY;
            }
        }
    }
}

Ключ сохраняет предыдущие смещения X и Y, а затем использует их для увеличения текущего смещения движения, чтобы получить правильное смещение агрегата.

Ответ 3

Что касается решения Corey Sunwold - я избавился от событий MouseUp и MouseDown, и я упростил метод MouseMove, используя MouseButtonState, как показано ниже). Я использую Canvas.SetLeft() и Canvas.SetTop() вместо RenderTransform, поэтому мне не нужно сохранять старую позицию из события MouseDown.

if (e.LeftButton == MouseButtonState.Pressed && draggableControl != null)
{
   //...
}

Ответ 4

Если кто-то хочет быстро увидеть этот эффект, вот минимальное решение, использующее только событие MouseMove.

Макет

<Canvas Background='Beige'
            Name='canvas'>
        <Rectangle Width='50'
                   Height='50'
                   Fill='LightPink'
                   Canvas.Left='350'
                   Canvas.Top='175'
                   MouseMove='Rectangle_MouseMove' />
    </Canvas>

Код позади

 private void OnMouseMove(object sender, MouseEventArgs e)
        {
            if (e.Source is Shape shape)
            {
                if (e.LeftButton == MouseButtonState.Pressed)
                {
                    Point p = e.GetPosition(canvas);
                    Canvas.SetLeft(shape, p.X - shape.ActualWidth / 2);
                    Canvas.SetTop(shape, p.Y - shape.ActualHeight / 2);
                    shape.CaptureMouse();
                }
                else
                {
                    shape.ReleaseMouseCapture();
                }
            }
        }

Ответ 5

У меня были некоторые проблемы с данными решениями и в итоге:

    public partial class UserControlDraggable : UserControl
{
    public UserControlDraggable()
    {
        InitializeComponent();

        MouseLeftButtonDown += new MouseButtonEventHandler(Control_MouseLeftButtonDown);
        MouseLeftButtonUp += new MouseButtonEventHandler(Control_MouseLeftButtonUp);
        MouseMove += new MouseEventHandler(Control_MouseMove);
    }

    private void Control_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        _isDragging = true;
        _mouseLocationWithinMe = e.GetPosition(this);

        CaptureMouse();
    }

    private void Control_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        _isDragging = false;
        this.ReleaseMouseCapture();
    }

    private void Control_MouseMove(object sender, MouseEventArgs e)
    {
        if (_isDragging)
        {
            var mouseWithinParent = e.GetPosition(Parent as UIElement);

            Canvas.SetLeft(this, mouseWithinParent.X - _mouseLocationWithinMe.X);
            Canvas.SetTop(this, mouseWithinParent.Y - _mouseLocationWithinMe.Y);
        }
    }

    protected bool _isDragging;
    Point _mouseLocationWithinMe;
}

Это в основном пример Кори, но использует намеки от Hawlett. Он работает ТОЛЬКО, когда родительский контейнер является холстом. Кроме того, он заслуживает того, чтобы его хватало с некоторыми ограничениями, чтобы пользователь не перетаскивал элемент управления в места, где это действительно не должно быть.

Ответ 6

Этот код отлично работает!

Button newBtn = new Button();
newBtn.AddHandler(Button.ClickEvent, new RoutedEventHandler(BtTable_Click));
newBtn.AddHandler(Button.PreviewMouseLeftButtonDownEvent, new MouseButtonEventHandler(BtTable_MouseLeftButtonDown));
newBtn.AddHandler(Button.PreviewMouseLeftButtonUpEvent, new MouseButtonEventHandler(BtTable_MouseLeftButtonUp));
newBtn.AddHandler(Button.PreviewMouseMoveEvent, new MouseEventHandler(BtTable_MouseMove));

Кнопка перемещения

private object movingObject;
private double firstXPos, firstYPos;
private int ButtonSize = 50;

private void BtTable_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    Button newBtn = sender as Button;
    Canvas canvas = newBtn.Parent as Canvas;

    firstXPos = e.GetPosition(newBtn).X;
    firstYPos = e.GetPosition(newBtn).Y - ButtonSize;

    movingObject = sender;

    // Put the image currently being dragged on top of the others
    int top = Canvas.GetZIndex(newBtn);
    foreach (Button child in canvas.Children)
        if (top < Canvas.GetZIndex(child))
            top = Canvas.GetZIndex(child);
    Canvas.SetZIndex(newBtn, top + 1);
    Mouse.Capture(null);
}

private void BtTable_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    Button newBtn = sender as Button;
    Canvas canvas = newBtn.Parent as Canvas;

    movingObject = null;

    // Put the image currently being dragged on top of the others
    int top = Canvas.GetZIndex(newBtn);
    foreach (Button child in canvas.Children)
        if (top > Canvas.GetZIndex(child))
            top = Canvas.GetZIndex(child);
    Canvas.SetZIndex(newBtn, top + 1);
    Mouse.Capture(newBtn);
}

private void BtTable_MouseMove(object sender, MouseEventArgs e)
{
    if (e.LeftButton == MouseButtonState.Pressed && sender == movingObject)
    {
        Button newBtn = sender as Button;
        Canvas canvas = newBtn.Parent as Canvas;
        // Horizontal
        double newLeft = e.GetPosition(canvas).X - firstXPos - canvas.Margin.Left;
        // newLeft inside canvas right-border?
        if (newLeft > canvas.Margin.Left + canvas.ActualWidth - newBtn.ActualWidth)
            newLeft = canvas.Margin.Left + canvas.ActualWidth - newBtn.ActualWidth;
        // newLeft inside canvas left-border?
        else if (newLeft < canvas.Margin.Left)
            newLeft = canvas.Margin.Left;

        newBtn.SetValue(Canvas.LeftProperty, newLeft);

        //Vertical
        double newTop = e.GetPosition(canvas).Y - firstYPos - canvas.Margin.Top;
        // newTop inside canvas bottom-border?
        // -- Bottom --
        if (newTop > canvas.Margin.Top + canvas.ActualHeight - newBtn.ActualHeight - ButtonSize)
            newTop = canvas.Margin.Top + canvas.ActualHeight - newBtn.ActualHeight - ButtonSize;
        // newTop inside canvas top-border?
        // -- Top --
        else if (newTop < canvas.Margin.Top - ButtonSize)
            newTop = canvas.Margin.Top - ButtonSize;

        newBtn.SetValue(Canvas.TopProperty, newTop);
    }
}

Счастливое кодирование;)

Ответ 7

Я реализовал это как для приложения WPF, так и для UWP. И добавьте весь код в пользовательский элемент управления, а не элемент управления, который его использует, вы можете изменить его в соответствии с вашими потребностями.

WPF

public partial class DragUserControl : UserControl
{
    public DragUserControl()
    {
        InitializeComponent();
    }

    object MovingObject;
    double FirstXPos, FirstYPos;

    private void Button_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        this.MovingObject = this;
        FirstXPos = e.GetPosition(MovingObject as Control).X;
        FirstYPos = e.GetPosition(MovingObject as Control).Y;

        Canvas canvas = this.Parent as Canvas;
        if (canvas != null)
        {
            canvas.PreviewMouseMove += this.MouseMove;
        }
    }

    private void MouseMove(object sender, MouseEventArgs e)
    {
        /*
         * In this event, at first we check the mouse left button state. If it is pressed and 
         * event sender object is similar with our moving object, we can move our control with
         * some effects.
         */
        Canvas canvas = sender as Canvas;

        Point canvasPoint = e.GetPosition(canvas);
        Point objPosition = e.GetPosition((MovingObject as FrameworkElement));
        if (e.LeftButton == MouseButtonState.Pressed)
        {
            if (MovingObject != null)
            {
//This condition will take care that control should not go outside the canvas.
                    if ((e.GetPosition((MovingObject as FrameworkElement).Parent as FrameworkElement).X - FirstXPos > 0) && (e.GetPosition((MovingObject as FrameworkElement).Parent as FrameworkElement).X - FirstXPos < canvas.ActualWidth - (MovingObject as FrameworkElement).ActualWidth))
                    {
                        (MovingObject as FrameworkElement).SetValue(Canvas.LeftProperty, e.GetPosition((MovingObject as FrameworkElement).Parent as FrameworkElement).X - FirstXPos);
                    }

//This condition will take care that control should not go outside the canvas.
                    if ((e.GetPosition((MovingObject as FrameworkElement).Parent as FrameworkElement).Y - FirstYPos > 0) && (e.GetPosition((MovingObject as FrameworkElement).Parent as FrameworkElement).Y - FirstYPos < canvas.ActualHeight - (MovingObject as FrameworkElement).ActualHeight))
                    {
                        (MovingObject as FrameworkElement).SetValue(Canvas.TopProperty, e.GetPosition((MovingObject as FrameworkElement).Parent as FrameworkElement).Y - FirstYPos);
                    }
                }
            }
        }

        private void Ellipse_PreviewMouseLeftButtonUp_1(object sender, MouseButtonEventArgs e)
        {
            MovingObject = null;
        }
    }

Button_MouseLeftButtonDown - это событие нажатия кнопки, через которое вы хотите перетащить элемент управления.

UWP

 public sealed partial class DragUserControl : UserControl
    {
        MovingObject;
        double FirstXPos, FirstYPos;

        public DragUserControl()
        {
            InitializeComponent();
        }

       private void Ellipse_PointerPressed(object sender, PointerRoutedEventArgs e)
        {
            this.MovingObject = this;

            FirstXPos = e.GetCurrentPoint(MovingObject as Control).Position.X;
            FirstYPos = e.GetCurrentPoint(MovingObject as Control).Position.Y;

            Canvas canvas = this.Parent as Canvas;
            if (canvas != null)
            {
                canvas.PointerMoved += Canvas_PointerMoved;
            }
        }

        private void Canvas_PointerMoved(object sender, PointerRoutedEventArgs e)
        {
            if (MovingObject != null)
            {
                Canvas canvas = sender as Canvas;

                Point canvasPoint = e.GetCurrentPoint(canvas).Position;
                Point objPosition = e.GetCurrentPoint((MovingObject as FrameworkElement)).Position;
                if (e.GetCurrentPoint(MovingObject as Control).Properties.IsLeftButtonPressed) //e.Pointer.IsInContact ==true)
                {
//This condition will take care that control should not go outside the canvas
                    if ((e.GetCurrentPoint((MovingObject as FrameworkElement).Parent as FrameworkElement).Position.X - FirstXPos > 0) && (e.GetCurrentPoint((MovingObject as FrameworkElement).Parent as FrameworkElement).Position.X - FirstXPos < canvas.ActualWidth - (MovingObject as FrameworkElement).ActualWidth))
                    {
                        (MovingObject as FrameworkElement).SetValue(Canvas.LeftProperty, e.GetCurrentPoint((MovingObject as FrameworkElement).Parent as FrameworkElement).Position.X - FirstXPos);
                    }

//This condition will take care that control should not go outside the canvas
                    if ((e.GetCurrentPoint((MovingObject as FrameworkElement).Parent as FrameworkElement).Position.Y - FirstYPos > 0) && (e.GetCurrentPoint((MovingObject as FrameworkElement).Parent as FrameworkElement).Position.Y - FirstYPos < canvas.ActualHeight - (MovingObject as FrameworkElement).ActualHeight))
                    {
                        (MovingObject as FrameworkElement).SetValue(Canvas.TopProperty, e.GetCurrentPoint((MovingObject as FrameworkElement).Parent as FrameworkElement).Position.Y - FirstYPos);
                    }
                }
            }
        }

        private void Ellipse_PointerReleased(object sender, PointerRoutedEventArgs e)
        {
            MovingObject = null;
        }
}

Ellipse_PointerPressed - событие щелчка эллипса, через которое вы хотите перетащить элемент управления.