最近在写Shell Extension方面的东西,看到了这篇文章,抄在这里。
原文地址http://blog.csdn.net/vcbear/archive/2002/01/25/5990.aspx
同时也找到了一个与Drag and Drop有关的例子,地址:http://www.codeproject.com/useritems/NSExtDragDrop.asp
vcbear 
关于
Windows 的外壳扩展编程,拖放是比较简单的一种,在网上可以找到不少介绍这个技巧的文章。大部分是介绍使用 MFC 的 COleDropTarget 实现的,我觉得一般使用 COleDropTarget 已经很好了,但是我习惯在一些程序模块中,完全的不使用 MFC, 比如纯 SDK 编程 , 还有用在 ATL 的时候 ,MFC 是相当累赘的。所以 COleDropTarget 在这个意义上讲不够完美。
参考了
MSDN 以及 www.CodeProject.com 的相关文章和代码( by Thomas Blenkers )之后,我发现拖放实际上主要使用了 IDropTarget 的接口方法,非常简单,不妨直接面对原始 IDropTarget 实现自己的拖放类。
作为学习笔记,就有了这么一篇文字,以抛砖引玉:
IDropTarget
是系统留给支持拖放的客户程序的一个纯虚接口,事先没有对接口的任何函数进行实现,而是让用户通过实现接口函数来接管拖放的结果。IDropTarget
接口有以下成员函数:
COM成员函数
 
QueryInterface 
AddRef 
Release 
    
    
    接管拖放事件的成员函数:
     
    
    
    DragEnter
    DragOver
    DragLeave
    Drop
    
    也就是说,要在客户程序里实现以上
    7个函数的实体。
    系统在检测到拖放发生的时候,会在合适的时候依次调用客户程序里实现的
    IDropTarget接口相应函数,检查用户在这些函数里返回的标志,决定鼠标外观表现和拖放结果。
     
    
     
    
     
    实现
    IDropTarget接口为此建立一个基类为
