WinForms - Using Custom Cursors With Drag & Drop

Skill

WinForms - Using Custom Cursors With Drag & Drop

Posted in:

A week or so ago, a comment was left on the WinForms Custom Cursors tutorial asking about how to change the cursor during a drag and drop operation. This is a great question, because, well, its not at all obvious how to do. The first time I needed to do it, it took me quite a while to find out how - and I had to piece the answer together from a number of different sources.

But no more! That comment reminded me of that pain, so I decided to write up a tutorial explaining exactly how to set the cursor to whatever you want during a drag drop operation. This tutorial won't go in depth into the standard Drag & Drop code - there are dozens and dozens of tutorials out there that go through basic WinForms drag-drop (although, if you'd like us to write one of our own, just let us know in the comments). So without further ado, a picture of the simple app we will be creating:

Tree View With Some Nodes

So what do we have here? Well, its a tree view with some nodes, and an empty list box. We are going to make it so that you can drag a node from the tree view into the list box - a pretty simple application of drag and drop. What is going to make this special is that the cursor will be the text of the node you are dragging when your mouse is over the list box, like so:

Dragging a tree view

So lets start building this class. I'm going to assume you know how to use the form designer to pull in a list view and a tree view, so I'm not going to explain/show the designer created code :P. In the code below, the variable _DragSrc is the Tree View, and the variable _DragDest is the List View. First we are going to set up the basic drag drop stuff:

public partial class DragDropCursorTest : Form
{
  private const int MinimumDragDistance = 4;
  private Point _OrigMousePoint;

  public DragDropCursorTest()
  {
    InitializeComponent();

    _DragSource.MouseDown += DragSource_MouseDown;

    _DragDest.DragDrop += DragDest_DragDrop;
    _DragDest.DragEnter += DragDest_DragEnter;
    _DragDest.AllowDrop = true;
  }

  private void DragSource_MouseDown(object sender, MouseEventArgs e)
  {
    if (e.Button != MouseButtons.Left)
      return;

    if (_DragSource.GetNodeAt(e.Location) == null)
      return;

    _OrigMousePoint = e.Location;
    _DragSource.MouseMove += DragSource_MouseMove;
    _DragSource.MouseUp += DragSource_MouseUp;
  }

  private void DragSource_MouseUp(object sender, MouseEventArgs e)
  {
    _DragSource.MouseMove -= DragSource_MouseMove;
    _DragSource.MouseUp -= DragSource_MouseUp;
  }

  private void DragSource_MouseMove(object sender, MouseEventArgs e)
  {
    if (Math.Abs(e.X - _OrigMousePoint.X) < MinimumDragDistance
        && Math.Abs(e.Y - _OrigMousePoint.Y) < MinimumDragDistance
        && e.Button == MouseButtons.Left)
      return;

    _DragSource.MouseMove -= DragSource_MouseMove;
    _DragSource.MouseUp -= DragSource_MouseUp;

    if (e.Button != MouseButtons.Left)
      return;

    _DragSource.SelectedNode = _DragSource.GetNodeAt(_OrigMousePoint);

    _DragSource.DoDragDrop(_DragSource.SelectedNode.Text, DragDropEffects.Copy);
  }

  private void DragDest_DragEnter(object sender, DragEventArgs e)
  {
    if (!e.Data.GetDataPresent(typeof(String)))
    {
      e.Effect = DragDropEffects.None;
      return;
    }

    e.Effect = DragDropEffects.Copy;
  }

  private void DragDest_DragDrop(object sender, DragEventArgs e)
  {
    if (!e.Data.GetDataPresent(typeof(String)))
      return;

    string str = e.Data.GetData(typeof(String)) as String;

    if (String.IsNullOrEmpty(str))
      return;

    _DragDest.Items.Add(str);
  }
}

So with the code above, you can drag a node from the tree view and drop it in the list box (And the standard drag copy cursor will be used). Lets do a quick walkthrough of what this code is doing. In the constructor, I attach to 3 events - the MouseDown on the tree view, and the DragEnter and DragDrop events on the list view. Also, we set the property AllowDrop to true on the list view - because otherwise, the list view would never receive any drag events. We will take a look at the action in the DragSource_MouseDown function first.

