Introduction
I was working on an FTP client application where I needed to support
drag/drop with Windows Explorer. Dropping files from Explorer into the client
was trivial and didn't cause me too much trouble, but dropping files back into
Explorer was not as easy. Using CF_HDROP
was ruled out because the
source file would not physically exist, as it would have to be downloaded from
the FTP server before Explorer can get at it. After playing around with various
stuff, I finally hit upon using CFSTR_FILEDESCRIPTOR
and
CFSTR_FILECONTENTS
, which suited my purposes. In this article, I'll
demonstrate a simple dialog app that will let you drag non-existent files from
your app into Explorer.
Basic Technique
- Derive a class from
COleDataSource
- Allocate global memory and create data in
CFSTR_FILEDESCRIPTOR
format, and then use CacheGlobalData
on the
COleDataSource
derived class - Override
OnRenderFileData
in this derived class - In the
OnRenderFileData
override, handle
CFSTR_FILECONTENTS
, and write directly to the CFSTR_FILECONTENTS
Creating the demo app
Generate a default Dialog based MFC application with Visual Studio 2005 (or
an earlier version if you don't have 2005). Use the resource editor to add a
List control to the dialog, and associate a DDX control variable with it of type
CListCtrl
and name it m_fileList
. Now add the
following code to the OnInitDialog
to setup the List control.
BOOL CExplorerDelayDropDlg::OnInitDialog()
{
CDialog::OnInitDialog();
. . .
AfxOleInit();
m_fileList.SetExtendedStyle(LVS_EX_FULLROWSELECT);
m_fileList.InsertColumn(0, _T("File"), LVCFMT_LEFT,75);
m_fileList.InsertColumn(1, _T("Details"), LVCFMT_LEFT,175);
for(TCHAR c = _T('A'); c < _T('F'); c++)
{
CString text1, text2;
text1.Format(_T("%c.txt"),c);
text2.Format(_T("File full of %cs"),c);
m_fileList.SetItemText(
m_fileList.InsertItem(c - _T('A'),text1),1,text2);
}
return TRUE;
}
The code merely fills up the List control with some dummy file names. Note
how I have made a call to AfxOleInit
(you don't need to do this if
your app already supports OLE).
Deriving a class from COleDataSource
Since we are using delayed data rendering, we need to derive a class from
COleDataSource
so that we can override OnRenderFileData
(the default version merely returns FALSE
). So, the first step is
to add a class named CMyOleDataSource
(derived from
COleDataSource
) to the project.
class CMyOleDataSource : public COleDataSource
{
DECLARE_DYNAMIC(CMyOleDataSource)
Now, we need to add an override for OnRenderFileData
as shown
below.
BOOL CMyOleDataSource::OnRenderFileData(
LPFORMATETC lpFormatEtc,CFile* pFile)
{
if(lpFormatEtc->cfFormat ==
RegisterClipboardFormat(CFSTR_FILECONTENTS))
{
HGLOBAL hGlob = NULL;
const int buffSize = 512;
hGlob = GlobalAlloc(GMEM_FIXED, buffSize);
if(hGlob)
{
LPBYTE pBytes = (LPBYTE)GlobalLock(hGlob);
if(pBytes)
{
memset(pBytes, (int) m_Files.GetAt(
lpFormatEtc->lindex)[0], buffSize);
pFile->Write(pBytes,buffSize);
}
GlobalUnlock(hGlob);
}
GlobalFree(hGlob);
return TRUE;
}
return COleDataSource::OnRenderFileData(
lpFormatEtc, pFile);
}
Note how in the above code, I create dummy files by filling up 512 bytes with
a specific character that identifies the file. In a more real life scenario,
you'd have to retrieve a specific file, identified by the
lindex
parameter, and then retrieve that file from a remote source or perhaps
from an archive.
Now, we just need to handle the LVN_BEGINDRAG
notification as
shown below. You can either use the Properties box to add a handler, or manually
add an ON_NOTIFY
handler in the dialog class.
void CExplorerDelayDropDlg::OnBeginDrag(NMHDR *pNMHDR, LRESULT *pResult)
{
UINT uFileCount = m_fileList.GetSelectedCount();
UINT uBuffSize = sizeof(FILEGROUPDESCRIPTOR) +
(uFileCount-1) * sizeof(FILEDESCRIPTOR);
HGLOBAL hFileDescriptor = GlobalAlloc (
GHND | GMEM_SHARE, uBuffSize );
if(hFileDescriptor)
{
FILEGROUPDESCRIPTOR* pGroupDescriptor =
(FILEGROUPDESCRIPTOR*) GlobalLock ( hFileDescriptor );
if(pGroupDescriptor)
{
FILEDESCRIPTOR* pFileDescriptorArray =
(FILEDESCRIPTOR*)((LPBYTE)pGroupDescriptor + sizeof(UINT));
pGroupDescriptor->cItems = uFileCount;
POSITION pos = m_fileList.GetFirstSelectedItemPosition();
int index = 0;
m_DataSrc.m_Files.RemoveAll();
while( NULL != pos )
{
int nSelItem = m_fileList.GetNextSelectedItem( pos );
ZeroMemory(&pFileDescriptorArray[index],
sizeof(FILEDESCRIPTOR));
lstrcpy ( pFileDescriptorArray[index].cFileName,
m_fileList.GetItemText( nSelItem, 0 ) );
m_DataSrc.m_Files.Add(
pFileDescriptorArray[index].cFileName);
pFileDescriptorArray[index].dwFlags =
FD_FILESIZE|FD_ATTRIBUTES;
pFileDescriptorArray[index].nFileSizeLow = 512;
pFileDescriptorArray[index].nFileSizeHigh = 0;
pFileDescriptorArray[index].dwFileAttributes =
FILE_ATTRIBUTE_NORMAL;
index++;
}
}
else
{
GlobalFree ( hFileDescriptor );
}
}
GlobalUnlock ( hFileDescriptor );
FORMATETC etcDescriptor = {
RegisterClipboardFormat(CFSTR_FILEDESCRIPTOR),
NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
m_DataSrc.CacheGlobalData ( RegisterClipboardFormat(
CFSTR_FILEDESCRIPTOR), hFileDescriptor, &etcDescriptor );
FORMATETC etcContents = {
RegisterClipboardFormat(CFSTR_FILECONTENTS),
NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
m_DataSrc.DelayRenderFileData(RegisterClipboardFormat(
CFSTR_FILECONTENTS), &etcContents);
DROPEFFECT dwEffect = m_DataSrc.DoDragDrop (
DROPEFFECT_COPY | DROPEFFECT_MOVE );
if(dwEffect == DROPEFFECT_NONE )
{
GlobalFree( hFileDescriptor );
}
*pResult = 0;
}
Conclusion
That's it. Obviously, this just shows the bare techniques. You'd need to
write extra code to make the whole process smooth. For example, if you are
pulling the file from a remote device, there'd be a delay before the file gets
written, in which case, you'd need to show a progress bar, or ensure that your
main app does not freeze up entirely. But the basic technique will remain the
same.
Reference
For more on Drag/Drop with Explorer, read Mike Dunn's excellent article :
How to Implement Drag and Drop Between Your Program and Explorer
which explains how to use CF_HDROP
to transfer existing files to
Explorer.
History
- Sep/13/2006 - Article first published