OLE数据传输(OLE Drag和Drop随笔)

该指南的目的在于读者能够在他们自己的程序实现完整的拖拽功能。自Window95以来,DragDrop已经成为Window程序的一个标准功能,随着COMOLE成为主流技术,程序能和Window Shell甚至其他Window程序无缝交互。这个弹性是以高昂的代价为基础的,说的婉转点,写一个COMOLE支持的程序完全是一个噩梦。

本指南目的在于帮助你轻松克服写一个OLE接口支持的拖拽程序的困难。通常,我们使用纯WIN32 API基础。然而,我会使用C++而不是C,因为C++是写COM接口程序的首选语言;我也会解释怎么样以简单的方式转换成C语言。

我有意以几个部分来写这个指南,主要的原因是太多的信息。另外,Drag-and-drop组件也使他们有各自不同的主题,因此我采用了这种方法。指南的第一部分(实际上就是该部分)简单介绍OLE 拖拽,后面的指南着重于拖拽;第23部分介绍OLE数据传输IDataObject接口。第4部分看一下IEnumFORMATETC接口,第56部分介绍drag源和drop目标。

推荐阅读

我强烈推荐你研究一下下面的信息,因为我是从那里学习COMOLE拖拽的。

1.         msdn.microsoft.com

每个win32相关的起始之处。

2.         Inside OLE 2nd edition

该书中有许多有用的信息,被作为OLD的圣经。它有点老了,但包含每个你需要知道的东西。MSDN中包含了该书的一个软COPY,也许一直再那里;因特耐特上也有许多PDFCHM的版本。

3.         ftp://ftp.microsoft.com/softlib/msfiles

微软的FTP服务器包含几百个以前的资源,到目前为止我发现的最有用的东西是两个小文件:drgdrps.exedrgdrpt.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 DragDrop

拖放是用来描述使用鼠标将数据从一个地方传输到另一个地方的短语。

每个拖放操作包含三个元素,当然这些元素是COM对象,需要支持拖放功能的程序都必须实现这三个元素。

1.         IDropSource接口表示拖放操作的源。IDropSource包含产生可视化的方法,取消或完成拖放操作的方法。

2.         IDropTarget接口用来表示拖放操作的目标对象。

3.         IDataObject接口用来表示拖放操作过程中传输的数据。

注意,一个程序不需要支持所有的COM接口;如果你想定义一个drop目标,那么仅仅实现IDropTarget接口,同样,如果一个需要支持作为数据源的程序应该支持IDropSourceIDataObject接口。当然,程序也可以实现三个接口,从而在同一个程序中支持拖放操作。

dragdrop04.gif


上面的图描述了拖放操作中需要支持的关键组件;花点时间来理解一下图的内容。左边的方块是拖放操作的出发点,它已经创建了两个COM对象,每个暴露一个接口(IDataObjectIDropSource),OLE通过他们来执行拖放操作。

右边的方块表示拖放操作的目标,其创建一个COM对象(IDropTarget接口)。当鼠标被拖动过目标窗口时,OLE传递一个IDataObject接口到目标对象,这是源暴露给目标的数据对象。对象不能以任何方式得到一个副本,仅仅COM接口变为可用。当目标从数据对象提取数据时,OLE/COM运行时负责函数调用已经通过进程边界的数据传输。

上面的例子中,源和目标可以是同一个进程,也可以是不同的进程。在那里实现并不重要,因为OLE运行时(实际上是COM)负责数据对象在目标进程中激活。

开始拖放

任何程序想要使用OLE函数时首先需要做的是在启动的时候调用OleInitialize并且在结束的时候调用OleUninitialize。这么说不是很准确,最好说想要使用OLE的线程必须调用这些函数,因为COMOLE必须在每个线程中被初始化和释放。

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 IDataObjectIDropSource对象被拖放操作的发起者创建。创建这两个对象并不是琐碎的事情,因此我们在下一部分介绍。另外我们这里一直没有提到创建任何GUI相关的对象(例如窗口),实际上一个drop源是独立于任何窗口的单独实体,即使拖放操作在窗口程序处理WM_MOUSEMOVE消息的时候初始化的。

