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?

ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
Mark
  • 14,820
  • 17
  • 99
  • 159

2 Answers2

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;
    }
}
BrunoLM
  • 97,872
  • 84
  • 296
  • 452
  • 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? http://stackoverflow.com/questions/32597184/wpf-drag-and-drop-with-adorner-using-mouse-and-touch – Misiu Sep 16 '15 at 06:10
  • 1
    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. – BradleyDotNET Jan 15 '16 at 23:35
  • 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? –  Feb 19 '16 at 13:05
  • 3
    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; – Jacob Jan 22 '18 at 23:13
  • This can get even more complicated with high-DPI displays that are being scaled if your application is not DPI aware. See https://msdn.microsoft.com/en-us/library/windows/desktop/ff684173(v=vs.85).aspx and also https://stackoverflow.com/questions/3981509/how-to-get-mouse-position-related-to-desktop-in-wpf – dansan Mar 25 '18 at 02:46
  • 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. – tCoe May 09 '18 at 15:28
  • 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. – Chebz Jun 10 '22 at 18:11
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.

thatguy
  • 21,059
  • 6
  • 30
  • 40
Quartermeister
  • 57,579
  • 7
  • 124
  • 111
  • 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: http://dotnetslackers.com/ado_net/re-191632_generic_wpf_drag_and_drop_adorner.aspx – Mark Jun 28 '10 at 00:49
  • 2
    @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. – Quartermeister Jun 28 '10 at 01:10
  • cool ill take a look at that, Ive got to stop skimming so many articles – Mark Jun 28 '10 at 02:18
  • 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. – ScottFoster1000 Apr 10 '19 at 21:02
  • The Blog entry has been moved and the link is broken. I think the link adress is now: https://learn.microsoft.com/de-de/archive/blogs/jaimer/drag-amp-drop-in-wpf-explained-end-to-end – BerndGit Apr 13 '20 at 14:21