Drop Target

OLE Drag and Drop

Updated 6 Dec 2006

Many thanks to Davide Chiodi from Italy who has very kindly converted the drop-target code into a Pure C implementation - download now!

Welcome to the sixth part of the "OLE Drag and Drop" tutorial series! This article will concentrate on implementing a small application which will act as a drop-target. What this means is that our application will be capable of receiving objects (be they files, pictures or text) which are dragged onto it.

We will implement an IDropTarget COM interface which will allow any OLE application to drag it's data over our application. This will take the form of a simple EDIT control which can act as a target for dropped CF_TEXT data. Hopefully you will be able to take the code presented here and "drag" it straight into your own apps ;-)

Become a "Drop Target"

In order for a window to accept data from a drap-drop operation, it must be registered as a "drop target". There is an OLE API call - RegisterDragDrop - which is used for this very purpose. The function prototype looks like this:

WINOLEAPI RegisterDragDrop(HWND hwnd, IDropTarget * pDropTarget);

The first parameter to this function is the window handle, of the window that is destined to be a drop target. The second parameter is a pointer to the IDropTarget COM object. The COM/OLE runtime will call the methods on this interface during the course of a drag-drop operation.

Likewise there is an OLE API call to remove drag-and-drop functionality from a window:

WINOLEAPI RevokeDragDrop(HWND hwnd);

All that is required from us is to called RegisterDragDrop when our window is created, and RevokeDragDrop when our window is destroyed. Before we can call RegisterDragDrop though, we need to construct a COM object which supports the IDropTarget interface.

The IDropTarget Interface

The IDropTarget Interface is relatively simple, with only four functions that need to be implemented. Of course there is also the IUnknown interface which needs to be implemented but we have already covered that earlier.

IDropTarget Methods Description
DragEnter Determines whether a drop can be accepted and its effect if it is accepted.
DragOver Provides target feedback to the user through the DoDragDrop function.
DragLeave Causes the drop target to suspend its feedback actions.
Drop Drops the data into the target window.

Each one of these four functions will be called by the COM/OLE runtime whenever an "object" is dragged over our registered window. Each function has a different task, as shown in the table above. It is up to us to provide the implementations of these functions.

Implementing IDropTarget

The IDropTarget interface is (in my experience) very difficult to write without using "application specific" code. i.e. there is no easy way to make a generic IDropTarget COM object which can be re-used between all of your applications.

This is because the requirement of IDropTarget is to show graphical feedback in your target window whenever an object is dragged over it, and also the application-specific code to access the data object's content.

Out of all the drag+drop interfaces, the IDropTarget is the one that would be best integrated directly into your window class. For example, supposing you have implemented a custom window using a C++ class - the best method to add drop-target support to this window is have your custom-window class inherit directly from IDropTarget, rather than having a separate CDropTarget class. This means that your drop-target code would have full access to all of your internal window state.

However, for the time-being here is the CDropTarget class in all it's glory:

class CDropTarget : public IDropTarget
{
public:
    // IUnknown implementation
    HRESULT __stdcall QueryInterface (REFIID iid, void ** ppvObject);
    ULONG   __stdcall AddRef (void);
    ULONG   __stdcall Release (void);

    // IDropTarget implementation
    HRESULT __stdcall DragEnter(IDataObject * pDataObject, DWORD grfKeyState, POINTL pt, DWORD * pdwEffect);
    HRESULT __stdcall DragOver(DWORD grfKeyState, POINTL pt, DWORD * pdwEffect);
    HRESULT __stdcall DragLeave(void);
    HRESULT __stdcall Drop(IDataObject * pDataObject, DWORD grfKeyState, POINTL pt, DWORD * pdwEffect);

    // Constructor
    CDropTarget(HWND hwnd);
    ~CDropTarget();

private:
    // internal helper function
    DWORD DropEffect(DWORD grfKeyState, POINTL pt, DWORD dwAllowed);
    bool  QueryDataObject(IDataObject *pDataObject);

    // Private member variables
    long   m_lRefCount;
    HWND   m_hWnd;
    bool   m_fAllowDrop;

    // Other internal window members
    
};

As well as the reference count, we need to store two additional variables: The m_hWnd variable is the window handle of the drop-target is needed so we can provide visual feedback during the drag-drop operation. The m_fAllowDrop is used to indicate whether or not the dataobject being dropped on us contains useful data. This is so we don't have to continually query the dataobject - basically its an optimization trick.

