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