Handling Drag and Drop of Email Attachments

Environment: VC6 SP3, VB6 SP3, NT4 SP5

So, you would like to allow an e-mail (Outlook) attachment to be dropped onto
one of your controls.  Seems like a simple enough task.  After all,
doing this from Explorer is very simple (especially in VB).  However,
e-mail attachments from Outlook are different beasts altogether.  They are
not actually files (until you make them files).  The clipboard formats used
are ones used by the Windows Shell.  There is very little documentation on
how to handle these clipboard formats and no code samples (believe me, I’ve
searched the universe).  To make matters worse for VB programmers, VB can’t
handle these formats at all.  This article and the attached code is a
result much research and help from 3 different Microsoft support reps on 3
separate incidents which are all related to solving this problem.  I
imagine this will be handled in VB someday just like an Explorer file drop (and
it should be noted that Outlook Express file attachments are
handled like Explorer file attachments – the vbCFFiles format).

This article is applicable to both C++ and VB programs.  If you are
using VB, you have no choice but to create a C++ component to handle this
task.  I’ve provided one here. If you are using C++, then all you need is
the code in the Drop method of this C++ component.  

The C++/ATL component provided (UIHelper.DLL), provides a UIFileDragDrop
interface with 2 methods:

RegisterFileDrop( ) and UnRegisterFileDrop( )

These would be used by a VB program to let the C++ component handle the Drop
event of an e-mail attachment.

The 2 events provided: DragOver( ) and FilesDropped( ), are
provided to allow the VB program to be informed of  DragOver( ) and
DragDrop events that took place in the C++ component.  The FilesDropped(
)
event passes the names of the files dropped as a comma-separated string so
that the VB application can do something appropriate with the files (e.g. store
the path name(s) somewhere).

The VB sample provided implements these methods and events and allows e-mail
attachments to be dropped onto a tree control.  It puts the file names in
as the text (and key) for each tree node.  It places each new node at the
currently selected node location.

I will go into a little more detail now on the inner workings of the code,
but if you want to dig in now,  just download the
code and take a look.  There really isn’t much code and there are some
comments placed near the not-so-obvious sections to make reading a little
easier.

The nitty-gritty details:

There are several issues that need to be addressed to solve this problem:

  • Registering the C++ component as the drop target
  • Re-registering the VB drop target interface
  • Deciphering the data provided in the Drop event

Registering the C++ component as the drop target

In the VB application, on the OLEDragOver( ) event of your control,
you need to check to see if the data is an e-mail attachment.  Since VB
doesn’t support these, I had to do some hacking to find out that -16370 is the
format type for the FileDescriptor structure.  If it is an e-mail
attachment, then we can create a UIFileDragDrop object and call RegisterFileDrop(
)
passing the window handle of our control and optionally a path name to
indicate where the files should be stored (I put this in for my benefit – you
may not need it).  This is the code from the VB sample.

Private Sub TreeView1_OLEDragOver(Data As
MSComctlLib.DataObject, Effect As Long, Button As Integer, Shift As Integer, x As Single, y As Single, State As Integer)
   If (Data.GetFormat(-16370) = True) Then ‘e-mail attachment
      Set UIDragDrop = New UIFileDragDrop
      Dim location As String
      location = "c:tmp"
      If (Dir(location, vbDirectory) = “”) Then
         MkDir location
      End If
      UIDragDrop.RegisterFileDrop TreeView1.hWnd, location
   End If
   Call HandleDragOver(x, y)
End Sub

Notice the HandleDragOver( ) call.  This function was added so
that all DragOver( ) messages are handled the same whether they came from
VB or the C++ component.  In the sample we wanted to highlight the tree
node under the cursor and expand any un-expanded nodes:

Private Sub HandleDragOver(ByVal x As Long, ByVal y As Long)
    Set TreeView1.SelectedItem = TreeView1.HitTest(x, y)
    TreeView1.DropHighlight = TreeView1.SelectedItem
    If (Not TreeView1.SelectedItem Is Nothing) Then
        If (Not TreeView1.SelectedItem.Expanded) Then
            TreeView1.SelectedItem.Expanded = True
        End If
    End If
End Sub

