Как перетащить UserControl внутрь Canvas

У меня есть Canvas, в котором пользователь может добавлять подклассы UserControl, содержащие форму. Пользователь должен иметь возможность перетаскивать эти UserControl по холсту.

Как лучше всего это сделать с WPF?


person loris    schedule 29.09.2009    source источник


Ответы (7)


Это делается в 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, созданный с помощью этого кода, тогда все перетаскиваемые элементы управления должны расширять ПеретаскиваемыйКонтроль.

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

this.MyDataGrid.LostMouseCapture += (sender, e) => { this.isDragging = false; };
person Corey Sunwold    schedule 29.09.2009
comment
Отличный ответ! Именно то, что я искал. Скопируйте и вставьте, и это сработало. И более элегантно, чем мои попытки. Я надеюсь, что это тоже поможет лори;) - person Marcel B; 30.09.2009
comment
Отличная работа! С некоторыми незначительными изменениями в обработчике перемещения мыши он работает отлично. Большое спасибо! - person loris; 30.09.2009
comment
@loris: Мне интересно услышать, какие изменения вам нужно было внести и почему вы их сделали. Я уже использовал этот точный код в нескольких проектах, и он работал нормально. Может быть, я что-то упустил из виду. - person Corey Sunwold; 01.10.2009
comment
Это действительно помогло мне. Большое спасибо за публикацию этого. - person Ciel; 04.02.2010
comment
Это не обязательно должно быть на холсте. ... Почему это ограничение? - person serhio; 20.09.2010
comment
Вопрос, однако, относился к Canvas, как к родителю... Сложно ли изменить эту логику для родителя Canvas? - person serhio; 20.09.2010
comment
@Corey: Нет, у него какая-то странная дельта, когда mouseDown на холсте. Вы же сами написали: 1. Это не обязательно должно быть в канве. - person serhio; 20.09.2010
comment
В Control_MouseLeftButtonDown я обнаружил, что мне нужно заменить clickPosition = e.GetPosition(this); на clickPosition = e.GetPosition(this.Parent as UIElement);. До внесения этого изменения элементы, на которые я нажимал, прыгали вниз и вправо (поскольку он получал начальную позицию в пределах границ самого элемента, а не холста хостинга). - person Jeff; 03.05.2013
comment
@CoreySunwold: ваш пример работает для меня. Я использую его в tabcontrol. Пользовательский элемент управления можно перетащить в любое место вкладки. Теперь я хочу знать, как сделать так, чтобы граница пользовательского элемента управления не пересекала родительский элемент управления? Я имею в виду, что часть пользовательского элемента управления становится невидимой, если я перетащу ее за родительскую границу. Как это предотвратить? - person Abhishek; 30.05.2014
comment
@Abhishek Если вы каким-то образом знаете размеры родительского элемента управления из Control_MouseMove, вы можете выполнить проверку, чтобы убедиться, что координаты преобразования X и Y не находятся за пределами родительского элемента управления. - person Corey Sunwold; 01.06.2014

Ответ Кори в основном правильный, но в нем отсутствует один важный элемент: память о том, каким было последнее преобразование. В противном случае, когда вы перемещаете элемент, отпустите кнопку мыши, а затем щелкните этот элемент еще раз, преобразование сбрасывается на (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, а затем их использование для увеличения смещения текущего движения, чтобы получить правильное совокупное смещение.

person DonBoitnott    schedule 03.05.2018
comment
Это не удается при третьем щелчке по перемещаемому объекту. Однако если удалить последнее условие IF в событии MouseMove, оно будет работать правильно. - person SezMe; 12.02.2021

Если кто-то хочет попробовать минимальное решение, вот решение с использованием события MouseMove.

Макет

<Canvas 
  Background='Beige'
  Name='canvas'>

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

</Canvas>

Код

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();
    }
  }
}
person Themelis    schedule 09.10.2019
comment
Я не могу не подчеркнуть, что иногда самые простые решения являются лучшими. Это отлично работает! Спасибо - person Rafael Ventura; 01.11.2020

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

if (e.LeftButton == MouseButtonState.Pressed && draggableControl != null)
{
   //...
}
person Hawlett    schedule 30.04.2015
comment
Круто, мне просто интересно, почему мой контроль продолжает возвращаться в исходное положение!! Вы ответили на мой вопрос до того, как я его задал. +1 - person Duncan Groenewald; 25.07.2015
comment
на самом деле, как вы получаете расстояние X и Y для перемещения без события MouseDown? - person Duncan Groenewald; 25.07.2015
comment
Я проголосовал против, потому что вы не предоставили код. У меня была та же проблема, что и у Дункана, и, видимо, вы нашли решение... просто не показали его. - person CoderForHire; 04.08.2015

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

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

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

person Wally Hynds    schedule 15.02.2016
comment
Да, это было опубликовано с некоторыми оговорками. Он отлично работал для меня в определенной среде. - person Wally Hynds; 28.08.2016

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

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

Удачного программирования ;)

person Community    schedule 15.09.2017

Я реализовал это как для 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 — это событие нажатия кнопки, через которую вы хотите перетащить элемент управления.

Универсальная платформа

 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 — это событие щелчка эллипса, через которое вы хотите перетащить элемент управления.

person Rahul Sonone    schedule 05.10.2016