Share via


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

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 100x100 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 ExplorerDragging from Windows Explorer Dragging to Windows ExplorerDragging 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

  • Anonymous
    April 09, 2008
    Unsupported functions... Can I get a little code snippet or help for adding the necessarry implementation for the unsupported functions? thx Greg

  • Anonymous
    April 10, 2008
    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).

  • Anonymous
    April 24, 2008
    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

  • Anonymous
    April 24, 2008
    I also implemented GetDataHere in Part 3.

  • Anonymous
    April 25, 2008
    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

  • Anonymous
    April 25, 2008
    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

  • Anonymous
    April 26, 2008
    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.

  • Anonymous
    June 03, 2008
    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 ?

  • Anonymous
    July 07, 2008
    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.

  • Anonymous
    July 11, 2008
    Don't supose you have a C++ 2008 version of this?

  • Anonymous
    August 26, 2008
    The comment has been removed

  • Anonymous
    November 02, 2008
    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!

  • Anonymous
    January 15, 2009
    The comment has been removed

  • Anonymous
    January 16, 2009
    The comment has been removed

  • Anonymous
    January 16, 2009
    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.

  • Anonymous
    February 05, 2009
    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.

  • Anonymous
    February 06, 2009
    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!

  • Anonymous
    February 18, 2009
    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.

  • Anonymous
    February 18, 2009
    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.

  • Anonymous
    February 18, 2009
    James, sorry, no, I don't have a C++ version of this.

  • Anonymous
    February 18, 2009
    Kirk, I think I saw that bug in the past. Did you ever resolve it? I have not.

  • Anonymous
    February 18, 2009
    Diego, sorry, I'm not your man for that issue.

  • Anonymous
    February 18, 2009
    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.

  • Anonymous
    March 29, 2009
    The comment has been removed

  • Anonymous
    March 31, 2009
    Karin, I'll see if I can reproduce your error and get back to you.

  • Anonymous
    June 23, 2009
    The comment has been removed

  • Anonymous
    June 24, 2009
    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.

  • Anonymous
    November 22, 2009
    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

  • Anonymous
    November 22, 2009
    Thierry, I'm not sure actually. Usually, it is the target that decides whether to accept the dragged data.

  • Anonymous
    April 03, 2010
    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.

  • Anonymous
    July 05, 2012
    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。