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:
- Using a "custom" cursor created from a
MemoryStream
initialized from an Image
, as shown in this article.
- Using the
ImageList_BeginDrag
and related APIs, as shown in this article: Dragging tree nodes in C#.
- 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 FORMATETC
s with their corresponding STGMEDIUM
s. The m_storage
is the vector that stores all the formats. The idea is to not allow duplicate FORMATETC
s 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.