Drag Drop 入门介绍

鼠标拖拽简介

初衷

完成一个功能,运行我们的程序后,每打开一个explorer 窗口,向该窗口拷贝我们的文件。

使用

在资源管理器中进行拖拽操作,没有键盘操作的话。不同分区间默认为拷贝,同分区间默认为move操作。

如果加上键盘操作,则会规定操作的类型如下:

Shift       move

Ctrl                    copy

Alt             link

另外:

Drag系统文件                           link

Drop 开始菜单和toobar      link

Drag And Drop 初步介绍

Drag、Drop 字面上很好理解,拖和放。

WM_DROPFILES 这个消息我们现在几乎不用了,应用程序几乎都是用的OLE 技术。

OLE 技术中每个拖放的操作都由三个元素组成,这些元素都是COM 对象,必须由任何想要支持拖放的应用程序来实现。

1.      源                                                     :IDropSource接口,包含生成视觉反馈以及取消或完成拖放操作的方法。

2.      目标                                                :IDropTarget 接口

3.      正在传输的数据                         :由IDataObject接口表示

 

作为,需要1,3两个接口

作为目标,仅仅需要2一个接口即可。

 

使用OLE 的线程必须调用OleInitialize,完成时调用OleUninitializeCOM OLE 必须在每个线程的基础上被初始化和销毁

 

拖放过程的核心:

WINOLEAPI DoDragDrop(
   IDataObject  *pDataObject,   // Pointer to the data object
   IDropSource  *pDropSource,   // Pointer to the source
   DWORD         dwOKEffect,    // Effectsallowed by the source
   DWORD       * pdwEffect     // Pointer to effects on the source
   );
 


当调用该函数,进入一个模态的消息循环,这时候,鼠标和键盘消息将会被监视

 

如果目标应用程序想要作为拖放操作的接收者,调用WINOLEAPIRegisterDragDrop(

  

 HWND        hwnd,         // Handle to a window that can accept drops
   IDropTarget  *pDropTarget    // Pointer to object that is to be target of drop
   );

该函数将该窗口注册到OLE 运行,当鼠标移动到该窗口上,OLE 调用IDropTarget 界面中的方法通知应用程序拥有该窗口,拖放操作正在进行。当窗口关闭的时候,必须调用

WINOLEAPI RevokeDragDrop(
   HWND   hwnd                 // Handle to a window that can accept drops
   );


该API 使用COM 的函数注销指定的窗口,并释放注册用的IDropTarget对象及其接口函数。

OLE 数据传输的方法

OLE 数据传输的核心是IDataObjectCOM 接口,IDataObject 提供了一种从一个应用程序传输和访问数据到另一个应用程序的方法。核心还是使用的剪贴板。

首先介绍两个结构体

 
typedef struct
{
   CLIPFORMAT     cfFormat;     // Clipboard format 
    DVTARGETDEVICE*ptd;          //(NULL)       Target device for rendering
    DWORD          dwAspect;     // (DV_CONTENT) How much detail is requiredfor data rendering
    LONG           lindex;       //(-1)         Used when data is splitacross page boundaries
    DWORD          tymed;        // Storage medium used fordata transfer (HGLOBAL, IStream etc)
     
} FORMATETC;


 

该结构体描述了一个IDdataObject 可以提供或接收的数据的类型,基本上是标准Windows 剪贴板格式(CF_TEXT)的扩展。

cfFormat:

用于标识FORMATETC结构的剪贴板格式。这可以是内置的格式,如CF_TEXT或CF_BITMAP,或者使用RegisterClipboardFormat注册的自定义格式。

ptd:

指向DVTARGETDEVICE结构的指针,它提供有关数据呈现的设备的信息。对于正常的剪贴板操作和拖放操作,通常为NULL。

dwAspect:

描述用于呈现数据的细节数量。通常这将是DVASPECT_CONTENT,意思是“全部内容”,但可以描述较小的细节,如缩略图或图标。

lindex:

