Click here to Skip to main content
Click here to Skip to main content

Windows Explorer style ghost drag image in a C# application

, 22 Sep 2006
Rate this:
Please Sign up or sign in to vote.
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 Frown | :-( ), 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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

X3m
Software Developer VuVirt Ltd.
Bulgaria Bulgaria
No Biography provided

Comments and Discussions

 
Question2012 PinmemberMember 380021020-Jan-13 22:30 
GeneralArtifacts when using individual styles for subitems [modified] PinmemberAndrej Grobler1-Aug-09 16:06 
GeneralRe: Artifacts when using individual styles for subitems PinmemberX3m2-Dec-09 2:13 
GeneralRe: Artifacts when using individual styles for subitems PinmemberX3m2-Dec-09 2:16 
GeneralA note about strmbasd.lib PinmemberPawprint5-Jan-09 11:17 
GeneralRe: A note about strmbasd.lib PinmemberX3m1-Dec-09 9:52 
GeneralPlease post VB.NET version PinmemberUltraWhack10-Dec-08 8:37 
GeneralRe: Please post VB.NET version PinmemberX3m1-Dec-09 9:53 
QuestionAnyone gotten this to work in VS2008? PinmemberChiPlastique5-Aug-08 11:25 
AnswerRe: Anyone gotten this to work in VS2008? Pinmembersprice8627-Aug-08 0:52 
GeneralRe: Anyone gotten this to work in VS2008? Pinmemberkyriacos michael9-Jun-09 2:42 
QuestionMoving multiple images with custom ListView [modified] PinmemberLuigi Cordova20-Dec-07 22:46 
QuestionMoving multiple images with custom ListView [modified] PinmemberLuigi Cordova4-Jan-08 5:36 
AnswerRe: Moving multiple images with custom ListView PinmemberX3m1-Dec-09 9:47 
GeneralRe: Moving multiple images with custom ListView PinmemberX3m2-Dec-09 2:15 
QuestionUsing ShellUtils.dll in Visual Basic PinmemberMillard Filmore28-Jun-07 16:11 
AnswerRe: Using ShellUtils.dll in Visual Basic PinmemberX3m29-Jun-07 2:03 
GeneralDrag Drop Problem.Pls help PinmemberChintan.Desai7-Jun-07 2:08 
GeneralRe: Drag Drop Problem.Pls help PinmemberX3m7-Jun-07 3:36 
GeneralImage not visible when "Show Window content while dragging" is not set PinmemberPrabhuDev28-Mar-07 20:19 
GeneralRe: Image not visible when "Show Window content while dragging" is not set PinmemberX3m28-Mar-07 20:27 
GeneralRe: Image not visible when "Show Window content while dragging" is not set PinmemberPrabhuDev30-Apr-07 0:33 
GeneralRe: Image not visible when "Show Window content while dragging" is not set PinmemberX3m30-Apr-07 8:08 
GeneralRe: Image not visible when "Show Window content while dragging" is not set PinmemberPrabhuDev8-May-07 3:10 
GeneralRe: Image not visible when "Show Window content while dragging" is not set PinmemberX3m12-May-07 2:24 
GeneralIDataObject Implementation in VC++ PinmemberPrabhuDev26-Mar-07 23:55 
GeneralRe: IDataObject Implementation in VC++ PinmemberX3m26-Mar-07 23:58 
GeneralRe: IDataObject Implementation in VC++ PinmemberPrabhuDev27-Mar-07 13:28 
GeneralImage misleading PinmemberSteven Roebert18-Sep-06 22:01 
GeneralRe: Image misleading PinmemberX3m18-Sep-06 23:00 
GeneralRe: Image misleading PinmemberSteven Roebert19-Sep-06 0:26 
GeneralRe: Image misleading PinmemberX3m19-Sep-06 1:34 
GeneralRe: Image misleading PinmemberX3m19-Sep-06 1:37 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140827.1 | Last Updated 22 Sep 2006
Article Copyright 2006 by X3m
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid