OLE Drag&Drop 介绍

第一部分:OLE Drag&Drop 介绍

一、基本概念


       拖放,是指用鼠标拖动的方法,在不同程序的窗口之间、同一程序的不同窗口之间或同一程序同一窗体的不同控件之间,进行移动、复制、粘贴数据等操作的技术。拖放操作是依靠操作系统来完成的,被拖动的对象首先向操作系统注册一种它所使用数据的格式,并且按照这种格式来提供数据,拖放结束时,目标窗口提取出数据,并根据提取的数据生成相应的对象。

       拖放方式有两种,一种是OLE拖放(这个比较复杂)和文件管理器拖放。它们利用了不同的机制。

        二、文件管理器拖放

       只能处理文件名,比如,你可以在资源管理器中选择一个或多个文件,拖到应用程序上面(这个程序的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,上面都有详细说明。

        三、OLE拖放原理

       拖放是用来描述用鼠标装数据从一个地方传输到另一个地方,OLE拖放比文件管理拖放更加通用得多,能在不同的应用程序中交互任务类型的数据(先要向系统注册该类型)。每一个拖放操作都须包含三个元素,当然,这些都是COM对象,需要我们去实现这些接口。

       1)IDropSource,表示拖放操作的源,它包含了拖放目标要使用的数据(IDataObject),它能在拖放的过程生成一些可视化的反馈,如设置鼠标样式等。
       2)IDropTarget,表示拖放操作的目标,它决定拖放的效果、接收任何合法数据、给拖放源一些反馈等。
       3)IDataObject,表示拖放源与目标之间传输的数据。

       注意了,一个应用程序不需要支持所有的COM接口。如果你的程序想做为一个拖放目标,那么你就实现IDropTarget接口,同样的,如果你需要支持作为数据源的程序,那么就应该实现IDropSource和IDataObject接口。当然,程序也可以同时实现这三个接口,从而可以在同一程序中支持拖放操作。

第一部分:OLE <wbr>Drag&Drop <wbr>介绍

       上面的图描述了拖放操作中所需要的关键组件。左边的表示拖放操作的源,它已经包含了两个对象,一个是IDropSource,另一个是IDataObject,最终是通过API函数DoDragDrop来发现拖放操作。

       右边描述了拖放操作的目标,它需要实现IDropTarget接口。这个目标中,它能接收IDataObject对象。当鼠标拖动到目标窗体时,OLE传递一个IDataObject接口到目标对象上面,这是源传给目标的数据,它不能以任何方式得到一个副本。这目标这边它可以根据IDataObject得到该数据格式,从而判断这种格式是不是目标能识别的,如果不能识别,就不接收。它本质上需要调用RegisterDragDrop来把当前目标窗体注册成一个拖放目标。

       注意,上面的图所示,源和目标可以是同一进程,也可以是不同进程。在使用之前,需要调用COM和OLE的初始化,使用完后,再调用其反初始化。

       WINOLEAPI OleInitialize(LPVOID pvReserved);
       WINOLEAPI OleUninitialize();

       四、开始拖放

       上面也提到了,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时,它会进入一个模态的消息循环,用来监视鼠标和简单的消息,它是会阻塞住的。

        五、注册目标

       要想一个窗体成为一个拖放的接收方,它必须调用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,如果传输的数据很大,就有可能用到虚拟内存,效率不高。

    一、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,它上面讲得比较详细。

    二、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;

        三、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等。
     
        四、用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才行。

第三部分:IDataObject实现

第二部分我们介绍了OLE数据传输的相关知识,这一节主要讲怎么实现一个IDataObject接口。然后再给出一个例子。

首先我们要明白,IDataObject是一个COM接口,我们就必须得创建一个类,实现这个接口的每一个方法,包括它的基类的方法。

SdkDataObject .h 头文件如下: 

#ifdef __cplusplus
#ifndef _SDKDATAOBJECT_H_
#define _SDKDATAOBJECT_H_

