Drag & Drop 全解析

 
一、基本概念
拖放,是指用鼠标拖动的方法,在不同程序的窗口之间、同一个程序的不同窗口之间或同一程序同一窗口的不同控件之间,进行移动、复制和粘贴等操作的技术。拖放操作是在操作系统的帮助下完成的。被拖动的对象首先向操作系统注册它使用的数据格式,并按指定的数据格式提供数据,拖放操作结束时,接收拖放的窗口按指定的数据格式提取有关数据,并根据提取的数据生成相应的对象。
二、两种拖放方式
拖放有两种类型: OLE 拖放和文件管理器拖放。这两种方式是完全不同的机制。文件管理器拖放只能处理文件名,通过映射目的窗口的 WM_DROPFILES 消息,窗口就可以收到拖放进来的文件名。 OLE 拖放则更加通用一些,它允许你拖放可同时被保存在剪贴板上的任何数据。本文首先介绍文件管理器拖放,然后再介绍 OLE 拖放,最后给出一个用 OLE 实现的,支持文件拖放操作的增强列表控件 CListCtrlEx
三、文件管理器拖放原理及实例
这种方式的实质就是产生一个消息 WM_DROPFILES 。技术上没有什么难点,主要用到下面几个 API 函数: DragQueryFile DragQueryPoint DragFinish 。它们的原型和注解分别如下:
UINT DragQueryFile(HDROP hDrop, UINT iFile, LPTSTR lpszFile, UINT cch)
本函数用来取得拖放的文件名。其中, hDrop 是一个指向含有被拖放的文件名的结构体的句柄; iFiles 是要查询的文件序号,因为一次可能同时拖动很多个文件; lpszFiles 是出口缓冲区指针,保存 iFiles 指定序号的文件的路径名, cch 指定该缓冲区的大小。有两点值得注意,第一,如果我们在调用该函数的时候,指定 iFile 0xFFFFFFFF ,则 DragQueryFile 将忽略 lpszFile cch 参数,返回本次拖放操作的文件数目;第二,如果指定 lpszFile NULL ,则函数将返回实际所需的缓冲区长度。
BOOL DragQueryPoint(HDROP hDrop, LPPOINT lppt);
本函数用来获取,当拖放操作正在进行时,鼠标指针的位置。第二个参数 lppt 是一个指向 POINT 结构体的指针,用来保存文件放下时,鼠标指针的位置。窗口可以调用该函数以查询文件是否落在自己的窗口矩形中。
void DragFinish(HDROP hDrop);
当拖放操作处理完毕后需调用该函数释放系统分配来传输文件名的内存。
首先,建立一个对话框工程,确保选中对话框的 Accept Files 属性。如果不选,也可以在窗口创建的时候(譬如 OnCreate 函数中)调用 DragAcceptFiles(TRUE) ,效果是一样的。
然后映射 WM_DROPFILES 消息。该消息处理函数原型如下: void OnDropFiles(HDROP hDrop) ,注意入口参数为 HDROP 型,它是一个结构体指针,所指向的结构体中包含了被拖放的文件的名称。接下来我们主要要完成两个动作:第一,通过调用 DragQueryFile 并指定其 iFile 参数为 0xFFFFFFFF ,得到本次拖放操作的文件数目;第二步,用一个循环依次取出各个文件名。示例如下:
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);
}
四、OLE拖放原理
MFC 为实现对象拖放提供了如下类: COleDataSource COleDataObject COleDropTarget COleDropSource 。下面分别介绍它们,然后通过一个实例讲述实现的具体步骤。
·   COleDataSource
启动一次拖放操作,保存拖放的数据,并向系统提供拖放对象的数据。类中重要的成员函数根据用途分为如下三种:
1 用于设定提供数据的方式和使用的数据格式。
提供数据的方式有两种,一种是即时方式,另一种是延迟方式。延迟方式不需要立即提供数据,当需要提供数据时,系统将调用对应的函数来获得数据,一般都是重载 OnRenderData 函数或其他虚函数,以响应数据请求。数据格式可以是 CF_TEXT 等常用的剪贴板格式,也可以是自己利用函数 RegisterClipboardFormat 函数注册的特定格式。
CacheData :提供指定格式的数据,格式由结构 STGMEDIUM 指定,即时方式;
CacheGlobalData :利用全局句柄 HGLOBAL ,为指定格式提供数据,即时方式,适用小数据量;
DelayRenderData :使用延迟方式按指定格式提供数据,当系统需要数据时,会调用函数 OnRenderGlobalData/OnRenderData 来取得数据;
DelayRenderFileData :使用延迟方式利用 CFile 为指定格式提供数据,当需要数据时,会调用函数 OnRenderFileData 来取得数据;
2 响应请求,提供数据
OnRenderFileData :为延迟方式提供 CFile 型数据。
OnRenderGlobalData :为延迟方式提供 HGLOBAL 数据。
OnRenerData :为延迟方式提供各种所支持的类型的数据。
3 实施拖放操作
DoDragDrop :开始实施拖放操作
 