仅在数据跨页边界分割时使用,不用于简单的OLE传输。

tymed:

它描述了用于保存数据的“存储介质”的类型。这个成员从“类型的媒介” 对应于windows.h 中的 TYMED_XXX值

存储OLE数据

typedef struct
{
    DWORD tymed;
     
    union
    {
        HBITMAP       hBitmap;
        HMETAFILEPICT hMetaFilePict;
        HENHMETAFILE   hEnhMetaFile;
        HGLOBAL       hGlobal;
        LPWSTR        lpszFileName;
        IStream       *pstm;
        IStorage      *pstg;
    };
     
    IUnknown*pUnkForRelease;
     
} STGMEDIUM;


 

tymed:

这必须与FORMATETC结构中的相同

hBitmap / hGlobal等:实际数据。只有其中之一将是有效的,这取决于价值TYMED。

pUnkForRelease:可选的指向IUnknown接口的指针,数据的接收者应在其上调用Release。当此字段为NULL时,收件人有责任释放内存句柄。该ReleaseStgMedium API调用在这里很有用,因为它需要释放STGMEDIUM的数据内容。

 

IDataObject 定义的接口函数

接口定义

IDataObject : public IUnknown
   {
   public:
       virtual /* [local] */ HRESULT STDMETHODCALLTYPE GetData(
            /* [unique][in] */ FORMATETC *pformatetcIn,
            /* [out] */ STGMEDIUM *pmedium) = 0;
       
       virtual /* [local] */ HRESULT STDMETHODCALLTYPE GetDataHere(
            /* [unique][in] */ FORMATETC *pformatetc,
            /* [out][in] */ STGMEDIUM *pmedium) = 0;
       
       virtual HRESULT STDMETHODCALLTYPE QueryGetData(
            /* [unique][in] */ __RPC__in_opt FORMATETC *pformatetc) = 0;
       
       virtual HRESULT STDMETHODCALLTYPE GetCanonicalFormatEtc(
            /* [unique][in] */ __RPC__in_opt FORMATETC *pformatectIn,
            /* [out] */ __RPC__out FORMATETC *pformatetcOut) = 0;
       
       virtual /* [local] */ HRESULT STDMETHODCALLTYPE SetData(
            /* [unique][in] */ FORMATETC *pformatetc,
            /* [unique][in] */ STGMEDIUM *pmedium,
            /* [in] */ BOOL fRelease) = 0;
       
       virtual HRESULT STDMETHODCALLTYPE EnumFormatEtc(
            /* [in] */ DWORD dwDirection,
            /* [out] */ __RPC__deref_out_opt IEnumFORMATETC **ppenumFormatEtc) = 0;
       
       virtual HRESULT STDMETHODCALLTYPE DAdvise(
            /* [in] */ __RPC__in FORMATETC *pformatetc,
            /* [in] */ DWORD advf,
            /* [unique][in] */ __RPC__in_opt IAdviseSink *pAdvSink,
            /* [out] */ __RPC__out DWORD *pdwConnection) = 0;
       
       virtual HRESULT STDMETHODCALLTYPE DUnadvise(
            /* [in] */ DWORD dwConnection) = 0;
       
       virtual HRESULT STDMETHODCALLTYPE EnumDAdvise(
            /* [out] */ __RPC__deref_out_opt IEnumSTATDATA **ppenumAdvise) = 0;
       
    };


 

对于简单的OLE 拖放来说,仅仅需要GetData QueryGetData和 EnumFormatEtc三个成员函数

GetData

得到在FORMATETC结构中描述的数据,并通过STGMEDIUM结构进行传输。

QueryGetData

确定数据对象是否能够呈现FORMATETC结构中描述的数据

EnumFormatEtc

创建并返回一个指向IEnumFORMATETC接口的指针,以枚举数据对象产生的FORMATETC对象

 

使用IDataObject 访问剪贴板

