上一张我们着重介绍了怎么样使用OLE和IDataObject来访问windows粘贴板。本章主要实现一个IDataObject接口,然后使用我们完成的数据对象来存储文本“Hello World”到粘贴板中。
创建一个COM接口-IDataObject
为了创建一个COM对象,我们需要定义一个实现所有这些函数的C++类,并且让COM的虚函数表为我们自动包含,我们使用C++类继承:
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);
stgmed
// Constructor / Destructor
CDataObject (FORMATETC *fmtetc, STGMEDIUM *, int count);
~CDataObject ()
private:
LONG m_lRefCount;
int LookupFormatEtc(FORMATETC *pFormatEtc);
};
上面列出了所有IDataObject成员,包括IUnknown接口成员,这是因为我们现在需要实现整个COM对象,因此每个成员必须正确的包含。
由于IUnknown函数我们在前面已经介绍了,我们继续介绍IDataObject函数。有些好的消息,同时也有些坏的消息;好的消息是,不是所有饿函数都需要实现,在IDataObject的9个函数中,我们仅仅需要实现3个来支持OLE的拖放操作,因此显著节省了我们的工作量。
坏的消息是:一般我们已经实现了IDataObject方法,我们需要实现完全独立的COM接口-IEnumFORMATETC接口。然而到这步还有很大的距离,因此让我们以一个简单分配新IDataObject的实例作为一个开始。
构造IDataObject
IDataObject的主要任务是允许一个消费者查询数据,这些查询从QueryData或EnumFormatEtc调用来发起的,因此,IDataObject需要知道存储什么样的数据格式,并且在消费者需要数据的时候,它能够提供。
我们因此需要找到一些办法来以FORMATETC结构的形式用真正的数据片来组装IDataObject且说明数据是什么。
IDataObject在C++类构造函数的时候组装,为了更弹性,可能需要添加内部帮助程序来执行这个任务,但对于我们简单实现仅在构造函数中使用。
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];
}
}
构造函数执行两个重要的任务,首先是初始化COM对象引用记数为1。我看到过许多不正确的COM代码,他们初始化记数为0,COM规约明确地声明,一个COM对象必须以“1”作为生命周期的开始,如果你记得,一个记数为0的COM对象应该被删除,因此它应该从不应该被初始化为这个值。
第二个任务是在类构造函数中做一个私有的FORMATETC和STGMEDIUM的副本。数据对象不是每个STGMEDIUM结构体内部的所有者,它纯粹是引用并且在请求调用GetData的时候复制数据。
创建IDataObject对象
现在我们有一个定义良好的IDataObject构造函数,我可以写一个包装函数来隐藏类的细节:
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;
}
现在创建一个IDataObject变的非常简单:
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);
许多IDataObject的实现包含许多接口内部执行内存分配的程序指定编码;在这个实现后面的思想是可以提供一个用于各种程序的通用IDataObject。好了,在创建数据对象之前有点工作需要做就是创建FORMATETC和STGMEDIUM结构,但这很容易被隔离,并且不会污染接口编码。
IDataObject::QueryGetData
该成员函数在某程序想检查IDataObject看是否包含指定类型的数据时候调用。一个指向FORMATETC结构的指针作为一个参数,且IDataObject::QueryGetData来检查这个结构且返回一个值来指示请求的数据是否可用。
HRESULT __stdcall IDataObject::QueryGetData(FORMATETC *pFormatEtc)
{
return (LookupFormatEtc(pFormat) == -1) ? DV_E_FORMATETC : S_OK;
}
这个例子中的QueryGetData函数非常简单,我们放弃私有协助函数-LookupFormatEtc的所有工作:
int CDataObject::LookupFormatEtc(FORMATETC *pFormatEtc)
{
// 轮流检查格式看是否能找到匹配的格式
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;
}
上面的函数尽量在我们数据对象的可用结构中查找一个与指定FORMATETC结构匹配的对象,如果找到一个匹配的,就简单的返回相应m_pFormatEtc数组的索引,如果找不到,返回-1表示一个错误。
注意,在if从句中的位与操作符:
if( m_pFormatEtc[i].tymed & pFormatEtc->tymed )
AND操作符用在这里是因为FORMATETC::tymed成员实际上是一个位标志,它能够包含不止一个值;例如:QueryGetData的调用者可以完全指定一个FORMATETC::tymed值(TYMED_HGLOBAL|TYMED_ISTREAM)就意味着你支持HGLOBAL或IStream吗?
IDataObject::GetData
GetData函数和QueryGetData有许多相似之处,除了如果支持请求的数据格式,它必须返回指定的存储类型。
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;
}
同样要调用内部协助函数LookupFormatEtc来检查是否支持请求的数据格式,如果支持,相应的STGMEDIUM数据被复制到调用者提供的结构。
注意,现在调用DupGlobalMem程序,这是一个协助函数,它返回指定HGLOBAL内存的HANDLE的副本,并且必须返回部分,因为每个GetData调用都要求一个新的数据副本。
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;
}
我们需要同样的程序来支持TYMED_xxx存储类型,但现在我们设想实现的支持格式是IStream。
IDataObject::EnumFormatEtc
这是最后需要自己动手的成员,不幸的是这个成员函数实现如此简单,但也要求我们写IEnumFORMATETC对象。
HRESULT __stdcall CDataObject::EnumFormatEtc (DWORD dwDirection, IEnumFORMATETC **ppEnumFormatEtc)
{
// OLE仅仅支持得到方向成员
if(dwDirection == DATADIR_GET)
{
// 在WIN2K下,你可以调用AIP函数SHCreateStdEnumFmtEtc来完成,但为了支持//所有的window平台,我们需要实现IEnumFormatEtc。
return CreateEnumFormatEtc(m_NumFormats, m_FormatEtc, ppEnumFormatEtc);
}
else
{
// the direction specified is not supported for drag+drop
return E_NOTIMPL;
}
}
看到上面的代码,你会提到SHCreateStdEnumFmtEtc这个API调用,它能够代表我们创建IEnumFORMATETC接口,不幸的是,这个API仅仅在WIN2K上可用,因此,我们需要提供其他创建IEnumFORMATETC对象。
因此下面的旅程中,我们将提供一个CreateEnumFormatEtc的完整实现,来代替Shell API调用。
不支持的IDataObject函数
仍然有一些IDataObject函数需要实现,而同时每个函数必须是一个有效的程序,有个简单的办法可以指定给OLE,我们不支持这些拖放操作以外的函数。
IDataObject::DAdvise、IDataObject::EnumDAdvise和IDataObject::DUnadivise函数简单的返回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;
}
GetDataHere只需要实现IStream和IStorage接口来支持数据对象,在我们的例子中,我们只支持HGLOBAL数据,因此返回DATA_E_FORMATETC是一个明智的选择。
HRESULT CDataObject::GetDataHere (FORMATETC *pFormatEtc, STGMEDIUM *pMedium)
{
return DATA_E_FORMATETC;
}
SetData和GetCanonicalFormatEtc也只要简单的实现,本例中可以返回E_NOTIMPL值,即使我们返回错误的值,一个GetCanonicalFormatEtc记名票据,输出的FORMATETC结构ptd成员应该是0。
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;
}
添加数据到粘贴板
好了,这里有一个简单那的程序用来通过OLE和数据对象来添加“Hello World”到Windows的粘贴板。
#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 clipboard
OleSetClipboard (pDataObject);
OleFlushClipboard ();
pDataObject->Release();
}
// cleanup
ReleaseStgMedium (&stgmed);
OleUninitialize ();
return 0;
}