Keep in mind that once we register the C++ component as the drop target, all
message are sent to that component (not VB).  This includes the DragOver(
)
, DrageEnter( ), DragLeave( ), and Drop( ) messages
until we revoke the C++ components interface and re-register the VB
interface.  This is the reason that the DragOver( ) event is
provided by the UIFileDragDrop interface.

Private Sub UIDragDrop_DragOver(ByVal x As Long, ByVal y As Long)
    ‘Convert pixels to Twips
    x = x * Screen.TwipsPerPixelX
    y = y * Screen.TwipsPerPixelY
    Call HandleDragOver(x, y)
End Sub

Notice we had to convert pixels to Twips, since our VB
application is using Twips.  Also, in VB the coordinates are relative to
the client (as opposed to the screen in C++).  So before UIFileDragDrop::DragOver(
)
is called, the coordinates are converted using ScreenToClient( ).

Also, note we are only passing the x, y
coordinates.  There is other information that could be passed (e.g. the
current keyboard state).  I’ll leave that up to you if you need it (I
didn’t).

Re-registering the VB drop target interface

The key code in the RegisterFileDrop( ) method is the following:

m_OriginalDropTargetInterface = (IDropTarget *)
GetProp((HWND)hWnd, "OleDropTargetInterface");

m_OriginalDropTargetInterface->AddRef();

hr = RevokeDragDrop((HWND) hWnd); // Revoke the VB interface
if (SUCCEEDED(hr))
    hr = RegisterDragDrop((HWND) hWnd, pDT); // Register our C++ interface

Before we Revoke VB’s drop target interface, we need to save it away so that
we can restore it after the Drop( ) event.  This is accomplished
with the undocumented/unsupported call to GetProp( ).  It turns out
that (if you care) the IDropTarget interface pointer is attached to the window
via the SetProp( ) API call.  If you know the name (in this case
"OleDropTargetInterface"), you can get the
interface pointer via the property name with GetProp( ).  In order
to figure out what the name was, I had to use the EnumProps( ) API function and
look for something meaningful (luckily they named it appropriately).  Next
we need to do an AddRef( ) on that interface pointer, because the call to
RevokeDragDrop( ) will do a Release( ) and we don’t want it to go away
yet.  Once we Revoke the VB interface, we can register our own interface in
the C++ component.  At this point all Drag events will get sent to the C++
interface and VB won’t get anything.

To unregister our interface and re-register VB’s
interface after the Drop event has completed we need the following code (taken
from UnRegisterFileDrop( )):

RevokeDragDrop((HWND) hWnd); //Revoke our current interface
if (m_OriginalDropTargetInterface) { //Re-register the VB interface
    hr = RegisterDragDrop((HWND) hWnd, m_OriginalDropTargetInterface);
    m_OriginalDropTargetInterface->Release(); //Release the reference we added in RegisterFileDrop
}

One other note:  The UnRegisterFileDrop( ) method is also called
from the DragLeave( ) event.  This fixes a little side-affect which
happens if the drop doesn’t take place.  If the drop doesn’t take place,
there would be no way to re-register the VB drop target interface.  So, if
we always call UnregisterFileDrop( ) on the DragLeave( ) event we
won’t be left hanging in limbo.  As long as the VB application calls RegisterFileDrop(
)
on the DragOver( ) event we are fine (i.e. we will always
re-register the C++ interface ont he first DragOver( ) event).  This
would be a little cleaner if VB had the DragEnter( ) and DragLeave( )
events, but you only get these in C++.

Deciphering the data provided in the Drop event

This is the part that applies to both C++ and VB programs.  You need to
decipher the data provided in the Drop event in the C++ interface.  There
are 2 types of data that are provided with the e-mail attachment: FileDescriptor
and FileContents.  The FileDescriptor has file information for each file
like the name, size, date/time, etc.  The FileContents data provides an
interface pointer to an IStream object that you can use to read the file
contents and then write to a file.  So it’s up to you to create a file and
write the contents to it.  

It should be noted that the FileDescriptor data is actually a
FILEGROUPDESCRIPTOR structure which contains the number of files (cItems
variable) and an array of FILEDESCRIPTOR structures (one for each file).

 