#include "SdkCommon.h"
#include "SdkDropSource.h"


typedef struct _DATASTORAGE
{
    FORMATETC *m_formatEtc;
    STGMEDIUM *m_stgMedium;

} DATASTORAGE_t, *LPDATASTORAGE_t;

class CLASS_DECLSPEC SdkDataObject : public IDataObject
{
public:

    SdkDataObject(SdkDropSource *pDropSource = NULL);
    BOOL IsDataAvailable(CLIPFORMAT cfFormat);
    BOOL GetGlobalData(CLIPFORMAT cfFormat, void **ppData);
    BOOL GetGlobalDataArray(CLIPFORMAT cfFormat, HGLOBAL *pDataArray, DWORD dwCount);
    BOOL SetGlobalData(CLIPFORMAT cfFormat, void *pData, BOOL fRelease = TRUE);
    BOOL SetGlobalDataArray(CLIPFORMAT cfFormat, HGLOBAL *pDataArray, DWORD dwCount, BOOL fRelease = TRUE);
    BOOL SetDropTip(DROPIMAGETYPE type, PCWSTR pszMsg, PCWSTR pszInsert);
 
    // The com interface.
    IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv);
    IFACEMETHODIMP_(ULONG) AddRef();
    IFACEMETHODIMP_(ULONG) Release();
    IFACEMETHODIMP GetData(FORMATETC *pformatetcIn, STGMEDIUM *pmedium);
    IFACEMETHODIMP SetData(FORMATETC *pformatetc, STGMEDIUM *pmedium, BOOL fRelease);
    IFACEMETHODIMP GetDataHere(FORMATETC *pformatetc , STGMEDIUM *pmedium );
    IFACEMETHODIMP QueryGetData(FORMATETC *pformatetc);
    IFACEMETHODIMP GetCanonicalFormatEtc(FORMATETC *pformatetcIn, FORMATETC *pformatetcOut);
    IFACEMETHODIMP EnumFormatEtc(DWORD dwDirection, IEnumFORMATETC **ppenumFormatEtc);
    IFACEMETHODIMP DAdvise(FORMATETC *pformatetc , DWORD advf , IAdviseSink *pAdvSnk , DWORD *pdwConnection);
    IFACEMETHODIMP DUnadvise(DWORD dwConnection);
    IFACEMETHODIMP EnumDAdvise(IEnumSTATDATA **ppenumAdvise);

private:

    ~SdkDataObject(void);
    SdkDataObject(const SdkDataObject&);
    SdkDataObject& operator = (const SdkDataObject&);
    HRESULT CopyMedium(STGMEDIUM* pMedDest, STGMEDIUM* pMedSrc, FORMATETC* pFmtSrc);
    HRESULT SetBlob(CLIPFORMAT cf, const void *pvBlob, UINT cbBlob);

private:

    volatile LONG           m_lRefCount;        //!< The reference of count
    SdkDropSource          *m_pDropSource;      //!< The pointer to CDropSource object
    vector<DATASTORAGE_t>   m_dataStorageCL;    //!< The collection of DATASTORAGE_t structure
};

#endif // _SDKDATAOBJECT_H_
#endif // __cplusplus

 

上面SdkDataObject.h定义了类SdkDataObject类,它实现了IDataObject接口,包括IUnknown接口。

几点说明如下:

1、SdkDataObject类里面也声明了拷贝构造、赋值操作符等,而且是私有的,就是不想让这个对象可以复制

2、IsDataAvailable:判断指定的格式是否支持。

3、GetGlobalData:得到全局的数据。

4、SetGlobalData:设置全局的数据。

5、CopyMedium:复制媒体数据。

6、上面列出的这些函数,几乎都是辅助函数,我设计时是根据我自己的业务需求来设计的,不同的需求可能不同,但最本质的都是实现一些COM接口。同时,我还定义了一个结构体DATASTORAGE_t,用来保存数据格式对,把设置的数据格式对存在一个vector中。