·   COleDataObject
用于代表拖放的数据,它是作为 COleDataSource 类的成员,类中主要成员函数有:
BeginEnumFormat :为枚举数据格式作准备;
 
 
GetNextFormat :返回下一个数据格式;
IsDataAvailable 检查指定的数据格式是否可用;
GetData :按指定数据格式,获得数据;
GetFileData :按指定数据格式,获得 CFile 型数据;
GetGlobalData :按指定数据格式,获得 HGLOBAL 型数据;
 
·   COleDropTarget
用于在窗口和 OLE 库之间提供通讯机制。任何一个窗口,要想能够接收拖放,必须包含一个 COleDropTarget 对象,并注册之。其中的成员函数可分为两大类:
1 注册
Register :注册该对象,以便使窗口能够接收拖放对象
2 响应拖放过程中的动作 ( 虚函数 )
OnDragEnter :当鼠标首次进入窗口时被调用;
OnDragLeave :当鼠标移出窗口时被调用;
OnDragOver :当鼠标停留在窗口内时,被重复调用;
OnDrop 当鼠标在窗口内落下被调用;
虚函数 onDragEnter OnDragOver 的返回值具有重要的含义,一般为以下三种之一:
DROPEFFECT_MOVE :移动操作,允许对象落在此窗口,落下时要删除原来的对象;
DROPEFFECT_COPY :复制操作,允许对象落在此窗口,落下时不删除原来的对象;
DROPFFECT_NONE :不允许对象落在此窗口;
 
·   COleDropSource
COleDropSource 允许数据被拖放到一个拖放目标,它负责对何时启动一个拖放操作进行决断,反馈拖放操作状态,以及判断拖放操作何时结束。这个类比较简单,用得也较少。它的成员函数只有三个:
GiveFeedback :用于改变拖放期间鼠标的光标,把拖放状态反馈给用户知晓;
OnBeginDrag :在拖放期间捕捉鼠标指针,当应用程序框架觉得可能要发生一个拖放操作时,它会调用该函数;
QueryContinueDrag :检测拖放操作是否还在继续中。
 
五、OLE拖放实现
MFC 本身的 CView 类是支持拖放操作的,通过研究 CView 类的源码,大体知道它的实现原理是这样的: CView 类中有一个 COleDropTarget 类的对象,在视图窗口初始化时,调用 COleDropTarget 类成员函数 Register() ,以此在系统中注册该视图窗口为拖放接收窗口。当进行拖放操作的鼠标指针处于视图窗口范围内时, COleDropTarge 类会做出反应,它的 OnDragEnter OnDragOver OnDropEx OnDrop 等成员函数被依次调用,这些函数默认均是调用与其相对应的 CView 类成员函数 OnDragEnter OnDragOver OnDropEx OnDrop 等,程序员只需重载这些 CView 类成员函数,即可对拖动的过程及结果进行控制。
因为 COleDropTarget 默认只对 CView 提供支持,所以如果要让其他的窗口支持拖放,我们必须同时对要支持拖放的窗口类和 COleDropTarget 类进行派生。把对拖放操作具体进行处理的代码封装成派生窗口类的成员函数,然后重载 COleDropTarget 中对应的五个虚函数,当它接收到拖放动作时,调用窗口派生类的处理函数即可。但这里有一个问题,就是我们怎么知道何时调用派生类的处理函数呢?答案是运用 RTTI 技术。如果 COleDropTarget 派生类收到的窗口指针类型,就是我们派生的窗口类,那么就调用它的处理函数,否则调用基类进行处理。
首先生成一个对话框工程,添加二个新类。
第一个类名为 CListCtrlEx ,父类为 CListCtrl 。添加完毕后,在 CListCtrlEx 的定义头文件中加入 DECLARE_DYNAMIC(CListCtrlEx) ,在其实现文件中加入 IMPLEMENT_DYNAMIC(CListCtrlEx,CListCtrl) ,这样就对 CListCtrlEx 类添加了 RTTI 运行期类型识别( Run Time Type Information )支持。
第二个类名为 COleDropTargetEx ,父类为 COleDataTarget
CListCtrlEx 中添加 COleDropTargetEx 类的对象,并添加下列公有虚函数的声明:
       virtual BOOL Initialize();
       virtual DROPEFFECT OnDragOver(CWnd* pWnd, COleDataObject* pDataObject, DWORD dwKeyState, CPoint point);
       virtual DROPEFFECT OnDropEx(CWnd* pWnd, COleDataObject* pDataObject, DROPEFFECT dropEffect, DROPEFFECT dropList, CPoint point);
       virtual BOOL OnDrop(CWnd* pWnd, COleDataObject* pDataObject, DROPEFFECT dropEffect, CPoint point);
       virtual void OnDragLeave(CWnd* pWnd);
