The Shell Drag/Drop Helper Object Part 2: IDropSourceHelper

 

Raymond Chen
Microsoft Corporation

February 2000

Summary: The Microsoft® Windows® 2000 shell provides a new object, the shell drag/drop helper object, which allows you to take advantage of the shell drag/drop user interface, such as drag images and alpha blending. This is the second in a pair of articles that describe how you can use this object in your application to provide a richer user experience. (10 printed pages)

Contents

Accessing the IDragSourceHelper Interface Preparing Your IDataObject A Sample CDataObject Preparing Your IDragSource Touchdown

ms997502.ddhelp_pt2_1(en-us,MSDN.10).gif

Figure 1. Providing a custom drag image

Figure 1 shows a sample program that is using a picture of itself as the drag/drop feedback image (mostly because I was too lazy to design a cool image). A "real" application would use a bitmap representation of the object being dragged.

Accessing the IDragSourceHelper Interface

The IDragSourceHelper interface allows you to provide an image that the shell (or any other object using the shell drag/drop interface) will use as part of the drag/drop feedback. This interface is not quite as easy to use as IDropTargetHelper, however.

There are two objects involved in sourcing a drag/drop operation, namely the IDragSource and the IDataObject. Both require special preparation in order to take advantage of the IDragSourceHelper interface.

Preparing Your IDataObject

The key attribute of the data object is that it must be able to accept the arbitrary clipboard formats and medium types in its IDataObject::SetData method. This means you will need to maintain an array or other data structure to record the incoming FORMATETC and STGMEDIUM objects when your IDataObject::SetData method is called, you need to offer these formats on request, and you need to enumerate the formats as available.

There isn't room in this article to provide sample code, so I'll just have to touch on the highlights.

A Sample CDataObject

I will describe here (briefly) one way of implementing a data object that meets the above requirements. Note that you need not use this technique; it is provided only as an illustration of how you can create a data object that accepts arbitrary clipboard formats and medium types.

The basic idea behind the data object is quite simple: Keep an array of all the FORMATETC structures that have been set into the data object, with their associated STGMEDIUM structures. But, as we all know, the devil is in the details.

We start with the class declaration:

class CDataObject : public IDataObject, public IPersist {
    ...
private:
    typedef struct {
        FORMATETC   fe;
        STGMEDIUM   stgm;
    } DATAENTRY, *LPDATAENTRY;      /* Each active FORMATETC gets one of these */

    HRESULT FindFORMATETC(FORMATETC *pfe, LPDATAENTRY *ppde, BOOL fAdd);
    HRESULT AddRefStgMedium(STGMEDIUM *pstgmIn, STGMEDIUM *pstgmOut, BOOL fCopyIn);

    LPDATAENTRY m_rgde;            /* Array of active DATAENTRY entries */
    int m_cde;                     /* Size of m_rgde */

    CDataObject() : m_rgde(NULL), m_cde(0) { }
    ~CDataObject();
};

The destructor frees all the memory the data object had allocated or taken ownership of:

CDataObject::~CDataObject()
{
    for (int ide = 0; ide < m_cde; ide++) {
        CoTaskMemFree(m_rgde[ide].fe.ptd);
        ReleaseStgMedium(&m_rgde[ide].stgm);
    }
    CoTaskMemFree(m_rgde);
}

The CDataObject::FindFORMATETC method looks for (and perhaps adds) the provided FORMATETC structure in the data object. If the value is being retrieved rather than added, it also validates that the requested TYMED is supported:

HRESULT
CDataObject::FindFORMATETC(FORMATETC *pfe, LPDATAENTRY *ppde, BOOL fAdd)
{
    *ppde = NULL;

    /* Comparing two DVTARGETDEVICE structures is hard, so we don't even try */
    if (pfe->ptd != NULL) return DV_E_DVTARGETDEVICE;

    /* See if it's in our list */
    for (int ide = 0; ide < m_cde; ide++) {
        if (m_rgde[ide].fe.cfFormat == pfe->cfFormat &&
            m_rgde[ide].fe.dwAspect == pfe->dwAspect &&
            m_rgde[ide].fe.lindex == pfe->lindex) {
            if (fAdd || (m_rgde[ide].fe.tymed & pfe->tymed)) {
                *ppde = &m_rgde[ide];
                return S_OK;
            } else {
                return DV_E_TYMED;
            }
        }
    }

    if (!fAdd) return DV_E_FORMATETC;

    LPDATAENTRY pdeT = (LPDATAENTRY)CoTaskMemRealloc(m_rgde,
                                        sizeof(DATAENTRY) * (m_cde+1));
    if (pdeT) {
        m_rgde = pdeT;
        m_cde++;
        m_rgde[ide].fe = *pfe;
        ZeroMemory(&pdeT[ide].stgm, sizeof(STGMEDIUM));
        *ppde = &m_rgde[ide];
        return S_OK;
    } else {
        return E_OUTOFMEMORY;
    }
}

