How to make a user control draggable on screen like a window

14,158

Solution 1

Based upon information in @DmitryMartovoi's answer, I have come up with a way to make this work. I'm still giving Dmitry a +1 as I wouldn't have been able to figure this out without his contribution.

What I did was I created a TranslateTransform in my UserControl's constructor and assigned it to its RenderTransform property:

RenderTransform = new TranslateTransform();

In the XAML, I named the Border control that the user clicks on to drag the whole control:

<Border Background="{DynamicResource PopupBackground}"
        BorderBrush="{DynamicResource PopupBorder}"
        BorderThickness="5,5,5,0"
        MouseLeftButtonDown="Grid_MouseLeftButtonDown"
        MouseLeftButtonUp="Grid_MouseLeftButtonUp"
        MouseMove="Grid_MouseMove"
        Name="TitleBorder">

    . . .
</Border>

Finally, I modified the various Mouse event handlers as follows:

private void Grid_MouseLeftButtonDown( object sender, MouseButtonEventArgs e ) {
    CurrentMousePosition = e.GetPosition( Parent as Window );
    TitleBorder.CaptureMouse();
}

private void Grid_MouseLeftButtonUp( object sender, MouseButtonEventArgs e ) {
    if ( TitleBorder.IsMouseCaptured ) {
        TitleBorder.ReleaseMouseCapture();
    }
}

private void Grid_MouseMove( object sender, MouseEventArgs e ) {
    Vector diff = e.GetPosition( Parent as Window ) - CurrentMousePosition;
    if ( TitleBorder.IsMouseCaptured ) {
        ( RenderTransform as TranslateTransform ).X = diff.X;
        ( RenderTransform as TranslateTransform ).Y = diff.Y;
    }
}

This works beautifully. The entire UserControl and all of its contents move smoothly when you drag the Border, keeping up with the mouse. And the entire UserControl does not move if you click anywhere else on its surface.

Thanks again to @DmitryMartovoi for the code he supplied.

EDIT: I am editing this answer because the above code, while it worked, wasn't perfect. Its flaw is that the control would pop back to its original location on screen when you clicked on the title bar area and before you started dragging. This was annoying and totally wrong.

The approach I came up with that actually worked flawlessly involved first putting the control in a Canvas. It's important that the parent of the control be a Canvas or the following code won't work. I also stopped using the RenderTransform. I added a private property called canvas of type Canvas. I added a Loaded event handler to the popup control to do some important initialization:

private void KeyboardPopup_Loaded( object sender, RoutedEventArgs e ) {
    canvas = Parent as Canvas;
    if ( canvas == null ) {
        throw new InvalidCastException( "The parent of a KeyboardPopup control must be a Canvas." );
    }    
}

With all of this done, here are the modified Mouse event handlers:

private void TitleBorder_MouseLeftButtonDown( object sender, MouseButtonEventArgs e ) {
    StartMousePosition = e.GetPosition( canvas );
    TitleBorder.CaptureMouse();
}

private void TitleBorder_MouseLeftButtonUp( object sender, MouseButtonEventArgs e ) {
    if ( TitleBorder.IsMouseCaptured ) {
        Point mousePosition = e.GetPosition( canvas );
        Canvas.SetLeft( this, Canvas.GetLeft( this ) + mousePosition.X - StartMousePosition.X );
        Canvas.SetTop ( this, Canvas.GetTop ( this ) + mousePosition.Y - StartMousePosition.Y );
        canvas.ReleaseMouseCapture();
    }
}

private void TitleBorder_MouseMove( object sender, MouseEventArgs e ) {
    if ( TitleBorder.IsMouseCaptured && e.LeftButton == MouseButtonState.Pressed ) {
        Point mousePosition = e.GetPosition( canvas );

        // Compute the new Left & Top coordinates of the control
        Canvas.SetLeft( this, Canvas.GetLeft( this ) + mousePosition.X - StartMousePosition.X );
        Canvas.SetTop ( this, Canvas.GetTop ( this ) + mousePosition.Y - StartMousePosition.Y );
        StartMousePosition = mousePosition;
    }
}

