Wrong mouse position in drag and drop inside grid class

2.6k views Asked by At

I'm basically writing a simple floating panel whose location can be changed via dragging its title bar (which is a grid itself). But I can't get it working! It seems MouseEventArgs.GetPosition returns a wrong point. What am I missing here?

public class FloatingPanel : Grid
    {
        Grid gridTitle;

        bool dragging = false;
        Point lastPos;

        public FloatingPanel(UserControl gadget)
        {
            this.MouseMove += FloatingPanel_MouseMove;

            gridTitle = new Grid();
            gridTitle.Height = 25;
            gridTitle.VerticalAlignment = System.Windows.VerticalAlignment.Top;
            gridTitle.Background = Brushes.Cyan;
            gridTitle.MouseLeftButtonDown += gridTitle_MouseLeftButtonDown;
            gridTitle.MouseLeftButtonUp += gridTitle_MouseLeftButtonUp;
            this.Children.Add(gridTitle);

            this.Height = gadget.Height + 25;
            this.Width = gadget.Width;

            gadget.VerticalAlignment = System.Windows.VerticalAlignment.Bottom;
            this.Children.Add(gadget);
        }

        void gridTitle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            dragging = false;
        }

        void gridTitle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            lastPos = Mouse.GetPosition(this);
            dragging = true;
        }

        void FloatingPanel_MouseMove(object sender, MouseEventArgs e)
        {
            if(dragging)
            {
                Vector delta = e.GetPosition(this) - lastPos;
                this.Margin = new Thickness(this.Margin.Left + delta.X, this.Margin.Top + delta.Y, this.Margin.Right, this.Margin.Bottom);
                lastPos = e.GetPosition(this);
            }
        }
    }  

I've also tried using System.Windows.Forms.Cursor.Position and System.Windows.Forms.Control.MousePosition which give the position on the screen. But no luck.
Solution: The problem was solved by 3 modifications(as Sphinxxx pointed out):
- Using MouseEventArgs.GetPosition(null) instead of MouseEventArgs.GetPosition(this)
- Capturing and releasing the mouse in mousedown and mouseup events using Mouse.Capture(gridTitle) and Mouse.Capture(null)
- Setting the grid's horizontal and vertical alignment. (This seems odd to me. Why does nod setting the alignment cause a problem?)

1

There are 1 answers

9
Sphinxxx On BEST ANSWER

In _MouseMove, you're trying to calculate the movement using e.GetPosition(this), but that only gets the mouse pointer position relative to your Grid. You need to find the position relative to some other UI element, e.g. the containing Window in order to know how much the grid should move.

Try e.GetPosition(null) (that is null instead of this) in both _MouseLeftButtonDown and _MouseMove to calculate correct deltas.

This article illustrates the difference: Getting the Mouse Position Relative to a Specific Element

EDIT: More robust FloatingPanel:

In the constructor, avoid having a gadget that may end up on top of the title bar by putting them in two separate RowDefinitions (and let WPF handle widths and heights):

public FloatingPanel(FrameworkElement gadget)
{
    gridTitle = new Grid();
    gridTitle.Height = 25;
    gridTitle.Background = Brushes.Cyan;

    gridTitle.MouseLeftButtonDown += gridTitle_MouseLeftButtonDown;
    gridTitle.MouseMove += gridTitle_MouseMove;
    gridTitle.MouseLeftButtonUp += gridTitle_MouseLeftButtonUp;

    //Create two grid rows - one to hold the title bar..
    this.RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto });
    Grid.SetRow(gridTitle, 0);
    this.Children.Add(gridTitle);

    //..and one two hold the gadget:
    this.RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto });
    Grid.SetRow(gadget, 1);
    this.Children.Add(gadget);
}

In the ..ButtonDown/..ButtonUp handlers, make the title bar "capture" (and release) the mouse movements so the mouse pointer doesn't "slide off" when moving too fast:

void gridTitle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    //Relese a previous capture:
    Mouse.Capture(null);
    dragging = false;
}

void gridTitle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    lastPos = Mouse.GetPosition(null);

    //Capture the mouse to ensure we get all future mouse movements:
    Mouse.Capture(gridTitle);
    dragging = true;
}

EDIT 2: Alternative without Mouse.Capture():

...
gridTitle.MouseLeftButtonDown += gridTitle_MouseLeftButtonDown;

//gridTitle.MouseMove += gridTitle_MouseMove;

//The parent Window isn't available yet here in the constructor,
//so we must wait for our Loaded event to hook it up:
this.Loaded += (s, e) => { Window.GetWindow(this).MouseMove += gridTitle_MouseMove; };

gridTitle.MouseLeftButtonUp += gridTitle_MouseLeftButtonUp;
...