使用IDropTarget接口同时支持文本和文件拖放
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接口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 和 STDMEDIUMFORMATETC是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 该结构。另外,如果需要的话,可以通知用户鼠标指针已经离开了拖放区域。
如果鼠标没有离开窗口,而是在窗口内释放按纽,那么拖放时间的“放”就在这时发生,
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 { if(::DragQueryFile(hDrop,i,Buffer,sizeof(Buffer))) { //Got the FileName in Buffer } } return true; } 获得的 CDropTargetEx类使用非常简单: 在客户窗口的相关文件中,定义一个CDropTargetEx实例:CDropTargetEx DropTarget; 在窗口创建之后,将窗口句柄进行拖放注册: DropTarget.DragDropRegister(hWnd); 或者 DropTarget.DragDropRegister(hWnd,MK_CONTROL|MK_LBUTTON); 表示鼠标左键按下并且按住 对于获取拖放的结果,我使用的是回调函数方式: 回调原形 在适当的地方(比如窗口的实现 void _stdcall DropCallBack(LPCSTR Buffer,int type) 并且将其地址赋于 DropTarget.SetCallBack(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; } } 总结:使用 要注意的小问题是: 这个例子相当简单,还可以简化,比如取消 对于拖放文件,还有一个更简单的方法:响应 对于拖放的全面阐述,请参见 附