DoDragDrop被调用时,进入一个摸态的消息循环,用来监视鼠标和简单的消息。

接收DragDrop数据

一个程序想作为拖放操作的接收方,它必须调用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
   );
这个 APIOLE 来反初始化指定的窗口,并且释放注册时使用的 IDropTarget 接口和进程中的 DropTarget 对象。

OLE数据传输的核心是IDataObject COM接口,一个IDataObject提供从一个程序到另一个程序传输和访问数据的方法。最通用的OLE数据传输是窗口粘贴板,当然也有拖放。IDataObject是一到多个数据的有效的COM包装。

在我们调查IDataObject任何细节之前,两个重要的数据结构你必须熟悉:FORMATETCSTGMEDIUM接口,他们用来描述和存储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_TEXTCF_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),IStreamTYPED_ISTREAM)等等。相应的联合中的元素是数据的句柄。

2.         hBitmap/hGlobal等:实际的数据,仅仅他们中的一个是有效的,这依赖于tymed的值。

3.         pUnkForRelease:一个可选的指针,指向IUnknown接口,数据的接收方应该调用其Release方法。当这个字段是NULL时,接收方有责任释放内存句柄。ReleaseStgMedium API调用在这里非常有用,它负责释放STGMEDIUMS的数据内容,因此实际上我们不需要做什么。

STGMEDIUM结构是传统的windows HGLOBAL内存句柄的扩展,同时支持HGLOBAL(且一直是最常用的),同时支持许多其他的类型,最有用的是IStreamIStorage通用COM接口。

总之,结构体FORMATETCSTGMEDIUM一起用来描述和存储OLE数据实体的。FORMATETC通常用来从IDataObject请求指定类型的数据,同时STGMEDIUM结构用来接收和保存请求的数据。

传输OLE数据

IDataObject接口提供了从一个程序到另一个程序传递数据的方法,IDataObject在两个情况下非常有用:粘贴板和拖放。如果设计精细,可以用一个COM对象来同时实现粘贴板和拖放操作。

下面的表列出了IDataObject成员函数,按照他们在接口虚表中出现的顺序。为了简化的原因,IUnknown方法(AddRefReleaseQueryInterface)没有列出。

IDataObject方法

描述

GetData

RenderFORMATETC结构体中描述的数据,并通过STGMEDIUM结构体来传递数据

GetDataHere

RenderFORMATETC结构体中的数据,并通过调用者分配的STGMEDIUM结构体传输数据。

QueryGetData

判断数据对象是否可以renderFORMATETC结构中描述的数据

GetCanonicalFormatEtc

提供一个潜在不同的但逻辑上相同的FORMATETC结构体。

SetData

提供一个通过FORMATECT结构和STGMEDIUM结构描述的源数据对象。

EnumFormatEtc

创建并返回一个IEnumFORMATETC接口的指针来枚举数据对象支持的FORMATETC对象。

DAdvise

创建一个在数据对象和通知接收器之间的连接,因此通知接收器能接收到数据对象中通知的改变。

DUnadvise

销毁一个前面使用DAdvise方法安装的通知

EnumDAdvise

创建和返回一个指向枚举当前通知连接的接口指针。

这个表看起来很漂亮,我们也看到EnumFormatEtc方法并发现我们不得不同时实现IEnumFORMATETC接口,它有13个成员函数,不包括IUnknown方法,到这里我们还没有考虑IDropSourceIDropTarget

庆幸的是,为了简化OLE拖放,我们仅仅需要实现GetDataQueryGetDataEnumFormatEtc,因此这节省了我们许多工作。

使用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 程序调用会失败,我们什么也不打印。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值