Initialize 函数用于注册 CListCtrlEx 成为拖放接收窗口;
OnDragOver 在拖放鼠标进入窗口时被调用。此函数的返回值决定了后续的动作的类型:如果返回 DROPEFFECT_MOVE ,则产生一个剪切动作;如果返回 DROPEFFECT_COPY ,则产生一个复制动作,如果返回 DROPEFFECT_NONE ,则不会产生拖放动作,因为 OnDropEx OnDrop 函数将不会被调用( OnDragLeave 函数仍会被调用)。
OnDropEx 函数会在 OnDrop 函数之前调用,如果 OnDropEx 函数没有对拖放动作进行处理,则应用程序框架会接着调用 OnDrop 函数进行处理。所以必须要在派生类中重载 OnDropEx 函数——即使什么动作都都没有做——否则我们的 OnDrop 函数将不会被执行到,因为没有重载的话,将会调用基类的 OnDropEx 函数,而基类的 OnDropEx 函数对拖放是进行了处理的——尽管不是我们所想要的动作。当然你也可以把对拖放进行处理的动作放在 OnDropEx 中——那样就不需要重载 OnDrop 了。
OnDragLeave 函数会在鼠标离开窗口时被调用,在此可以进行一些简单的清理工作。譬如在 OnDragEnter 或者 OnDragOver 函数中,我们改变了光标的形态,那么此时我们就应该把光标恢复过来。
这些函数中最重要的是 OnDrop 函数,拖放动作将在此进行处理,它的全部源码如下:
BOOL CListCtrlEx::OnDrop(CWnd* pWnd, COleDataObject* pDataObject, DROPEFFECT dropEffect, CPoint point)
{
       UINT              nFileCount = 0;
       HDROP           hDropFiles = NULL;
       HGLOBAL        hMemData = NULL;
 
       AfxMessageBox("OnDrop");
       if(pDataObject->IsDataAvailable(CF_HDROP))
       {
              hMemData = pDataObject->GetGlobalData(CF_HDROP);
              hDropFiles = (HDROP)GlobalLock((HGLOBAL)hMemData); // 锁定内存块
              if(hDropFiles != NULL)
              {
                 
char chTemp[_MAX_PATH+1] = {0};
                     nFileCount = DragQueryFile(hDropFiles, 0xFFFFFFFF, NULL, 0);
                     for(UINT nCur=0; nCur 遍历取得每个文件名
                     {
                            ZeroMemory(chTemp, _MAX_PATH+1);
                DragQueryFile(hDropFiles, nCur, (LPTSTR)chTemp, _MAX_PATH+1);
                            AddAllFiles(chTemp);
                     }
              }
              GlobalUnlock(hMemData);
              return TRUE;
       }
       else
       {
              return FALSE;
       }
}
在第二个类 COleDropTarget 中添加如下对应的函数:
    virtual DROPEFFECT OnDragEnter(CWnd* pWnd, COleDataObject* pDataObject, DWORD dwKeyState, CPoint point);
    virtual DROPEFFECT OnDragOver(CWnd* pWnd, COleDataObject* pDataObject, DWORD dwKeyState, CPoint point);
       virtual DROPEFFECT OnDropEx(CWnd* pWnd, COleDataObject* pDataObject, DROPEFFECT dropEffect, DROPEFFECT dropList, CPoint point);
       virtual BOOL OnDrop(CWnd* pWnd, COleDataObject* pDataObject, DROPEFFECT dropEffect, CPoint point);
       virtual void OnDragLeave(CWnd* pWnd);
它们的动作都差不多:先用 RTTI 判断窗口指针 pWnd 的类型,如果是 CListCtrlEx ,则调用 CListCtrlEx 中对应的处理函数,否则调用基类的处理函数。以 OnDrop 为例:
BOOL COleDropTargetEx::OnDrop(CWnd* pWnd, COleDataObject* pDataObject, DROPEFFECT dropEffect, CPoint point)
{
       CListCtrlEx*     pListCtrlEx = NULL;
      
       ASSERT_VALID(this);
       ASSERT(IsWindow(pWnd->m_hWnd));
      
       if(pWnd->IsKindOf(RUNTIME_CLASS(CListCtrlEx)))
       {
              pListCtrlEx = (CListCtrlEx*)pWnd;
              return pListCtrlEx->OnDrop(pWnd, pDataObject, dropEffect, point);
       }
       else
       {
 
return COleDropTarget::OnDrop(pWnd, pDataObject, dropEffect, point);    
       }
}
 
至此,我们成功地为 CListCtrlEx 添加了文件拖入操作的支持。一个完整的拖放操作,还包括拖出动作,所以必须要为该类再添加拖出操作,即,将列表中的某一项或者多项拖出成为一个文件。这就需要用到另一个类: COleDataSource 。具体步骤如下:
CListCtrlEx 中加入一个 COleDataSource 的实例,并映射列表框的 LVN_BEGINDRAG 消息处理函数,在此我们添加拖出操作的代码。
实现拖出非常简单,只需要依次调用 COleDataSource 的三个函数即可: Empty 用于清空原先对象中缓存的数据, CacheGlobalData 用来缓存数据以进行拖放操作,最后调用 DoDragDrop 启动本次拖放操作。
但在调用之前,必须要做一些准备工作。主要的任务就是创建一个 DROPFILES 结构体,并拷贝要拖放的文件名到结构体后的内存中。 DROPFILES 结构体定义了 CF_HDROP 剪贴板格式,紧跟它后面的是一系列被拖放文件的路径名。它的定义如下:
typedef struct _DROPFILES
{
    DWORD     pFiles;  // 文件名起始地址
    POINT      pt;     // 鼠标放下的位置,坐标由 fNC 成员指定
    BOOL        fNC;    // TRUE 表示适用屏幕坐标系,否则使用客户坐标系
    BOOL        fWide;  // 文件名字符串是否使用宽字符
} DROPFILES, FAR* LPDROPFILES;
拖放之前的准备动作的代码如下:
uBufferSize = sizeof(DROPFILES) + uBufferSize + 1;
    hMemData = GlobalAlloc(GPTR,uBufferSize);
    ASSERT(hMemData != NULL);
      
       lpDropFiles = (LPDROPFILES)GlobalLock(hMemData); // 锁定之 , 并设置相关成员
       ASSERT(lpDropFiles != NULL);
       lpDropFiles->pFiles = sizeof(DROPFILES);
#ifdef _UNICODE
       lpDropFiles->fWide = TRUE;
#else
       lpDropFiles->fWide = FALSE;
#endif
 
       // 把选中的所有文件名依次复制到 DROPFILES 结构体后面 ( 全局内存中 )
       pItemPos = strSelectedList.GetHeadPosition();
       pszStart = (char*)((LPBYTE)lpDropFiles + sizeof(DROPFILES));
       while(pItemPos != NULL)
       {
              lstrcpy(pszStart, (LPCTSTR)strSelectedList.GetNext(pItemPos));
        pszStart = strchr(pszStart,'/0') + 1; // 下次的起始位置是上一次结尾 +1
       }
准备完毕之后就可以进行拖放了,拖放动作有 DoDragDrop 函数触发,其原型如下:
DROPEFFECT DoDragDrop(
DWORD dwEffects = DROPEFFECT_COPY|DROPEFFECT_MOVE|DROPEFFECT_LINK, LPCRECT lpRectStartDrag = NULL,
COleDropSource* pDropSource = NULL
);
这里, dwEffects 指定了允许施加于本 COleDataSource 实例之上的动作集:剪切、复制或无动作。
    lpRectStartDrag 指示拖放操作真正开始的矩形,如果鼠标没有移出该矩形,则拖放操作视作放弃处理。如果本成员设为 NULL ,则该起始矩形将为一个像素大小。
 
 pDropSource 表明拖放所使用的 COleDataSource 对象。
而该函数的返回值,则表明本次拖放操作所实际产生的效果,至于具体产生何种效果,则由系统决定。譬如在拖放时按住 Shift 键,将产生剪切效果;按住 Ctrl 键,将产生复制效果,等等。
拖放的代码如下:
       m_oleDataSource.Empty();
       m_oleDataSource.CacheGlobalData(CF_HDROP, hMemData);
       DropResult = m_oleDataSource.DoDragDrop(DROPEFFECT_MOVE|DROPEFFECT_COPY);
最后一点要注意的是,在 Windows NT 4.0 以上的系统中,即使实际产生的是 DROPEFFECT_MOVE 动作, DoDragDrop 函数也只返回 DROPEFFECT_NONE 。产生这个问题的原因在于, Windows NT 4.0 Shell 会直接移动文件本身来对移动操作进行优化。返回值 DROPEFFECT_MOVE 最初的含义,就是通知执行拖放操作的应用程序去删除原位置上的文件。但是因为 Shell 已经替应用程序完成了这个(删除)动作,所以,函数返回 DROPEFFECT_NONE 。要想知道文件是否真的被移动了也很简单,只要在函数返回之后检查一下原位置上的文件是否存在就可以了。
Windows 9x 系列的操作系统也会对移动进行同样的优化动作,但是它不会返回 DROPEFFECT_NONE 来代替 DROPEFFECT_MOVE 。详细的解释参见 MS 知识库 Q182219

一、对象拖放

对象拖放是指对某一指定的对象,利用鼠标拖动的方法,在不同应用的窗口之间、同一

应用的不同窗口之间或同一应用的同一窗口内进行移动、复制(粘贴)等操作的技术。

对象拖放是在操作系统的帮助下完成的。被拖动的对象首先指定使用的数据格式,并按

指定的数据格式提供数据;在拖放结束时,接收拖放对象的窗口按指定的数据格式提取

有关数据,并根据提取的数据生成对象。

二、MFC中用于对象拖放的类

MFC为实现对象拖放提供以下类:

1、C01eDataSouroe用于启动一次拖放操作,并向系统提供拖放对象的数据。类中重要

的成员函数有如下三种:

a.设定提供数据的方式和使用的数据格式,提供数据的方式有两种,一种是即时方式,

另一种是延迟方式;延迟方式不需要立即提供数据,当需要提供数据时,系统将请求

数据,应当重载onRendrData()或其他虚函数,以响应对数据的请求。

数据格式可以是CF—TEXT等常用的剪贴板格式,也可以是自己利用

函数RegisterClipboardFormat(…)函数注册的特殊格式。 CacheData() 利用结构STGMEDIUM,为指定格式提供数据

CacheGlobalData() 利用全局句柄HGLOBAL,为指定格式提供数据

DelayRenderData() 使用延迟方式按指定格式数据提供数据

DelayRenderFileData() 使用延迟方式利用CFile为指定格式提供数据

b.响应请求,提供数据

onRenderFileData() 为延迟方式提供CFile型数据。

onRenderGlobalData() 为延迟方式提供HGLOBAL数据。

onRenerData 为延迟方式提供各种所支持的类型的数据。

c.实施拖放操作

DoDragDrop() 开始实施拖放操作

2、C01eDataTarget用于接收拖放对象的目标窗口,一个窗口要想能够接收拖放对象,必

须包含一个C01eDataTarget对象,并注册该对象。类中主要成员函数。

a.注册

Register() 并注册该对象,以便使窗口能够接收拖放对象

b.响应拖放过程中的动作(虚成员函数)

onDragEnter() 当鼠标首次进入窗口时被调用

onDragLeave() 当鼠标移出窗口时被调用

onDragOver() 当鼠标在窗口内,重复调用被调用

onDrop() 当鼠标在窗口内落下被调用

虚函数onDragEnter()和OnDragOver()的返回值具有重要的含义,为以下数值之一;

DROPEFFECT_MOVE:允许对象落在此窗口,落下时要删除原业的对象

DROPEFFECT_COPY:允许对象落在此窗口,落下时不删除原来的对象

DROPFFECT_NONE: 不允许对象落在此窗口

3、COleDataObject用于接收拖放对象,类中主要成员函数有:

a.确定可以使用的数据格式

BeginEnumFormats() 为枚举数据格式作准备

GetNextFormat() 返回下一个数据格式

IsDataAvailable() 检查指定的数据格式是否可用

b.获取数据

GetData() 按指定数据格式,获得数据

GetFileData() 按指定数据格式,获得GFile型数据

GetGloba1Data() 按指定数据格式,获得HGLOBAL型数据

三、利用MFC实现对象拖放

1、拖放操作的启动

拖放操作的启动

拖放操作一般是从单击鼠标左键开始。在消息WM_LBUTTONDOWN的响应函数

onLButtonDown(…)中,首先要判定是否选定了某一对象,如果未选定或选定多个,

则不能进行拖放操作;如果选定了一个对象,则可以进行拖放操作。

要启动一次拖放操作,需要先准备一个COleDataSource对象。注意到类

COleClientIten和类COleServerItem都是从类COleDataSource上派生的,如果选定

的是COleClientItem对象或者是COleClientItem对象,则可以直接使用所选定的对象;

否则,需要生成一个COleDataSourxe对象,值得注意的是:需要象上文中所说的,应

该指定使用的数据格式,并按指定格式提供对象的有关数据。

下面给出准备数据源的例子:

class myDataSource:public COleDataSource

{

public:

CString str;

protected:

virtual BOOL onRenderFileData(LPFORMATETC,CFile*);

//.....

};

BOOL myDataSource::onRenderFileData(LPFORMATETC lpFormatEtc,CFile*pFile)

{

if(lpFormatEtc->cfFormat==CF_TEXT)

{

pFile,Write(str,str.GetLength());

return TRUE;

}

COleDataSource::OnRenderFileDataSource::OnRenderFileData(lpFormatEtc,pFile);

return FALSE;

}

程序中可以按如下方式,指定数据格式。

myDataSource*pItemDragDrop=new myDataSource;

pItemDragDrop->str="0123456789ABCDEF";

pItemDragDrop->Delayrenderfiledata(CF_TEXT,NULL);

准备好COleDataSource对象之后,调用此对象的成员函数DoDragDrop(...)启动对象拖

放操作。

需要注意的是,函数DoDragDrop(...)并不立即返回,而是要等到鼠标按钮弹起之后。

从执行函数DoDragDrop(...)到从该函数返回,鼠标经过的窗口,接收到一系列的消息,

这些窗口可以根据消息作出相应的动作。

启运拖放操作,可以使用以下语句:

int DragEffect=pItemDragDrop->DoDragDrop(rect,ptOffset,FALSE,DROPEFFECT_MOVE);

2、拖放对象的接收缺省情况下,一般的窗口是不能接收拖放对象的。要使窗口可以接收

拖放对象,需要在窗口类定义中加放成员对象COleDataTarget,并在生成窗口时调用函数

COledataTarger::register().

例如:
Class myView : public CScro11View
{
private:
COleDataTarget oleTarget;
protected:
virtual int OnCreate(LPCREATESTRUCT);
//.....
}
int myView::onCreate(LPCREATESTRUCT lpCreateStruct)
{
if(CScrollView::OnCreate(lpCreateStruct)==-1)
return -1;
dropTarget.Register(this);
return 0;
}
为实现拖放对象的接收,还应为消息

WM_DRAGENTER,WM_DRAGMOVE,WM_DRAGLEVER,WM_DRAGDROP提供响应函数。

消息WM_DRAGMOVE的响应函数OnDragMove(...)应根据鼠标在窗口中的位置,返回以下

数值:

DROPEFFECT_MOVE——表明可以把对象复制到现在的窗口、现在的位置

DROPEFFECT_CIPY——表明可以把对象从原来的窗口、原来的位置移到现在的窗口、现

在的位置

DROPEFFECT_NONE——表明不能在该窗口的该位置放下

例如:

DROPEFFECT myView::OnDragEnter(COleData ject*pDataobject,DWORD dwKeyState,CPoint point)

{

return DROPEFFECT_MOVE;

}

DROPEFFECT myView::OnDragOver(ColeDatalbject*pDataobject, DWORD dwKeyState,CPoint point)

{

return DROPEFFECT_MOVE:

}

消息WM_DRAGDROP的响应函数OnDrop(COleDataject*pDataObject,DROPEFFECT

dropEffect,CPoint point)应处理拖动对象放下后的工作。该函数的参数pDataobjec指

向一个COledataObject对象,利用指针,可以获取有关数据。该函数的一般实现是:

a.检查对象的数据格式:利用COleDataObject::IsDataAvailable()

b.按指定的格式获取数据:利用COleDataObject::GetFileData()等函数

c.建立对象(可能与原对象同,也可能不建立对象,仅使用对象中的数据):利用以上步

骤得到的数据。

例如:

CString str;

myDataSource*pMyData;

if(IsDataAvailable(CF_TEXT))

{

CFile file=GetFileData(CF_TEXT);

file.Read(str,16);

CClientDC dc(this);

dc.TextOut(100,50,str,16);

pMyData=new myDataSource;

pMyData->str=str;

}

对于COleClientItem或COleServerItem对象,可以按以下方法重建对象:

COleClient*pItem=GetDocument()->CreateNewItem();

pItem->CreateForm(pDataObject)

3、拖放操作的结束函数DoDragDrop()返回时,拖放过程结束。函DoDragDrop()的返回

值,表明了对象的拖放结果。

DROPEFFECT_MOVE:对象被放到他处,需删除原对象

DROPEFFECT_COPY:对象被复制到他处,不删除原对象 DROPEFFECT_NONE:未能实现拖效,无需删除原对象

例如:

int DragEffect=pItemTracking->DoDranDrop(rect,ptOffset,FALSE,DROPEFFECT_MOVE);
switch(DragEffect)

{

case DROPEFFECT_MOVE:

delete pItemTracking;

GetDocument()->UpdateAllltems(NULL);

GetDocument()->UpdateAllliews(NULL);

break;

case DROPEFFECT_MOVE;

case DROPEFFECT_NONE:

default:

break;

}

CListCtrlEx:一个支持文件拖放和实时监视的列表控件——用未公开API函数实现Shell实时监视

一、需求
无论何时,当你在Explorer窗口中创建、删除或重命名一个文件夹/文件,或者插入拔除移动存储器时,Windows总是能非常快速地更新它所有的视图。有时候我们的程序中也需要这样的功能,以便当用户在Shell中作出创建、删除、重命名或其他动作时,我们的应用程序也能快速地随之更新。
二、原理
Windows
内部有两个未公开的函数(注:在最新的MSDN中,已经公开了这两个函数),分别叫做SHChangeNotifyRegisterSHChangeNotifyDeregister,可以实现以上的功能。这两个函数位于Shell32.dll中,是用序号方式导出的。这就是为什么我们用VC自带的Depends工具察看Shell32.dll时,找不到这两个函数的原因。SHChangeNotifyRegister的导出序号是2;而SHChangeNotifyDeregister的导出序号是4
SHChangeNotifyRegister
可以把指定的窗口添加到系统的消息监视链中,这样窗口就能接收到来自文件系统或者Shell的通知了。而对应的另一个函数,SHChangeNotifyDeregister,则用来取消监视钩挂。SHChangeNotifyRegister的原型和相关参数如下:
ULONG SHChangeNotifyRegister
(         
HWND  hwnd,
    int   fSources,
    LONG  fEvents,
    UINT    wMsg,
    Int  cEntries,
    SHChangeNotifyEntry *pfsne
);
其中:
hwnd
将要接收改变或通知消息的窗口的句柄。
fSource
指示接收消息的事件类型,将是下列值的一个或多个(注:这些标志没有被包括在任何头文件中,使用者须在自己的程序中加以定义或者直接使用其对应的数值)
SHCNRF_InterruptLevel
0x0001
。接收来自文件系统的中断级别通知消息。
SHCNRF_ShellLevel
0x0002
。接收来自ShellShell级别通知消息。
SHCNRF_RecursiveInterrupt
0x1000
。接收目录下所有子目录的中断事件。此标志必须和SHCNRF_InterruptLevel 标志合在一起使用。当使用该标志时,必须同时设置对应的SHChangeNotifyEntry结构体中的fRecursive成员为TRUE(此结构体由函数的最后一个参数pfsne指向),这样通知消息在目录树上是递归的。
SHCNRF_NewDelivery
0x8000
。接收到的消息使用共享内存。必须先调用SHChangeNotification_Lock,然后才能存取实际的数据,完成后调用SHChangeNotification_Unlock函数释放内存。
fEvents
要捕捉的事件,其所有可能的值请参见MSDN中关于SHChangeNotify函数的注解。
wMsg
产生对应的事件后,发往窗口的消息。
cEntries
pfsne
指向的数组的成员的个数。
pfsne
SHChangeNotifyEntry
结构体数组的起始指针。此结构体承载通知消息,其成员个数必须设置成1,否则SHChangeNotifyRegister或者SHChangeNotifyDeregister将不能正常工作(但是据我试验,如果cEntries设为大于1的值,依然可以注册成功,不知何故)。
如果函数调用成功,则返回一个整型注册标志号,否则将返回0。同时系统就会将hwnd指定的窗口加入到操作监视链中,当有文件操作发生时,系统会向hwnd标识的窗口发送wMsg指定的消息,我们只要在程序中加入对该消息的处理函数就可以实现对系统操作的监视了。
如果要退出程序监视,就要调用另外一个未公开得函数SHChangeNotifyDeregister来取消程序监视。该函数的原型如下:
BOOL SHChangeNotifyDeregister(ULONG ulID);
其中ulID指定了要注销的监视注册标志号,如果卸载成功,返回TRUE,否则返回FALSE
三、实例
在前一文中,我们派生了一个支持文件拖放的列表控件CListCtrlEx,但它还有一个小小的缺憾,就是当用户把一个文件拖放进来之后,如果用户在Shell中对该文件进行移动、删除、重命名等操作时,CListCtrlEx将不能保证实时更新。经过上面的讨论,现在就让我们为CListCtrlEx加上实时文件监控功能。
在使用这两个函数之前,必须要先声明它们的原型,同时还要添加一些宏和结构定义。我们在原工程中添加一个ShellDef.h头文件,然后加入如下声明:
#define SHCNRF_InterruptLevel  0x0001 //Interrupt level notifications from the file system
#define SHCNRF_ShellLevel   0x0002 //Shell-level notifications from the shell
#define SHCNRF_RecursiveInterrupt  0x1000 //Interrupt events on the whole subtree
#define SHCNRF_NewDelivery   0x8000 //Messages received use shared memory

typedef struct
{
    LPCITEMIDLIST pidl; //Pointer to an item identifier list (PIDL) for which to receive notifications
    BOOL fRecursive; //Flag indicating whether to post notifications for children of this PIDL
}SHChangeNotifyEntry;

typedef struct
{
    DWORD dwItem1;  // dwItem1 contains the previous PIDL or name of the folder.
    DWORD dwItem2;  // dwItem2 contains the new PIDL or name of the folder.
}SHNotifyInfo;

typedef ULONG
(WINAPI* pfnSHChangeNotifyRegister)
(
 HWND hWnd,
 int  fSource,
 LONG fEvents,
 UINT wMsg,
 int  cEntries,
 SHChangeNotifyEntry* pfsne 
);

typedef BOOL (WINAPI* pfnSHChangeNotifyDeregister)(ULONG ulID);
这些宏和函数的声明,以及参数含义,如前所述。下面我们要在CListCtrlEx体内添加两个函数指针和一个ULONG型的成员变量,以保存函数地址和返回的注册号。
接下来我们为CListCtrlEx类添加一个公有成员函数Initialize,在其中,我们首先进行加载Shell32.dll以及初始化函数指针动作,接着调用注册函数向Shell注册。用户在使用我们提供的CListCtrlEx类时,应当在CListCtrlEx创建完毕后跟着调用Initialize函数以确保注册了消息监视钩子。否则将失去实时监视功能。初始化函数如下(已略去涉及OLE初始化的部分):
BOOL CListCtrlEx::Initialize()
{
 
…………
 //
加载Shell32.dll
 m_hShell32 = LoadLibrary("Shell32.dll");
 if(m_hShell32 == NULL)
 {
  return FALSE;
 }

 //取函数地址
 m_pfnDeregister = NULL;
 m_pfnRegister = NULL;
 m_pfnRegister = (pfnSHChangeNotifyRegister)GetProcAddress(m_hShell32,MAKEINTRESOURCE(2));
 m_pfnDeregister = (pfnSHChangeNotifyDeregister)GetProcAddress(m_hShell32,MAKEINTRESOURCE(4));
 if(m_pfnRegister==NULL || m_pfnDeregister==NULL)
 {
  return FALSE;
 }

 SHChangeNotifyEntry shEntry = {0};
 shEntry.fRecursive = TRUE;
 shEntry.pidl = 0;
 m_ulNotifyId = 0;
 
 //
注册Shell监视函数
 m_ulNotifyId = m_pfnRegister(
        GetSafeHwnd(),
        SHCNRF_InterruptLevel|SHCNRF_ShellLevel,
        SHCNE_ALLEVENTS,
        WM_USERDEF_FILECHANGED, //
自定义消息
        1,
        &shEntry
       );
 if(m_ulNotifyId == 0)
 {
  MessageBox("Register failed!","ERROR",MB_OK|MB_ICONERROR);
  return FALSE;
 }
 return TRUE;
}
CListCtrlEx类中再添加如下函数。该函数的作用是从PIDL中解出实际字符路径。
CString CListCtrlEx::GetPathFromPIDL(DWORD pidl)
{
    char szPath[MAX_PATH];
    CString strTemp = _T("");
    if(SHGetPathFromIDList((struct _ITEMIDLIST *)pidl, szPath))
 {
        strTemp = szPath;
    }
    return strTemp;
}
   
现在我们的程序就可以接收到来自Shell或文件系统的通知消息了,只要编写一个处理自定义消息的函数,就可以对系统范围内的更改动作作出我们希望的响应动作。这里lParam参数中存放的是当前发生的事件(譬如SHCNE_CREATE),wParam参数中存放的是SHNotifyInfo结构。下面是一段框架代码:
LRESULT CListCtrlEx::OnFileChanged(WPARAM wParam, LPARAM lParam)
{
    CString  strOriginal = _T("");
 CString  strCurrent = _T("");
 SHNotifyInfo* pShellInfo = (SHNotifyInfo*)wParam;

 strOriginal = GetPathFromPIDL(pShellInfo->dwItem1);
 if(strOriginal.IsEmpty())
 {
  return NULL;
 }

 switch(lParam)
 {
 case SHCNE_CREATE:
  break;

 case SHCNE_DELETE:
  break;
 case SHCNE_RENAMEITEM:
  break;
    }
    return NULL; 
}
四、总结
至此我们完成了对CListCtrlEx的扩充工作,通过这两个未公开的API函数,编写一个文件夹/文件监视器不再是一件难事。当然同样的功能也可以通过编写设备驱动程序来,但这种方法来实现,难度大,周期长,开发上也有不少困难。

 UINT ThreadFunc(LPVOID pParam)
{
 HWND hwnd=(HWND)pParam;   //Window to notify
 HANDLE hChange = ::FindFirstChangeNotification(_T("C://"),
               TRUE,FILE_NOTIFY_CHANGE_FILE_NAME|FILE_NOTIFY_CHANGE_DIR_NAME);
 if(hChange==INVALID_HANDLE_VALUE)
 {
  return (UINT)-1;
 }
 while(...)
 {
  ::WaitForSingleObject(hChange,INFINITE);
  ::PostMessage(hwnd,WM_USER_CHANGE_NOTIFY,0,2);
  ::FindNextChangeNotification((hChange));        //Reset
 }
 ::FindCloseChangeNotification(hChange);
 return 0;
}

你可能感兴趣的:(Drag & Drop 全解析)