2408,02资管与拖放
原文
介绍
演示如何勾挂拖放,这样程序可接受资管窗口的拖放,并变成拖放源,这样用户可拖文件到资管窗口中.
示例项是一个MFC应用,本文假定你熟悉C++,MFC及COM对象和接口的使用.
该程序是MultiFiler,一个小工具,其作用类似拖放"临时区".可拖放多个文件到MultiFiler中,它再按列表显示所有文件.
然后,可分别用上档或Ctrl键,告诉资管,移动或复制原文件,来拖动文件回资管窗口.
使用资管拖放
资管允许你在资管窗口和桌面间拖放文件.当你开始拖放操作时,从拖放的资管窗口(放源),创建一个实现IDataObject接口的COM对象,并在该对象中放入一些数据.
拖放进的窗口(放目标),然后使用IDataObject方法读取该数据;因此它知道正在拖放的文件.
如果使用ClipSpy等查看器检查IDataObject中包含的数据,会看到资管,在数据对象中放置了多种数据格式.
重要格式是CF_HDROP.其他格式是资管注册供其自己使用的自定义格式.如果编写一个按放目标注册其窗口的应用,且知道如何读取CF_HDROP数据,则可接受拖放文件.
同样,如果可用CF_HDROP数据填充数据对象,资管让应用变成拖放源.则,CF_HDROP格式?
DROPFILES数据结构
CF_HDROP格式到底是什么?表明,它只是一个DROPFILES结构.还有个只是DROPFILES结构指针的HDROP类型.
DROPFILES是个简单结构.以下是它的定义:
struct DROPFILES
{DWORD pFiles; //文件列表偏移,POINT pt; //(客户坐标)放置点,BOOL fNC; //是在非客户区,且`pt`在屏幕坐标BOOL fWide; //宽符标志
};
结构定义中没有列举文件名列表.按双无效结尾的串列表表示该列表.但是它实际上在fWide成员后立即存储它,pFiles保存列表在内存中(相对结构开头)的偏移.
拖放中使用的唯一其他成员是,指示文件名是美标符还是统一符的fWide.
接受从资管中拖放
接受拖放比启动拖放更简单.
窗口有两个方法可接受拖放.或从窗口3.1保留的使用WM_DROPFILES消息.或按OLE放目标注册窗口.
老办法WM_DROPFILES
要用旧方法,首先在窗口中设置"接收文件"风格.
如果想要运行时设置此风格,调用DragAcceptFiles()API,该API带两个参数.第一个是主窗口句柄,第二个是表示可接受拖放的真.
如果主窗口是CView而不是对话框,则需要运行时设置此风格.
总之,你的窗口会变成放目标.当你从资管窗口拖放文件或目录到窗口中时,该窗口会收到一条WM_DROPFILES消息.
WM_DROPFILES消息的WPARAM是一个列举了要拖放文件的HDROP.有三个API可来从HDROP中取文件列表:DragQueryFile(),DragQueryPoint()和DragFinish().
1,DragQueryFile()干2件事:返回正在拖放的文件数,并枚举文件列表.DragQueryPoint()返回DROPFILES结构的pt成员.DragFinish()释放在拖放过程中分配的内存.
2,DragQueryFile()带四个参数:HDROP,要返回的文件名的索引,调用者分配的用来保存名字的缓冲及缓冲(按符)的大小.
如果按-1索引传递,则DragQueryFile()返回列表中的文件数.否则,它返回文件名中的字符数.你可对照0测试此返回值,以判断是否成功调用.
3,DragQueryPoint()带两个参数:HDROP和接收DROPFILES结构的pt成员中值的点结构的指针.DragFinish()只接受一个HDROP参数.
典型的WM_DROPFILES处理器如下:
void CMyDlg::OnDropFiles ( HDROP hdrop )
{UINT uNumFiles;TCHAR szNextFile [MAX_PATH];//取被拖放的文件的`#`.uNumFiles = DragQueryFile ( hdrop, -1, NULL, 0 );for ( UINT uFile = 0; uFile < uNumFiles; uFile++ ){//从`HDROP`信息中取下个文件名.if ( DragQueryFile ( hdrop, uFile, szNextFile, MAX_PATH ) > 0 ){//操作`szNextFile`中名字.}}//释放内存.DragFinish ( hdrop );
}
如果只想要文件列表,则不需要DragQueryPoint().
新方法,使用OLE放目标
按OLE放目标注册窗口来接受拖放.一般,这样做需要编写实现IDropTarget接口的C++类.但是,MFC有一个COleDropTarget类可帮助处理它.
根据主窗口是对话框还是CView,该过程会略有不同.
把CView设为放目标
CView已内置了一些拖放支持,但一般不会激活它.要激活它,需要添加COleDropTarget成员变量到视图中,然后在视图的OnInitialUpdate()中调用其Register()函数,来把视图变成放目标,如下:
void CMyView::OnInitialUpdate()
{CView::OnInitialUpdate();//按放目标注册视图.`m_droptarget`是`CMyView`的`COleDropTarget`成员.m_droptarget.Register ( this );
}
完成后,覆盖当用户拖放你的视图时调用的四个虚函数:
1,OnDragEnter():光标进入窗口时调用.
2,OnDragOver():光标移进窗口时调用.
3,OnDragLeave():光标离开窗口时调用.
4,OnDrop():用户在你的窗口放置时调用.
OnDragEnter()
OnDragEnter()是调用的第一个函数.它的原型是:
DROPEFFECT CView::OnDragEnter( COleDataObject* pDataObject, DWORD dwKeyState, CPoint point );
参数是:
1,pDataObject:包含正在拖放数据的COleDataObject指针.
2,dwKeyState:一组指示点击了哪个鼠标按钮及按下哪些(如果有)上档键标志.标志包括MK_CONTROL,MK_SHIFT,MK_ALT,MK_LBUTTON,MK_MBUTTON和MK_RBUTTON.
3,点:按视图客户坐标表示的光标位置.
OnDragEnter()返回一个告诉OLE是否接受放置的DROPEFFECT值,如果接受,则应显示哪个光标.值及其含义为:
1,DROPEFFECT_NONE:不接受该放置.光标将变为停止.
2,DROPEFFECT_MOVE:按放目标移动数据.光标将变为移动.
3,DROPEFFECT_COPY:按放目标复制数据.光标将变为复制:
4,DROPEFFECT_LINK:按放目标链接数据.光标将变为复制链接.
一般,在OnDragEnter()中,可检查正在拖放的数据,并查看它是否符合条件.如果不符合,则返回DROPEFFECT_NONE以拒绝拖放.
否则,可相应返回其他值之一.
OnDragOver()
如果从OnDragEnter()返回的值不是DROPEFFECT_NONE,则每当鼠标光标移进窗口时,都会调用OnDragOver().OnDragOver()的原型是:
DROPEFFECT CView::OnDragOver ( COleDataObject* pDataObject, DWORD dwKeyState, CPoint point );
参数和返回值与OnDragEnter()相同.OnDragOver()允许根据光标位置和上档键状态返回不同DROPEFFECT值.
如,如果主视图窗口有几个显示不同信息列表的区域,且只想在一个部分放置,需要检查点参数中的光标位置,如果光标不在该区域,则返回DROPEFFECT_NONE.
对上档键,一般会如下响应它们:
1,按下上档键(在dwKeyState中是MK_SHIFT):返回DROPEFFECT_MOVE.
2,按下CONTROL键(MK_CONTROL):返回DROPEFFECT_COPY.
3,都按了(MK_SHIFT|MK_CONTROL):返回DROPEFFECT_LINK.
这些只是准则,但最好遵守它们.
如,在MultiFiler中,OnDragOver()总是返回DROPEFFECT_COPY.只需确保返回正确的值,这样光标准确地用户指示如果放置进窗口会怎样.
OnDragLeave()
如果用户拖出窗口而不放置,则调用OnDragLeave().原型是:
void CView::OnDragLeave();
它无参或返回值,目的是让你清理在OnDragEnter()和OnDragOver()时分配的内存.
OnDrop()
如果用户拖过你的窗口(且没有从最近一次调用OnDragOver()返回DROPEFFECT_NONE),则会调用OnDrop(),这样可操作拖放.OnDrop()的原型是:
BOOL CView::OnDrop ( COleDataObject* pDataObject, DROPEFFECT dropEffect, CPoint point );
dropEffect参数等于OnDragOver()的最后返回值,其他参数与OnDragEnter()相同.如果成功拖放,则返回值为真,否则为假.
OnDrop()是动作的地方,可任意操作数据.在MultiFiler中,把放置文件添加到主窗口的列表控件中.
按放目标设置对话框
如果主窗口是个对话框(或不是从CView继承的内容),则情况会稍微困难一些.因为基本COleDropTarget实现按仅适合CView继承的窗口设计,因此需要从COleDropTarget继承一个新类,并覆盖上述四种方法.
典型的COleDropTarget继承类声明如下:
class CMyDropTarget : public COleDropTarget
{
public:DROPEFFECT OnDragEnter ( CWnd* pWnd, COleDataObject* pDataObject, DWORD dwKeyState, CPoint point );DROPEFFECT OnDragOver ( CWnd* pWnd, COleDataObject* pDataObject, DWORD dwKeyState, CPoint point );BOOL OnDrop ( CWnd* pWnd, COleDataObject* pDataObject, DROPEFFECT dropEffect, CPoint point );void OnDragLeave ( CWnd* pWnd );CMyDropTarget ( CMyDialog* pMainWnd );virtual ~CMyDropTarget();
protected:CMyDialog* m_pParentDlg; //在`构造器`中初化>
};
在此例中,给构造器传递了主窗口的指针,因此放目标方法可发送消息,并在对话框中干其他操作.
然后,可实现前面描述的四种拖放方法.唯一区别是附加的调用时光标所悬停窗口指针的CWnd*参数.
有该新类后,把放目标成员变量添加到对话框中,并在OnInitDialog()中,调用其Register()函数:
BOOL CMyDialog::OnInitDialog()
{//按放目标注册对话.`m_droptarget`是`CMyDialog`的`CMyDropTarget`成员.m_droptarget.Register ( this );
}
访问CDataObject中的HDROP数据
如果使用OLE放目标,则拖放函数接收COleDataObject指针.这是个实现IDataObject并包含在开始拖放时拖放源创建的所有数据的MFC类.
你需要一些代码来在数据对象中,查找CF_HDROP并取HDROP句柄.取得HDROP后,可如前用DragQueryFile()读取已放置文件的列表.
以下是从COleDataObject取HDROP的代码:
BOOL CMyDropTarget::OnDrop ( CWnd* pWnd, COleDataObject* pDataObject, DROPEFFECT dropEffect, CPoint point )
{HGLOBAL hg;HDROP hdrop;//从`数据`对象取`HDROP`数据.hg = pDataObject->GetGlobalData ( CF_HDROP );if ( NULL == hg )return FALSE;hdrop = (HDROP) GlobalLock ( hg );if ( NULL == hdrop ){GlobalUnlock ( hg );return FALSE;}//在此处读文件列表...GlobalUnlock ( hg );//返回`TRUE/FALSE`以指示成功/失败
}
两个方法总结
处理WM_DROPFILES:
1,窗口3.1的保留;未来可能会删除它.
2,无法自定义拖放过程,只能在拖放后操作.
3,不能检查被拖放的原始数据.
4,如果不需要优雅的自定义,该方法更容易编码.
使用OLE放目标:
1,使用COM接口,这是个现代且更好支持的机制.
2,CView和COleDropTarget提供良好的MFC支持.
3,允许完全控制拖放操作.
4,允许访问原始IDataObject,这样可访问任意数据格式.
5,需要更多的代码,但一旦编写一次,就可剪切其并粘贴到新项中.
MultiFiler如何接受拖放
当用户拖放到MultiFiler窗口上时,把所有放置文件都添加到列表控件中.
MultiFiler会自动消除重复文件,因此在列表中文件只出现一次.
启动拖放
要让资管接受拖放文件,只需创建一些CF_HDROP数据,并把它放进数据对象中.
因为它的大小并不总是相同,创建DROPFILES结构有点麻烦.
当你在列表控件中,选择文件并拖放它们时,MultiFiler会启动拖放操作.发生时,该控件会发送一条LVN_BEGINDRAG通知消息,这样MultiFiler会创建数据对象,并移交给OLE以开始拖放操作.
创建DROPFILES的步骤如下:
1,在列表控件中,枚举所有选中项,在串列表中放入它们.
2,跟踪添加到串列表中的每个串的长度.
3,为DROPFILES自身和文件名列表分配内存.
4,填充DROPFILES成员.
5,复制文件名列表到分配的内存中.
现在,介绍MultiFiler代码,这样你可确切地了解如何设置DROPFILES.
第一步是在一个列表中放入所有选中的文件名,并跟踪保存所有串的内存.
void CMultiFilerDlg::OnBegindragFilelist(NMHDR* pNMHDR, LRESULT* pResult)
{
CStringList lsDraggedFiles;
POSITION pos;
CString sFile;
UINT uBuffSize = 0;//对列表中的每个选中项,在`lsDraggedFiles`中放入文件名.`c_FileList`是对话框的`CListCtrl`.pos = c_FileList.GetFirstSelectedItemPosition();while ( NULL != pos ){nSelItem = c_FileList.GetNextSelectedItem ( pos );//在`sFile`中放入文件名sFile = c_FileList.GetItemText ( nSelItem, 0 );lsDraggedFiles.AddTail ( sFile );//计算保存此串期望的#符数.uBuffSize += lstrlen ( sFile ) + 1;}
此时,uBuffSize保存(按符)包括无效的所有串的总长度.最后加1,表示无效来终止列表,然后乘以sizeof(TCHAR)以按字节转换符.
然后,添加sizeof(DROPFILES)以取得最终期望缓冲大小.
//为最终的`无效`符和`DROPFILES`结构的大小额外加1.
uBuffSize = sizeof(DROPFILES) + sizeof(TCHAR) * (uBuffSize + 1);
现在知道需要多少内存,可分配它.拖放操作时,使用GlobalAlloc()从堆中分配内存:
HGLOBAL hgDrop;
DROPFILES* pDrop;//对`DROPFILES`结构,从堆中分配内存.hgDrop = GlobalAlloc ( GHND | GMEM_SHARE, uBuffSize );if ( NULL == hgDrop )return;
然后,用GlobalLock()直接访问内存:
pDrop = (DROPFILES*) GlobalLock ( hgDrop );
if ( NULL == pDrop ){GlobalFree ( hgDrop );return;}
现在可开始填充DROPFILES.GlobalAlloc()调用中的GHND标志初化内存为零,因此只需要设置几个成员:
//填充`DROPFILES`结构.pDrop->pFiles = sizeof(DROPFILES);
#ifdef _UNICODE//如果要为`统一`编译,请在结构中设置`统一`标志以指示它包含`统一`串.pDrop->fWide = TRUE;
#endif
注意,pFiles成员不指示DROPFILES结构的大小;它是filelist的偏移.但是因为文件列表在结构尾的正后,因此其偏移与结构的大小相同.
现在可复制所有文件名到内存中,然后解锁缓冲.
TCHAR* pszBuff;//在`DROPFILES`结构结束后的内存中,复制所有文件名.pos = lsDraggedFiles.GetHeadPosition();pszBuff = (TCHAR*) (LPBYTE(pDrop) + sizeof(DROPFILES));while ( NULL != pos ){lstrcpy ( pszBuff, (LPCTSTR) lsDraggedFiles.GetNext ( pos ) );pszBuff = 1 + _tcschr ( pszBuff, '\0' );}GlobalUnlock ( hgDrop );
下一步是构造一个COleDataSource对象,并在里面放置数据.还需要一个描述(CF_HDROP)剪切板格式的FORMATETC结构和数据的(HGLOBAL)存储方式.
COleDataSource datasrc;
FORMATETC etc = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };//在数据源中放入数据.datasrc.CacheGlobalData ( CF_HDROP, hgDrop, &etc );
现在,可启动拖放操作,但还有个细节要注意.因为MultiFiler接受拖放操作,因此它会很高兴地接受我们启动的拖放操作.
因此,给数据源添加另一位数据,来注册自定义剪切板格式.OnDragEnter()和OnDragOver()函数检查此格式,如果有它,则不接受.
HGLOBAL hgBool;hgBool = GlobalAlloc ( GHND | GMEM_SHARE, sizeof(bool) );if ( NULL == hgBool ){GlobalFree ( hgDrop );return;}//在数据源中放入数据.etc.cfFormat = g_uCustomClipbrdFormat;datasrc.CacheGlobalData ( g_uCustomClipbrdFormat, hgBool, &etc );
注意,不必按指定值设置数据.
现在已把数据放在一起,可开始拖放操作了!调用COleDataSource的在完成拖放前不会返回的DoDragDrop()方法.
唯一参数是多个指示允许用户操作的DROPEFFECT值.它返回一个指示用户想对数据的操作的DROPEFFECT值,或是否中止拖放或未被目标接受的DROPEFFECT_NONE.
DROPEFFECT dwEffect;dwEffect = datasrc.DoDragDrop ( DROPEFFECT_COPY | DROPEFFECT_MOVE );
这里,只允许复制和移动.在拖放过程中,用户可按Ctrl或上档键来更改操作.
有时,传递DROPEFFECT_LINK不会使资管创建快捷方式,因此上面调用中不包含DROPEFFECT_LINK.
DoDragDrop()返回后,检查返回值.如果是DROPEFFECT_MOVE或DROPEFFECT_COPY,则成功拖放,因此从主窗口的列表控件中删除所有选中的文件.
如果是DROPEFFECT_NONE,事情就有点麻烦了.
在NT上,必须手动检查是否已移动文件,如果是,则从列表控件中删除它.
如果对MultiFiler的工作原理感兴趣,请查看MultiFiler源码.
最后是,如果取消拖放,则释放分配的内存.如果完成,则放目标拥有内存,不能释放它.下面是检查DoDragDrop()返回值的代码.
switch ( dwEffect ){case DROPEFFECT_COPY:case DROPEFFECT_MOVE:{//复制或移动`文件`.注意:不要调用`GlobalFree()`,因为放目标释放了`数据`.**省略了来删除列表控件项的代码. **}break;case DROPEFFECT_NONE:{//**省略了NT/2000的代码,来检查操作是否真成功.**拖放操作未被接受或被取消,因此应该调用GlobalFree()清理. GlobalFree ( hgDrop );GlobalFree ( hgBool );}break;}
}
其他详情
还可右击MultiFiler列表控件,以取包含四个命令的环境菜单.它们来管理列表中的选择和清理列表,非常简单.
壳支持叫CLSID_DragDropHelper的新coclass,它有两个接口:IDragSourceHelper和IDropTargetHelper.
IDropTargetHelper绘画拖放图像.它有四个名字应该很熟悉的方法:DragEnter(),DragOver(),DragLeave()和Drop().
你只需普通拖放处理,确定返回的DROPEFFECT,然后调用与COleDropTarget方法对应的IDropTargetHelper方法.
IDropTargetHelper方法需要DROPEFFECT来正确绘画拖放图像,因此需要先确定它.
如果查看使用CView的MultiFiler示例,会看到两个成员变量:
IDropTargetHelper* m_piDropHelper;
bool m_bUseDnDHelper;
在视图的构造器中,代码创建拖放助手COM对象并取IDropTargetHelper接口.根据此操作是否成功,设置m_bUseDnDHelper,这样其他函数知道是否可用该COM对象.
CMultiFilerView::CMultiFilerView() : m_bUseDnDHelper(false), m_piDropHelper(NULL)
{//创建`壳`拖放助手对象的实例.if ( SUCCEEDED( CoCreateInstance ( CLSID_DragDropHelper, NULL, CLSCTX_INPROC_SERVER, IID_IDropTargetHelper, (void**) &m_piDropHelper ) )){m_bUseDnDHelper = true;}
}
然后,四个拖放函数调用IDropTargetHelper方法.下面是一例:
DROPEFFECT CMultiFilerView::OnDragEnter(COleDataObject* pDataObject, DWORD dwKeyState, CPoint point)
{
DROPEFFECT dwEffect = DROPEFFECT_NONE;//**省略,来确定dwEffect的代码.**调用拖放助手.if ( m_bUseDnDHelper ){//`DnD`助手需要一个`IDataObject`接口,因此请从`COleDataObject`取一个接口.注意,`假`参数表明`GetIDataObject`不会处理`AddRef()`返回的接口,因此不要`Release()`它. IDataObject* piDataObj = pDataObject->GetIDataObject ( FALSE );m_piDropHelper->DragEnter ( GetSafeHwnd(), piDataObj, &point, dwEffect );}return dwEffect;
}
GetIDataObject()是COleDataObject中返回IDataObject接口的一个未记录的函数.
最后,视图的析构器释放COM对象.
CMultiFilerView::~CMultiFilerView()
{if ( NULL != m_piDropHelper )m_piDropHelper->Release();
}
顺便,如果没有安装PlatformSDK,则可能没有IDropTargetHelper接口和关联GUID的定义.我在每个MultiFiler示例中都包含了必要的定义;
只需取消注释它们,你就可以开始了.
如果想在当MultiFiler是拖放源时,使用IDragSourceHelper绘画整齐的拖放图像,则要自己摸索了.
