第一部分:OLE Drag&Drop 介绍
1.基本概念
拖放,是指用鼠标拖动的方法,在不同程序的窗口之间、同一程序的不同窗口之间或同一程序同一窗体的不同控件之间,进行移动、复制、粘贴数据等操作的技术。拖放操作是依靠操作系统来完成的,被拖动的对象首先向操作系统注册一种它所使用数据的格式,并且按照这种格式来提供数据,拖放结束时,目标窗口提取出数据,并根据提取的数据生成相应的对象。
拖放方式有两种,一种是OLE拖放(这个比较复杂)和文件管理器拖放。它们利用了不同的机制。
2.文件管理器拖放
只能处理文件名,比如,你可以在资源管理器中选择一个或多个文件,拖到应用程序上面(这个程序的dwExStyle要加上WS_EX_ACCEPTFILES),那么这个窗体就会收到一个WM_DROPFILES消息。它主要用到下面几个API函数:DragQueryFile、DragQueryPoint、DragFinish。函数原型如下:
a)UINT DragQueryFile(HDROP hDrop, UINT iFile, LPTSTR lpszFile, UINT cch)
b)BOOL DragQueryPoint(HDROP hDrop, LPPOINT lppt)
c)void DragFinish(HDROP hDrop)
一般用法如下:
void CListCtrlEx::OnDropFiles(HDROP hDrop)
{
char szFilePathName[_MAX_PATH+1] = {0};
UINT nNumOfFiles = DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0); //得到文件个数
for (UINT nIndex=0 ; nIndex< nFileCount; ++nIndex)
{
DragQueryFile(hDrop, nIndex, szFilePathName, _MAX_PATH); //得到文件名
}
DragFinish(hDrop);
}
由于这里不重点讲这种方式的拖放,所以具体的用法讲参考MSDN,上面都有详细说明。
3.OLE拖放原理
拖放是用来描述用鼠标装数据从一个地方传输到另一个地方,OLE拖放比文件管理拖放更加通用得多,能在不同的应用程序中交互任务类型的数据(先要向系统注册该类型)。每一个拖放操作都须包含三个元素,当然,这些都是COM对象,需要我们去实现这些接口。
1)IDropSource,表示拖放操作的源,它包含了拖放目标要使用的数据(IDataObject),它能在拖放的过程生成一些可视化的反馈,如设置鼠标样式等。
2)IDropTarget,表示拖放操作的目标,它决定拖放的效果、接收任何合法数据、给拖放源一些反馈等。
3)IDataObject,表示拖放源与目标之间传输的数据。
注意了,一个应用程序不需要支持所有的COM接口。如果你的程序想做为一个拖放目标,那么你就实现IDropTarget接口,同样的,如果你需要支持作为数据源的程序,那么就应该实现IDropSource和IDataObject接口。当然,程序也可以同时实现这三个接口,从而可以在同一程序中支持拖放操作。
上面的图描述了拖放操作中所需要的关键组件。左边的表示拖放操作的源,它已经包含了两个对象,一个是IDropSource,另一个是IDataObject,最终是通过API函数DoDragDrop来发现拖放操作。
右边描述了拖放操作的目标,它需要实现IDropTarget接口。这个目标中,它能接收IDataObject对象。当鼠标拖动到目标窗体时,OLE传递一个IDataObject接口到目标对象上面,这是源传给目标的数据,它不能以任何方式得到一个副本。这目标这边它可以根据IDataObject得到该数据格式,从而判断这种格式是不是目标能识别的,如果不能识别,就不接收。它本质上需要调用RegisterDragDrop来把当前目标窗体注册成一个拖放目标。
注意,上面的图所示,源和目标可以是同一进程,也可以是不同进程。在使用之前,需要调用COM和OLE的初始化,使用完后,再调用其反初始化。
WINOLEAPI OleInitialize(LPVOID pvReserved);
WINOLEAPI OleUninitialize();
4.开始拖放
上面也提到了,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
);
在调用这个方法时,就要把你实现的源(IDragSource)和数据(IDataObject)传给他,至于如何创建源和数据的对象,在后面介绍。
当调用DoDragDrop时,它会进入一个模态的消息循环,用来监视鼠标和简单的消息,它是会阻塞住的。
5.注册目标
要想一个窗体成为一个拖放的接收方,它必须调用RegisterDragDrop函数来注册。其原型如下:
WINOLEAPI RegisterDragDrop(
HWND hwnd, // Handle to a window that can accept drops
IDropTarget * pDropTarget // Pointer to object that is to be target of drop
);
它的第一个参数就是目标窗体的句柄。
与它作用相反的函数是RevokeDragDrop函数,释放注册时用到的IDropTarget接口对象。这个函数在窗体销毁时应该调用。
WINOLEAPI RevokeDragDrop(
HWND hwnd // Handle to a window that can accept drops
);
第二部分:OLE 数据传送(Data Transfer)
这一部分讲一讲OLE数据传输相关知识,多数来自于MSDN和网上其同志们的观点,加以总结而成。
COM接口提供了一种用于在不同的应用程序中交换数据的机制,这就是我们要讲的数据对象,对应的COM接口就是IDataObject。
大多数平台,包括Windows,都定义了一个用于在应用程序之间传输数据的标准协议,基于一系列的剪切板的函数。应用程序使用这些函数可以共享数据,即使它们的数据格式相差甚远,一般说来,剪切板有两个缺点:
1,不能指定目标设备,不灵活。
2,如果传输的数据很大,就有可能用到虚拟内存,效率不高。
1.OLE数据描述
一般情况下,数据传输会用到两种方式,一种是剪切板,另一种是DragDrop。不管用哪种方式,都要用到IDataObject这个COM接口,在关注IDataObject之前,我们必须要了解数据传输所要用到一些辅助结构,FORMATETC和STGMEDIUM,它们用来描述OLE数据的格式和存储等信息。
FORMATETC结构体,用来表示IDataObject提供或接收的数据类型,是标准Windows粘贴格式(CF_TEXT)的扩展,它包含了数据格式外,还有一些其他信息,看看它的定义:
typedef struct tagFORMATETC
{
CLIPFORMAT cfFormat; // 数据格式,如CF_TEXT
DVTARGETDEVICE *ptd; // 一般为NULL,目标设备
DWORD dwAspect; // DV_CONTENT rendering详细信息
LONG lindex; // 一般为-1
DWORD tymed; // 用于数据传输的存储媒体(HGLOBAL,IStream)
} FORMATETC, *LPFORMATETC;
FORMATETC结构的成员描述如下:
cfFormat:数据格式,可以是系统定义的(CF_TEXT、CF_BITMAP等),也可以是用RegisterClipboardFormat注册的自定义格式。
ptd:一般为NULL,提供已经rendered数据的设备信息,正常的粘贴板操作和拖放都是NULL。
dwAspect:描述数据的细节信息,有DV_CONTENT、DVASPECT_THUMBNAIL等。
lindex:最常用的值是-1。
tymed:描述用于存储数据的存储媒体,TYMED_XXX等值。
具体的描述,大家可以参考MSDN,它上面讲得比较详细。
2.OLE数据存储
结构体STGMEDIUM(STORAGE MEDIUM的缩写)提供一个用来存储数据的容器,因此叫存储媒体:
typedef struct
{
DWORD tymed; // TYEMD_HGLOBAL、TYPED_ISTREAM等。
union
{
HBITMAP hBitmap;
HMETAFILEPICT hMetaFilePict;
HENHMETAFILE hEnhMetaFile;
HGLOBAL hGlobal;
LPWSTR lpszFileName;
IStream *pstm;
IStorage *pstg;
};
IUnknown *pUnkForRelease;
} STGMEDIUM;
3.IDataObject成员
下面看一看IDataObject接口的成员方法:
GetData:Render在FORMATETC结构体中描述的数据,并通过STGMEDIUM结构体来传递数据。
GetDataHere:同上一方法相似,只是STGMEDIUM结构的内在是由调用者分配的。
QueryGetData:决定数据对象是否能够render在FORMATETC结构中描述的数据。
GetCanonicalFormatEtc:提供一下潜在不同的但逻辑上相同的FORMATETC结构。
SetData:提供一个用FORMATETC结构和STGMEDIUM结构描述的数据源对象。
EnumFormatEtc:创建并返回一个IEnumFORMATETC接口的指针来枚举数据对象支持的FORMATETC。
DAdvise:创建一个在数据对象和通知接收器之间的连接,因此通知接收器收到数据对象中通知的改变。
DUnadvise:销毁一个前面使用DAdvise方法安装的通知。
EnumDAdvise:创建和返回一个指向枚举当前通知的接口指针。
上面的接口,不用每个方法都要实现,只实现几个重要的,如GetData、SetData等。
4.用IDataObject来访问剪切板(Clipboard)
先看看简单的访问剪切板的代码,很了一下IDataObject如何使用。
代码如下:
void TestGetDataFromClipboard()
{
OleInitialize(NULL);
IDataObject *pDataObject = NULL;
HRESULT hr = OleGetClipboard(&pDataObject);
if (SUCCEEDED(hr))
{
FORMATETC fmtetc = { CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM stgmed;
hr = pDataObject->GetData(&fmtetc, &stgmed);
if (SUCCEEDED(hr))
{
PVOID pData = GlobalLock(stgmed.hGlobal);
CHAR *pText = (CHAR*)pData;
MessageBoxA(NULL, pText, "GetDataFromClipboard", 0);
GlobalUnlock(stgmed.hGlobal);
ReleaseStgMedium(&stgmed);
}
SAFE_RELEASE(pDataObject);
}
OleUninitialize();
}
上面的方法演示了如何从剪切板里面取得字符串数据,最本质的方法还是通过IDataObject::GetData方法来取得数据。取数据的类型在FORMATETC结构体里面指定。
注意,对于CF_TEXT,根据MSDN上面的解释,字符串必须是ANSI,所以要把通过GlobalLock得到的指针(PVOID)转换成CHAR*,最后调用ReleaseStgMedium来释放程序分配的内存。
好了,到这里,应该明白数据传输格式及数据存储是怎么回了,这里只是一个大概,还有很多细节需要仔细阅读MSDN才行。