Here, we ignore the mouse down if it is not the left mouse, and we also ignore it if where the user clicked is not over a node in the tree. Otherwise, we save the initial click point in the field _OrigMousePoint, and attach two more events to the tree view. These are MouseMove, so we can actually detect when the drag starts, and MouseUp so we can detect if the user didn't actually want to drag. The DragSource_MouseUp is really simple - we just detach our hooks from the MouseUp and MouseMove events. The DragSource_MouseMove is a bit more complicated.

This is because it is generally nice to have a drag threshold, since most of the time users do not keep there mouse perfectly still. We don't want to initiate a drag operation unless the user really wants it, so in this case we wait until the mouse has moved MinimumDragDistance in either the X or the Y direction (in this case 4 pixels). But once we have passed that threshold, we can detach our hooks from the MouseMove and MouseUp events, and (if the left mouse button is down), actually initiate the drag. First, just as a nicety, we set the selected node to the node that was clicked on (the tree view default behavior does not select a node until MouseUp). And then we start a drag drop operation by calling DoDragDrop on the tree view, with the text of the node as the data, and the only allowed D&D effect being Copy.

Now that we got something to start dragging, lets take a look at the dropping side of things. First, we have the function DragDest_DragEnter, which is attached to the DragEnter event on _DragDest (the list view). This event will be fired whenever the mouse enters the list view while dragging something. A little side note here, in case people were wondering - the regular mouse events do not fire during a drag operation - so you will not get events like MouseEnter, MouseLeave, or MouseMove. You have to attach to the corresponding DragEnter, DragLeave, or DragOver events.

When our DragDest_DragEnter function gets called, we first check what kind of data the drag is carrying. In this case, we only accept strings, so if there is no string data present, we set the drag drop effect to None. This will cause the cursor to be the "No" symbol while over the list view. If there is string data preset, we set the effect to Copy, and we will get a standard "copy" cursor over the list view.

And now for the actual drop - DragDest_DragDrop. This is called when the user drops something on the list view. We again check there is string data, and if there isn't, we do nothing. Otherwise, we pull the string out, and add it to the list view.

Ok, so all that was for your standard drag & drop. But what we actually care about today is changing the cursor - we don't want to use the standard "copy" cursor in this case. And to do that, we have to take a look at yet another event in this drag and drop world - the GiveFeedback event. This is an event that you can attach to on the source of the drag. It gets called whenever anything changes about the drag - for instance, when someone changes the current effect (like we do in the DragDest_DragEnter function). So in the case of this code, we attach the following function to the GiveFeedback event on the tree view:

private void DragSource_GiveFeedback(object sender, GiveFeedbackEventArgs e)
{
  e.UseDefaultCursors = e.Effect != DragDropEffects.Copy;
}

The main thing you can do with this event is the exact thing we want to do - we want to turn off the default cursors. So here, I am saying that if the drag effect is equal to Copy, we do not want to use the default cursors. You could potentially set the actual cursor here as well, but I'm not going to do that in this case. I'm going to add code to the DragDest_DragEnter function:

private void DragDest_DragEnter(object sender, DragEventArgs e)
{
  if (!e.Data.GetDataPresent(typeof(String)))
  {
    e.Effect = DragDropEffects.None;
    return;
  }

  e.Effect = DragDropEffects.Copy;
  string str = e.Data.GetData(typeof(String)) as String;

  SizeF size;
  Font f = new Font(FontFamily.GenericSansSerif, 10, FontStyle.Bold);
  using (Bitmap tmpBmp = new Bitmap(1, 1))
    using (Graphics g = Graphics.FromImage(tmpBmp))
      size = g.MeasureString(str, f);  

  Bitmap bitmap = new Bitmap((int)Math.Ceiling(size.Width),
      (int)Math.Ceiling(size.Height));
  using(Graphics g = Graphics.FromImage(bitmap))
    g.DrawString(str, f, Brushes.Black, 0, 0);

  Cursor.Current = CreateCursor(bitmap, 0, 0);

  bitmap.Dispose();
  f.Dispose();
}

Now that we've set UseDefaultCursors to false, we can actually set the cursor. However, we can't do it the normal way - by setting the Cursor property on the control we care about to the cursor we want. Instead, we have to set the static variable Current on the Cursor class to the cursor we want - this is because drag drop operations don't ever pay attention to the cursors set for an individual control.

So here (if there is string data) we pull it out, and then measure it. You can't measure a string without a graphics object, so we initially create a tiny bitmap just so we can get a graphics object out. Once we have the correct size of the string, we make a new bitmap, and draw the string on that bitmap. Then, of course, we create a cursor out of that bitmap (using the cursor creation code from here, and we set the current cursor to that new cursor. Finally, we clean up and dispose some things.

