DRAG & DROP
2011年03月08日
使用IDropTarget接口同时支持文本和文件拖放
关于Windows的外壳扩展编程,拖放是比较简单的一种,在网上可以找到不少介绍这个技巧的文章。大部分是介绍使用MFC的COleDropTarget实现的,
我觉得一般使用COleDropTarget已经很好了,但是我习惯在一些程序模块中,完全的不使用MFC,比如纯SDK编程,还有用在ATL的时候,MFC是相当累
赘的。所以COleDropTarget在这个意义上讲不够完美。
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(STGMEDIU M)); 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 is Drag and Drop to this Windows ,Open it?",Buffer);
if(MessageBox(hMainWnd,Buf,"Question",MB_YESNO)==I DYES)
{
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的代码
二、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; nCurm_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);
}
}
//倒霉的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|DROPEFF ECT_COPY);
最后一点要注意的是,在Windows NT 4.0以上的系统中,即使实际产生的是DROPEFFECT_MOVE动作,DoDragDrop函数也只返回DROPEFFECT_NONE。
产生这个问题的原因在于,Windows NT 4.0的Shell会直接移动文件本身来对移动操作进行优化。返回值DROPEFFECT_MOVE最初的含义,就是通知
执行拖放操作的应用程序去删除原位置上的文件。但是因为Shell已经替应用程序完成了这个(删除)动作,所以,函数返回DROPEFFECT_NONE。
要想知道文件是否真的被移动了也很简单,只要在函数返回之后检查一下原位置上的文件是否存在就可以了
2011年03月08日
使用IDropTarget接口同时支持文本和文件拖放
关于Windows的外壳扩展编程,拖放是比较简单的一种,在网上可以找到不少介绍这个技巧的文章。大部分是介绍使用MFC的COleDropTarget实现的,
我觉得一般使用COleDropTarget已经很好了,但是我习惯在一些程序模块中,完全的不使用MFC,比如纯SDK编程,还有用在ATL的时候,MFC是相当累
赘的。所以COleDropTarget在这个意义上讲不够完美。
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(STGMEDIU M)); 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 is Drag and Drop to this Windows ,Open it?",Buffer);
if(MessageBox(hMainWnd,Buf,"Question",MB_YESNO)==I DYES)
{
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的代码
二、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; nCurm_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);
}
}
//倒霉的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|DROPEFF ECT_COPY);
最后一点要注意的是,在Windows NT 4.0以上的系统中,即使实际产生的是DROPEFFECT_MOVE动作,DoDragDrop函数也只返回DROPEFFECT_NONE。
产生这个问题的原因在于,Windows NT 4.0的Shell会直接移动文件本身来对移动操作进行优化。返回值DROPEFFECT_MOVE最初的含义,就是通知
执行拖放操作的应用程序去删除原位置上的文件。但是因为Shell已经替应用程序完成了这个(删除)动作,所以,函数返回DROPEFFECT_NONE。
要想知道文件是否真的被移动了也很简单,只要在函数返回之后检查一下原位置上的文件是否存在就可以了