#include 
#include 
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 aHGLOBAL
    if(pDataObject->GetData(&fmtetc,&stgmed) == S_OK)
    {
        // We need to lock the HGLOBAL handle because we can't
        // be sure if this is GMEM_FIXED (i.e. normal heap) data ornot
        char *data = (char*)GlobalLock(stgmed.hGlobal);
 
        printf("%s\n",data);
 
        // cleanup
        GlobalUnlock(stgmed.hGlobal);
        ReleaseStgMedium(&stgmed);
    }
}
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;
}


实现IDataObject

为了创建我们自己的COM对象,我们按照函数顺序继承IDataObject对象的所有函数。以及IUnknown类的成员函数。

 

其中我们仅仅需要实现三个函数,上面我们已经提到过了。

另外,我们需要提供一个IEnumFORMATETC接口

 

构造函数

为了之后的消费者“IDropTarget”能够得到我们的数据,我们需要提供其需要的数据,Drop对象会调用QueryDataEnumFormatEtc获得数据。我们需要找到一些使用实际数据填充IDataObject的方法,以FORMATETC结构的形式告诉它数据是什么

 

CDataObject::CDataObject(FORMATETC *fmtetc,STGMEDIUM *stgmed, int count)
{
    // reference countmust ALWAYS start at 1
    m_lRefCount   = 1;
    m_nNumFormats  =count;
 
    m_pFormatEtc  = new FORMATETC[count];
    m_pStgMedium  = new STGMEDIUM[count];
 
    for(int i = 0; i < count; i++)
    {
        m_pFormatEtc[i]= fmtetc[i];
        m_pStgMedium[i]= stgmed[i];
    }
}


 

COM 对象的引用计数初始化为1,并将传入的两个结构体数组保存。

创建IDataObject

HRESULT CreateDataObject(FORMATETC *fmtetc, STGMEDIUM*stgmeds, UINT count, IDataObject**ppDataObject)
{
    if(ppDataObject == 0)
        return E_INVALIDARG;
 
    *ppDataObject = new CDataObject(fmtetc, stgmeds, count);
 
    return (*ppDataObject) ? S_OK : E_OUTOFMEMORY;
}


 

QueryGetData

HRESULT __stdcall CDataObject::QueryGetData(FORMATETC*pFormatEtc)
{
    return (LookupFormatEtc(pFormat) == -1) ?DV_E_FORMATETC : S_OK;
}
int CDataObject::LookupFormatEtc(FORMATETC*pFormatEtc)
{
    // check each of ourformats in turn to see if one matches
    for(int i = 0; i tymed)   &&
            m_pFormatEtc[i].cfFormat== pFormatEtc->cfFormat &&
            m_pFormatEtc[i].dwAspect== pFormatEtc->dwAspect)
        {
            //return index of stored format
            returni;
        }
    }
 
    // error, format notfound
    return -1;
}


 

GetData

HRESULT __stdcall CDataObject::GetData (FORMATETC*pFormatEtc, STGMEDIUM *pStgMedium)
{
    int idx;
 
    // try to match thespecified FORMATETC with one of our supported formats
    if((idx =LookupFormatEtc(pFormatEtc)) == -1)
        return DV_E_FORMATETC;
 
    // found a match -transfer data into supplied storage medium
    pMedium->tymed          = m_pFormatEtc[idx].tymed;
    pMedium->pUnkForRelease = 0;
 
    // copy the data intothe caller's storage medium
    switch(m_pFormatEtc[idx].tymed)
    {
    case TYMED_HGLOBAL:
        pMedium->hGlobal    = DupGlobalMem(m_pStgMedium[idx].hGlobal);
        break;
 
    default:
        return DV_E_FORMATETC;
    }
    return S_OK;
}
HGLOBAL DupGlobalMemMem(HGLOBAL hMem)
{
    DWORD   len    = GlobalSize(hMem);
    PVOID   source = GlobalLock(hMem);
    PVOID   dest   = GlobalAlloc(GMEM_FIXED,len);
 
    memcpy(dest, source,len);
    GlobalUnlock(hMem);
    return dest;
}