And now, with that code, you will get the text of the node as the cursor over the list view instead of the normal copy cursor! Here is all the code in its full glory:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace DragDropCursorTest
{
  public partial class DragDropCursorTest : Form
  {
    public struct IconInfo
    {
      public bool fIcon;
      public int xHotspot;
      public int yHotspot;
      public IntPtr hbmMask;
      public IntPtr hbmColor;
    }

    private const int MinimumDragDistance = 4;
    private Point _OrigMousePoint;

    public DragDropCursorTest()
    {
      InitializeComponent();

      _DragSource.GiveFeedback += DragSource_GiveFeedback;
      _DragSource.MouseDown += DragSource_MouseDown;

      _DragDest.DragDrop += DragDest_DragDrop;
      _DragDest.DragEnter += DragDest_DragEnter;
      _DragDest.AllowDrop = true;
    }

    [DllImport("user32.dll")]
    public static extern IntPtr CreateIconIndirect(ref IconInfo icon);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool GetIconInfo(IntPtr hIcon, ref IconInfo pIconInfo);

    public static Cursor CreateCursor(Bitmap bmp,
        int xHotSpot, int yHotSpot)
    {
      IconInfo tmp = new IconInfo();
      GetIconInfo(bmp.GetHicon(), ref tmp);
      tmp.xHotspot = xHotSpot;
      tmp.yHotspot = yHotSpot;
      tmp.fIcon = false;
      return new Cursor(CreateIconIndirect(ref tmp));
    }

    private void DragSource_MouseDown(object sender, MouseEventArgs e)
    {
      if (e.Button != MouseButtons.Left)
        return;

      if (_DragSource.GetNodeAt(e.Location) == null)
        return;

      _OrigMousePoint = e.Location;
      _DragSource.MouseMove += DragSource_MouseMove;
      _DragSource.MouseUp += DragSource_MouseUp;
    }

    private void DragSource_MouseUp(object sender, MouseEventArgs e)
    {
      _DragSource.MouseMove -= DragSource_MouseMove;
      _DragSource.MouseUp -= DragSource_MouseUp;
    }

    private void DragSource_MouseMove(object sender, MouseEventArgs e)
    {
      if (Math.Abs(e.X - _OrigMousePoint.X) < MinimumDragDistance &&
          Math.Abs(e.Y - _OrigMousePoint.Y) < MinimumDragDistance &&
          e.Button == MouseButtons.Left)
        return;

      _DragSource.MouseMove -= DragSource_MouseMove;
      _DragSource.MouseUp -= DragSource_MouseUp;

      if (e.Button != MouseButtons.Left)
        return;

      _DragSource.SelectedNode = _DragSource.GetNodeAt(_OrigMousePoint);

      _DragSource.DoDragDrop(_DragSource.SelectedNode.Text, DragDropEffects.Copy);
    }

    private void DragSource_GiveFeedback(object sender, GiveFeedbackEventArgs e)
    {
      e.UseDefaultCursors = e.Effect != DragDropEffects.Copy;
    }

    private void DragDest_DragEnter(object sender, DragEventArgs e)
    {
      if (!e.Data.GetDataPresent(typeof(String)))
      {
        e.Effect = DragDropEffects.None;
        return;
      }

      e.Effect = DragDropEffects.Copy;
      string str = e.Data.GetData(typeof(String)) as String;

      SizeF size;
      Font f = new Font(FontFamily.GenericSansSerif, 10, FontStyle.Bold);
      using (Bitmap tmpBmp = new Bitmap(1, 1))
        using (Graphics g = Graphics.FromImage(tmpBmp))
          size = g.MeasureString(str, f);  

      Bitmap bitmap = new Bitmap((int)Math.Ceiling(size.Width),
          (int)Math.Ceiling(size.Height));
      using(Graphics g = Graphics.FromImage(bitmap))
        g.DrawString(str, f, Brushes.Black, 0, 0);

      Cursor.Current = CreateCursor(bitmap, 0, 0);

      bitmap.Dispose();
      f.Dispose();
    }

    private void DragDest_DragDrop(object sender, DragEventArgs e)
    {
      if (!e.Data.GetDataPresent(typeof(String)))
        return;

      string str = e.Data.GetData(typeof(String)) as String;

      if (String.IsNullOrEmpty(str))
        return;

      _DragDest.Items.Add(str);
    }
  }
}

