WPF 4 Drag and Drop with visual element as cursor
Asked Answered
D

2

10

I have a WPF 4 app which I want to enable drag and drop with, currently I have it working with a basic drag and drop implementation, but I have found that it would be much better if, instead of the mouse cursor changing over to represent the move operation, I could use an image underneath my finger.

My drag and drop operation is initiated inside a custom user control, so I will need to insert a visual element into the visual tree and have it follow my finger around, perhaps I should enable the ManipulationDelta event on my main window, check for a boolean then move the item around?

Definition answered 28/6, 2010 at 0:20 Comment(0)
B
11

There is an example of using a custom drag cursor at Jaime Rodriguez msdn blog. You can handle the GiveFeedback event and change the mouse cursor, but to use a custom Visual the author creates a new Window and updates the position on QueryContinueDrag.

Babb answered 28/6, 2010 at 0:38 Comment(5)
I did actually read that article, and the issue I can see is that the mouse cursor can only be set to a System.Windows.Input.Cursor which I dont see how to use a UIElement... (which I want to have animations and more). Thanks for the help though. I was actually thinking of using a custom version of this library: dotnetslackers.com/ado_net/…Definition
@Mark: I think the article addresses that in the section "Next scenario: Getting fancy and using the Visual we are dragging for feedback [instead of a cursor]" by creating a separate Window to follow the mouse like a cursor. I haven't actually tried it, though.Babb
cool ill take a look at that, Ive got to stop skimming so many articlesDefinition
What I don't like about the new window solution is that it affects the operating system, when you do the drag, the icon on the task tray is no longer selected because it's not the active window.Sunglass
The Blog entry has been moved and the link is broken. I think the link adress is now: learn.microsoft.com/de-de/archive/blogs/jaimer/…Febrile
P
33

From the mentioned article I was able to simplify a little. Basically what you need to do is subscribe in 3 events:

  • PreviewMouseLeftButtonDownEvent: The event that runs when you press the left button, you can start the drag action by invoking DragDrop.DoDragDrop
  • DropEvent: The event that runs when you drop something (control must have AllowDrop set to true in order to accept drops)
  • GiveFeedbackEvent: The event that runs all the time allowing you to give constant feedback

DragDrop.DoDragDrop(draggedItem, draggedItem.DataContext, DragDropEffects.Move); the first parameter is the element you are dragging, then the second is the data it is carrying and last the mouse effect.

This method locks the thread. So everything after its call will only execute when you stop dragging.

In the drop event you can retrieve the data you sent on the DoDragDrop call.

The source for my tests are located bellow, and the result is:

Sample drag n' drop (gif)

Full Source

MainWindow.xaml

<Window x:Class="TestWpfPure.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:uc="clr-namespace:TestWpfPure"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <ListBox x:Name="CardListControl" AllowDrop="True" ItemsSource="{Binding Items}" />
    </Grid>
</Window>

Card.xaml

<UserControl x:Class="TestWpfPure.Card"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Border x:Name="CardBorder" BorderBrush="Black" BorderThickness="3" HorizontalAlignment="Left" Height="40" VerticalAlignment="Top" Width="246" RenderTransformOrigin="0.5,0.5" CornerRadius="6">
            <TextBlock Text="{Binding Text}" TextWrapping="Wrap" FontFamily="Arial" FontSize="14" />
        </Border>
    </Grid>
</UserControl>

MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Effects;
using System.Windows.Shapes;