The control stays where you dropped it when you click on the title bar to move it a second time, and it only moves when you click on the title bar. Clicking anywhere else in the control does nothing, and dragging is smooth and responsive.

Solution 2

You can simply use MouseDragElementBehavior.

UPD Important thing about MouseDragElementBehavior behavior:

The MouseDragElementBehavior behavior doesn't work for any controls that handle MouseClick events (Button, TextBox, and ListBox controls, for example). If you need the ability to drag a control of one of these types, make that control a child of a control that can be dragged (a border, for example). You can then apply the MouseDragElementBehavior behavior to the parent element.

You can also implement your own drag behavior like this:

public class DragBehavior : Behavior<UIElement>
{
    private Point elementStartPosition;
    private Point mouseStartPosition;
    private TranslateTransform transform = new TranslateTransform();

    protected override void OnAttached()
    {
        Window parent = Application.Current.MainWindow;
        AssociatedObject.RenderTransform = transform;

        AssociatedObject.MouseLeftButtonDown += (sender, e) => 
        {
            elementStartPosition = AssociatedObject.TranslatePoint( new Point(), parent );
            mouseStartPosition = e.GetPosition(parent);
            AssociatedObject.CaptureMouse();
        };

        AssociatedObject.MouseLeftButtonUp += (sender, e) =>
        {
            AssociatedObject.ReleaseMouseCapture();
        };

        AssociatedObject.MouseMove += (sender, e) =>
        {
            Vector diff = e.GetPosition( parent ) - mouseStartPosition;
            if (AssociatedObject.IsMouseCaptured)
            {
                transform.X = diff.X;
                transform.Y = diff.Y;
            }
        };
    }
}

Solution 3

http://www.codeproject.com/Tips/442276/Drag-and-Drop-WPF-Controls This is the awesome solution I got after spending lot of time. Although example shown here are normal controls but after some changes you can make it work for user controls also.

Share:
14,158

Related videos on Youtube

Tony Vitabile
Author by

Tony Vitabile

Updated on September 26, 2022

