Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Windows Explorer style ghost drag image in a C# application

0.00/5 (No votes)
22 Sep 2006 1  
Utilizing IDragSourceHelper and IDropTargetHelper interfaces in a C# application via a managed C++ library.

Sample image

Introduction

Have you noticed the cool ghost image that Windows Explorer produces when you start dragging files/folders from it? Well, I wanted to implement this in a C# project when dragging files between it and Windows Explorer. After a lot of Googling, I found out that there are three directions to start working on, as follows:

  1. Using a "custom" cursor created from a MemoryStream initialized from an Image, as shown in this article.
  2. Using the ImageList_BeginDrag and related APIs, as shown in this article: Dragging tree nodes in C#.
  3. Using the IDragSourceHelper and IDropTargetHelper interfaces.

I needed to show the ghost image only when dragging to/from applications that also support the IDragSourceHelper and IDropTargetHelper interfaces, like Windows Explorer, Internet Explorer, and maybe some MS Office programs. So my only option was to use the IDragSourceHelper and IDropTargetHelper interfaces in my C# program when dragging files to/from a ListView control embedded into it. Also, none of the first two methods could provide as close and smooth a ghost image as the one implemented by Microsoft.

The Problem

It's not a big deal to instantiate and use the IDropTargetHelper interface in a C# app when dragging into it. But the real problem is to initialize the IDragSourceHelper to provide your ghost image to other applications when dragging out of your application. These two interfaces use the DataObject supplied from the application during drag/drop to store and transport the ghost image data. So in order to use IDragSourceHelper, your application should provide an IDataObject (COM interface) implementation that can take arbitrary formats. I.e., an IDataObject implementation that has its SetData implemented to take and store any format "set" by external objects, because the IDragSourceHelper object will try to use the SetData method of our IDataObject implementation to store the ghost image data. As you know, .NET provides a DataObject class and an IDataObject interface in the System.Windows.Forms namespace. And actually, the DataObject class does implement not only the System.Windows.Forms.IDataObject, but the COM IDataObject interface as well. The last one could be seen after you try to Marshall.QueryInterface for IDataObject on the DataObject class. Unfortunately, the DataObject class doesn't implement the SetData method from the COM interface, and just returns E_NOTIMPL, which automatically prevents the IDragSourceHelper object from working.

My Solution

I came to the conclusion that in order to use the IDragSourceHelper object in a .NET oriented way, I'll need to implement the COM IDataObject interface from scratch, to make a connection between my implementation and the .NET DataObject class somehow, and make a .NET wrapper for the DoDragDrop API function instead of using the Control.DoDragDrop method. As a former C++ developer, I was too lazy to mess around with all the interop stuff, and decided to implement everything in a managed C++ library project. I implemented unmanaged CDataObject and CEnumFormatEtc classes as an implementation for the IDataObject and IEnumFORMATETC COM interfaces. This article helped me a lot in doing this: OLE Drag and Drop. But in order to make my CDataObject to take arbitrary formats, I changed the class to use an STL vector to store the formats and data, and made my own implementation of the SetData method:

STDMETHODIMP CDataObject::SetData(LPFORMATETC pFE , 
             LPSTGMEDIUM pSM, BOOL fRelease)
{
    if (pFE == NULL || pSM == NULL)
        return E_INVALIDARG;

    for(Storage::iterator it = m_storage.begin(); 
                           it != m_storage.end(); ++it)
    {
        if (pFE->tymed & it->lpFmt->tymed &&
                pFE->dwAspect == it->lpFmt->dwAspect &&
                pFE->cfFormat == it->lpFmt->cfFormat)
        {
            m_storage.erase(it);
            break;
        }
    }

    FORMATETC* fetc=new FORMATETC;
    STGMEDIUM* pStgMed = new STGMEDIUM;

    if (fetc == NULL || pStgMed == NULL)
        return E_OUTOFMEMORY;

    ZeroMemory(fetc, sizeof(FORMATETC));
    ZeroMemory(pStgMed, sizeof(STGMEDIUM));

    *fetc = *pFE;

    if (fRelease)
        *pStgMed = *pSM;
    else
        CopyStgMedium(pSM, pStgMed);

    DataStorage storage;

    storage.lpFmt = fetc;
    storage.lpMed = pStgMed;

    m_storage.push_back(storage);

    return S_OK;

}

