该指南的目的在于读者能够在他们自己的程序实现完整的拖拽功能。自Window95以来,Drag和Drop已经成为Window程序的一个标准功能,随着COM和OLE成为主流技术,程序能和Window Shell甚至其他Window程序无缝交互。这个弹性是以高昂的代价为基础的,说的婉转点,写一个COM或OLE支持的程序完全是一个噩梦。
本指南目的在于帮助你轻松克服写一个OLE接口支持的拖拽程序的困难。通常,我们使用纯WIN32 API基础。然而,我会使用C++而不是C,因为C++是写COM接口程序的首选语言;我也会解释怎么样以简单的方式转换成C语言。
我有意以几个部分来写这个指南,主要的原因是太多的信息。另外,Drag-and-drop组件也使他们有各自不同的主题,因此我采用了这种方法。指南的第一部分(实际上就是该部分)简单介绍OLE 拖拽,后面的指南着重于拖拽;第2和3部分介绍OLE数据传输IDataObject接口。第4部分看一下IEnumFORMATETC接口,第5和6部分介绍drag源和drop目标。
推荐阅读
我强烈推荐你研究一下下面的信息,因为我是从那里学习COM、OLE拖拽的。
1. msdn.microsoft.com
每个win32相关的起始之处。
2. Inside OLE 2nd edition
该书中有许多有用的信息,被作为OLD的圣经。它有点老了,但包含每个你需要知道的东西。MSDN中包含了该书的一个软COPY,也许一直再那里;因特耐特上也有许多PDF和CHM的版本。
3. ftp://ftp.microsoft.com/softlib/msfiles
微软的FTP服务器包含几百个以前的资源,到目前为止我发现的最有用的东西是两个小文件:drgdrps.exe和drgdrpt.exe。他们是自解压的ZIP文件,包含了简单的drop源和drop目标程序的完整代码,为了可以简单的访问这些文件,你仅仅需要输入下面的命令:
ftp ftp.microsoft.com
username "ftp"
password "ftp"
1. cd softlib/mslfiles
bin
get drgdrps.exe
get drgdrpt.exe
bye
4. 微软技术论文-OLE for Idiots系列,What OLE is Really about等,这些论文虽然很老了,但他们在今天依然有用。在GOOGLE中可以轻松查询到。
OLE Drag和Drop
拖放是用来描述使用鼠标将数据从一个地方传输到另一个地方的短语。
每个拖放操作包含三个元素,当然这些元素是COM对象,需要支持拖放功能的程序都必须实现这三个元素。
1. IDropSource接口表示拖放操作的源。IDropSource包含产生可视化的方法,取消或完成拖放操作的方法。
2. IDropTarget接口用来表示拖放操作的目标对象。
3. IDataObject接口用来表示拖放操作过程中传输的数据。
注意,一个程序不需要支持所有的COM接口;如果你想定义一个drop目标,那么仅仅实现IDropTarget接口,同样,如果一个需要支持作为数据源的程序应该支持IDropSource和IDataObject接口。当然,程序也可以实现三个接口,从而在同一个程序中支持拖放操作。
上面的图描述了拖放操作中需要支持的关键组件;花点时间来理解一下图的内容。左边的方块是拖放操作的出发点,它已经创建了两个COM对象,每个暴露一个接口(IDataObject和IDropSource),OLE通过他们来执行拖放操作。
右边的方块表示拖放操作的目标,其创建一个COM对象(IDropTarget接口)。当鼠标被拖动过目标窗口时,OLE传递一个IDataObject接口到目标对象,这是源暴露给目标的数据对象。对象不能以任何方式得到一个副本,仅仅COM接口变为可用。当目标从数据对象提取数据时,OLE/COM运行时负责函数调用已经通过进程边界的数据传输。
上面的例子中,源和目标可以是同一个进程,也可以是不同的进程。在那里实现并不重要,因为OLE运行时(实际上是COM)负责数据对象在目标进程中激活。
开始拖放
任何程序想要使用OLE函数时首先需要做的是在启动的时候调用OleInitialize并且在结束的时候调用OleUninitialize。这么说不是很准确,最好说想要使用OLE的线程必须调用这些函数,因为COM和OLE必须在每个线程中被初始化和释放。
WINOLEAPI OleInitialize (LPVOID pvReserved);
WINOLEAPI OleUninitialize ();
非常核心的OLE拖放是一个API调用DoDragDrop,函数原型如下:
WINOLEAPI DoDragDrop(
IDataObject * pDataObject, // Pointer to the data object
IDropSource * pDropSource, // Pointer to the source
DWORD dwOKEffect, // Effects allowed by the source
DWORD * pdwEffect // Pointer to effects on the source
);
一个程序想初始化拖放操作,他必须首先调用这个函数,但在调用DoDragDrop之前,两个重要的步骤必须完成。
在调用DoDragDrop ,IDataObject和IDropSource对象被拖放操作的发起者创建。创建这两个对象并不是琐碎的事情,因此我们在下一部分介绍。另外我们这里一直没有提到创建任何GUI相关的对象(例如窗口),实际上一个drop源是独立于任何窗口的单独实体,即使拖放操作在窗口程序处理WM_MOUSEMOVE消息的时候初始化的。
当DoDragDrop被调用时,进入一个摸态的消息循环,用来监视鼠标和简单的消息。
接收Drag和Drop数据
一个程序想作为拖放操作的接收方,它必须调用RegisterDragDrop函数,当然,这个程序也必须调用与源程序一样调用OleInitialize/OleUninitialize函数。
WINOLEAPI RegisterDragDrop(
HWND hwnd, // Handle to a window that can accept drops
IDropTarget * pDropTarget // Pointer to object that is to be target of drop
);
看一下上面函数原型显示了最后一个拖放操作组件-IDropTarget COM接口。RegisterDragDrop同时要求一个窗口的句柄。该窗口被OLE运行时注册,因此,当鼠标拖过该窗口时,OLE能调用IDropTarget接口的方法来通知拥有该窗口的程序正在进行一个拖放操作。
当这个窗口被销毁时,应该调用RevokeDragDrop API:
WINOLEAPI RevokeDragDrop(
HWND hwnd // Handle to a window that can accept drops
);
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 程序调用会失败,我们什么也不打印。