EnumFormatEtc

HRESULT __stdcall CDataObject::EnumFormatEtc (DWORD dwDirection, IEnumFORMATETC**ppEnumFormatEtc)
{
    // only the getdirection is supported for OLE
    if(dwDirection == DATADIR_GET)
    {
        //for Win2k+ you can use the SHCreateStdEnumFmtEtc API call, however
        //to support all Windows platforms we need to implement IEnumFormatEtc ourselves.
        return SHCreateStdEnumFmtEtc (m_NumFormats,m_FormatEtc, ppEnumFormatEtc);
    }
    else
    {
        //the direction specified is not supported for drag+drop
        return E_NOTIMPL;
    }
}


我们直接使用SHCreateStdEnumFmtEtc就好了

这样是可以的。对的。

Drag源程序的编写

首先来看上面介绍过的函数

WINOLEAPI DoDragDrop(
   IDataObject  *pDataObject,   // Pointer to the data object
   IDropSource  *pDropSource,   // Pointer to the source
   DWORD         dwOKEffect,    // Effectsallowed by the source
   DWORD       * pdwEffect     // Pointer to effects on the source
   );


前两个参数是COM 接口,一个是IDataObject我们已经在上面介绍过,第三个参数是一个DWORD 值,取自DROPEFFECT_xxx,通常为:DROPEFFECT_COPY和DROPEFFECT_MOVE的组合,如果只允许我们的源复制数据,只指定DROPEFFECT_COPY。

最后一个参数包含OLE 希望源执行的“效果”或动作,即用户是否选择移动或复制数据。

IDropSource接口

IDropSource只需要实现两个功能:

QueryContinueDrag    根据鼠标按钮和,键的状态确定是否继续,取消或完成拖放操作

GiveFeedback         提供一种拖放源的方法,根据上面列出的修饰键的状态(即鼠标按钮,转义,控制等)给出视觉反馈

当拖动修改键的状态发生变化,这两个函数都将由COM/OLE 运行时调用

实现IDropSource

class CDropSource : public IDropSource
{
public:
    //
    // IUnknown members
    //
    HRESULT __stdcallQueryInterface    (REFIID iid, void ** ppvObject);
    ULONG  __stdcallAddRef           (void);
    ULONG  __stdcall Release          (void);
 
    //
    // IDropSourcemembers
    //
    HRESULT __stdcallQueryContinueDrag (BOOL fEscapePressed, DWORD grfKeyState);
    HRESULT __stdcallGiveFeedback      (DWORD dwEffect);
 
    //
    // Constructor /Destructor
    //
    CDropSource();
    ~CDropSource();
private:
 
    //
    // private membersand functions
    //
    LONG      m_lRefCount;
};


对象的构造函数除了初始化对象的引用计数之外,不执行任何任务。

HRESULT QueryContinueDrag(
   BOOL   fEscapePressed,       // Is the  key being pressed?
   DWORD  grfKeyState,          // Current state of keyboard modifier keys
   );

函数的三个可能返回值

S_OK                 拖放操作继续

DRAGDROP_S_DROP      应该完成拖放操作

DRAGDROP_S_CANCEL    拖放操作被取消,不会出现拖放操作。

 

Drag And Drop操作的一些习惯操作:

1. 按下:Escape键时,取消拖放操作。

2. 当鼠标左键释放,执行Drop

 

HRESULT __stdcallCDropSource::QueryContinueDrag(BOOL fEscapePressed, DWORD grfKeyState)
{
    // if the Escape keyhas been pressed since the last call, cancel the drop
    if(fEscapePressed ==TRUE)
        returnDRAGDROP_S_CANCEL;  
 
    // if the LeftMousebutton has been released, then do the drop!
    if((grfKeyState &MK_LBUTTON) == 0)
        returnDRAGDROP_S_DROP;
 
    // continue with thedrag-drop
    return S_OK;
}


