Window下拖放操作Drag & Drop 全解析

原文地址:http://blog.vckbase.com/isrobert/archive/2007/11/13/30625.html


一、基本概念

拖放,是指用鼠标拖动的方法,在不同程序的窗口之间、同一个程序的不同窗口之间或同一程序同一窗口的不同控件之间,进行移动、复制和粘贴等操作的技术。拖放操作是在操作系统的帮助下完成的。被拖动的对象首先向操作系统注册它使用的数据格式,并按指定的数据格式提供数据,拖放操作结束时,接收拖放的窗口按指定的数据格式提取有关数据,并根据提取的数据生成相应的对象。

二、两种拖放方式

拖放有两种类型:OLE拖放和文件管理器拖放。这两种方式是完全不同的机制。文件管理器拖放只能处理文件名,通过映射目的窗口的WM_DROPFILES消息,窗口就可以收到拖放进来的文件名。OLE拖放则更加通用一些,它允许你拖放可同时被保存在剪贴板上的任何数据。本文首先介绍文件管理器拖放,然后再介绍OLE拖放,最后给出一个用OLE实现的,支持文件拖放操作的增强列表控件CListCtrlEx

三、文件管理器拖放原理及实例

这种方式的实质就是产生一个消息WM_DROPFILES。技术上没有什么难点,主要用到下面几个API函数:DragQueryFileDragQueryPointDragFinish。它们的原型和注解分别如下:

UINT DragQueryFile(HDROP hDrop, UINT iFile, LPTSTR lpszFile, UINT cch)

本函数用来取得拖放的文件名。其中,hDrop是一个指向含有被拖放的文件名的结构体的句柄;iFiles是要查询的文件序号,因为一次可能同时拖动很多个文件;lpszFiles是出口缓冲区指针,保存iFiles指定序号的文件的路径名,cch指定该缓冲区的大小。有两点值得注意,第一,如果我们在调用该函数的时候,指定iFile0xFFFFFFFF,则DragQueryFile将忽略lpszFilecch参数,返回本次拖放操作的文件数目;第二,如果指定lpszFileNULL,则函数将返回实际所需的缓冲区长度。

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为实现对象拖放提供了如下类:COleDataSourceCOleDataObjectCOleDropTargetCOleDropSource。下面分别介绍它们,然后通过一个实例讲述实现的具体步骤。

·  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 当鼠标在窗口内落下被调用;

虚函数onDragEnterOnDragOver的返回值具有重要的含义,一般为以下三种之一:

DROPEFFECT_MOVE:移动操作,允许对象落在此窗口,落下时要删除原来的对象;

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

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

 

·  COleDropSource

COleDropSource允许数据被拖放到一个拖放目标,它负责对何时启动一个拖放操作进行决断,反馈拖放操作状态,以及判断拖放操作何时结束。这个类比较简单,用得也 较少。它的成员函数只有三个:

GiveFeedback:用于改变拖放期间鼠标的光标,把拖放状态反馈给用户知晓;

OnBeginDrag:在拖放期间捕捉鼠标指针,当应用程序框架觉得可能要发生一个拖放操作时,它会调用该函数;

QueryContinueDrag:检测拖放操作是否还在继续中。

五、OLE拖放的实现   

    MFC本身的CView类是支持拖放操作的,通过研究CView类的源码,大体知道它的实现原理是这样的:CView类中有一个COleDropTarget类的对象,在视图窗口初始化时,调用COleDropTarget类成员函数Register(),以此在系统中注册该视图窗口为拖放接收窗口。当进行拖放操作的鼠标指针处于视图窗口范围内时,COleDropTarge类会做出反应,它的OnDragEnterOnDragOverOnDropExOnDrop等成员函数被依次调用,这些函数默认均是调用与其相对应的CView类成员函数OnDragEnterOnDragOverOnDropExOnDrop等,程序员只需重载这些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,则不会产生拖放动作,因为OnDropExOnDrop函数将不会被调用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<nfilecount; font="" <="" ++ncur)="">遍历取得每个文件名
			{
				ZeroMemory(chTemp, _MAX_PATH+1);
				DragQueryFile(hDropFiles, nCur, (LPTSTR)chTemp, _MAX_PATH+1);
				AddAllFiles(chTemp);
			}
		}
		GlobalUnlock(hMemData);
		return TRUE;
	}
   
	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);
	}
	
	return COleDropTarget::OnDrop(pWnd, pDataObject, dropEffect, point);
}
 
//倒霉的64K限制,只能再截断了:(



至此,我们成功地为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.0Shell会直接移动文件本身来对移动操作进行优化。返回值DROPEFFECT_MOVE最初的含义,就是通知执行拖放操作的应用程序去删除原位置上的文件。但是因为Shell已经替应用程序完成了这个(删除)动作,所以,函数返回DROPEFFECT_NONE。要想知道文件是否真的被移动了也很简单,只要在函数返回之后检查一下原位置上的文件是否存在就可以了。

Windows 9x系列的操作系统也会对移动进行同样的优化动作,但是它不会返回DROPEFFECT_NONE来代替DROPEFFECT_MOVE

///
20071113  - 在CRichText控件中拖拽
void CDdssaaDlg::OnOLEDragDrop(LPDISPATCH FAR* Data, long FAR* Effect, short FAR* Button, short FAR* Shift, float FAR* x, float FAR* y) 
{
	CComQIPtr< IDataObject > piDataObj;
	piDataObj = *Data;
	COleDataObject psOleDataObj;
	psOleDataObj.Attach(piDataObj);
	UINT              nFileCount = 0;
	HDROP           hDropFiles = NULL;
	HGLOBAL        hMemData = NULL;
	if(psOleDataObj.IsDataAvailable(CF_HDROP)) 
	{
		hMemData = psOleDataObj.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 < nFileCount; nCur++)
			{
				//遍历取得每个文件名
				ZeroMemory(chTemp, _MAX_PATH+1);
				DragQueryFile(hDropFiles, nCur, (LPTSTR)chTemp, _MAX_PATH+1);
				MessageBox(chTemp);//打印文件名
			}
		}
		GlobalUnlock(hMemData);
	}
	psOleDataObj.Detach();
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值