欢迎来到OLE拖放指南第二部分;本部分的目的在于解释在OLE环境中,程序之间怎么样表示和传输数据。
OLE数据传输的核心是IDataObject COM接口,一个IDataObject提供从一个程序到另一个程序传输和访问数据的方法。最通用的OLE数据传输是窗口粘贴板,当然也有拖放。IDataObject是一到多个数据的有效的COM包装。
在我们调查IDataObject任何细节之前,两个重要的数据结构你必须熟悉:FORMATETC和STGMEDIUM接口,他们用来描述和存储OLE数据。
描述OLE数据
FORMATETC接口(发音“format et cetera”)用来表示IDataObject提供(或接收)的数据类型,是标准window粘贴板格式(CF_TEXT等)的扩展,因此除了基本的粘贴板格式之外,还包含了数据怎么样rendered和存储。
typedef struct
{
CLIPFORMAT cfFormat; // 粘贴板格式
DVTARGETDEVICE *ptd; // (NULL) rendering的目标设备
DWORD dwAspect; // (DV_CONTENT) rendering的详细程度
LONG lindex; // (-1) 在数据通过页面边界分割的时候使用
DWORD tymed; // 用于数据传输的存储媒体(HGLOBAL,IStream)
} FORMATETC;
FORMATETC结构的成员如下描述:
cfFormat:粘贴板格式,用来表示FORMATETC结构。可以是内建的格式(例如:CF_TEXT或CF_BITMAP)或者用RegisterClipboardFormat注册的自定义格式。
Ptd:指向DVTARGETDEVICE结构,提供已经rendered数据的设备信息。正常的粘贴板操作和拖放操作都是NULL。
dwAspect:描述用户怎么样render数据的大量细节。通常这个是DVASPECT_CONTECT,表示全内容,但也可以描述较少的信息,例如:图标。
Lindex:仅仅在当数据通过页面边界被分割的时候使用,它不用于简单的OLE传输,因此该值几乎总是-1。
Typemed:这是一个有趣的成员;因为其描述了用于存储数据的存储媒体类型。该成员名字自词组“Type of Medium”;该值在window.h中定义的TYMED_XXX等值。
因此有了这个数据结构,OLE已经提供了一个描述消费者什么样的数据已经怎么样render这个数据。
存储OLE数据
结构体STGMEDIUM(STORAGE MEDIUM的缩写)提供一个用来存储数据的容器,因此叫存储媒体:
typedef struct
{
DWORD tymed;
union
{
HBITMAP hBitmap;
HMETAFILEPICT hMetaFilePict;
HENHMETAFILE hEnhMetaFile;
HGLOBAL hGlobal;
LPWSTR lpszFileName;
IStream *pstm;
IStorage *pstg;
};
IUnknown *pUnkForRelease;
} STGMEDIUM;
这个结构定义看起来比较复杂,但是有用的仅仅三个成员,因为未命名联合合并了所有内容作为一个实体共享同样的存储空间。
1. tymed:这个成员必须和FORMATETC结构相同,这个成员指定已经存储的媒体类型,例如,全局数据(TYMED_HGLOBAL),IStream(TYPED_ISTREAM)等等。相应的联合中的元素是数据的句柄。
2. hBitmap/hGlobal等:实际的数据,仅仅他们中的一个是有效的,这依赖于tymed的值。
3. pUnkForRelease:一个可选的指针,指向IUnknown接口,数据的接收方应该调用其Release方法。当这个字段是NULL时,接收方有责任释放内存句柄。ReleaseStgMedium API调用在这里非常有用,它负责释放STGMEDIUMS的数据内容,因此实际上我们不需要做什么。
STGMEDIUM结构是传统的windows HGLOBAL内存句柄的扩展,同时支持HGLOBAL(且一直是最常用的),同时支持许多其他的类型,最有用的是IStream和IStorage通用COM接口。
总之,结构体FORMATETC和STGMEDIUM一起用来描述和存储OLE数据实体的。FORMATETC通常用来从IDataObject请求指定类型的数据,同时STGMEDIUM结构用来接收和保存请求的数据。
传输OLE数据
IDataObject接口提供了从一个程序到另一个程序传递数据的方法,IDataObject在两个情况下非常有用:粘贴板和拖放。如果设计精细,可以用一个COM对象来同时实现粘贴板和拖放操作。
下面的表列出了IDataObject成员函数,按照他们在接口虚表中出现的顺序。为了简化的原因,IUnknown方法(AddRef,Release和QueryInterface)没有列出。
IDataObject方法 | 描述 |
GetData | Render在FORMATETC结构体中描述的数据,并通过STGMEDIUM结构体来传递数据 |
GetDataHere | Render在FORMATETC结构体中的数据,并通过调用者分配的STGMEDIUM结构体传输数据。 |
QueryGetData | 判断数据对象是否可以render在FORMATETC结构中描述的数据 |
GetCanonicalFormatEtc | 提供一个潜在不同的但逻辑上相同的FORMATETC结构体。 |
SetData | 提供一个通过FORMATECT结构和STGMEDIUM结构描述的源数据对象。 |
EnumFormatEtc | 创建并返回一个IEnumFORMATETC接口的指针来枚举数据对象支持的FORMATETC对象。 |
DAdvise | 创建一个在数据对象和通知接收器之间的连接,因此通知接收器能接收到数据对象中通知的改变。 |
DUnadvise | 销毁一个前面使用DAdvise方法安装的通知 |
EnumDAdvise | 创建和返回一个指向枚举当前通知连接的接口指针。 |
这个表看起来很漂亮,我们也看到EnumFormatEtc方法并发现我们不得不同时实现IEnumFORMATETC接口,它有13个成员函数,不包括IUnknown方法,到这里我们还没有考虑IDropSource和IDropTarget。
庆幸的是,为了简化OLE拖放,我们仅仅需要实现GetData、QueryGetData和EnumFormatEtc,因此这节省了我们许多工作。
使用IDataObject来访问粘贴板
为了使在我们的OLE旅程中放松一下,下面我们来看一个简单的通过OLE来访问粘贴板的例子:
WINOLEAPI OleGetClipboard(IDataObject ** ppDataObj);
这个简单的Windows API调用用来返回一个IDataObject,它提供用来一个干净地访问WINDOWS粘贴板内容的好接口。注意,我们在本例中不需要实现IDataObject接口,我们仅仅需要知道接口怎么样工作的,一个简单的访问粘贴板内容的程序如下:
#include <windows.h>
int main(void)
{
IDataObject *pDataObject;
// Initialize COM and OLE
if(OleInitialize(0) != S_OK)
return 0;
// Access the data on the clipboard
if(OleGetClipboard(&pDataObject) == S_OK)
{
// access the IDataObject using a separate function
DisplayDataObject (pDataObject);
pDataObject->Release();
}
// Cleanup
OleUninitialize();
return 0;
}
OLE API调用非常简单,且它是直接来访问IDataObject对象:
void DisplayDataObject(IDataObject *pDataObject)
{
FORMATETC fmtetc = { CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM stgmed;
// ask the IDataObject for some CF_TEXT data, stored as a HGLOBAL
if(pDataObject->GetData(&fmtetc, &stgmed) == S_OK)
{
// 我们必须锁定HGLOBAL句柄,因为我们不能确信这是否是一个GEM_FIXED数据
char *data = GlobalLock(stgmed.hGlobal);
printf("%s/n", data);
// cleanup
GlobalUnlock(stgmed.hGlobal);
ReleaseStgMedium(&stgmed);
}
}
上面的代码演示了最常用的访问IDataObject的方法,数据通过调用IDataObject::GetData来请求,我们构造一个FORMATETC对象,它指定了我们想要访问的数据的类型,在这个例子中,标准的CF_TEXT数据缓冲区以HGLOBAL内存对象来存储。
数据返回到我们提供的STGMEDIUM结构体中,一旦我们锁定并显示数据,清理和调用标准的ReleaseStgMedium API来释放存储在STGMEDIUM结构中的数据就简单了。
注意,代码中仅仅当文本被选择到粘贴板的时候才工作,也就是说,如果没有 CF_TEXT 被存储到粘贴板,粘贴板的 IDataObject::GetData 程序调用会失败,我们什么也不打印。