The DataStorage is a structure that keeps the FORMATETCs with their corresponding STGMEDIUMs. The m_storage is the vector that stores all the formats. The idea is to not allow duplicate FORMATETCs in the vector. That's why, first, we have to search and erase any previous FORMATETC found. In order to make it .NET oriented, I implemented a DataObjectEx class derived from the .NET DataObject class. The DataObjectEx acts as a wrapper for the CDataObject implementation. It simply instantiates the CDataObject in its constructor, and deletes it in its Finalize method. Also, in order to "hide" from the .NET developers the actual CDataObject implementation, I also made the DataObjectEx to copy a reference of the data it contains into the internal CDataObject. For this purpose, I overrode the SetData methods of the DataObject class, as shown here:

void DataObjectEx::SetData(Object* o)
{
    DataObject::SetData(o);
    CacheData(o->GetType()->ToString());
}

void DataObjectEx::SetData(String* s, Object* o)
{
    DataObject::SetData(s, o);
    CacheData(s);
}

void DataObjectEx::SetData(Type* t, Object* o)
{
    DataObject::SetData(t, o);
    CacheData(t->ToString());
}

void DataObjectEx::SetData(String* s, bool b, Object* o)
{
    DataObject::SetData(s, b, o);
    CacheData(s);
}

void _CacheData(LPFORMATETC lpFetc, LPDATAOBJECT pSrc, 
                                    LPDATAOBJECT pDest)
{
    STGMEDIUM stgMed;
    if(SUCCEEDED(pSrc->QueryGetData(lpFetc)))
    {
        pSrc->GetData(lpFetc, &stgMed);
        pDest->SetData(lpFetc, &stgMed, TRUE);
    }
}

void DataObjectEx::CacheData(String* s)
{
    IntPtr punk = Marshal::GetIUnknownForObject(this);
        
    Guid theGuid ("0000010E-0000-0000-C000-000000000046");
    IntPtr currentDataObjPtr;
    Marshal::QueryInterface(punk, &theGuid, �tDataObjPtr);

    LPDATAOBJECT pdto = (LPDATAOBJECT)currentDataObjPtr.ToPointer();

    FORMATETC fetc;
    fetc.ptd = NULL;
    fetc.dwAspect = DVASPECT_CONTENT;
    fetc.lindex = -1;
    fetc.tymed = (DWORD) -1;

    if(s == DataFormats::FileDrop)
    {
        fetc.cfFormat = CF_HDROP;
        _CacheData(&fetc, pdto, _pDataObject);
    }
    else if(s == DataFormats::Bitmap)
    {
        fetc.cfFormat = CF_BITMAP;
        _CacheData(&fetc, pdto, _pDataObject);
    }
    else if(s == DataFormats::Text)
    {
        fetc.cfFormat = CF_UNICODETEXT;
        _CacheData(&fetc, pdto, _pDataObject);
        fetc.cfFormat = CF_TEXT;
        _CacheData(&fetc, pdto, _pDataObject);
    }
    else if(s == DataFormats::Dif)
    {
        fetc.cfFormat = CF_DIF;
        _CacheData(&fetc, pdto, _pDataObject);
    }
    else if(s == DataFormats::Dib)
    {
        fetc.cfFormat = CF_DIB;
        _CacheData(&fetc, pdto, _pDataObject);
        fetc.cfFormat = CF_DIBV5;
        _CacheData(&fetc, pdto, _pDataObject);
    }
    else if(s == DataFormats::EnhancedMetafile)
    {
        fetc.cfFormat = CF_ENHMETAFILE;
        _CacheData(&fetc, pdto, _pDataObject);
    }
    else if(s == DataFormats::MetafilePict)
    {
        fetc.cfFormat = CF_METAFILEPICT;
        _CacheData(&fetc, pdto, _pDataObject);
    }
    else if(s == DataFormats::Palette)
    {
        fetc.cfFormat = CF_PALETTE;
        _CacheData(&fetc, pdto, _pDataObject);
    }
    else if(s == DataFormats::PenData)
    {
        fetc.cfFormat = CF_PENDATA;
        _CacheData(&fetc, pdto, _pDataObject);
    }
    else if(s == DataFormats::Riff)
    {
        fetc.cfFormat = CF_RIFF;
        _CacheData(&fetc, pdto, _pDataObject);
    }
    else if(s == DataFormats::SymbolicLink)
    {
        fetc.cfFormat = CF_SYLK;
        _CacheData(&fetc, pdto, _pDataObject);
    }
    else if(s == DataFormats::Tiff)
    {
        fetc.cfFormat = CF_TIFF;
        _CacheData(&fetc, pdto, _pDataObject);
    }
    else if(s == DataFormats::Locale)
    {
        fetc.cfFormat = CF_LOCALE;
        _CacheData(&fetc, pdto, _pDataObject);
    }
    else if(s == DataFormats::OemText)
    {
        fetc.cfFormat = CF_OEMTEXT;
        _CacheData(&fetc, pdto, _pDataObject);
    }
    else 
    {
        IntPtr ptr = Marshal::StringToCoTaskMemUni(s);
        LPWSTR lpstr = (LPWSTR)ptr.ToPointer();
        fetc.cfFormat = ::RegisterClipboardFormatW(lpstr);
        _CacheData(&fetc, pdto, _pDataObject);
        Marshal::FreeCoTaskMem(ptr);
    }

    Marshal::Release(punk);
    Marshal::Release(pdto);
}