IDropTarget的类:
    class CDropTargetEx : public IDropTarget 
    
    IDropTarget接口在OLEIDL.h里定义,为纯虚接口。
    在CDropTargetEx里依次声明接口所包含的7个函数,原形为:
    
    HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void ** ppvObject);
    ULONG STDMETHODCALLTYPE AddRef(void);
    ULONG STDMETHODCALLTYPE Release(void);
    HRESULT STDMETHODCALLTYPE DragOver(DWORD grfKeyState, 
    
    
    
    
    
    
    
    
    POINTL pt, 
    DWORD *pdwEffect);
    
    HRESULT STDMETHODCALLTYPE DragEnter(IDataObject * pDataObject, 
    
    
    
    
    
    
    
    
    DWORD grfKeyState, POINTL pt, 
    DWORD * pdwEffect);
    
    HRESULT STDMETHODCALLTYPE DragLeave(void);
    HRESULT STDMETHODCALLTYPE Drop(IDataObject *pDataObj, 
    
    
    
    
    
    
    
    
    DWORD grfKeyState, 
    POINTL pt, 
    DWORD __RPC_FAR *pdwEffect);
    
    (为了实现Addref计数,还有一个ULONG tb_RefCount成员变量是必须的。QueryInterface,AddRef,Release这3个函数的实现是COM知识中最基本的,请参见附例)
    
    在讲解
    IDropTarget其他函数的具体实现之前,有必要介绍一下一个你可能永远不会直接调用但是确实存在的函数:DoDragDrop函数.此函数在某数据源的数据被拖动的时候就被调用,它负责
    
    IDropTarget接口
     
    
    随时跟踪鼠标和键盘的状态,根据状态决定调用其DrageEnter,DragMove,Drop或DragLeave接口
     
    
    从这些接口获取客户程序的返回值,根据这些值和用户界面以及数据源进行交互。
     
    可以说
    DoDragDrop控制拖放的整个过程,我们要做的,只是将这个过程里发生的事件,接管下来并得到相应的信息,和DoDragDrop进行交互而已。了解了这一点有助于我们理解为什么通过区区一个接口4个函数就可以实现了拖放的效果,因为系统为我们已经做了很多。
    另一个非常重要的
    API是RegisterDragDrop,这个函数的原形是这样的:
    WINOLEAPI RegisterDragDrop(
    
    
    
    
    
    
    HWND hwnd, 
    IDropTarget * pDropTarget
    );
    
    不用被
    WINOLEAPI吓到,这是一个宏:
    #define STDAPI EXTERN_C HRESULT STDAPICALLTYPE
    
    也就是表示一个标准的
    WIN API函数,返回一个HRESULT的值。
    函数
    RegisterDragDrop的作用是告诉系统:某个窗口(hwnd参数指定)可以接受拖放,接管拖放的接口是pDropTarget。
    记住在调用
    RegisterDragDrop之前,一定要先调用OleInitialize初始化OLE环境。
    在类
    CDropTargetEx里设计了一个函数
    BOOL CDropTargetEx::DragDropRegister(HWND hWnd,
    DWORD AcceptKeyState=|MK_LBUTTON)
    {
    
    
    if(!IsWindow(hWnd))return false;
    HRESULT s = ::RegisterDragDrop (hWnd,this);
    if(SUCCEEDED(s))
    {
    m_hTargetWnd = hWnd;
    m_AcceptKeyState = AcceptKeyState;
    return true;
    }
    else { return false; }
    }
    
    在这个函数里调用
    RegisterDragDrop,将this指针传入,表示本类实现了IDropTarget.,由本类接管拖放事件。另外顺便定义了一下拖放鼠标和键盘特性常数,对这个类来说,我希望默认的只接受鼠标左键的拖放,所以,默认的AcceptKeyState值是MK_LBUTTON。相关的键盘鼠标常数还有MK_SHIFT,MK_ALT,MK_RBOTTON,MK_MBUTTON,MK_BOTTON等几个,我想这个几个常数从字面上就可以理解它的意思了。这些常数可以用“位与”的操作组合。
    以下具体讨论
    IDropTarget的拖放相关接口函数(4个),这里的拖放对象以文本和文件为主。
     
    
     
    
        
        
        DragEnter
         
        
        当你用鼠标选中了某一个文件或一段文本,并且将鼠标移到某个可以接受拖放(已经调用过
        RegisterDragDrop)的窗口里,DragEnter将第一时间被调用。再看一下其原形:
        HRESULT DragEnter( IDataObject * pDataObject,
        
        
        
             DWORD grfKeyState,
             POINTL pt, 
              DWORD * pdwEffect   )
        
        pDataobject 
        是从拖放的原数据中传递过来的一个IDataObject
接口实例,包含数据对象的一些相关方法,可以通过此接口获得数据。
        grfKeyState 
        为DragEnter
被调用时当前的键盘和鼠标的状态,包含上面介绍过的键盘鼠标状态常数。
        pt
        表示鼠标所在的点。是以整个屏幕为参考坐标的。
        pdwEffect
        是DoDragDrop
提供的一个DWORD
指针,客户程序通过这个指针给DoDragDrop
返回特定的状态。有效的状态包括:
        DROPEFFECT_NONE=0 表示此窗口不能接受拖放。
        DROPEFFECT_MOVE=1 表示拖放的结果将使源对象被删除
        DROPEFFECT_COPY=2 表示拖放将引起源对象的复制。
        DROPEFFECT_LINK =4 表示拖放源对象创建了一个对自己的连接
        DROPEFFECT_SCROLL=0x80000000表示拖放目标窗口正在或将要进行卷滚。此标志可以和其他几个合用
        对于拖放对象来说,一般只要使用DROPEFFECT_NONE和DROPEFFECT_COPY即可。
        在DragEnter里要做什么呢?主要是告知拖放已经进入窗口区域,并判断是否支持某具体类型的拖放。
        首先,要判断键盘的状态。在调用DragDropRegister时我传入了一个AcceptKeyState并将其保存在m_AcceptKeyState成员变量里,现在可以拿它跟这里得到的grfKeyState比较:
        
        if(grfKeyState!=m_AcceptKeyState )
        {
        *pdwEffect = DROPEFFECT_NONE;
        return S_OK;
        }
        
        如果键盘和鼠标的状态和我期望的不一样,那么
        pdwEffect里返回DROPEFFECT_NONE表示不接受拖放。
        然后
        ,判断拖放过来的IDataObject对象里有没有我感兴趣的数据。
        这里要介绍的是两个关键的结构体
        FORMATETC和STDMEDIUM
        FORMATETC
        是OLE