7、成员变量volatile LONG m_lRefCount,表示当前类的引用计数,构造函数一定要初始化为1,不能是0,其中volatile的意思就是说,告诉编译器不要其优化,每次要用访问这个值时,都是到它的地址上去取,而不是从寄存器中读取。


==================================================================================================

 

SdkDataObject.cpp的实现

构造函数,很简单,就是进行一些初始化操作,注意引用计数一定要是1,而不是0。

SdkDataObject::SdkDataObject(SdkDropSource *pDropSource)
{
    m_pDropSource = pDropSource;
    m_lRefCount = 1;
}

 

析构函数,负责释放内存,这个函数是私有的,调用者只能调用Release来释放它。 

SdkDataObject::~SdkDataObject(void)
{
    m_lRefCount = 0;

    int nSize = (int)m_dataStorageCL.size();
    for (int i = 0; i < nSize; ++i)
    {
        DATASTORAGE_t dataEntry = m_dataStorageCL.at(i);
        ReleaseStgMedium(dataEntry.m_stgMedium);
        SAFE_DELETE(dataEntry.m_stgMedium);
        SAFE_DELETE(dataEntry.m_formatEtc);
    }
}

 

QueryInterface内部实现最终也是调用API来实现。

STDMETHODIMP SdkDataObject::QueryInterface(REFIID riid, void **ppv)
{
    static const QITAB qit[] =
    {
        QITABENT(SdkDataObject, IDataObject),
        { 0 },
    };
    return QISearch(this, qit, riid, ppv);
}

 

AddRef和Release实现方法就相对简单了,几乎所有的COM接口实现都一样。

STDMETHODIMP_(ULONG) SdkDataObject::AddRef()
{
    return InterlockedIncrement(&m_lRefCount);
}

STDMETHODIMP_(ULONG) SdkDataObject::Release()
{
    ULONG lRef = InterlockedDecrement(&m_lRefCount);
    if (0 == lRef)
    {
        delete this;
    }
    return m_lRefCount;
}

 

GetData和SetData是相当重要的方法,就是向你写的Data Object要数据和传数据。内部必须把这些数据存起来。同时,这两个方法还依赖CopyMedium函数,这个用来复制数据。这个方法的实现后面会说明。GetDataHere这里没有实现,直接返回E_NOTIMPL。 

STDMETHODIMP SdkDataObject::GetData(FORMATETC *pformatetcIn, STGMEDIUM *pmedium)
{
    if ( (NULL == pformatetcIn) || (NULL == pmedium) )
    {
        return E_INVALIDARG;
    }

    pmedium->hGlobal = NULL;

    int nSize = (int)m_dataStorageCL.size();
    for (int i = 0; i < nSize; ++i)
    {
        DATASTORAGE_t dataEntry = m_dataStorageCL.at(i);
        if( (pformatetcIn->tymed & dataEntry.m_formatEtc->tymed) &&
            (pformatetcIn->dwAspect == dataEntry.m_formatEtc->dwAspect) &&
            (pformatetcIn->cfFormat == dataEntry.m_formatEtc->cfFormat) )
        {
            return CopyMedium(pmedium, dataEntry.m_stgMedium, dataEntry.m_formatEtc);
        }
    }

    return DV_E_FORMATETC;
}

STDMETHODIMP SdkDataObject::SetData(FORMATETC *pformatetc, STGMEDIUM *pmedium, BOOL fRelease)
{
    if ( (NULL == pformatetc) || (NULL == pmedium) )
    {
        return E_INVALIDARG;
    }

    if ( pformatetc->tymed != pmedium->tymed )
    {
        return E_FAIL;
    }

    FORMATETC* fetc = new FORMATETC;
    STGMEDIUM* pStgMed = new STGMEDIUM;
    ZeroMemory(fetc, sizeof(FORMATETC));
    ZeroMemory(pStgMed, sizeof(STGMEDIUM));

    *fetc = *pformatetc;

    if ( TRUE == fRelease )
    {
        *pStgMed = *pmedium;
    }
    else
    {
        CopyMedium(pStgMed, pmedium, pformatetc);
    }

    DATASTORAGE_t dataEntry = { fetc, pStgMed };
    m_dataStorageCL.push_back(dataEntry);

    return S_OK;
}