namespace TestWpfPure
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public ObservableCollection<Card> Items { get; set; }

        private readonly Style listStyle = null;
        private Window _dragdropWindow = null;

        public MainWindow()
        {
            InitializeComponent();

            Items = new ObservableCollection<Card>(new List<Card>
            {
                new Card { Text = "Task #01" },
                new Card { Text = "Task #02" },
                new Card { Text = "Task #03" },
                new Card { Text = "Task #04" },
                new Card { Text = "Task #05" },
            });

            listStyle = new Style(typeof(ListBoxItem));
            listStyle.Setters.Add(new Setter(ListBoxItem.AllowDropProperty, true));
            listStyle.Setters.Add(new EventSetter(ListBoxItem.PreviewMouseLeftButtonDownEvent, new MouseButtonEventHandler(CardList_PreviewMouseLeftButtonDown)));
            listStyle.Setters.Add(new EventSetter(ListBoxItem.DropEvent, new DragEventHandler(CardList_Drop)));
            listStyle.Setters.Add(new EventSetter(ListBoxItem.GiveFeedbackEvent, new GiveFeedbackEventHandler(CardList_GiveFeedback)));

            CardListControl.ItemContainerStyle = listStyle;

            DataContext = this;
        }

        protected void CardList_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (sender is ListBoxItem)
            {
                var draggedItem = sender as ListBoxItem;
                var card = draggedItem.DataContext as Card;

                card.Effect = new DropShadowEffect
                {
                    Color = new Color { A = 50, R = 0, G = 0, B = 0 },
                    Direction = 320,
                    ShadowDepth = 0,
                    Opacity = .75,
                };
                card.RenderTransform = new RotateTransform(2.0, 300, 200);

                draggedItem.IsSelected = true;

                // create the visual feedback drag and drop item
                CreateDragDropWindow(card);
                DragDrop.DoDragDrop(draggedItem, draggedItem.DataContext, DragDropEffects.Move);
            }
        }

        protected void CardList_Drop(object sender, DragEventArgs e)
        {
            var droppedData = e.Data.GetData(typeof(Card)) as Card;
            var target = (sender as ListBoxItem).DataContext as Card;

            int targetIndex = CardListControl.Items.IndexOf(target);

            droppedData.Effect = null;
            droppedData.RenderTransform = null;

            Items.Remove(droppedData);
            Items.Insert(targetIndex, droppedData);

            // remove the visual feedback drag and drop item
            if (this._dragdropWindow != null)
            {
                this._dragdropWindow.Close();
                this._dragdropWindow = null;
            }
        }

        private void CardList_GiveFeedback(object sender, GiveFeedbackEventArgs e)
        {
            // update the position of the visual feedback item
            Win32Point w32Mouse = new Win32Point();
            GetCursorPos(ref w32Mouse);

            this._dragdropWindow.Left = w32Mouse.X;
            this._dragdropWindow.Top = w32Mouse.Y;
        }

        private void CreateDragDropWindow(Visual dragElement)
        {
            this._dragdropWindow = new Window();
            _dragdropWindow.WindowStyle = WindowStyle.None;
            _dragdropWindow.AllowsTransparency = true;
            _dragdropWindow.AllowDrop = false;
            _dragdropWindow.Background = null;
            _dragdropWindow.IsHitTestVisible = false;
            _dragdropWindow.SizeToContent = SizeToContent.WidthAndHeight;
            _dragdropWindow.Topmost = true;
            _dragdropWindow.ShowInTaskbar = false;

            Rectangle r = new Rectangle();
            r.Width = ((FrameworkElement)dragElement).ActualWidth;
            r.Height = ((FrameworkElement)dragElement).ActualHeight;
            r.Fill = new VisualBrush(dragElement);
            this._dragdropWindow.Content = r;


            Win32Point w32Mouse = new Win32Point();
            GetCursorPos(ref w32Mouse);


            this._dragdropWindow.Left = w32Mouse.X;
            this._dragdropWindow.Top = w32Mouse.Y;
            this._dragdropWindow.Show();
        }


        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        internal static extern bool GetCursorPos(ref Win32Point pt);

        [StructLayout(LayoutKind.Sequential)]
        internal struct Win32Point
        {
            public Int32 X;
            public Int32 Y;
        };
    }
}

Card.xaml.cs

using System.ComponentModel;
using System.Windows.Controls;

namespace TestWpfPure
{
    /// <summary>
    /// Interaction logic for Card.xaml
    /// </summary>
    public partial class Card : UserControl, INotifyPropertyChanged
    {
        private string text;
        public string Text
        {
            get
            {
                return this.text;
            }
            set
            {
                this.text = value;

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("Text"));
            }
        }

        public Card()
        {
            InitializeComponent();

            DataContext = this;
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}
Pinwheel answered 15/1, 2015 at 23:45 Comment(7)
I know this question is super old, but maybe You could help me. I've used Your solution to drag image to listbox, works great when I release mouse on valid element, if I release mouse while dragging and not on valid target small window next to cursor stays on screen. Could You look at my question? #32597684Sewing
This is an absolutely epic answer. The only thing I added was a call to Close on the window after DoDragDrop so it would close no matter where the drag finished.Dowser
Is there a way to implement this with the card class as just a regular class and the control template defined in the ListBox DataTemplate?Berrios
Also you may need to convert from screen coordinates to application coordinates. Application.Current.MainWindow.PointFromScreen(new Point(w32Mouse.X, w32Mouse.Y)); And if you want the window to minimize with the application: _dragdropWindow.Topmost = false; _dragdropWindow.Owner = Application.Current.MainWindow;Radioisotope
This can get even more complicated with high-DPI displays that are being scaled if your application is not DPI aware. See msdn.microsoft.com/en-us/library/windows/desktop/… and also #3982009Hendricks
Am I missing where the rectangle is added to the Mouse Cursor? My app is running fine and dragging and dropping, but the rectangle doesn't attach to the cursor.Multiparous
I know this post is ancient, but this does not address one major flow - dropping outside of the control will "freeze" the element. There doesn't seem to be any way of handling that outside of some hacky low-level mouse event hook.Aspersorium
B
11

There is an example of using a custom drag cursor at Jaime Rodriguez msdn blog. You can handle the GiveFeedback event and change the mouse cursor, but to use a custom Visual the author creates a new Window and updates the position on QueryContinueDrag.

Babb answered 28/6, 2010 at 0:38 Comment(5)
I did actually read that article, and the issue I can see is that the mouse cursor can only be set to a System.Windows.Input.Cursor which I dont see how to use a UIElement... (which I want to have animations and more). Thanks for the help though. I was actually thinking of using a custom version of this library: dotnetslackers.com/ado_net/…Definition
@Mark: I think the article addresses that in the section "Next scenario: Getting fancy and using the Visual we are dragging for feedback [instead of a cursor]" by creating a separate Window to follow the mouse like a cursor. I haven't actually tried it, though.Babb
cool ill take a look at that, Ive got to stop skimming so many articlesDefinition
What I don't like about the new window solution is that it affects the operating system, when you do the drag, the icon on the task tray is no longer selected because it's not the active window.Sunglass
The Blog entry has been moved and the link is broken. I think the link adress is now: learn.microsoft.com/de-de/archive/blogs/jaimer/…Febrile

© 2022 - 2024 — McMap. All rights reserved.