Implementing IDataObject
9 minute read •
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.