STDMETHODIMP SdkDataObject::GetDataHere(FORMATETC *pformatetc , STGMEDIUM *pmedium)
{
    UNREFERENCED_PARAMETER(pformatetc);
    UNREFERENCED_PARAMETER(pmedium);
    return E_NOTIMPL;
}

 

QueryGetData函数,用来查询指定的数据是否支持,这个方法跟自己提供的IsDataAvailable功能相似,只是接口复杂一点,IsDataAvailable内部也是调用QueryGetData来实现的。

STDMETHODIMP SdkDataObject::QueryGetData(FORMATETC *pformatetc)
{
    if ( NULL == pformatetc )
    {
        return E_INVALIDARG;
    }
    if ( !(DVASPECT_CONTENT & pformatetc->dwAspect) )
    {
        return DV_E_DVASPECT;
    }
    HRESULT hr = DV_E_TYMED;
    int nSize = m_dataStorageCL.size();
    for (int i = 0; i < nSize; ++i)
    {
        DATASTORAGE_t dataEnrty = m_dataStorageCL.at(i);
        if ( dataEnrty.m_formatEtc->tymed & pformatetc->tymed )
        {
            if ( dataEnrty.m_formatEtc->cfFormat == pformatetc->cfFormat )
            {
                return S_OK;
            }
            else
            {
                hr = DV_E_CLIPFORMAT;
            }
        }
        else
        {
            hr = DV_E_TYMED;
        }
    }
    return hr;
}

 

关于EnumFormatEtc函数,一般我是调用Shell API来实现,这个方法很重要,用来说明当前这个Data Object支持什么格式。目前这里面只支持CF_UNICODETEXT。

STDMETHODIMP SdkDataObject::EnumFormatEtc(DWORD dwDirection, IEnumFORMATETC **ppenumFormatEtc)
{
    if ( NULL == ppenumFormatEtc )
    {
        return E_INVALIDARG;
    }
    *ppenumFormatEtc = NULL;
    HRESULT hr = E_NOTIMPL;
    if ( DATADIR_GET == dwDirection )
    {
        FORMATETC rgfmtetc[] =
        {
            { CF_UNICODETEXT, NULL, DVASPECT_CONTENT, 0, TYMED_HGLOBAL },
        };
        hr = SHCreateStdEnumFmtEtc(ARRAYSIZE(rgfmtetc), rgfmtetc, ppenumFormatEtc);
    }
    return hr;
}

 

IDataObject::DAdvise、IDataObject::EnumDAdvise和IDataObject::DUnadivise函数简单的返回OLE_E_ADVISENOTSUPPORTED。

STDMETHODIMP SdkDataObject::DAdvise(FORMATETC *pformatetc , DWORD advf , IAdviseSink *pAdvSnk , DWORD *pdwConnection)
{
    UNREFERENCED_PARAMETER(pformatetc);
    UNREFERENCED_PARAMETER(advf);
    UNREFERENCED_PARAMETER(pAdvSnk);
    UNREFERENCED_PARAMETER(pdwConnection);
    return E_NOTIMPL;
}

STDMETHODIMP SdkDataObject::DUnadvise(DWORD dwConnection)
{
    UNREFERENCED_PARAMETER(dwConnection);
    return E_NOTIMPL;
}
 
STDMETHODIMP SdkDataObject::EnumDAdvise(IEnumSTATDATA **ppenumAdvise)
{
    UNREFERENCED_PARAMETER(ppenumAdvise);
    return E_NOTIMPL;
}

 

CopyMedium实现如下:

HRESULT SdkDataObject::CopyMedium(STGMEDIUM* pMedDest, STGMEDIUM* pMedSrc, FORMATETC* pFmtSrc)
{
    if ( (NULL == pMedDest) || (NULL ==pMedSrc) || (NULL == pFmtSrc) )
    {
        return E_INVALIDARG;
    }
    switch(pMedSrc->tymed)
    {
    case TYMED_HGLOBAL:
        pMedDest->hGlobal = (HGLOBAL)OleDuplicateData(pMedSrc->hGlobal, pFmtSrc->cfFormat, NULL);
        break;
    case TYMED_GDI:
        pMedDest->hBitmap = (HBITMAP)OleDuplicateData(pMedSrc->hBitmap, pFmtSrc->cfFormat, NULL);
        break;
    case TYMED_MFPICT:
        pMedDest->hMetaFilePict = (HMETAFILEPICT)OleDuplicateData(pMedSrc->hMetaFilePict, pFmtSrc->cfFormat, NULL);
        break;
    case TYMED_ENHMF:
        pMedDest->hEnhMetaFile = (HENHMETAFILE)OleDuplicateData(pMedSrc->hEnhMetaFile, pFmtSrc->cfFormat, NULL);
        break;
    case TYMED_FILE:
        pMedSrc->lpszFileName = (LPOLESTR)OleDuplicateData(pMedSrc->lpszFileName, pFmtSrc->cfFormat, NULL);
        break;
    case TYMED_ISTREAM:
        pMedDest->pstm = pMedSrc->pstm;
        pMedSrc->pstm->AddRef();
        break;
    case TYMED_ISTORAGE:
        pMedDest->pstg = pMedSrc->pstg;
        pMedSrc->pstg->AddRef();
        break;
    case TYMED_NULL:
    default:
        break;
    }
    pMedDest->tymed = pMedSrc->tymed;
    pMedDest->pUnkForRelease = NULL;
    if(pMedSrc->pUnkForRelease != NULL)
    {
        pMedDest->pUnkForRelease = pMedSrc->pUnkForRelease;
        pMedSrc->pUnkForRelease->AddRef();
    }
    return S_OK;
}

