Implementing IDataObject

OLE Drag and Drop

Updated 6 Dec 2006

Many thanks to Davide Chiodi from Italy who has very kindly converted the data-obect code into a Pure C implementation - the download link is available at the bottom of this article!

In the last part of the tutorial we looked at how to access the Windows clipboard using OLE and the IDataObject. In this part we will be implementing the IDataObject interface, and using our completed data object to store the text "Hello World" into the Windows clipboard.

Creating a COM interface - IDataObject

In order to create our own COM object, we need to define a C++ class which implements all of these functions, and in order for the COM virtual-function table to be automatically included for us, we will use C++ class inheritance:

class CDataObject : public IDataObject
{
public:

    // IUnknown members
    HRESULT __stdcall QueryInterface (REFIID iid, void ** ppvObject);
    ULONG   __stdcall AddRef (void);
    ULONG   __stdcall Release (void);

    // IDataObject members
    HRESULT __stdcall GetData               (FORMATETC *pFormatEtc,  STGMEDIUM *pmedium);
    HRESULT __stdcall GetDataHere           (FORMATETC *pFormatEtc,  STGMEDIUM *pmedium);
    HRESULT __stdcall QueryGetData          (FORMATETC *pFormatEtc);
    HRESULT __stdcall GetCanonicalFormatEtc (FORMATETC *pFormatEct,  FORMATETC *pFormatEtcOut);
    HRESULT __stdcall SetData               (FORMATETC *pFormatEtc,  STGMEDIUM *pMedium,  BOOL fRelease);
    HRESULT __stdcall EnumFormatEtc         (DWORD      dwDirection, IEnumFORMATETC **ppEnumFormatEtc);
    HRESULT __stdcall DAdvise               (FORMATETC *pFormatEtc,  DWORD advf, IAdviseSink *, DWORD *);
    HRESULT __stdcall DUnadvise             (DWORD      dwConnection);
    HRESULT __stdcall EnumDAdvise           (IEnumSTATDATA **ppEnumAdvise);

    // Constructor / Destructor
    CDataObject(FORMATETC *fmtetc, STGMEDIUM *stgmed, int count);
    ~CDataObject();

private:
    // any private members and functions
    LONG m_lRefCount;

    int LookupFormatEtc(FORMATETC *pFormatEtc);
};

Notice that all of the IDataObject members have been listed - even the IUnknown interface members. This is because we are now implementing an entire COM object, so every member function must be included in the correct order.

With the IUnknown functions already visited in a previous tutorial, we can move onto the IDataObject functions. There is some good news and some bad news. The good news is, not all of the functions need to be implemented! Out of the nine functions, only three are required for OLE drag and drop, so this cuts down our work enormously.

The bad news is once we've implemented the IDataObject methods, we need to implement an entirely separate COM interface - the IEnumFORMATETC interface. We're a little way off this step yet, so let's start with simply allocating a new instance of IDataObject.

Constructing IDataObject

The IDataObject's main task is to allow a "consumer" to query it for data. These queries will take the form of calls to either QueryData or EnumFormatEtc. Therefore the IDataObject needs to know what data formats it should store, and when a consumer asks for the data, it should be able to provide it.

We therefore need to find some method to populate the IDataObject with real pieces of data and also tell it what the data is, in the form of FORMATETC structures.

The IDataObject will be populated with data during the call to it's C++ class constructor. For more flexibility it may make sense to use the IDataObject::SetData routine to perform this task, but for our simple implementation using the constructor makes sense for now.

CDataObject::CDataObject(FORMATETC *fmtetc, STGMEDIUM *stgmed, int count)
{
    // reference count must ALWAYS start at 1
    m_lRefCount    = 1;
    m_nNumFormats  = count;

    m_pFormatEtc   = new FORMATETC[count];
    m_pStgMedium   = new STGMEDIUM[count];

    for(int i = 0; i < count; i++)
    {
        m_pFormatEtc[i] = fmtetc[i];
        m_pStgMedium[i] = stgmed[i];
    }
}

The constructor performs two important tasks. The first is to initialize the COM object's reference count to 1. I see alot of incorrect COM code where reference counts begin at zero. The COM specifications clearly state that a COM object must begin life with a reference count of 1. If you think about it, a reference count of zero means that the COM object should be deleted, so it should never be initialized to this value.

The second task is to make a private copy of the FORMATETC and STGMEDIUM structures specified in the class constructor. The data object won't take ownership of the data inside each STGMEDIUM structure, it will merely reference it, and duplicate the data only when requested during a call to GetData.

Creating IDataObject

Now that we have a well-defined constructor for IDataObject, we can write a wrapper function which will hide the class details:

HRESULT CreateDataObject(FORMATETC *fmtetc, STGMEDIUM *stgmeds, UINT count, IDataObject **ppDataObject)
{
    if(ppDataObject == 0)
        return E_INVALIDARG;

    *ppDataObject = new CDataObject(fmtetc, stgmeds, count);

    return (*ppDataObject) ? S_OK : E_OUTOFMEMORY;
}

So creating an IDataObject is now very simple:

FORMATETC fmtetc = { CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM stgmed = { TYMED_HGLOBAL, { 0 }, 0 };

stgmed.hGlobal = StringToHandle("Hello, World!");

IDataObject *pDataObject;

CreateDataObject(&fmtetc, &stgmed, 1, &pDataObject);

Alot of implementations of IDataObject include alot of application-specific code inside the interface which performs the memory allocations. The idea behind this implementation is to provide a generic IDataObject which can be used in a variety of different applications. OK, so a little bit of work needs to be done up-front to create the FORMATETC and STGMEDIUM structures before creating the data object, but this can be easily isolated and doesn't pollute the interface code.

IDataObject::QueryGetData

This member function is called whenever an application wants to test our IDataObject to see if it contains a specific type of data. A pointer to a FORMATETC structure is passed as an argument, and it is the task of IDataObject::QueryGetData to inspect this structure and return a value to indicate if the requested data is available or not.

HRESULT __stdcall CDataObject::QueryGetData(FORMATETC *pFormatEtc)
{
    return (LookupFormatEtc(pFormat) == -1) ? DV_E_FORMATETC : S_OK;
}

The QueryGetData function is very simple in this case. We pass off all the work to a private helper function - LookupFormatEtc:

int CDataObject::LookupFormatEtc(FORMATETC *pFormatEtc)
{
    // check each of our formats in turn to see if one matches
    for(int i = 0; i < m_nNumFormats; i++)
    {
        if((m_pFormatEtc[i].tymed    &  pFormatEtc->tymed)   &&
            m_pFormatEtc[i].cfFormat == pFormatEtc->cfFormat &&
            m_pFormatEtc[i].dwAspect == pFormatEtc->dwAspect)
        {
            // return index of stored format
            return i;
        }
    }

    // error, format not found
    return -1;
}

The helper function above tries to match the specified FORMATETC structure against one of the available structures belonging to our data object. If it finds one that matches, it simply returns an index to the appropriate entry in the m_pFormatEtc array. If no match is found, an error value of -1 is returned.

Note the use of the bitwise-AND operator in the if-clause:

if( m_pFormatEtc[i].tymed & pFormatEtc->tymed ) 

The AND operator is used here because the FORMATETC::tymed member is actually a bit-flag which can contain more than one value. For example, the caller of QueryGetData could quite legitimetly specify a FORMATETC::tymed value of (TYMED_HGLOBAL | TYMED_ISTREAM), which basically means "Do you support HGLOBAL or IStream?".

IDataObject::GetData

The GetData function is similar in many ways to QueryGetData, the exception being that if the requested data format is supported, it must be returned into the specified storage-medium structure.

HRESULT __stdcall CDataObject::GetData (FORMATETC *pFormatEtc, STGMEDIUM *pStgMedium)
{
    int idx;

    // try to match the specified FORMATETC with one of our supported formats
    if((idx = LookupFormatEtc(pFormatEtc)) == -1)
        return DV_E_FORMATETC;

    // found a match - transfer data into supplied storage medium
    pMedium->tymed           = m_pFormatEtc[idx].tymed;
    pMedium->pUnkForRelease  = 0;

    // copy the data into the caller's storage medium
    switch(m_pFormatEtc[idx].tymed)
    {
    case TYMED_HGLOBAL:
        pMedium->hGlobal     = DupGlobalMem(m_pStgMedium[idx].hGlobal);
        break;

    default:
        return DV_E_FORMATETC;
    }
    return S_OK;
}

The same internal helper function LookupFormatEtc is used to check if the requested data format is supported. If it is, then the appropriate STGMEDIUM data is copied into the caller-supplied structure.

Note that call to the DupGlobalMem routine. This is a helper function which returns a duplicate of the specified HGLOBAL memory handle, and is required because each call to GetData must result in a fresh copy of the data.

HGLOBAL DupGlobalMemMem(HGLOBAL hMem)
{
    DWORD   len    = GlobalSize(hMem);
    PVOID   source = GlobalLock(hMem);
    PVOID   dest   = GlobalAlloc(GMEM_FIXED, len);

    memcpy(dest, source, len);
    GlobalUnlock(hMem);
    return dest;
}

We will need similar routines to support the other TYMED_xxx storage types. For now the only additional format I imagine being implemented is IStream.

IDataObject::EnumFormatEtc

This is the last member that requires any real programming effort. Its unfortunate that it whilst this member function is so simple to implement, it also requires us to start writing the IEnumFORMATETC object as well.

HRESULT __stdcall CDataObject::EnumFormatEtc (DWORD dwDirection, IEnumFORMATETC **ppEnumFormatEtc)
{
    // only the get direction is supported for OLE
    if(dwDirection == DATADIR_GET)
    {
        // for Win2k+ you can use the SHCreateStdEnumFmtEtc API call, however
        // to support all Windows platforms we need to implement IEnumFormatEtc ourselves.
        return CreateEnumFormatEtc(m_NumFormats, m_FormatEtc, ppEnumFormatEtc);
    }
    else
    {
        // the direction specified is not supported for drag+drop
        return E_NOTIMPL;
    }
}

If you look at the code comment above, you can see mention of the SHCreateStdEnumFmtEtc API call. What this does is create an IEnumFORMATETC interface on our behalf, requiring no work from ourselves. Unfortunately this API is only available on Windows 2000 and above, so we have to provide an alternative method to create an IEnumFORMATETC object.

Therefore in the next tutorial we will provide a full implementation of CreateEnumFormatEtc, a replacement for the Shell API call.

Unsupported IDataObject functions

There are still a number of IDataObject functions that need to be implemented. Whilst every function must be a valid routine, there is a simple method to indicate to OLE that we don't support the functionality that these routines might offer outside the world of drag and drop.

The IDataObject::DAdvise, IDataObject::EnumDAdvise and IDataObject::DUnadvise functions simply need to return the value OLE_E_ADVISENOTSUPPORTED.

HRESULT CDataObject::DAdvise (FORMATETC *pFormatEtc, DWORD advf, IAdviseSink *pAdvSink, 
                                                                 DWORD *pdwConnection)
{
    return OLE_E_ADVISENOTSUPPORTED;
}

HRESULT CDataObject::DUnadvise (DWORD dwConnection)
{
    return OLE_E_ADVISENOTSUPPORTED;
}

HRESULT CDataObject::EnumDAdvise (IEnumSTATDATA **ppEnumAdvise)
{
    return OLE_E_ADVISENOTSUPPORTED;
}

IDataObject::GetDataHere can only be implemented if the IStream and IStorage interfaces are supported by the data object. In our case we only support HGLOBAL data, so returning DATA_E_FORMATETC seems a sensible choice.

HRESULT CDataObject::GetDataHere (FORMATETC *pFormatEtc, STGMEDIUM *pMedium)
{
    return DATA_E_FORMATETC;
}

IDataObject::SetData and IDataObject::GetCanonicalFormatEtc are also simple to implement - the value E_NOTIMPL can be returned in this case. One special note about GetCanonicalFormatEtc - even though we return an error value, the output FORMATETC structure's "ptd" member (the pointer-to-DVTARGETDEVICE) must be set to zero:

HRESULT CDataObject::GetCanonicalFormatEtc (FORMATETC *pFormatEct, FORMATETC *pFormatEtcOut)
{
    // Apparently we have to set this field to NULL even though we don't do anything else
    pFormatEtcOut->ptd = NULL;
    return E_NOTIMPL;
}

HRESULT CDataObject::SetData (FORMATETC *pFormatEtc, STGMEDIUM *pMedium,  BOOL fRelease)
{
    return E_NOTIMPL;
}

Adding Data to the Clipboard

OK, so here's a little program to add "Hello World" to the Windows clipboard using OLE and data objects.

#include <windows.h>

int main(void)
{
    OleInitialize(0);

    IDataObject *pDataObject;
    FORMATETC fmtetc = { CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
    STGMEDIUM stgmed = { TYMED_HGLOBAL, { 0 }, 0 };

    stgmed.hGlobal = StringToHandle("Hello, World!", -1);

    // create the data object
    if(CreateDataObject(&fmtetc, &stgmed, 1, &pDataObject) == S_OK)
    {
        // add data to the clipboardOleSetClipboard(pDataObject);
        OleFlushClipboard();
        pDataObject->Release();
    }

    // cleanup
    ReleaseStgMedium(&stgmed);
    OleUninitialize();
    return 0;
}

Unfortunately this program won't work yet because we havn't implemented IEnumFORMATETC and the CreateEnumFormatEtc function. If you hold on for a moment though...

Coming up in Part 4 - Implementing IEnumFORMATETC

The next part of this tutorial series will be dedicated to writing a single function CreateEnumFormatEtc, which will be a drop-in replacement for the SHCreateStdEnumFmtEtc API call. Our implementation will have exactly the same semantics and will return a pointer to a genuine IEnumFORMATETC COM object which will be fully detailed.