数据交换的一个关键结构,对某种设备,数据,和相关媒体做了格式上的描述。
        其定义为
        
        typedef struct tagFORMATETC 
        { 
        
        
        CLIPFORMAT cfFormat; 
        DVTARGETDEVICE *ptd; 
        DWORD dwAspect; 
        LONG lindex; 
        DWORD tymed; 
        
        }
        FORMATETC, *LPFORMATETC;
        在这里我们最感兴趣的是
        cfFormat和tymed两个数据。cfFormat是标准的“粘帖板”数据类型比如CF_TEXT之类。tymed表示数据所依附的媒介,比如内存,磁盘文件,存储对象等等。其他的成员可以参见MSDN。
        一个典型的
        FORMATETC结构变量定义如下:
        FORMATETC cFmt = {(CLIPFORMAT) CF_TEXT, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
        
        IDataObject提供了一个GetData接口来获取其实例里包含的数据,比如:
        
        STGMEDIUM stgMedium;
        ret = pDataObject->GetData(&cFmt, &stgMedium);
        
        GetData传入cFmt,以指出所感兴趣的数据,并将返回在stgMedium结构里。
        
        STGMEDIUM
        的定义如下1
        typedef struct tagSTGMEDIUM 
        { 
        DWORD tymed; 
        [switch_type(DWORD), switch_is((DWORD) tymed)] 
        union { 
        [case(TYMED_GDI)] HBITMAP hBitmap; 
        [case(TYMED_MFPICT)] HMETAFILEPICT hMetaFilePict; 
        [case(TYMED_ENHMF)] HENHMETAFILE hEnhMetaFile; 
        [case(TYMED_HGLOBAL)] HGLOBAL hGlobal; 
        [case(TYMED_FILE)] LPWSTR lpszFileName; 
        [case(TYMED_ISTREAM)] IStream *pstm; 
        [case(TYMED_ISTORAGE)] IStorage *pstg; 
        [default] ; 
        }; 
        [unique] IUnknown *pUnkForRelease; 
        }STGMEDIUM; 
        typedef STGMEDIUM *LPSTGMEDIUM; 
        
        看起来颇为复杂,其实主要是一系列句柄或数据对象接口的联合,根据数据具体的类型,使用其中之一即可。
        tymed和FORMATETC里一样,指出数据的载体类型(遗憾的是它不能指出具体的标准类型比如CF_TEXT或者其他)。至于pUnkForRelease,是源数据指定的一个接口,用来传递给ReleaseStgMedium函数,如果它不为NULL,则ReleaseStgMedium函数使用这个接口释放数据。如果为NULL,则ReleaseStgMedium函数使用默认的IUnknown接口。对于常规的拖放来说,这个对象指针应该为NULL.
        得到了句柄或数据对象接口,也相当于得到了拖放的数据。
        定义一个特定的
        FORMATETC结构实例传递给IDataObject的GetData,可以直接询问和获取某一种特定的数据。如果我们对我们想要的数据是非常确定的,这是比较有效率的方法。但是如果我们期望能够对拖放的对象进行自适应的话,我们可以采取枚举IDataObject里包含的所有数据类型的方案。这就要用到IEnumFORMATETC接口了。
        IEnumFORMATETC接口从IDataObject接口里获取:
        
        
        IEnumFormatETC *pEnumFmt = NULL;
        ret = pDataObject->EnumFormatEtc (DATADIR_GET,&pEnumFmt);
        
        如果获取成功,则可以通过
        IEnumFORMATETC接口的Next方法,来枚举所有的数据格式:
        pEnumFmt->Reset (); 
        HRESULT Ret=S_OK
        while(Ret!=S_OK)
        
        
        {
        
        Ret
        =pEnumFmt->Next(1,&cFmt,&Fetched);
        if(SUCCEEDED(ret))
        if( cFmt.cfFormat == CF_TEXT
        ||cFmt.cfFormat == CF_HDROP)
        {
        
        if(GetDragData(pDataObject,cFmt)) 
        EnterResult = true;
        }
        }
        
        第一个参数表示一次获取的
        FORMATETC结构数据的数量,cFmt是一个FORMATETC指针,指向一个数据缓冲,用来返回FORMATETC数据。,Fetched是Next调用后得到的FORMATETC数据个数。一般一次获取一个,直到Next返回不为S_OK。
        我们可以对每个得到
        cFmt调用IDataObject->GetData方法,但是一般来说,一个数据对象包含的数据不止一种,而且一般有一些自定义的数据类型(关于自定义数据类型,参见:RegisterClipboardFormat,如果要自己实现Drag/Drop源数据,这个函数是有用的),对此我们不感兴趣,因为这里只要求处理文本和文件的拖动,为此,只处理cfFormat为CF_TEXT和CF_HROP的数据:
        GetDragData
        为CDropTargetEx
类的一个成员函数:
        ///////////////////////////////////////////////////
        
        //Get The DragData from IDataObject ,save in HANDEL
        BOOL CDropTargetEx::GetDragData(IDataObject *pDataObject,FORMATETC cFmt)
        {
        HRESULT ret=S_OK;
        STGMEDIUM stgMedium;
        ret = pDataObject->GetData(&cFmt, &stgMedium);//GetData(CF_TEXT, &stgMedium);
        if (FAILED(ret))
        {
        return FALSE;
        }
        
        if (stgMedium.pUnkForRelease != NULL)
        {
        return FALSE;
        }
        ///////////////////////////////////////////
        switch (stgMedium.tymed)
        {
        case TYMED_HGLOBAL:
        {
        LPDRAGDATA pData = new DRAGDATA;
        pData->cfFormat = cFmt.cfFormat ;
        memcpy(&pData->stgMedium,&stgMedium,sizeof(STGMEDIUM));
        m_Array.push_back(pData);
        return true;
        break;
        }
        default:
        // type not supported, so return error
        {
        ::ReleaseStgMedium(&stgMedium);
        }
        break;
        }
        return false;
        }
        
        在这个成员函数里,根据
        cFmt,调用IDataObject->GetData函数获得数据(对于CF_TEXT和CF_HROP来说,数据的媒介载体tymed都是HGLOBAL类型的)。
        在具体实现的时候,我定义了一个结构:
        
        typedef struct _DRAGDATA
        {
        
        
        int cfFormat;
        
        
        STGMEDIUM stgMedium;
        }DRAGDATA,*LPDRAGDATA;
        
         
        将
        STGMEDIUM和数据类型(比如CF_TEXT,记录在cfFormat)都记录在DRAGDATA里。并且使用了一个vector数组,将这个结构保存在数组里。对于不是我们想要的STGMEDIUM数据,我们马上调用ReleaseStgMedium函数进行释放,免得造成内存泄露。
        这样,
        DragEnter的工作就基本完成了,最后需要做的就是给DoDragDrop返回相应的状态:如果我们获得了想要的数据就给* pdwEffect赋值为DROPEFFECT_COPY,否则,就是DROPEFFECT_NONE;
        如果支持拖放,鼠标形状将变成一个有接受意义的图标,否则,是一个拒绝意义的图标。
        
         
        
         
        
            
            
            DragOver
             
            
            鼠标拖动对象进入窗口之后,将会在窗口范围内移动,这时
            DoDragDrop就会调用IDropTarget的DragOver接口。其原形为:
            HRESULT DragOver(
            DWORD grfKeyState
            POINTL pt, 
            
            
            DWORD * pdwEffect 
            
            )
            相对来说对于这个接口方法的实现可以简单的多:只要根据
            grfKeyState判断键盘和鼠标的状态是否符合要求,根据pt传入的鼠标点判断该点是否支持拖放(比如将拖放区域限制在窗口的一部分的话),然后为*pdwEffect赋值为DROPEFFECT_COPY或DROPEFFECT_NONE.当然,还可以做一些你喜欢的事情,比如把鼠标坐标打印到屏幕上。不过为了性能和安全起见,建议不要做延时明显的操作。
             
            
             
            
                
                
                DragLeave:
                 
                
                这个方法没有传入参数,相当简单。
                当拖动的鼠标离开了窗口区域,这个方法将被调用,你可以在这里写一些清理内存的代码。在
                CDropTargetEx类里,由于在DragEnter里new了一些数据结构,并加到一个指针数组里,所以我必须在这里对此数据进行清理,对此结构里的STDMEDIUM调用ReleaseStgMedium然后Delete该结构。
                另外,如果需要的话,可以通知用户鼠标指针已经离开了拖放区域。
                
                 
                
                 
                
                    
                    Drop
                    如果鼠标没有离开窗口,而是在窗口内释放按纽,那么拖放时间的“放”就在这时发生,
                    IDropTarget接口的Drop方法被调用。其原形为
                    HRESULT Drop(
                    IDataObject * pDataObject, 
                    
                     
                    DWORD grfKeyState, 
                    POINTL pt, 
                    
                     
                    DWORD * pdwEffect 
                     
                    )
                    有些资料建议在这里才调用
                    pDataObject->GetData方法获取数据,在CDropTargetEx类里,数据实际上已经在DragEnter里获取了。这样做的理由是我希望一开始就获得数据,从它本身进行判断是否支持拖放,而不是在“放”的时候才判断是否合法数据。
                    既然数据已经获得,那么我就可以从保存数据的指针数组里提取出
                    STGMEDIUM数据来,并根据数据的具体格式进行处理(最后一定要记住对STGMEDIUM进行ReleaseStgMedium)
                    对于
                    CF_TEXT类型的数据,STGMEDIUM的成员hGlobal里包含的是一段全局内存数据。获取这些数据的方法是:
                    
                    
                    TCHAR *pBuff = NULL;
                    pBuff=(LPSTR)GlobalLock(hText);
                    GlobalUnlock(hText);
                    
                    则得到一个指向内存数据的指针pBuff。在我这个例子里一般是一段
                    "\0"结尾的文本字符串。这样就实现了文本的拖放。
                    对于
                    CF_HDROP类型的数据,STGMEDIUM成员hGlobal是一个HDROP类型的句柄。通过这个句柄,可以获得拖放的文件列表。如:
                    BOOL CDropTargetEx::ProcessDrop(HDROP hDrop)
                    {
                    UINT iFiles,ich =0;
                    TCHAR Buffer[MAX_PATH]="";
                    memset(&iFiles,0xff,sizeof(iFiles));
                    
                    
                    int Count = ::DragQueryFile(hDrop,iFiles,Buffer,0); //Get the Drag _Files Number.
                    
                     
                    if(Count)
                    for (int i=0;i<Count;i++)
                    {
                    if(::DragQueryFile(hDrop,i,Buffer,sizeof(Buffer)))
                    {
                    
                    
                    
                    
                    
                    
                    
                    
                    //Got the FileName in Buffer
                    
                    }
                    }
                     
                    ::DragFinish(hDrop);
                    
                    
                    return true; 
                    
                    }
                    
                    获得的
                    Buffer是就是拖放的文件名,如果拖放的是多个文件,在for循环里可以依次获取这些文件的文件名。这样就实现了文件的拖放。
                     
                    
                     
                    
                     
                    
                    CDropTargetEx
                    类使用非常简单:
                    在客户窗口的相关文件中,定义一个
                    CDropTargetEx实例:CDropTargetEx DropTarget;
                    在窗口创建之后,将窗口句柄进行拖放注册: 
                    
                    DropTarget.DragDropRegister(hWnd);
                    
                    或者 
                    
                    DropTarget.DragDropRegister(hWnd,MK_CONTROL|MK_LBUTTON);
                    
                    表示鼠标左键按下并且按住
                    Ctrl键的拖放有效;
                    对于获取拖放的结果,我使用的是回调函数方式:
                    回调原形
                    typedef VOID (_stdcall *DROPCALLBACK)(LPCSTR Buffer,int type);
                    在适当的地方(比如窗口的实现
                    CPP里)定义函数DropCallback:
                    void _stdcall DropCallBack(LPCSTR Buffer,int type)
                    
                    并且将其地址赋于
                    DropTarget实例:
                    DropTarget.SetCallBack(DropCallBack);
                    
                    这样,拖放文本到客户窗口,回调函数将被调用,参数
                    Buffer为拖放的文本,format为CF_TEXT。而拖放文件的时候,对每个被拖放的文件都调用一次回调函数,参数Buffer为文件全路径名,format为CF_HDROP。
                    示例的
                    DropCallBack代码为:
                    void _stdcall DropCallBack(LPCSTR Buffer,int format)
                    {
                    switch(format)
                    {
                    case CF_TEXT:
                    {
                    SetWindowText(hEdit,Buffer);
                    break;
                    }
                    case CF_HDROP:
                    {
                    TCHAR Buf[2048]="";
                    sprintf(Buf,"File : <%s> is Drag and Drop to this Windows ,Open it?",Buffer);
                    if(MessageBox(hMainWnd,Buf,"Question",MB_YESNO)==IDYES)
                    {
                    ShellExecute(0,"open",Buffer,"","",SW_SHOW);
                    }
                    }
                    default:
                    
                    
                    
                    
                    break;
                    
                    }
                    }
                    
                     
                    
                    总结
                    :使用IDropTarget实现通用的拖放,只要实现其7个接口,并且对得到的IDataObject用正确的格式(FORMATETC)调用正确的GetData获取数据,返回DROPEFFECT决定拖放的特征和结果,并处理拖放结果即可。
                    要注意的小问题是:
                    
                        
                        - 要调用OleInitialize而不是CoInitialize或CoInitializeEx对COM进行初始,否则RegisterDragDrop将不会成功,返回的错误是E_OUTOFMEMORY--内存不够,无法进行该操作。
                        
 
                        
                         - 调用ReleaseStgMedium释放STGMEDIUM里的数据,而不是直接对其hGlobal成员调用CloseHandle.
                        
 
                        
                         - 拖放操作关系到两个进程的数据交换,会将两个进程都堵塞,直到拖放完成为止,所以,在接管拖放的接口方法中,不要进行过于耗时的运算。
                        
 
                         
                    
                    这个例子相当简单,还可以简化,比如取消
                    vector,将获得HGLOBAL句柄作为成员变量存储,或者将获取数据的操作全部放到Drop方法里。
                    对于拖放文件,还有一个更简单的方法:响应
                    WM_DROPFILES消息。步骤是:
                    
                    DropAccepFiles,使该窗口可以接受文件拖放。
                     
                    
                    响应WM_DROPFILES消息,其wParam就是HDROP句柄
                     
                    
                    对此句柄调用DropQueryFiles获取拖放文件列表并结束拖放,参见上面关于ProcessDrop的代码
                     
                    对于拖放的全面阐述,请参见
                    MSDN->PlatformSDK Document->User Interface Services->Windows Shell里关于“Transferring Shell Objects with Drag-and-Drop and the Clipboard”一章。Windows Shell系统提供了很多接口,让用户利用和扩充这些接口,很方便的开发和使用丰富的shell服务,确实是一种很聪明的设计。
                    
                     
                    
                    
                    附
                    例子和代码