As you can see, every SetData method just calls the internal CacheData method, which in turn copies a reference from the DataObjectEx object to its internal CDataObject object. It simply queries for the COM IDataObject interface on our DataObjectEx object, and copies the requested FORMATETC with a reference to the corresponding STGMEDUIM into the CDataObject. The actual copying is done in the _CacheData function. I also implemented the DragSource class with two static DoDragDrop functions as a replacement for the Control.DoDragDrop method. It instantiates, internally, an IDropTarget implementation, and creates an instance of the IDragSourceHelper object. It also takes care of firing the corresponding GiveFeedback and QueryContinueDrag events to the control that requested the drag/drop operation. Please check its source in the zip file provided.

So, all your application should do to start dragging with a ghost image is:

DataObjectEx data = new DataObjectEx();
data.SetData(DataFormats.FileDrop, (string[])files.ToArray(typeof(string)));
DragDropEffects res = ShellUtils.DragSource.DoDragDrop(data, listView, 
                      DragDropEffects.Copy | DragDropEffects.Move, 
                      PointToClient(MousePosition));

Initialize a DataObjectEx object, fill it with the required data, and call the DoDragDrop method of the DragSource class with a reference to the control that initialized the drag-drop operation.

Points of Interest

The above call will initialize the IDragSourceHelper object by calling its InitializeFromWindow method. This method knows how to extract the ghost image of common controls like ListView and TreeView. But if you have a custom drawn list view, for instance (like in my case :-(), you'll have to use the second DoDragDrop method and supply a HBITMAP handle taken from an Image object, otherwise the InitializeFromWindow will not take a proper image. The second method will call the InitializeFromBitmap method of IDragSourceHelper, and it will be your responsibility to build the image (from the selected items, for instance). The IDragSourceHelper will take care of the actual fading, of course.

Using the idea of this implementation of DataObjectEx, you can extend it further more to provide a feedback when some special clipboard formats are "set" into your IDataObject implementation, like CFSTR_LOGICALPERFORMEDDROPPEFFECT, CFSTR_TARGETCLSID etc. The internal CDataObject could notify its parent DataObjectEx when such a situation occurs, and the DataObjectEx will know how to deal with it. If you need help with this, please let me know.

Conclusion

The solution I provided takes advantage of the IDragSourceHelper and IDropTargetHelper interfaces, by implementing the COM IDataObject interface and allowing the implementation to accept arbitrary formats. In order to use this solution in your project, you have to call EnableVisualStyles() in your Main function, or provide a manifest file for your application. In this demo project, I used the first approach, but the second one is much better and the correct one to use in commercial solutions.

To test the demo, just drag some files/folders into it, and then drag them out of it to Explorer, for instance.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here