The CDataObject::AddRefStgMedium method copies a STGMEDIUM structure and does the appropriate work to increment the reference count. The only subtlety occurs when the type medium is TYMED_HGLOBAL and there is no pUnkForRelease. If the data comes from an external source, the data object must copy the contents of the HGLOBAL, but if the data is being returned to an external source, we can avoid the copy by setting the data object itself (suitably reference-counted) as the pUnkForRelease:

HRESULT
CDataObject::AddRefStgMedium(STGMEDIUM *pstgmIn, STGMEDIUM *pstgmOut, BOOL fCopyIn)
{
    HRESULT hres = S_OK;
    STGMEDIUM stgmOut = *pstgmIn;

    if (pstgmIn->pUnkForRelease == NULL &&
        !(pstgmIn->tymed & (TYMED_ISTREAM | TYMED_ISTORAGE))) {
        if (fCopyIn) {
            /* Object needs to be cloned */
            if (pstgmIn->tymed == TYMED_HGLOBAL) {
                stgmOut.hGlobal = GlobalClone(pstgmIn->hGlobal);
                if (!stgmOut.hGlobal) {
                    hres = E_OUTOFMEMORY;
                }
            } else {
                hres = DV_E_TYMED;      /* Don't know how to clone GDI objects */
            }
        } else {
            stgmOut.pUnkForRelease = static_cast<IDataObject*>(this);
        }
    }

    if (SUCCEEDED(hres)) {
        switch (stgmOut.tymed) {
        case TYMED_ISTREAM:
            stgmOut.pstm->AddRef();
            break;
        case TYMED_ISTORAGE:
            stgmOut.pstg->AddRef();
            break;
        }
        if (stgmOut.pUnkForRelease) {
            stgmOut.pUnkForRelease->AddRef();
        }

        *pstgmOut = stgmOut;
    }

    return hres;
}

The CDataObject::AddRefStgMedium method employed the following helper function:

HGLOBAL GlobalClone(HGLOBAL hglobIn)
{
    HGLOBAL hglobOut = NULL;

    LPVOID pvIn = GlobalLock(hglobIn);
    if (pvIn) {
        SIZE_T cb = GlobalSize(hglobIn);
        HGLOBAL hglobOut = GlobalAlloc(GMEM_FIXED, cb);
        if (hglobOut) {
            CopyMemory(hglobOut, pvIn, cb);
        }
        GlobalUnlock(hglobIn);
    }

    return hglobOut;
}

Armed with these helper functions, the implementation of the data transfer methods of the IDataObject interface is relatively simple, save for some nagging subtleties.

The CDataObject::GetData method locates the appropriate DATAENTRY structure and uses the CDataObject::AddRefStgMedium helper method to return the data to the caller:

HRESULT CDataObject::GetData(FORMATETC *pfe, STGMEDIUM *pstgm)
{
    LPDATAENTRY pde;
    HRESULT hres = FindFORMATETC(pfe, &pde, FALSE);
    if (SUCCEEDED(hres)) {
        hres = AddRefStgMedium(&pde->stgm, pstgm, FALSE);
    }

    return hres;
}

The CDataObject::QueryGetData method goes through the motions of a GetData without actually returning the data:

HRESULT CDataObject::QueryGetData(FORMATETC *pfe)
{
    LPDATAENTRY pde;
    return FindFORMATETC(pfe, &pde, FALSE);
}

The CDataObject::SetData method starts by locating the matching DATAENTRY structure (creating it or releasing the old data, as appropriate). If the caller is transferring ownership of the object (fRelease = TRUE), we copy the storage medium without adjusting the reference count. Otherwise, we use our CDataObject::AddRefStgMedium helper method to copy the storage medium and retain a reference in the associated DATAENTRY structure:

HRESULT CDataObject::SetData(FORMATETC *pfe, STGMEDIUM *pstgm, BOOL fRelease)
{
    if (!fRelease) return E_NOTIMPL;

    LPDATAENTRY pde;
    HRESULT hres = FindFORMATETC(pfe, &pde, TRUE);
    if (SUCCEEDED(hres)) {
        if (pde->stgm.tymed) {
            ReleaseStgMedium(&pde->stgm);
            ZeroMemory(&pde->stgm, sizeof(STGMEDIUM));
        }

        if (fRelease) {
            pde->stgm = *pstgm;
            hres = S_OK;
        } else {
            hres = AddRefStgMedium(pstgm, &pde->stgm, TRUE);
        }
        pde->fe.tymed = pde->stgm.tymed;    /* Keep in sync */

        /* Subtlety!  Break circular reference loop */
        if (GetCanonicalIUnknown(pde->stgm.pUnkForRelease) ==
            GetCanonicalIUnknown(static_cast<IDataObject*>(this))) {
            pde->stgm.pUnkForRelease->Release();
            pde->stgm.pUnkForRelease = NULL;
        }
    }
    return hres;
}