GiveFeedback 函数对于每个应用程序通常是不同的,但这里仅仅是返回一个同样的结果,当然我们也可以丰富这个函数。

HRESULT __stdcall CDropSource::GiveFeedback(DWORD dwEffect)
{   
    return DRAGDROP_S_USEDEFAULTCURSORS;
}


dwEffect 告诉我们按下了哪些鼠标按钮,使用哪些键盘修饰符。通过简单返回DRAGDROP_S_USEDDEFAULTCURSORS,指示COM 在修饰符更改时自动更新鼠标光标。

Drop端程序的编写

注册自己为“Drop目标”

WINOLEAPI RegisterDragRrop(HWNDhWnd,IDropTarget* pDropTarget);

第一个窗口句柄,将注册为放置目标,第二个参数IDropTarget 指向IDropTarget COM 对象的指针,在拖放过程中,COM/OLE 运行时将调用此接口上的方法。

删除拖放功能

WINOLEAPI RevokeDragDrop(HWND hWnd);

实现

IDropTarget接口相对简单,四个功能加IUnknown接口

DragEnter  确定下降是否可以接受,并且如果被接受则影响它

DragOver   通过DoDragDrog 功能向用户提供目标反馈

DragLeave  导致Drop端暂停其反馈操作

Drop       将数据放入目标窗口

 

class CDropTarget : public IDropTarget
{
public:
    // IUnknownimplementation
    HRESULT __stdcall QueryInterface (REFIID iid, void ** ppvObject);
    ULONG   __stdcall AddRef (void);
    ULONG   __stdcall Release (void);
 
    // IDropTargetimplementation
    HRESULT __stdcall DragEnter(IDataObject *pDataObject, DWORD grfKeyState,POINTL pt, DWORD *pdwEffect);
    HRESULT __stdcall DragOver(DWORD grfKeyState, POINTL pt, DWORD * pdwEffect);
    HRESULT __stdcall DragLeave(void);
    HRESULT __stdcall Drop(IDataObject * pDataObject,DWORD grfKeyState, POINTLpt, DWORD * pdwEffect);
 
    // Constructor
    CDropTarget(HWND hwnd);
    ~CDropTarget();
 
private:
    // internal helperfunction
    DWORD DropEffect(DWORD grfKeyState, POINTL pt, DWORD dwAllowed);
    bool  QueryDataObject(IDataObject *pDataObject);
 
    // Private membervariables
    long   m_lRefCount;
    HWND   m_hWnd;
    bool   m_fAllowDrop;
 
    // Other internalwindow members
     
};


M_hWnd 为接受Drop 消息的窗口的句柄,m_fAllowDrop 用于指示是否被丢弃的数据对象对我们包含有用的数据。

DragEnter

当一个对象被拖动到我们的窗口,这是COM 调用的第一个函数:

HRESULT DragEnter(
   IDataObject *pDataObject,    // Pointer to the interface of the source dataobject
   DWORD        grfKeyState,    // Currentstate of keyboard modifier keys
   POINTL       pt,             //Current cursor coordinates
   DWORD *      pdwEffect       // Pointer to the effect of thedrag-and-drop operation
   );


 

IDataObject

第一个参数是通过拖放操作的源传递给我们的数据对象的另一个指针(通过COM)。IDataObject只是要删除的数据的“传输介质”。我们可以在DragEnter中查询数据对象,查看是否有任何我们想要的数据。

gfrKeyState

保存键盘修饰键的状态,例如Control,Alt和Shift以及鼠标按钮的状态。这是一个简单的DWORD变量,包含以下一个或多个位标志:MK_CONTROL,MK_SHIFT,MK_ALT,MK_BUTTON,MK_LBUTTON等。

Pt

一个POINTL结构,包含鼠标进入窗口时的坐标。在某些应用中,此参数将用于检查鼠标是否位于允许的下降区域之上,或者简单地用于定位某种“插入”光标来指示丢弃的数据将在哪里。

pdwEffect