STDMETHODIMP DragDropObject::Drop(IDataObject * pDataObj,
                                                                   
DWORD grfKeyState,
                                                                   
POINTL pt,
                                                                   
DWORD * pdwEffect)
{
    HRESULT hr = S_OK;

    if (pDataObj) {
        FILEGROUPDESCRIPTOR *file_group_descriptor;
        FILEDESCRIPTOR file_descriptor;

        // Important: these strings need to be non-Unicode (don’t compile UNICODE)
        unsigned short cp_format_descriptor = RegisterClipboardFormat(CFSTR_FILEDESCRIPTOR);
        unsigned short cp_format_contents = RegisterClipboardFormat(CFSTR_FILECONTENTS);

        //Set up format structure for the descriptor and contents
        FORMATETC descriptor_format = 
            {cp_format_descriptor, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
        FORMATETC contents_format = 
            {cp_format_contents, NULL, DVASPECT_CONTENT, -1, TYMED_ISTREAM};

        // Check for descriptor format type
        hr = pDataObj->QueryGetData(&descriptor_format);

        if (hr == S_OK) { 
            // Check for contents format type
            hr = pDataObj->QueryGetData(&contents_format);

            if (hr == S_OK) { 
               
// Get the descriptor information
               
STGMEDIUM storage= {0,0,0};

               
hr = pDataObj->GetData(&descriptor_format, &storage);
               
file_group_descriptor = (FILEGROUPDESCRIPTOR *) GlobalLock(storage.hGlobal);

               
// For each file, get the name and copy the stream to a file
               
_bstr_t file_list;
               
for (unsigned int file_index = 0; file_index < file_group_descriptor->cItems; file_index++) {
                   
file_descriptor = file_group_descriptor->fgd[file_index];
                   
contents_format.lindex = file_index;
                   
hr = pDataObj->GetData(&contents_format, &storage);

                   
if (hr == S_OK) { // Dump stream to a file
                       
char file_name[MAX_PATH+1];

                       
if (m_FileDragObject->m_Location == _bstr_t(“”)) // No path specified, so
                           
m_FileDragObject->m_Location = “.”; // use current directory

                           
sprintf(file_name, “%s\%s”, (char *) m_FileDragObject->m_Location, file_descriptor.cFileName);
                           
hr = StreamToFile(storage.pstm, file_name);
                           
if (file_index == 0)
                               
file_list = file_name;
                           
else
                               
file_list = file_list + _bstr_t(“,”) + _bstr_t(file_name);
                   
}
                   
// Let the VB application know about the drop operation completing and
                   
// give it a comma-separated list of file paths
                   
m_FileDragObject->Fire_FilesDropped(file_list);
               
}
               
GlobalUnlock(storage.hGlobal);
               
GlobalFree(storage.hGlobal);
            }
        }
    } 
    // We are done, so re-register the VB IDropTarget Interface
    m_FileDragObject->UnRegisterFileDrop((long) m_FileDragObject->m_hWnd);
    return hr;
}

The StreamToFile( ) method just reads from the stream and writes to a
file.  I used good-old C-runtime sopen( ) and write( ), but you can use
whatever you like.  I chose an arbitrary block size of 1024 (BLOCK_SIZE). 
The code follows:

HRESULT DragDropObject::StreamToFile(IStream *stream, char *file_name)
{
    byte buffer[BLOCK_SIZE];
    unsigned long bytes_read = 0;
    int bytes_written = 0;
    int new_file;
    HRESULT hr = S_OK;

    new_file = sopen(file_name, O_RDWR | O_BINARY | O_CREAT, SH_DENYNO, S_IREAD | S_IWRITE);
    if (new_file != -1) {
        do {
            hr = stream->Read(buffer, BLOCK_SIZE, &bytes_read);
            if (bytes_read)
               
bytes_written = write(new_file, buffer, bytes_read);
        } while (S_OK == hr && bytes_read == BLOCK_SIZE);
        close(new_file);
        if (bytes_written == 0)
            unlink(file_name);
    }
    else {
        unsigned long error;
        if ((error = GetLastError()) == 0L)
            error = _doserrno;
            hr = HRESULT_FROM_WIN32(errno);
    }
    return hr;
}

That’s it! I hope I can save just one person from the
agony I went through to solve this problem.

Downloads

Download C++ component – 13 Kb
Download VB demo – 3 Kb

More by Author

Must Read