IDropTarget::DragEnter

Let's look at the IDropTarget::DragEnter function first of all, because this is the first function that is called by COM when an object is dragged over our window:

HRESULT DragEnter(
   IDataObject * pDataObject,    // Pointer to the interface of the source data object
   DWORD         grfKeyState,    // Current state of keyboard modifier keys
   POINTL        pt,             // Current cursor coordinates
   DWORD *       pdwEffect       // Pointer to the effect of the drag-and-drop operation
   );

Look closely at the function prototype above, because it is important to understand what each of the parameters are used for.

  • IDataObject - the very first argument is another pointer to the data object passed to us (via COM) by the source of the drag-drop operation. The IDataObject is simply the "transport medium" for the data that is being dropped. We can query the data object during DragEnter to see if it has any data that we want.
  • gfrKeyState - holds the state of the keyboard modifier keys such as Control, Alt and Shift, and the state of the mouse buttons. It's a simple DWORD variable comprised using one or more of the following bit-flags: MK_CONTROL, MK_SHIFT, MK_ALT, MK_BUTTON, MK_LBUTTON etc.
  • pt - a POINTL structure, containing the coordinates of the mouse as it enters our window. In some applications this parameter would be used to check if the mouse was positioned over allowable drop areas, or used simply to position some kind of "insertion" cursor to indicate where the dropped data would go.
  • pdwEffect - pointer to a DWORD value that specifies the drop-effects that are allowed by the drop-source. This value is the same as the dwOKEffect value specified by the caller of DoDragDrop.

Our implementation of DragEnter needs to perform several common tasks, in addition to drawing graphical feedback.

  1. Inspect the supplied data object and decide if it contains any useful data or not.
  2. Inspect the keyboard state stored in grfKeyState and calculate what the drop-effect should be. i.e. if the Control key is held down, the drop-effect should be "copy", if Shift is held down, the drop-effect should be "move".
  3. Verify that the computed drop-effect is compatible with those allowed by the drop-source.
  4. Store the final drop-effect in the DWORD pointed to by pdwEffect.

Don't get caught up in the complexity of all this. The purpose of DragEnter is to simply say "yes or no" to the drag-drop operation, and to specify what the drop-effect should be so that the mouse-cursor can be updated by OLE.

HRESULT __stdcall CDropTarget::DragEnter(IDataObject *pDataObject, DWORD grfKeyState, 
                                              POINTL pt, DWORD *pdwEffect)
{
    // does the dataobject contain data we want?
    m_fAllowDrop = QueryDataObject(grfKeyState, pdwEffect, pDataObject);
	
    if(m_fAllowDrop)
    {
        // get the dropeffect based on keyboard state
        *pdwEffect = DropEffect(grfKeyState, pt, *pdwEffect);

        SetFocus(m_hWnd);
        PositionCursor(m_hWnd, pt);
    }
    else
    {
        *pdwEffect = DROPEFFECT_NONE;
    }    return S_OK;
}

Apart from setting focus to the underlying window and positioning the EDIT caret on the nearest character under the mouse, the DragEnter function has been simplified by delegating the functionality to two internal helper routines:

bool CDropTarget::QueryDataObject(IDataObject *pDataObject)
{
    FORMATETC fmtetc = { CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };

    // does the data object support CF_TEXT using a HGLOBAL?
    return pDataObject->QueryGetData(&fmtetc) == S_OK ? true : false;
}

QueryDataObject is a private member function which is used purely to inspect the supplied data object, and decide if it contains data that is meaningful to our drop-target. In our case, we only accept CF_TEXT data stored as a HGLOBAL, so this is what we ask for. A private member variable m_fAllowDrop is used to remember this decision.

DWORD CDropTarget::DropEffect(DWORD grfKeyState, POINTL pt, DWORD dwAllowed)
{
    DWORD dwEffect = 0;

    // 1. check "pt" -> do we allow a drop at the specified coordinates?
    // 2. work out that the drop-effect should be based on grfKeyState	
    if(grfKeyState & MK_CONTROL)
    {
        dwEffect = dwAllowed & DROPEFFECT_COPY;
    }
    else if(grfKeyState & MK_SHIFT)
    {
        dwEffect = dwAllowed & DROPEFFECT_MOVE;
    }

    // 3. no key-modifiers were specified (or drop effect not allowed), so
    //    base the effect on those allowed by the dropsource
    if(dwEffect == 0)
    {
        if(dwAllowed & DROPEFFECT_COPY) dwEffect = DROPEFFECT_COPY;
        if(dwAllowed & DROPEFFECT_MOVE) dwEffect = DROPEFFECT_MOVE;
    }

    return dwEffect;
}