指向DWORD值的指针,用于指定drop-source 允许的drop-effect 。此值与提供者调用DoDragDrop时指定的dwOKEffect相同(因为这个参数的忽视,浪费了好多时间,疯狂调试)

 

HRESULT __stdcall CDropTarget::DragEnter(IDataObject*pDataObject, DWORD grfKeyState,
                                              POINTLpt, DWORD *pdwEffect)
{
    // does thedataobject contain data we want?
    m_fAllowDrop =QueryDataObject(grfKeyState, pdwEffect, pDataObject);
     
    if(m_fAllowDrop)
    {
        //get the dropeffect based on keyboard state
        *pdwEffect= DropEffect(grfKeyState, pt, *pdwEffect);
 
        SetFocus(m_hWnd);
        PositionCursor(m_hWnd,pt);
    }
    else
    {
        *pdwEffect= DROPEFFECT_NONE;
    }   return S_OK;
}
 
bool CDropTarget::QueryDataObject(IDataObject *pDataObject)
{
    FORMATETC fmtetc = {CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
 
    // does the dataobject support CF_TEXT using a HGLOBAL?
    return pDataObject->QueryGetData(&fmtetc) ==S_OK ? true : false;
}
DWORD CDropTarget::DropEffect(DWORDgrfKeyState, POINTL pt, DWORD dwAllowed)
{
    DWORD dwEffect = 0;
 
    // 1. check"pt" -> do we allow a drop at the specified coordinates?
    // 2. work out thatthe drop-effect should be based on grfKeyState 
    if(grfKeyState &MK_CONTROL)
    {
        dwEffect= dwAllowed & DROPEFFECT_COPY;
    }
    else if(grfKeyState& MK_SHIFT)
    {
        dwEffect= dwAllowed & DROPEFFECT_MOVE;
    }
 
    // 3. nokey-modifiers were specified (or drop effect not allowed), so
    //   base the effect on those allowed by the dropsource
    if(dwEffect == 0)
    {
        if(dwAllowed& DROPEFFECT_COPY) dwEffect = DROPEFFECT_COPY;
        if(dwAllowed& DROPEFFECT_MOVE) dwEffect = DROPEFFECT_MOVE;
    }
 
    return dwEffect;
}


在这个实现中,我们让数据移动覆盖数据副本

DragOver

在拖放的声明周期内,当键盘修饰符的状态改变或鼠标移动,被调用。该函数指示基于键盘和鼠标位置的状态返回值。

HRESULT __stdcall CDropTarget::DragOver(DWORD grfKeyState, POINTL pt, DWORD * pdwEffect)
{
    if(m_fAllowDrop)
    {
        *pdwEffect= DropEffect(grfKeyState, pt, *pdwEffect);
        PositionCursor(m_hWnd,pt);
    }
    else
    {
        *pdwEffect= DROPEFFECT_NONE;
    }
 
    return S_OK;
}


DragLeave

鼠标光标移动到我们的放置目标窗口之外,或者按下Escape来取消拖放操作。给我们的程序一个释放资源或者改变界面的机会,这里我们仅仅返回S_OK

HRESULT __stdcall CDropTarget::DragLeave(void)
{
    return S_OK;
}


Drop

HRESULT __stdcall CDropTarget::Drop(IDataObject*pDataObject, DWORD grfKeyState,POINTL pt, DWORD *pdwEffect)
{
    PositionCursor(m_hWnd,pt);
 
    if(m_fAllowDrop)
    {
        DropData(m_hWnd,pDataObject);
        *pdwEffect= DropEffect(grfKeyState, pt, *pdwEffect);
    }
    else
    {
        *pdwEffect= DROPEFFECT_NONE;
    }
    return S_OK;
}


OLE 确定拖放将继续时,调用这个函数,我们得到与DragEnter中接收的IDataObject相同的接口指针,我们可以从中获取数据。

 

完整源码及参考:

http://www.catch22.net/tuts/ole-drag-and-drop

 

你可能感兴趣的:(基础知识)