HRESULT SdkDataObject::SetBlob(CLIPFORMAT cf, const void *pvBlob, UINT cbBlob)
{
    void *pv = GlobalAlloc(GPTR, cbBlob);
    HRESULT hr = pv ? S_OK : E_OUTOFMEMORY;
    if ( SUCCEEDED(hr) )
    {
        CopyMemory(pv, pvBlob, cbBlob);
        FORMATETC fmte = {cf, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
        // The STGMEDIUM structure is used to define how to handle a global memory transfer.
        // This structure includes a flag, tymed, which indicates the medium
        // to be used, and a union comprising pointers and a handle for getting whichever
        // medium is specified in tymed.
        STGMEDIUM medium = {};
        medium.tymed = TYMED_HGLOBAL;
        medium.hGlobal = pv;
        hr = this->SetData(&fmte, &medium, TRUE);
        if (FAILED(hr))
        {
            GlobalFree(pv);
        }
    }
    return hr;
}

==================================================================================================

 

下面给出了利用这个Data object 住剪切板复制一些数据。

这里调用了SetGlobalData函数来设置数据,上面没有给出这个实现,现在记住就行了,它内部是调用SetData来实现。设置的数据格式是CF_UNICODETEXT,因为目前这个DataObject只支持CF_UNICODETEXT格式,这个从EnumFormatEtc函数的实现就可以看出。你可以写一个控制台程序,加如下面两个方法,运行后,在记事本里按Ctrl + V,看看是不是把Hello World.粘贴了。

HGLOBAL CreateGlobalHandle(IN void* ptr, int size)
{
    HGLOBAL hGlobal = NULL;
    hGlobal = GlobalAlloc(GMEM_FIXED, size);
    if (NULL != hGlobal)
    {
        LPVOID pdest = GlobalLock(hGlobal);
        if (NULL != pdest)
        {
            memcpy_s(pdest, size, ptr, size);
        }
        GlobalUnlock(hGlobal);
    }
    return hGlobal;
}
 
void TestSdkDataObject()
{
    OleInitialize(NULL);
    SdkDataObject *pDataObject = new SdkDataObject(NULL);
    WCHAR *pText = L"Hello world.";
    HGLOBAL hGlobal = CreateGlobalHandle((void*)pText, sizeof(WCHAR) * (wcslen(pText) + 1));
    pDataObject->SetGlobalData(CF_UNICODETEXT, hGlobal, FALSE);
    HRESULT hr = OleSetClipboard(pDataObject);
    hr = OleFlushClipboard();
    SAFE_RELEASE(pDataObject);
    OleUninitialize();
}

 

这一节,我们给出了SdkDataObject的实现,有些实现还是很简单,关键是要明白它的本质。

第四部分:IDropSource实现

 

上一节,我们讲了如何实现一个自己的IDataObject接口,在开始这一部分之前,我还想再说一下,IDataObject有一个接口------ EnumFormatEtc,这个接口用来枚举当前data object所支持的数据格式,它相当重要。在上一节中,我们给出了它的一个实现,它内部本质是用API SHCreateStdEnumFmtEtc来实现的,这里再来看一看它的实现:

STDMETHODIMP SdkDataObject::EnumFormatEtc(DWORD dwDirection, IEnumFORMATETC **ppenumFormatEtc)
{
    if ( NULL == ppenumFormatEtc )
    {
        return E_INVALIDARG;
    }
    *ppenumFormatEtc = NULL;
    HRESULT hr = E_NOTIMPL;
    if ( DATADIR_GET == dwDirection )
    {
        FORMATETC rgfmtetc[] =
        {
            { CF_UNICODETEXT, NULL, DVASPECT_CONTENT, 0, TYMED_HGLOBAL },
        };
        hr = SHCreateStdEnumFmtEtc(ARRAYSIZE(rgfmtetc), rgfmtetc, ppenumFormatEtc);
    }
    return hr;
}

 

在调用API SHCreateStdEnumFmtEtc时,需要传一个FORMATETC的指针,上面的实现只给出了一种,即

{ CF_UNICODETEXT, NULL, DVASPECT_CONTENT, 0, TYMED_HGLOBAL }

 

当然这里面你还可以多给出几种。这里要注意了,根据MSDN上说明,SHCreateStdEnumFmtEtc的最低操作系统的版本是Windows 2000,也就是说,如果你的Data Object想在Windows 98操作系统之下也能工作,那么你就应当实现一个你自己的IEnumFORMATETC接口。事实上,实现这个接口并不难,考虑到位Windows 98操作系统太古老了,这里就不考虑实现IEnumFORMATETC接口了。

以上都是关于上一节的一点补充,好,开始正题。


1、实现IDropSource


我们需要实现的接口就是IDropSource了,它可以接收Drag过程之中的反馈,根据反馈来更改拖放源的状态,可以取消Drag操作等。它有两个方法:

QueryContinueDrag:决定拖放操作是否应当继续,通过返回DRAGDROP_S_CANCEL来取消拖放操作。DoDragDrop函数在检测到一个键盘或鼠标按钮状态变化时,就会调用这个函数。QueryContinueDrag必须根据传入的状态值来确定拖放操作是否继续,取消还是完成。

GiveFeedback:得到最终用户在拖放过程之中的视觉反馈。这个方法有一个参数:DWORD dwEffect,它是DROPEFFECT,表示当前的拖放状态,如DROPEFFECT_COPY,DROPEFFECT_MOVE等。这个函数一般用来改变鼠标的光标样式,或者根据DROPEFFECT来使拖放源高亮等。如果你想默认的光标样式,可以返回DRAGDROP_S_USEDEFAULTCURSORS。这个函数会在DoDragDrop循环中频繁调用,所以这个函数的实现应当尽可能高效。


下面给出IDropSource的实现

SdkDropSource.h 

#ifdef __cplusplus
#ifndef _SDKDROPSOURCE_H_
#define _SDKDROPSOURCE_H_

#include <shlobj.h>
#include <shlwapi.h>

class CLASS_DECLSPEC SdkDropSource : public IDropSource
{
public:

    SdkDropSource();
    ~SdkDropSource();

 // Methods of IUnknown
    IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv);
    IFACEMETHODIMP_(ULONG) AddRef(void);
    IFACEMETHODIMP_(ULONG) Release(void);

 // Methods of IDropSource
    IFACEMETHODIMP QueryContinueDrag(BOOL fEscapePressed, DWORD grfKeyState);
    IFACEMETHODIMP GiveFeedback(DWORD dwEffect);

private:

    volatile LONG   m_lRefCount;        //!< The reference count
};