The DropEffect helper function is used to compute the drop-effect based on the keyboard state and the effects allowed by the source.

First of all the grfKeyState variable is checked to see if either the Control or Shift keys are being used. The standard OLE behaviours for these keys are that Control should force a Copy of data, and Shift should force a Move of data. If both are held down, the the data should be Linked (i.e. the source should make a shortcut to the target), but we don't support this feature.

The important thing to note is the use of the "bitwise-AND" operator when assigning the drop-effect to dwEffect:

dwEffect = dwAllowed & DROPEFFECT_COPY;

The result of this assignment is simple - dwEffect will be assigned the value DROPEFFECT_COPY, but only if this value is present in the dwAllowed variable. This use of logic prevents us from forcing a drop-effect that is not allowed by the source.

The next stage is to decide what to do if no keyboard modifiers are present - i.e. Control or Shift are not in use. In this case we simply inspect the drop-effects allowed by the source and choose (in order of priority) which one to use - in our implementation, we let data moves override data copies.

IDropTarget::DragOver

The DragOver function will be called multiple times during the lifetime of a drag-drop operation. Therefore it is important for this function to be efficiently written. DragOver is called whenever the state of the keyboard modifiers change (i.e. shift/control etc), or when the mouse moves. It is the responsibilty of this function to indicate to OLE what the drop-effect will be based on the state of the keyboard and mouse position.

HRESULT __stdcall CDropTarget::DragOver(DWORD grfKeyState, POINTL pt, DWORD * pdwEffect)
{
    if(m_fAllowDrop)
    {
        *pdwEffect = DropEffect(grfKeyState, pt, *pdwEffect);
        PositionCursor(m_hWnd, pt);
    }
    else
    {
        *pdwEffect = DROPEFFECT_NONE;
    }

    return S_OK;
}

DragOver is very simple to write, because the logic is identical to that of DragEnter. We use the previously computed m_fAllowDrop and the DropEffect helper routine to return a drop-effect through the pdwEffect pointer.

IDropTarget::DragLeave

The DragLeave function is called whenever the mouse cursor is moved outside of our drop-target window, or the Escape key is pressed which cancels the drag-drop operation. It's prototype (and implementation) is really simple:

HRESULT __stdcall CDropTarget::DragLeave(void)
{
    return S_OK;
}

This is the most basic way to write this function. The only reason this function exists is so that applications that make heavy use of graphical feedback effects get a chance to clean up once the mouse moves out of the window. For example, imagine the following scenario: whenever something is dragged over a drop-target, the DragEnter function is used to change the colour of the window-border. In this case, the DragLeave function would be used to restore the window-border.

IDropTarget::Drop

The Drop function's prototype is exactly the same as the DragEnter function:

HRESULT __stdcall CDropTarget::Drop(IDataObject *pDataObject, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect)
{
    PositionCursor(m_hWnd, pt);

    if(m_fAllowDrop)
    {
        DropData(m_hWnd, pDataObject);
        *pdwEffect = DropEffect(grfKeyState, pt, *pdwEffect);
    }
    else
    {
        *pdwEffect = DROPEFFECT_NONE;
    }
    return S_OK;
}

This function is called when OLE has determined that the drag-drop will go ahead. We get the same interface pointer to the IDataObject that we received during DragEnter, which we can now retrieve data from to paste into our edit window.

The DropData helper function is used to access the CF_TEXT data inside the dataobject and insert it into the edit control. This routine is purely academic and as we already know how to access a dataobject I won't bother detailing it any further - just look at the sourcecode download if you are interested.

Conclusion

We've done it! It's taken six tutorials to get to this stage, but it was necessary to break up the subject matter into managable chunks.

So what have we accomplished?

At this stage we know how to implement IDataObject, IEnumFORMATETC, IDropTarget and IDropSource, as well as access the Windows clipboard using the new OLE functions.

There is still scope for further tutorials though. The next tutorial (or two) will look at dragging and dropping files (and filenames), and also using the IStream COM interface to stream file content between applications and the Windows Shell.

As always, I'd like to hear any feedback you may have on this tutorial series. More feedback equals more tutorials, so stay tuned!