Hopefully this tutorial anwsers everyone's questions about the interations between cursors and drag drop operations. You can download the Visual Studio solution for the application displayed above here if you would like. As always, questions and comments are welcome.

Laurent
03/18/2008 - 16:35

Woww! great article! Very usefull! Thanks for sharing.

reply

Filip
04/11/2008 - 03:18

Great article. I have been looking for this and I also have experienced the pain you described.
Thank you!

reply

Cong Thanh
04/15/2008 - 04:19

It 's really helpful article. After losing sleep yesterday, I can sleep tonight because I got a pill from your guidance.

Thanks a lot !

reply

CleanRabbit
04/30/2008 - 09:08

Thank you very much.
Very direct, very accurate.
With some tweaking, I can finally get cursors to display the information I need to make my application much more intuitive!

Thanks again.

CR

reply

Sap
06/06/2008 - 05:16

This was really helpfull.

But i have a different scenario.

I have 2 listboxes with autoscroll disabled. When i drag a item from one listbox to other the panel scroll bar should scroll. Can u help me?

Thanks in advance,
Sap

reply

MAcK
09/15/2008 - 07:15

Спасибо.

reply

Daniel Stutzbach
10/27/2008 - 15:04

I was able to solve the blurry text problem by adding the following just before the call to g.DrawString():

g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;

reply

Mark
11/12/2008 - 19:35

There's a problem with this code and I'm not sure where it is. It's fine for a demo, but if you use it a lot it causes memory problems. This occurs when you switch cursors back and forth, say to move on or off a control. Do it a dozen times and the app will lock up. I have only tested it so far as discover the hangup is Win32 GetIconInfo(). I tried using .Dispose on each bitmap, cursor, etc. I can't even use a GetLastError trap.

reply

Mark
11/13/2008 - 10:47

OK, if want to do what I did - switch the cursor as you pass over various controls, DON'T try and do it with the cursor itself.

I don't think garbage collection takes place during a drag operation, so that is the first source of memory problems. I explicitly blew away the .hbmMask and .hbmColor bitmaps in each IconInfo everytime the cursor was swapped out. This fixed one problem, but not another.

The way to do this is keep the cursor simple by selecting from Cursors class, then simply dragging an image with the cursor. Use the drag source GiveFeedback to update the image or picturebox or label or whatever, to .Location = new Point(), etc. You can use the mouse position or adjust this with the desktop location if needed. Then when the drag is dropped, get rid of the image or make it visible = false.

This is the closest you will come to the Windows Explorer-style drag and drop look and feel, and it shouldn't give you any memory exceptions.

reply

Aerdanel
03/24/2009 - 07:29

Maybe you can use this function :

[DllImport("gdi32.dll")]
public static extern bool DeleteObject(IntPtr handle);

It's a function to delete GDI objects, but unfortunately it didn't change anything for me..

Let me know if you found how to use it :)

reply

Anonymous
11/23/2008 - 17:29

cheers buddy, helped alot!

reply

Andras
03/17/2009 - 02:17

I tried to convert your code to vb.net but I have one question:

You say:
the variable _DragSrc is the Tree View, and the variable _DragDest is the List View.

But it's not declared in your code. Is this because of some difference between C# and VB.Net ? Or how do I get this working without the error: Name '_DragSource' is not defined

Thanks

reply

The Tallest
03/17/2009 - 09:38

_DragSrc and _DragDest are declared in the code behind file for the interface - I named and created them using the Windows Forms Designer. You can actually see the declaration if you download the zip file containing the project and take a look at the code behind file (the ".Designer.cs" one).

reply

Kevin G
04/21/2009 - 01:51

This was driving me nuts!

I want to say, thank you very much for writing this article.

However, I think you should call a little bit more attention to the 4 required elements you need for this. (like a NEON sign)

- CreateCursor API Call

- SourceControl.DoDragDrop

- GetFeedBack Event, shutting off default ccursor

- set Cursor.Current instead of control.cursor

but.. its cool.. you have it in there.. it just took a little studying (meaning one has to actually READ the articles that they pull up - unlike how I usually do..)

:D

reply

Add Comment

Put code snippets inside language tags:
[language] [/language]

Examples:
[javascript] [/javascript]
[actionscript] [/actionscript]
[csharp] [/csharp]

See here for supported languages.

Javascript must be enabled to submit anonymous comments - or you can login.

Sponsors