There is high subtlety in this code.

A circular reference can occur if the client passes a STGMEDIUM structure whose pUnkForRelease member is a pointer to the data object itself. One way this can happen is if a client calls the IDataObject::GetData method, then turns around and passes the same STGMEDIUM structure to the IDataObject::SetData method. If the circular reference were not broken, the data object would never be destroyed.

Detecting the circular reference requires us to check if the pUnkForRelease refers to the data object itself. Because this pointer may have traveled through OLE's marshaller, we cannot rely on the numerical value of the pointer remaining unchanged. (It might point to a marshalling stub object.) Consequently, we must rely on the rule for COM object identity: An explicit call to the IUnknown::QueryInterface method, requesting the IUnknown interface, will always return the same pointer.

To this end, we employ the following helper function, which returns a non-reference-counted pointer to canonical unknown:

IUnknown *GetCanonicalIUnknown(IUnknown *punk)
{
    IUnknown *punkCanonical;
    if (punk && SUCCEEDED(punk->QueryInterface(IID_IUnknown,
                                               (LPVOID*)&punkCanonical))) {
        punkCanonical->Release();
    } else {
        punkCanonical = punk;
    }
    return punkCanonical;
}

But wait, there's a double subtlety. You might think that we can get away with merely testing against the result of static_cast<IDataObject*>(this) to detect a reference to the data object itself (assuming, of course, that the CDataObject::QueryInterface method uses that same value as its canonical unknown). Alas, this does not work if the data object is aggregated, because the canonical unknown for an aggregated object is the canonical unknown for the outermost controlling unknown.

Therefore, we must pass the data object through the ordeal of GetCanonicalUnknown to ensure that a self-reference is properly detected.

Finally, there is a triple subtlety: Why do we know it is safe to call the IUnknown::Release method on the pUnkForRelease once we detect that it is the data object itself? The answer is that the call to release ourselves will not cause the data object to be destroyed, because there is still an outstanding reference to it in the IDataObject pointer held by the caller.

That enormous digression on implementing an appropriate data object complete, we can return to the task at hand, namely preparing our objects for use by the shell drag/drop helper object.

Preparing Your IDragSource

Start by creating a shell drag/drop helper object with the CoCreateInstance function, specifying IID_IDragSourceHelper as the interface. As with the IDropTargetHelper interface, you should give each drag source its own copy of this object so multiple drag sources don't interfere with each other:

    IDragSourceHelper *m_pdsh;

    /* This call might fail, in which case OLE sets m_pdsh = NULL */
    CoCreateInstance(CLSID_DragDropHelper, NULL, CLSCTX_INPROC_SERVER,
                     IID_IDropTargetHelper, (LPVOID*)&m_pdsh);

Again, because the shell drag/drop helper object is new for Windows 2000, the creation will fail on earlier versions of Windows, so you need to check for NULL before trying to use the interface.

Immediately before calling the DoDragDrop function, the drag source needs to store the image of the item being dragged into the data object. This is done in one of two ways:

  • Initializing from a bitmap

    The IDragSourceHelper::InitializeFromBitmap method lets you provide a bitmap (with associated information) that represents the object being dragged. In addition to providing the bitmap itself and some coordinates, you also provide a color-key in the SHDRAGIMAGE structure. All pixels in the source bitmap whose color matches the color-key will be treated as transparent. To create the screenshot shown in Figure 1, I used GetSysColor(COLOR_WINDOW) as the color-key, which makes the interior of my window appear transparent.

    After filling out the SHDRAGIMAGE structure, pass it and the data object to the IDragSourceHelper::InitializeFromBitmap method. If the function succeeds, the drag/drop helper object takes ownership of the bitmap. Otherwise, you should destroy the bitmap yourself to prevent a memory leak.

  • Initializing from a window

    Alternatively, you can make a window responsible for generating the image. The window procedure should process the DI_GETDRAGIMAGE registered window message, and in response, fill in the SHDRAGIMAGE structure with the appropriate information. The bitmap returned in the SHDRAGIMAGE structure will be destroyed by the drag/drop helper object when it is no longer needed; you should not destroy it yourself.

    Note that the associated window must reside in the same process as the drag source, because the DI_GETDRAGIMAGE message passes a pointer to the SHDRAGIMAGE structure as the lParam.

Touchdown

Once you've added support to the IDataObject::SetData method of your data object (which, of course, also should contain the actual object you are dragging around!), and you've set the image into the data object in one of the two methods just described, you can call DoDragDrop and ooh and aah as the image of the object being dragged appears in its alpha-blended glory.