#endif // _SDKDROPSOURCE_H_
#endif // __cplusplus


SdkDropSource.cpp 

#include "SdkDropSource.h"

SdkDropSource::SdkDropSource(void)
{
    m_lRefCount = 1;
}

SdkDropSource::~SdkDropSource(void)
{
    m_lRefCount = 0;
}

STDMETHODIMP SdkDropSource::QueryInterface(REFIID riid, void **ppv)
{
    static const QITAB qit[] =
    {
        QITABENT(SdkDropSource, IDropSource),
        { 0 }
    };

    return QISearch(this, qit, riid, ppv);
}

STDMETHODIMP_(ULONG) SdkDropSource::AddRef()
{
    return InterlockedIncrement(&m_lRefCount);
}

STDMETHODIMP_(ULONG) SdkDropSource::Release()
{
    ULONG lRef = InterlockedDecrement(&m_lRefCount);
    if (0 == lRef)
    {
        delete this;
    }
    return m_lRefCount;
}

STDMETHODIMP SdkDropSource::QueryContinueDrag(BOOL fEscapePressed, DWORD grfKeyState)
{
    if( TRUE == fEscapePressed )
    {
        return DRAGDROP_S_CANCEL;
    }

    // If the left button of mouse is released
    if( 0 == (grfKeyState & (MK_LBUTTON) )
    {
        return DRAGDROP_S_DROP;
    }

    return S_OK;
}

STDMETHODIMP SdkDropSource::GiveFeedback(DWORD dwEffect)
{
    UNREFERENCED_PARAMETER(dwEffect);
    return DRAGDROP_S_USEDEFAULTCURSORS;
}

这里没有什么多说明的,实现都很简单,重点说一下QueryContinueDrag的实现吧,它首先判断了fEscapePressed是否为TRUE,也就是说ESC按键是否按下,如果按下的话,就取消拖放操作。如果grfKeyState里面不包含了MB_LBUTTON的话(鼠标左键释放),就执行DRAGDROP_S_DROP,最后返回S_OK,让拖放操作继续。


2、开始拖放


实现了IDropSource,那么些我们要怎么开始拖放操作呢?

调用DoDragDrop API,它的原型如下:

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
);

 

第一个参数就是要传输的IDataObject接口,上一节我们已经讲过了。第二个参数就是拖放源,第三个参数是一个DWROD值,它表示源允许拖动效果,通常是DROPEFFECT_XXX值,如DROPEFFECT_MOVE和DROPEFFECT_COPY的联合。最后一个指向DWORD的指针,该值在DoDragDrop返回后,能得到最终执行完拖放后的效果,例如可能想知道用户到底是MOVE还是COPY等。

注意,当调用了DoDragDrop后,它会进行一个循环,在这个循环之中,它会调用IDropSource, IDropTarget的各种方法,它接管了鼠标事件,并且也是阻塞的,如果拖放没有完成,这个函数也不会继续往下执行。

反正记住了,我们应当调用DoDragDorp这个函数来开始拖放操作。还有一个API SHDoDragDrop函数,也可以开始一个拖放操作它的最低操作系统的版本是XP,而DoDragDrop是Windows 95。




  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值