Click here to Skip to main content
15,879,326 members
Articles / Desktop Programming / Windows Forms
Article

Windows Explorer style ghost drag image in a C# application

Rate me:
Please Sign up or sign in to vote.
4.29/5 (13 votes)
22 Sep 2006CPOL6 min read 128.1K   1.6K   60   33
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:

C#
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead VuVirt Ltd.
Bulgaria Bulgaria
Ivaylo Kachaunov, self-employed @ Vuvirt Ltd.

More than 15 years of professional experience in project management, software development, and customer-focused implementation in GUI, image/video/audio processing multimedia applications, web-oriented applications, database-oriented applications, embedded systems, games, 2D/3D graphics and animations.

Manage and perform overall R&D of various video/audio processing, multimedia and 3D rendering software projects and solutions: - Video mixing solutions using DirectX - DirectX, OpenGL, Unity rendering solutions and games - DirectX, OpenGL COM hooking - WASAPI, DirectShow, DirectSound COM hooking - Win32 API hooking

For further details:
LinkedIn: https://www.linkedin.com/in/vuvirt/
GitHub: VuVirt
Stackoverflow: vuvirt

Email: ivo.kachie@gmail.com; info@vuvirt.com

Comments and Discussions

 
Question2012 Pin
Member 380021020-Jan-13 22:30
Member 380021020-Jan-13 22:30 
GeneralArtifacts when using individual styles for subitems [modified] Pin
Andrej Grobler1-Aug-09 16:06
Andrej Grobler1-Aug-09 16:06 
GeneralRe: Artifacts when using individual styles for subitems Pin
Vuvirt2-Dec-09 2:13
Vuvirt2-Dec-09 2:13 
GeneralRe: Artifacts when using individual styles for subitems Pin
Vuvirt2-Dec-09 2:16
Vuvirt2-Dec-09 2:16 
GeneralA note about strmbasd.lib Pin
Pawprint5-Jan-09 11:17
Pawprint5-Jan-09 11:17 
GeneralRe: A note about strmbasd.lib Pin
Vuvirt1-Dec-09 9:52
Vuvirt1-Dec-09 9:52 
GeneralPlease post VB.NET version Pin
UltraWhack10-Dec-08 8:37
UltraWhack10-Dec-08 8:37 
GeneralRe: Please post VB.NET version Pin
Vuvirt1-Dec-09 9:53
Vuvirt1-Dec-09 9:53 
QuestionAnyone gotten this to work in VS2008? Pin
ChiPlastique5-Aug-08 11:25
ChiPlastique5-Aug-08 11:25 
AnswerRe: Anyone gotten this to work in VS2008? Pin
sprice8627-Aug-08 0:52
professionalsprice8627-Aug-08 0:52 
GeneralRe: Anyone gotten this to work in VS2008? Pin
kyriacos michael9-Jun-09 2:42
kyriacos michael9-Jun-09 2:42 
QuestionMoving multiple images with custom ListView [modified] Pin
Luigi Cordova20-Dec-07 22:46
Luigi Cordova20-Dec-07 22:46 
QuestionMoving multiple images with custom ListView [modified] Pin
Luigi Cordova4-Jan-08 5:36
Luigi Cordova4-Jan-08 5:36 
AnswerRe: Moving multiple images with custom ListView Pin
Vuvirt1-Dec-09 9:47
Vuvirt1-Dec-09 9:47 
GeneralRe: Moving multiple images with custom ListView Pin
Vuvirt2-Dec-09 2:15
Vuvirt2-Dec-09 2:15 
QuestionUsing ShellUtils.dll in Visual Basic Pin
Millard Filmore28-Jun-07 16:11
Millard Filmore28-Jun-07 16:11 
AnswerRe: Using ShellUtils.dll in Visual Basic Pin
Vuvirt29-Jun-07 2:03
Vuvirt29-Jun-07 2:03 
GeneralDrag Drop Problem.Pls help Pin
Chintan.Desai7-Jun-07 2:08
Chintan.Desai7-Jun-07 2:08 
GeneralRe: Drag Drop Problem.Pls help Pin
Vuvirt7-Jun-07 3:36
Vuvirt7-Jun-07 3:36 
GeneralImage not visible when "Show Window content while dragging" is not set Pin
PrabhuDev28-Mar-07 20:19
PrabhuDev28-Mar-07 20:19 
GeneralRe: Image not visible when "Show Window content while dragging" is not set Pin
Vuvirt28-Mar-07 20:27
Vuvirt28-Mar-07 20:27 
GeneralRe: Image not visible when "Show Window content while dragging" is not set Pin
PrabhuDev30-Apr-07 0:33
PrabhuDev30-Apr-07 0:33 
GeneralRe: Image not visible when "Show Window content while dragging" is not set Pin
Vuvirt30-Apr-07 8:08
Vuvirt30-Apr-07 8:08 
Hi,

Please check the Points of interest and try to use the second DoDragDrop method that calls InitializeFromBitmap. If yo ustill need help , please let me know.

Best regards,
X3m

GeneralRe: Image not visible when "Show Window content while dragging" is not set Pin
PrabhuDev8-May-07 3:10
PrabhuDev8-May-07 3:10 
GeneralRe: Image not visible when "Show Window content while dragging" is not set Pin
Vuvirt12-May-07 2:24
Vuvirt12-May-07 2:24 

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

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