Shell Style Drag and Drop in .NET (WPF and WinForms)


Series Links


This is part of a 3 part series:



  1. Shell Style Drag and Drop in .NET (WPF and WinForms)

  2. Shell Style Drag and Drop in .NET – Part 2

  3. Shell Style Drag and Drop in .NET – Part 3

Windows Explorer Drag Image
Window Explorer Drag Image

Introduction


If you’ve worked with .NET drag and drop, you may have noticed that the pretty images that Windows Explorer paints while dragging items does not come for free. In fact, by default, .NET will give you a rather ugly black and white cursor with the drag effect indicator (copy, move, none, etc). I wasn’t pleased with this, so I set out to create a fantastic drag and drop experience that integrates well into the Windows experience. Oh, and I wanted it to be all in C#.


Background


There are a couple of COM interfaces that help us integrate the shell style drag and drop with our .NET applications. They are:



  • IDragSourceHelper – Exposed by the Shell to allow an application to specify the image that will be displayed during a Shell drag-and-drop operation.
  • IDropTargetHelper – Exposes methods that allow drop targets to display a drag image while the image is over the target window.

Both of these interfaces are implemented by the class that is CoCreated using CLSID_DragDropHelper in the Windows SDK.


The Solution


The solution turns out to be fairly simple. Once we understand the usage of the IDragSourceHelper and IDropTargetHelper interfaces, we have to implement one interface and then we are done.


NOTE: This post does not go into the intricacies of implementing drag and drop in your .NET applications. It covers the usage of the IDragSourceHelper and IDropTargetHelper interfaces in order to show and set the Shell drag image.


The Interfaces

First, we need to declare the COM interfaces with their GUIDs so the runtime can CoCreateInstance and QueryInterface.


The CLSID that we need is CLSID_DragDropHelper from ShlGuid.h. We’ll associate that to a ComImport class called DragDropHelper:


[ComImport]
[Guid(
4657278A-411B-11d2-839A-00C04FD918D0)]
public class DragDropHelper { }

 

For the IDragSourceHelper interface, we find IID_IDragSourceHelper in ShObjIdl.h. We can also derive the declarations of the interface functions from the IDL:


[ComVisible(true)]
[ComImport]
[Guid(
DE5BF786-477A-11D2-839D-00C04FD918D0)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IDragSourceHelper
{
void InitializeFromBitmap(
[In, MarshalAs(UnmanagedType.Struct)]
ref ShDragImage dragImage,
[In, MarshalAs(UnmanagedType.Interface)] IDataObject dataObject);

void InitializeFromWindow(
[In] IntPtr hwnd,
[In]
ref Win32Point pt,
[In, MarshalAs(UnmanagedType.Interface)] IDataObject dataObject);
}


For IDropTargetHelper, we also find IID_IDropTargetHelper and the interface decalarations in ShObjIdl.h:


[ComVisible(true)]
[ComImport]
[Guid(
4657278B-411B-11D2-839A-00C04FD918D0)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IDropTargetHelper
{
void DragEnter(
[In] IntPtr hwndTarget,
[In, MarshalAs(UnmanagedType.Interface)] IDataObject dataObject,
[In]
ref Win32Point pt,
[In]
int effect);

void DragLeave();

void DragOver(
[In]
ref Win32Point pt,
[In]
int effect);

void Drop(
[In, MarshalAs(UnmanagedType.Interface)] IDataObject dataObject,
[In]
ref Win32Point pt,
[In]
int effect);

void Show(
[In]
bool show);
}


You may or may not have noticed that some of the types are not standard Framework types. We’ll get to those each in turn. For now, just make sure you distinguish that IDataObject refers to System.Runtime.InteropServices.ComTypes.IDataObject and not System.Windows.IDataObject or System.Windows.Forms.IDataObject.


A couple of helper structures are neccessary. The Win32Point and Win32Size structures have been declared in 101 places, but since I was making a standalone managed wrapper library, I went ahead and declared them again.


[StructLayout(LayoutKind.Sequential)]
public struct Win32Point
{
public int x;
public int y;
}

[StructLayout(LayoutKind.Sequential)]
public struct Win32Size
{
public int cx;
public int cy;
}


ShDragImage

The ShDragImage structure is the structure that defines the Shell drag image, and it is an instance of this structure that will live in the IDataObject as an internal clipboard format that the Shell uses for displaying its drag image.


[StructLayout(LayoutKind.Sequential)]
public struct ShDragImage
{
public Win32Size sizeDragImage;
public Win32Point ptOffset;
public IntPtr hbmpDragImage;
public int crColorKey;
}

There is nothing spectacular about this structure. The hbmpDragImage is a pointer to an HBitmap, and the crColorKey is an RGB value that specifies a transparent color for the drag image. I don’t know about pre-Vista, but Vista supports 32-bit drag images, including the alpha channel, so I just set the color key to a color that I don’t use, like Magenta.


Implementing the COM IDataObject Interface

There is a problem with using the System.Window.DataObject and System.Windows.Forms.DataObject. They both implement the COM interface IDataObject, but the COM IDataObject.SetData implementation throws a NotImplementedException if you use the default data store. Luckily, the Framework allows us to pass a COM IDataObject implementation as a parameter to the constructor to either of these classes (they are nearly the same whether you use System.Windows.DataObject or System.Windows.Forms.DataObject). Then, instead of throwing an exception, it will use your passed in object to set the data. In fact, the classes end up becoming just a .NET friendly wrapper for the underlying implementation that you provide. With that, we need to set off to implement the COM IDataObject interface. This is not terribly difficult, but will take some time to understand.


Before we jump to the implementation, let’s examine a couple of the managed structures that we’ll be dealing with:


public struct FORMATETC
{
public short cfFormat;
public IntPtr ptd;
public DVASPECT dwAspect;
public int lindex;
public TYMED tymed;
}

The FORMATETC structure defines a data format. The cfFormat member is a value indicating the data format (in the unmanaged API, CLIPFORMAT is the member’s type, which is defined as a short). This is something like text, HTML, bitmap, etc. The ptd member is used for device specific formats, but we are using .NET and aim for device independence, so we won’t use that. dwAspect indicates the aspect of the format. That is, is it the content (origingal), a thumbnail, etc. I don’t know what lindex is, but the docs say that the only valid value is -1. So there you go. The tymed member describes the type of the medium pointer, like HGLOBAL, HBITMAP, IUnknown, etc. The combination of cfFormat, dwAspect and tymed will make our unique key.


NOTE: The Framework exposes APIs to allow us to convert data formats between their numeric ID and their commonly known string name. The classes that expose the APIs are System.Windows.DataFormats and System.Windows.Forms.DataFormats. Either way you go, you have a list of predefined formats, as well as a static method (GetDataFormat or GetFormat respectively) to retrieve a staticly held data format object instance, which associates the numeric id and string name of the format. Internally, these managed APIs use the unmanaged GetClipboardFormatName API:


public struct STGMEDIUM
{
public TYMED tymed;
public IntPtr unionmember;
public object pUnkForRelease;
}

The STGMEDIUM structure defines a piece of data. We see the tymed member again, and it is the same as in FORMATETC. It describes the type of pointer of its unionmember member. pUnkForRelease is used to provide the unmanaged ReleaseStgMedium API a means of properly releasing COM pointers. We’ll talk more about ReleaseStgMedium in a minute.


OK, now let’s create our class declaration:


/// <summary>
/// Implements the COM version of IDataObject including SetData.
/// </summary>
/// <remarks>
/// <para>Use this object when using shell (or other unmanged) features
/// that utilize the clipboard and/or drag and drop.</para>
/// <para>The System.Windows.DataObject (.NET 3.0) and
/// System.Windows.Forms.DataObject do not support SetData from their COM
/// IDataObject interface implementation.</para>
/// <para>To use this object with .NET drag and drop, create an instance
/// of System.Windows.DataObject (.NET 3.0) or System.Window.Forms.DataObject
/// passing an instance of DataObject as the only constructor parameter. For
/// example:</para>
/// <code>
/// System.Windows.DataObject data = new System.Windows.DataObject(new DragDropLib.DataObject());
/// </code>
/// </remarks>
public class DataObject : IDataObject, IDisposable
{

Pretty simple. Note that IDataObject is System.Runtime.InteropServices.ComTypes.IDataObject. We also implement IDisposable, because we will be working with unmanaged resources that we’ll want to add deterministic deallocation to on top of the nondeterministic garbage collection.


Next thing is to define a couple of unmanaged Win32 APIs for P/Invoking:


#region Unmanaged functions

// These are helper functions for managing STGMEDIUM structures

[DllImport(
urlmon.dll)]
private static extern int CopyStgMedium(ref STGMEDIUM pcstgmedSrc, ref STGMEDIUM pstgmedDest);
[DllImport(
ole32.dll)]
private static extern void ReleaseStgMedium(ref STGMEDIUM pmedium);

#endregion // Unmanaged functions


These methods will be called to greatly simplify our implementation of IDataObject. CopyStgMedium is used to create a copy of the STGMEDIUM structure, including its internal pointer to unmanaged memory. The managed STGMEDIUM structure, as with many of the types we’ll be using in the implementation of IDataObject, live alongside the IDataObject interface in System.Runtime.InteropServices.ComTypes. The ReleaseStgMedium calls the appropriate unmanaged release function for the data pointer stored in an STGMEDIUM. The pointer may be a COM pointer, or an HGLOBAL handle, HBITMAP handle, etc. ReleaseStgMedium will identify it (by the tymed member of STGMEDIUM) and call the proper function. For more information about these APIs, refer to the unmanaged documentation on MSDN.


Moving right along. Let’s declare our inner storage mechanism:


// Our internal storage is a simple list
private IList<KeyValuePair<FORMATETC, STGMEDIUM>> storage;

For simplicity, I use generic list containing a key/value pair. I chose not to use a dictionary, because that implies I can hash the FORMATETC structure, and reliably compare them using the Equals implementation. I’m not providing that functionality, so I chose a straight forward list approach.


Our constructor simply allocates our storage list:


/// <summary>
/// Creates an empty instance of DataObject.
/// </summary>
public DataObject()
{
storage
= new List<KeyValuePair<FORMATETC, STGMEDIUM>>();
}

Before continuing, lets define some constants that are used:


#region COM constants

private const int S_OK = 0;
private const int S_FALSE = 1;

private const int OLE_E_ADVISENOTSUPPORTED = unchecked((int)0x80040003);

private const int DV_E_FORMATETC = unchecked((int)0x80040064);
private const int DV_E_TYMED = unchecked((int)0x80040069);
private const int DV_E_CLIPFORMAT = unchecked((int)0x8004006A);
private const int DV_E_DVASPECT = unchecked((int)0x8004006B);

#endregion // COM constants


These are HRESULT codes that the IDataObject documentation suggests the use of for certain circumstances. If you are unfamiliar with HRESULTs, they are COM’s way of returning status from a function call.


There are several functions that we will opt out of implementing. By returning the best error code, we can avoid confusion to the caller.


NOTE: Some functions return int and some return void. For runtime callable wrappers (or RCWs) you can choose whether to return an HRESULT as an int, or have the runtime generate an exception. The IDataObject is predefined for us, so we don’t get to choose, but we want to utilize the decisions made. The decision to return int will often be made when some return codes that are not errors, but are not S_OK, are expected. When implementing a function that returns int, you should return the HRESULT instead of throwing an exception for performance. If the function returns void, then you have to throw an exception.


#region Unsupported functions

public int DAdvise(ref FORMATETC pFormatetc, ADVF advf, IAdviseSink adviseSink, out int connection)
{
return OLE_E_ADVISENOTSUPPORTED;
}

public void DUnadvise(int connection)
{
throw Marshal.GetExceptionForHR(OLE_E_ADVISENOTSUPPORTED);
}

public int EnumDAdvise(out IEnumSTATDATA enumAdvise)
{
return OLE_E_ADVISENOTSUPPORTED;
}

public int GetCanonicalFormatEtc(ref FORMATETC formatIn, out FORMATETC formatOut)
{
formatOut
= formatIn;
return DV_E_FORMATETC;
}

public void GetDataHere(ref FORMATETC format, ref STGMEDIUM medium)
{
throw new NotSupportedException();
}

#endregion // Unsupported functions


I won’t discuss the purpose of these functions here. If you’d like more information, see the MSDN documentation. The reason for not implementing them is that I don’t believe there is any need for them for drag and drop. Keep in mind that IDataObject is also used for the clipboard (the data formats are also known as “clipboard formats”).


Before we get to implementing the rest of the IDataObject interface, we need a helper method:


/// <summary>
/// Creates a copy of the STGMEDIUM structure.
/// </summary>
/// <param name=”medium”>The data to copy.</param>
/// <returns>The copied data.</returns>
private STGMEDIUM CopyMedium(ref STGMEDIUM medium)
{
STGMEDIUM sm
= new STGMEDIUM();
int hr = CopyStgMedium(ref medium, ref sm);
if (hr != 0)
throw Marshal.GetExceptionForHR(hr);

return sm;
}


The CopyMedium method is a simple managed wrapper around the unmanaged CopyStgMedium method.


Now we can start to get to the meat of our implementation:


/// <summary>
/// Sets data in the specified format into storage.
/// </summary>
/// <param name=”formatIn”>The format of the data.</param>
/// <param name=”medium”>The data.</param>
/// <param name=”release”>If true, ownership of the medium’s memory will be transferred
/// to this object. If false, a copy of the medium will be created and maintained, and
/// the caller is responsible for the memory of the medium it provided.</param>
public void SetData(ref FORMATETC formatIn, ref STGMEDIUM medium, bool release)
{
// If the format exists in our storage, remove it prior to resetting it
foreach (KeyValuePair<FORMATETC, STGMEDIUM> pair in storage)
{
if ((pair.Key.tymed & formatIn.tymed) > 0
&& pair.Key.dwAspect == formatIn.dwAspect
&& pair.Key.cfFormat == formatIn.cfFormat)
{
storage.Remove(pair);
break;
}
}

// If release is true, we’ll take ownership of the medium.
// If not, we’ll make a copy of it.
STGMEDIUM sm = medium;
if (!release)
sm
= CopyMedium(ref medium);

// Add it to the internal storage
KeyValuePair<FORMATETC, STGMEDIUM> addPair =
new KeyValuePair<FORMATETC, STGMEDIUM>(formatIn, sm);
storage.Add(addPair);
}


The SetData method takes three parameters; the data format, the data, and whether or not to take ownership of the data. We want to overwrite existing data of the same format , so the first thing we do is locate the data in our storage and delete it if it is found. We identify the format by the equality of the three members discussed previously. The next thing to do is grab the medium. The release parameter indicates whether the caller would like to manage the memory of the medium, or if they’d like to hand it to us. If release is false, we create a copy of the medium, so that if the caller releases the memory, we can still access the value. If release is true, we’ll just copy the pointer value directly, and assume the caller won’t release it, because they told us to. The last thing to do is to add the pair to our inner list.


The next function to implement is the QueryGetData function:


/// <summary>
/// Determines if data of the requested format is present.
/// </summary>
/// <param name=”format”>The request data format.</param>
/// <returns>Returns the status of the request. If the data is present,
/// S_OK is returned. If the data is not present, an error code with the
/// best guess as to the reason is returned.</returns>
public int QueryGetData(ref FORMATETC format)
{
// We only support CONTENT aspect
if ((DVASPECT.DVASPECT_CONTENT & format.dwAspect) == 0)
return DV_E_DVASPECT;

int ret = DV_E_TYMED;

// Try to locate the data
// TODO: The ret, if not S_OK, is only relevant to the last item
foreach (KeyValuePair<FORMATETC, STGMEDIUM> pair in storage)
{
if ((pair.Key.tymed & format.tymed) > 0)
{
if (pair.Key.cfFormat == format.cfFormat)
{
// Found it
return S_OK;
}
else
{
// Found the medium type, but wrong format
ret = DV_E_CLIPFORMAT;
}
}
else
{
// Mismatch on medium type
ret = DV_E_TYMED;
}
}

return ret;
}


QueryGetData takes a format, and is meant to determine the existence of the data in the data storage. Error checks aside, we loop through our inner list, locating the format requested. If found, return S_OK. If not, we try to give a meaningful error code, but in the end, all the caller cares about is that the format is not present.


Now that the caller can query to see if data is present, we need to provide the GetData implementation to actually supply the data:


/// <summary>
/// Gets the specified data.
/// </summary>
/// <param name=”format”>The requested data format.</param>
/// <param name=”medium”>When the function returns, contains the requested data.</param>
public void GetData(ref FORMATETC format, out STGMEDIUM medium)
{
// Locate the data
foreach (KeyValuePair<FORMATETC, STGMEDIUM> pair in storage)
{
if ((pair.Key.tymed & format.tymed) > 0
&& pair.Key.dwAspect == format.dwAspect
&& pair.Key.cfFormat == format.cfFormat)
{
// Found it. Return a copy of the data.
medium = pair.Value;
return;
}
}

// Didn’t find it. Return an empty data medium.
medium = new STGMEDIUM();
}


You’ll recognize the familiar loop through storage, locating the exact format match. If found, we return the medium. Note that we still manage the unmanaged memory pointed to by the STGMEDIUM, but the caller can access the data safely as long as they don’t release their reference to our interface.


The last of our interface functions to implement is EnumFormatEtc:


/// <summary>
/// Gets an enumerator for the formats contained in this DataObject.
/// </summary>
/// <param name=”direction”>The direction of the data.</param>
/// <returns>An instance of the IEnumFORMATETC interface.</returns>
public IEnumFORMATETC EnumFormatEtc(DATADIR direction)
{
// We only support GET
if (DATADIR.DATADIR_GET == direction)
return new EnumFORMATETC(storage);

throw new NotImplementedException(OLE_S_USEREG);
}


We only support reading, so throw if they request write access through this method. I’m not sure when write access is used here, but I didn’t find it neccessary to implement (and neither does the Framework). If they are requesting read, we return a new instance of an internal class called EnumFORMATETC, which implements the IEnumFORMATETC COM interface. We’ll look at that as soon as we do some cleanup of our resources.


The remaining implementation of IDataObject is simply to provide proper resource handling. We provide a finalizer, as well as an implementation of IDisposable:


/// <summary>
/// Releases unmanaged resources.
/// </summary>
~DataObject()
{
Dispose(
false);
}

/// <summary>
/// Clears the internal storage array.
/// </summary>
/// <remarks>
/// ClearStorage is called by the IDisposable.Dispose method implementation
/// to make sure all unmanaged references are released properly.
/// </remarks>
private void ClearStorage()
{
foreach (KeyValuePair<FORMATETC, STGMEDIUM> pair in storage)
{
STGMEDIUM medium
= pair.Value;
ReleaseStgMedium(
ref medium);
}
storage.Clear();
}

/// <summary>
/// Releases resources.
/// </summary>
public void Dispose()
{
Dispose(
true);
}

/// <summary>
/// Releases resources.
/// </summary>
/// <param name=”disposing”>Indicates if the call was made by a
/// managed caller, or the garbage collector. True indicates that
/// someone called the Dispose method directly. False indicates that
/// the garbage collector is finalizing the release of the object
/// instance.</param>
private void Dispose(bool disposing)
{
if (disposing)
{
// No managed objects to release
}

// Always release unmanaged objects
ClearStorage();
}


If you’ve ever implemented IDisposable, there isn’t much to explain. If not, I’m simply using a common pattern to allow propert disposal of managed and unmanaged resources. The only thing worth explaining is the ClearStorage function, which loops through all the data values stored and calls the unmanaged ReleaseStgMedium API to handle release of the unmanaged pointer.


That does it for our IDataObject implementation. It doesn’t work by itself for .NET drag and drop, but if you instantiate System.Windows.DataObject (or System.Windows.Forms.DataObject) with an instance of our IDataObject implementation as the constructor parameter, you’ll be good to go.


Implementing the IEnumFORMATETC COM Interface

Implementing IEnumFORMATETC is really pretty straight forward, and since I have faith in my fellow programmers, I won’t spend too much time on it. I implement it as a private inner class to my DataObject class. This hides its existence from Intellisense, since the .NET programmer will likely never need to know it exists.


/// <summary>
/// Helps enumerate the formats available in our DataObject class.
/// </summary>
[ComVisible(true)]
private class EnumFORMATETC : IEnumFORMATETC
{

We’ll keep a private array of FORMATETC’s to enumerate over, as well as a current index into the array:


 


// Keep an array of the formats for enumeration
private FORMATETC[] formats;
// The index of the next item
private int currentIndex = 0;

 


I provide a couple useful constructors. Note that I copy the values into my private member array, so as to not step on any toes:


/// <summary>
/// Creates an instance from a list of key value pairs.
/// </summary>
/// <param name=”storage”>List of FORMATETC/STGMEDIUM key value pairs</param>
internal EnumFORMATETC(IList<KeyValuePair<FORMATETC, STGMEDIUM>> storage)
{
// Get the formats from the list
formats = new FORMATETC[storage.Count];
for (int i = 0; i < formats.Length; i++)
formats[i]
= storage[i].Key;
}

/// <summary>
/// Creates an instance from an array of FORMATETC’s.
/// </summary>
/// <param name=”formats”>Array of formats to enumerate.</param>
private EnumFORMATETC(FORMATETC[] formats)
{
// Get the formats as a copy of the array
this.formats = new FORMATETC[formats.Length];
formats.CopyTo(
this.formats, 0);
}


And now we can move onto the implementation of IEnumFORMATETC. The Clone function provides an exact clone, including state, of the enumerator:


/// <summary>
/// Creates a clone of this enumerator.
/// </summary>
/// <param name=”newEnum”>When this function returns,
/// contains a new instance of IEnumFORMATETC.</param>
public void Clone(out IEnumFORMATETC newEnum)
{
EnumFORMATETC ret
= new EnumFORMATETC(formats);
ret.currentIndex
= currentIndex;
newEnum
= ret;
}

Reset and Skip are straight forward. Reset sets the current position to the beginning, and Skip skips the specified number of elements:


/// <summary>
/// Resets the state of enumeration.
/// </summary>
/// <returns>S_OK</returns>
public int Reset()
{
currentIndex
= 0;
return 0; // S_OK
}

/// <summary>
/// Skips the number of elements requested.
/// </summary>
/// <param name=”celt”>The number of elements to skip.</param>
/// <returns>If there are not enough remaining elements to skip,
/// returns S_FALSE. Otherwise, S_OK is returned.</returns>
public int Skip(int celt)
{
if (currentIndex + celt > formats.Length)
return 1; // S_FALSE

currentIndex
+= celt;
return 0; // S_OK
}


The final piece is the Next function, which is simpler than it looks:


 


/// <summary>
/// Retrieves the next elements from the enumeration.
/// </summary>
/// <param name=”celt”>The number of elements to retrieve.</param>
/// <param name=”rgelt”>An array to receive the formats requested.</param>
/// <param name=”pceltFetched”>An array to receive the number of element
/// fetched.</param>
/// <returns>If the fetched number of formats is the same as the requested
/// number, S_OK is returned. There are several reasons S_FALSE may be
/// returned: (1) The requested number of elements is less than or equal to
/// zero. (2) The rgelt parameter equals null. (3) There are no more elements
/// to enumerate. (4) The requested number of elements is greater than one
/// and pceltFetched equals null or does not have at least one element in it.
/// (5) The number of fetched elements is less than the number of
/// requested elements.</returns>
public int Next(int celt, FORMATETC[] rgelt, int[] pceltFetched)
{
// Start with zero fetched, in case we return early
if (pceltFetched != null && pceltFetched.Length > 0)
pceltFetched[
0] = 0;

// This will count down as we fetch elements
int cReturn = celt;

// Short circuit if they didn’t request any elements, or didn’t
// provide room in the return array, or there are not more elements
// to enumerate.
if (celt <= 0 || rgelt == null || currentIndex >= formats.Length)
return S_FALSE;

// If the number of requested elements is not one, then we must
// be able to tell the caller how many elements were fetched.
if ((pceltFetched == null || pceltFetched.Length < 1) && celt != 1)
return S_FALSE;

// If the number of elements in the return array is too small, we
// throw. This is not a likely scenario, hence the exception.
if (rgelt.Length < celt)
throw new ArgumentException(
The number of elements in the return array is less than the
+ number of elements requested);

// Fetch the elements.
for (int i = 0; currentIndex < formats.Length && cReturn > 0;
i
++, cReturn, currentIndex++)
rgelt[i]
= formats[currentIndex];

// Return the number of elements fetched
if (pceltFetched != null && pceltFetched.Length > 0)
pceltFetched[
0] = celt cReturn;

// cReturn has the number of elements requested but not fetched.
// It will be greater than zero, if multiple elements were requested
// but we hit the end of the enumeration.
return (cReturn == 0) ? S_OK : S_FALSE;
}


 


The code comments explain it well enough that I won’t write a step by step description. Basically, the caller requests a certain number of elements, and I provide as many as I have available, and then return a status of S_OK, or S_FALSE if I couldn’t accommodate them.


And that’s it! Now we use it…


Putting it to Use


To use our solution, you can pretty much just implement your drag and drop like normal, then make sure to call the helper methods. When you start a drag and drop operation, make sure to use our implementation of IDataObject (wrapped in a Framework DataObject) for data storage, and then initialize it with the IDragSourceHelper. When you are accepting drag events, you need to use the helper methods on the IDropTargetHelper interface. The following example uses both of these interfaces in a minimal WinForms application.


NOTE: Remember that this post is not about the details of drag and drop in .NET, but specifically about utilizing the drag image provided by the Windows Shell. My example is simple, but demonstrates the features provided by the IDragSourceHelper and IDropTargetHelper interfaces.


Let’s start with a simple Form that has a single button in the middle of it. The button will be used as a drag and drop source, but the entire form will be used as a drop target:


 


class DragDropSample : Form
{
public DragDropSample()
{
this.AllowDrop = true;

Button bt = new Button();
bt.Anchor
= AnchorStyles.None;
bt.Size
= new Size(100, 100);
bt.Location
= new Point(
(ClientRectangle.Width
bt.Width) / 2,
(ClientRectangle.Height
bt.Height) / 2);
bt.Text
= Drag me;
bt.MouseDown
+= new MouseEventHandler(bt_MouseDown);

Controls.Add(bt);
}


 


Now implement the drag source code. Again, we aren’t trying to teach good drag and drop etiquette, we just want to show the power of the Shell drag image manager:


 


protected override void OnDragEnter(DragEventArgs e)
{
e.Effect
= DragDropEffects.Copy;
Point p
= Cursor.Position;
Win32Point wp;
wp.x
= p.X;
wp.y
= p.Y;
IDropTargetHelper dropHelper
= (IDropTargetHelper)new DragDropHelper();
dropHelper.DragEnter(
this.Handle, (ComIDataObject)e.Data, ref wp, (int)e.Effect);
}

protected override void OnDragOver(DragEventArgs e)
{
e.Effect
= DragDropEffects.Copy;
Point p
= Cursor.Position;
Win32Point wp;
wp.x
= p.X;
wp.y
= p.Y;
IDropTargetHelper dropHelper
= (IDropTargetHelper)new DragDropHelper();
dropHelper.DragOver(
ref wp, (int)e.Effect);
}

protected override void OnDragLeave(EventArgs e)
{
IDropTargetHelper dropHelper
= (IDropTargetHelper)new DragDropHelper();
dropHelper.DragLeave();
}

protected override void OnDragDrop(DragEventArgs e)
{
e.Effect
= DragDropEffects.Copy;
Point p
= Cursor.Position;
Win32Point wp;
wp.x
= p.X;
wp.y
= p.Y;
IDropTargetHelper dropHelper
= (IDropTargetHelper)new DragDropHelper();
dropHelper.Drop((ComIDataObject)e.Data,
ref wp, (int)e.Effect);
}


 


I didn’t split these functions into separate explanations, because they all do a very similar thing. First they determine the drag effect (I hard code Copy). Next, they locate the cursor and create a Win32Point instance to hold it. This can be a tricky area, because the coordinates are used by the Shell’s drag image manager to place the drag image. If you find your image seems to have a funny offset, consider converting the coordinates to or from screen/client space. This is especially important in WPF, where you must specify your coordinate space just to get the coordinate to begin with. Here, we get the cursor’s position, which is always screen space. After we determine the coordinate, we instantiate the DragDropHelper class and get a IDropTargetHelper interface pointer to it. We then simply call the relevant function have it update the drag image.


HINT: The IDropTargetHelper.DragEnter function requires an HWND (window handle) pointer. In WPF, you don’t have direct access to the window handles of controls, but you can get the parent WPF window handle by using the WindowsInteropHelper class in the System.Windows.Interop namespace (WindowsFormsIntegration.dll). Although I haven’t experimented too much, I have been able to call the function with IntPtr.Zero and still see good results.


That handle the drop part. You can make the button a drag source by adding this:


 


void bt_MouseDown(object sender, MouseEventArgs e)
{
Bitmap bmp
= new Bitmap(100, 100, PixelFormat.Format32bppArgb);
using (Graphics g = Graphics.FromImage(bmp))
{
g.Clear(Color.Magenta);
g.DrawEllipse(Pens.Blue,
20, 20, 60, 60);
}

DataObject data = new DataObject(new DragDropLib.DataObject());

ShDragImage shdi = new ShDragImage();
Win32Size size;
size.cx
= bmp.Width;
size.cy
= bmp.Height;
shdi.sizeDragImage
= size;
Point p
= e.Location;
Win32Point wpt;
wpt.x
= p.X;
wpt.y
= p.Y;
shdi.ptOffset
= wpt;
shdi.hbmpDragImage
= bmp.GetHbitmap();
shdi.crColorKey
= Color.Magenta.ToArgb();

IDragSourceHelper sourceHelper = (IDragSourceHelper)new DragDropHelper();
sourceHelper.InitializeFromBitmap(
ref shdi, data);

DragDropEffect effect = DoDragDrop(data, DragDropEffects.Copy);
}


 


This code needs a little more explanation, but you’ll quickly find it isn’t too complicated. First, we create a Bitmap. In this case, I create a 100×100 pixel bitmap and draw a blue circle on it. There is one catch, that if someone knows the answer I’d be happy to include it here. For some reason, if the alpha channel is 0, the drag image manager uses opaque black. It’s strange, because as long as the alpha channel isn’t 0, even if it is 1, you have full alphablending. So, I fill the entire Bitmap with my transparent color, in this case Magenta.


So after you have a Bitmap, which will likely not be handdrawn like mine, create an instance of DataObject. Don’t get confused. In my case, I am working in WinForms, so I create an instance of System.Windows.Forms.DataObject, passing it an instance of our COM IDataObject implementation. The next thing to do is create the ShDragImage structure and fill it with the relevant values. sizeDragImage is the size of the bitmap, in width by height pixels. The ptOffset is the offset of the cursor to the image. That is, if you drag from the middle of the button, you want the cursor to carry the drag image from the middle. If you drag from near the top left of the button, you want the cursor to carry the drag image from the top left corner.


The hbmpDragImage member of ShDragImage gets a handle to the Bitmap. This is straight forward.


NOTE: I may have a memory leak here, but I haven’t investigated. When passing the HBITMAP handle to InitializeFromBitmap, I don’t think the drag image manager takes ownership of the memory. Since DoDragDrop blocks, we could, and probably should, be deleting the HBITMAP after the DoDragDrop call.


The last member to fill is the crColorKey, which is the transparent color. Any pixels in the bitmap with this color will be rendered transparent.


OK, now create an instance of DragDropHelper and cast to an IDragSourceHelper. When we call InitializeFromBitmap, we pass the ShDragImage and our DataObject. It will fill the necessary data for the drag image. Now we can simply proceed with normal .NET drag and drop. We can add data to the DataObject, like text, files, html, etc. Then, when you drag it… well, see for yourself:







Dargging from Windows Explorer
Dragging from Windows Explorer

Dragging to Windows Explorer
Dragging to Windows Explorer

Source Code


I have the source code available here:















File Description
View DragDropLib.cs online Single file includes IDataObject implementation. Import to your existing projects.
Single file includes IDataObject implementation. Import to your existing projects.
Includes DragDropLib DLL project, WinForms sample, and WPF sample projects.

What’s Next?


I’ll come back with another post with nice wrapper classes/extensions to make managing drag and drop, either in WinForms or WPF, a snap. I’m thinking some nice wrapper classes and APIs for these low level APIs would be nice, so there are less name conflicts and tedious tasks, like rendering your Visual to a Bitmap, or declaring a Win32Point just so you can pass it by reference. For example, consider these .NET 3.5 extensions:


 


public static class DragSourceHelper
{
// System.Windows.IDataObject extensions
public static void Initialize(this IDataObject data, Window window, Point offset);
public static void Initialize(this IDataObject data, Visual visual, Point offset);
public static void Initialize(this IDataObject data, BitmapSource, Point offset);

// System.Windows.Forms.IDataObject extensions
public static void Initialize(this IDataObject data, Contol control, Point offset);
public static void Initialize(this IDataObject data, Bitmap bitmap, Point offset);
}


 


UPDATE: See Shell Style Drag and Drop in .NET – Part 2


Other Solutions and References


If you’ve done some Googling, you may have come across several solutions. These are some I came across, and used as a reference and/or comparison:


Comments (31)

  1. gcadmes says:

    Unsupported functions…

    Can I get a little code snippet or help for adding the necessarry implementation for the unsupported functions?

    thx

    Greg

  2. adamroot says:

    Which functions in particular are you looking for help with? I added support for IDataObject.DAdvise and IDataObject.DUnadvise in Part 3 (http://blogs.msdn.com/adamroot/pages/shell-style-drag-and-drop-in-net-part-3.aspx).

  3. gcadmes says:

    Hi Adam,

    What I am trying to do is drag a compressed archive file from a utility I wrote onto Explorer. I found a CodeProject artical where this is possible, but it doesn’t seem to work well in Vista. It accomplishes this by using the IDataObject.GetDataHere method. That is why I requested the unsupported functions.

    http://www.codeproject.com/KB/dotnet/DataObjectEx.aspx

    My hope is to use the great example you provided and modify it to fit my needs.

    Thanks for replying so quickly.

    Cheers,

    Greg

  4. adamroot says:

    I also implemented GetDataHere in Part 3.

  5. gcadmes says:

    Great! Thank you!

    In the SwfDataObjectExtensions.cs, when trying to SetDragImage(this IDataObject dataObject, Image image, System.Drawing.Point cursorOffset), in the first try/catch, the DeleteObject(hbmp) throws an EntryPointNotFoundException.

    Also, I had to comment out AllowDropDescription() as I am running this code on XP SP2.

    Any ideas will be greatly appreicated, and the code samples in your blog are equally appreciated.

    Cheers,

    Greg

  6. gcadmes says:

    I just discovered that the project I am working on must target the 2.0 Framework. After compiling the main core lib (DragDropLib) targeting the 2.0 Framework, there were several WPF compiler errors and warnings.

    Unfortunately, I don’t think I can utilize your library after all; unless of course I painstakingly modify the code to work for 2.0. Do you suppose that would be a huge undertaking? I think the error count was around 100 or so.

    Thanks again

    Greg

  7. adamroot says:

    Refreshing the code to work with .NET 2.0 should be a fairly simple task. For the most part, you can work with only the System.Windows.Forms version of the code, so don’t reference the WPF code. After that, you should see errors complaining about the .NET 3.5 extension method syntax (using "this" keyword on first parameter of static methods). Just remove "this", then fix all the "method doesn’t exist for class" errors that remain to point to the static methods.

    As for the DeleteObject problem, that seems strange. DeleteObject has been around since Windows 95/NT. You may want to investigate which GDI dll the function entry is referenced in, then use depends.exe (Dependency Walker) to verify the entry point exists in it.

  8. Nishita says:

    I am fairly new to Winforms and .NET. I have a winforms application in which there is a toolbox from which user selects which items to drag and drop on the control. Everytime i drag an item from the toolbox on the control i want to show the image of the toolbox item so that the user can drop the item within the printable bounds of the paper. What sections of your code will i be using ? Do i need to implement the COM interfaces ?

  9. Farhan says:

    Hi,

    A nice piece of code. I tried out the code.

    I have a custom control derived from a panel being used as a thumbnail. The thumbnails are generated dynamically. So a common event handler for all controls generated. I am using the panel.DrawToBitmap() function to generatethe drag drop image resembling the thumbnail itself. But the code works only for the last added thumbnail control and not all the controls.

  10. James says:

    Don’t supose you have a C++ 2008 version of this?

  11. Kirk says:

    I’ve got the drag descriptions working with a WinForms TreeView, but there’s one minor thing I can’t seem to sort out.  As I drag over nodes that can accept a drop (DragDropEffects == Move or Copy), I properly see the drag description I set (based on the target node name).  However, when I then move over another node in the tree that can’t accept a drop (DragDropEffects == None), I do see the proper drag image but the drop description text box is still visible, albeit fairly small and empty.  I was expecting only the drag image to appear — not an empty drop description text box.  I have a screen capture, but I don’t see a way to attach that here.  Any advice?

    Thanks in advance.

    -kirk

  12. Diego says:

    Hello!, i have a problem and i think you could help me. I’m working with drag drops functions because i need to do  a DROP into other windows app. My app knows the destination window, and i want to send a WM_DROPFILES message only to that window. I don’t want a user doing the drag&drop, i want my app doing the task. I’m trying tu use postmessage, but it’s doesn’t work. can you help me with this?. thanks!

  13. Scott says:

    I have been unable to track down wheat appears to be a bug in this.  I have used your sample as a guide for a WPF application I am working on.  I have set AllowDrop=True on my app’s window, and added the DragOver, Enter, Leave, Drop overrides to the code behind, exactly as in your sample (in fact this bug happens in your sample as well).

    If I drag an email message from Outlook over my app, it sets the drag drop effects to none, and it never changes.  These two messages are continually output into the app’s debug screen….

    A first chance exception of type ‘System.Runtime.InteropServices.COMException’ occurred in PresentationCore.dll

    A first chance exception of type ‘System.Runtime.InteropServices.COMException’ occurred in InterfaceUtilities.dll

    The error itself is occuring in WpfDropTargetHelperExtensions.cs file in the DragEnter() method at the

    dropHelper.DragEnter(windowHandle, (ComIDataObject)data, ref pt, (int)effect);

    line.

    If I remove the OnDrag-X overrides in the window’s code behind, then the error does not occur, however then the drag adorner becomes somewhat unpredicatable when drop or leave occurs.

    All I can figure is it may have something to do with the fact that there are no adorners when you drag from Outlook, causing the call to freak out?  I have not yet been able to come up with a fix for it though.

    Any ideas?

    Thanks,

    Scott

  14. Scott says:

    When dragging an Outlook message over your sample form, a series of exception messages are spit out into the debug output window…

    A first chance exception of type ‘System.Runtime.InteropServices.COMException’ occurred in PresentationCore.dll

    A first chance exception of type ‘System.Runtime.InteropServices.COMException’ occurred in WpfDataObjectExtensions.dll

    I was able to track down the exact message of the exception to "Invalid FORMATETC structure (Exception from HRESULT: 0x80040064(DV_E_FORMATETC))".  It is erroring during the calls being made in the window drag drop overrides.  The problem I have found with this in my application with this is that once this happens, drag and drop operations are not allowed from that point forward, even though I have controls that will accept this drop type.

    So far I have not been able to solve this, other than to do a check on GetDataPresent("FileGroupDescriptor") and bypass the shell drag drop altogether if it’s the data format.

    Do you have any suggestions to avoid this error, or allow the library to more gracefully handle this situation?

    Thanks,

    Scott

  15. Scott says:

    Further to my previous comment about dragging Outlook messages, I have noticed the same behavior when dragging text over the sample form.  Just to add to it, as long as the textbox that accepts text is the first textbox the mouse goes after entering the form, it will still allow drop.  However, if either the HTML or File textboxes are dragged over first, then the text textbox will not accept drops.

  16. folken says:

    You were asking about why an alpha channel of 0 goes to black, but anything else works just fine. I don’t know the answer, but venturing a guess I say it’s probably a divide by 0 problem buried way down in windows.

  17. Girly says:

    How do I use this great preveiw when I want to pass my own custom data in the DataObject? If I try to use SetData I get: "Cannot SetData on a frozen OLE data object." The OLE data object is the custom dataobject. Supporting preview for drag from windows is great but I would like to support my own formats as well and pass data around.

    I’m also struggeling with the position of the drag icon in my WPF application. But hopefully a little bit more testing will solve that.

    Thanks!

  18. adamroot says:

    Nishita, the easiest thing for you would be to take a look at the SWF example from Part 3. If you are going with your own implementation, using this post as a guide, you’ll need to implement the COM interface, and then learn to use the system IDragSourceHelper and IDropTargetHelper. Otherwise, just drop DragDropLib into your project (or compile and reference the appropriate DragDropLib.dll) and use it like the example uses it.

  19. adamroot says:

    Farhan, you have all the control when creating the drag image. I tried to supply some helpers to make it easy, you may just need to pull it apart and figure out how to generate the bitmap as desired.

  20. adamroot says:

    James, sorry, no, I don’t have a C++ version of this.

  21. adamroot says:

    Kirk, I think I saw that bug in the past. Did you ever resolve it? I have not.

  22. adamroot says:

    Diego, sorry, I’m not your man for that issue.

  23. adamroot says:

    Girly, you need to use SetDataEx, which is implemented as an extension methods to the appropriate DataObjects. This is a result of us providing the DataObject with a custom implementation. This is explained pretty thoroughly in Part 3.

  24. Karin says:

    I have a similar issue as did Scott, with a Invalid FORMATETC structure (Exception from HRESULT: 0x80040064 (DV_E_FORMATETC)) in IDropTargetHelper.DragEnter. It occurs when there is a standard drag and drop (not using the library) with text data. Such as this: System.Windows.DragDrop.DoDragDrop((ListView)sender, lc.Text, DragDropEffects.Copy);

    This results in that I can not drop the text. I can catch the exception and bypass it but it does not seem right that I should get the exception.

    It’s a bit strange since dragging from an input field do work (the built in support in the control). But when I do it in code I get the exception.

    Scott did you solve this?

  25. adamroot says:

    Karin, I’ll see if I can reproduce your error and get back to you.

  26. Rob says:

    Scott, Karin:

    I think that solution for this “Invalid FORMATC” problem is testing presence of DragImageBits data

           if (e.Data.GetDataPresent(“DragImageBits”))

             DropTargetHelper.DragEnter(sender as Control, e.Data, new Point(e.X, e.Y), e.Effect);

    It works for me. Can you confirm this Adam?

  27. adamroot says:

    Thanks, Rob. I hadn’t gotten to this, but that sounds right. You should only call DropTargetHelper.DragEnter if the DragImageBits are present. I’m surprised the shell’s implementation of IDropTargetHelper doesn’t play nicer, but this check could potentially be added to the DropTargetHelper.DragEnter method wrapper.

  28. Thierry says:

    Hi,

    We store various files in our database and I have a grid in my application in which I want to drag one or more records to windows explorer but only if the target is windows explorer, I would like to have a way for my application to know that the operation was successful and then only start extracting files from my database (when the drop event actually occured in explorer) and start copying them to the specific location where the “records” were dropped in the specific windows explorer.

    I thought I could maybe put a dummy file in the DoDragDrop event and when the event is returned, delete it and start extracting the files but right now I have no way of knowing a) if it was a successful target and b) what is the path of the windows explorer it was dropped on?

    Is this possible? Do you have any pointers?

    Thanks.

    Thierry

  29. adamroot says:

    Thierry, I’m not sure actually. Usually, it is the target that decides whether to accept the dragged data.

  30. Chad says:

    Fantastic work!

    Thanks for posting the solution. This saved me many hours of investigation and adds a nice professional touch to the app I’m working on.

  31. hi,i donwload the project —ShellDragDrop Part 3-2,it run at the environment of Windows 7 well,but it doesn‘t work at the environment of windows xp ,and it throw a exception that can't convert the COM  object of “DragDropLib.DragDropHelper” to ”DragDropLib.IDragSourceHelper2“

    what can i resolve this question 。I need your help,give me a solution。