Comments

  • Tony Vitabile
    Tony Vitabile over 1 year

    My WPF application has a UserControl which is supposed to look and behave like a popup window, but it isn't a window. The reason the control doesn't descend from the Window class is because it contains a third-party virtual on-screen keyboard, and that control has to be in the same window as the TextBox controls that it sends input characters to when you click on its buttons. If the keyboard control is not in the same window, it can't even see the TextBox controls.

    The problem I'm having is performance is abysmal when dragging the dialog around. It's sufficiently slow that the mouse comes off the drag area and it stops following the mouse. I need a better way.

    Here's an excerpt from the xaml for the control:

    <Grid Name="LayoutRoot">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Border Background="{DynamicResource PopupBackground}"
                BorderBrush="{DynamicResource PopupBorder}"
                BorderThickness="5,5,5,0"
                MouseLeftButtonDown="Grid_MouseLeftButtonDown"
                MouseLeftButtonUp="Grid_MouseLeftButtonUp"
                MouseMove="Grid_MouseMove">
        . . .
        </Border>
    </Grid>
    

    Here's the mouse event handlers:

        private void Grid_MouseLeftButtonDown( object sender, MouseButtonEventArgs e ) {
            Canvas canvas = Parent as Canvas;
            if ( canvas == null ) {
                throw new InvalidCastException( "The parent of a KeyboardPopup control must be a Canvas." );
            }
            DraggingControl = true;
            CurrentMousePosition = e.GetPosition( canvas );
            e.Handled = true;
        }
    
        private void Grid_MouseLeftButtonUp( object sender, MouseButtonEventArgs e ) {
            Canvas canvas = Parent as Canvas;
            if ( canvas == null ) {
                throw new InvalidCastException( "The parent of a KeyboardPopup control must be a Canvas." );
            }
    
            if ( DraggingControl ) {
                Point mousePosition = e.GetPosition( canvas );
    
                // Correct the mouse coordinates in case they go off the edges of the control
                if ( mousePosition.X < 0.0 ) mousePosition.X = 0.0; else if ( mousePosition.X > canvas.ActualWidth ) mousePosition.X = canvas.ActualWidth;
                if ( mousePosition.Y < 0.0 ) mousePosition.Y = 0.0; else if ( mousePosition.Y > canvas.ActualHeight ) mousePosition.Y = canvas.ActualHeight;
    
                // Compute the new Left & Top coordinates of the control
                Canvas.SetLeft( this, Left += mousePosition.X - CurrentMousePosition.X );
                Canvas.SetTop( this, Top += mousePosition.Y - CurrentMousePosition.Y );
            }
            e.Handled = true;
        }
    
        private void Grid_MouseMove( object sender, MouseEventArgs e ) {
            Canvas canvas = Parent as Canvas;
            if ( canvas == null ) {
                // It is not.  Throw an exception
                throw new InvalidCastException( "The parent of a KeyboardPopup control must be a Canvas." );
            }
    
            if ( DraggingControl && e.LeftButton == MouseButtonState.Pressed ) {
                Point mousePosition = e.GetPosition( canvas );
    
                // Correct the mouse coordinates in case they go off the edges of the control
                if ( mousePosition.X < 0.0 ) mousePosition.X = 0.0; else if ( mousePosition.X > canvas.ActualWidth  ) mousePosition.X = canvas.ActualWidth;
                if ( mousePosition.Y < 0.0 ) mousePosition.Y = 0.0; else if ( mousePosition.Y > canvas.ActualHeight ) mousePosition.Y = canvas.ActualHeight;
    
                // Compute the new Left & Top coordinates of the control
                Canvas.SetLeft( this, Left += mousePosition.X - CurrentMousePosition.X );
                Canvas.SetTop ( this, Top  += mousePosition.Y - CurrentMousePosition.Y );
    
                CurrentMousePosition = mousePosition;
            }
            e.Handled = true;
        }
    

    Note that the control must be placed inside a Canvas in the window that uses it.

    I can't use DragMove as it's a method of the Window class and this class descends from UserControl. How do I improve the performance of this control's dragging? Do I have to resort to Win32 APIs?

  • Tony Vitabile
    Tony Vitabile over 11 years
    While this works, it has problems. The first is that it only applies to the element you add the behavior to & its children. At first, I added the behavior to a Border that is the background of a "title bar" on the control. Only the Border was draggable and the rest of the control stayed put. When I apply the behavior to anything higher, like the UserControl itself or the Grid that holds everything, the behavior works if you drag anything within them, including the background and any of the buttons that make up the keys. This isn't acceptable, either.
  • Tony Vitabile
    Tony Vitabile over 11 years
    Is there a way that I can get the drag to work for the whole control without letting the user drag on the background or other controls in the template?
  • Dzmitry Martavoi
    Dzmitry Martavoi over 11 years
    You cant directly drag controls that handles MouseClick event. See upd.
  • Tony Vitabile
    Tony Vitabile over 11 years
    Thanks, but that behavior has the same exact problems as the MouseElementDragBehavior has. Note that I'm not applying the behavior to any buttons; it's just that even if a Button is a child of the control to which the behavior is applied, you can drag the entire thing by it. I need this behavior to work exactly the way that dragging a window works: You drag from the title bar only. I have an area on my control that is supposed to act like the title bar. I just need to make it so that the whole UserControl will follow that area when it is dragged.
  • Alejandro Vicaria
    Alejandro Vicaria about 2 years
    It specifically says that he cannot use DragMove because it's inherited from UserControl so this answer is incorrect.