作者:vcbear 時間:2001-09-28 09:47 出處:互聯(lián)網(wǎng) 責(zé)編:chinaitpower
摘要:使用IDropTarget接口同時支持文本和文件拖放
使用IDropTarget接口同時支持文本和
文件拖放
vcbear關(guān)于Windows的外殼擴展編程,拖放是比較簡單的一種,在網(wǎng)上可以找到不少介紹這個技巧的文章。大部分是介紹使用MFC的COleDropTarget實現(xiàn)的,我覺得一般使用COleDropTarget已經(jīng)很好了,但是我習(xí)慣在一些
程序模塊中,完全的不使用MFC,比如純SDK編程,還有用在ATL的時候,MFC是相當累贅的。所以COleDropTarget在這個意義上講不夠完美。
參考了MSDN以及
www.CodeProject.com的相關(guān)文章和代碼(by Thomas Blenkers)之后,我發(fā)現(xiàn)拖放實際上主要使用了IDropTarget的接口方法,非常簡單,不妨直接面對原始IDropTarget實現(xiàn)自己的拖放類。
作為學(xué)習(xí)筆記,就有了這么一篇文字,以拋磚引玉:
IDropTarget是
系統(tǒng)留給支持拖放的客戶
程序的一個純虛接口,事先沒有對接口的任何函數(shù)進行實現(xiàn),而是讓用戶通過實現(xiàn)接口函數(shù)來接管拖放的結(jié)果。IDropTarget接口有以下成員函數(shù):
基本COM成員函數(shù)
QueryInterface
AddRef
Release
接管拖放事件的成員函數(shù):
DragEnter
DragOver
DragLeave
Drop
也就是說,要在客戶
程序里實現(xiàn)以上7個函數(shù)的實體。
系統(tǒng)在檢測到拖放發(fā)生的時候,會在合適的時候依次調(diào)用客戶
程序里實現(xiàn)的IDropTarget接口相應(yīng)函數(shù),檢查用戶在這些函數(shù)里返回的標志,決定鼠標外觀表現(xiàn)和拖放結(jié)果。
實現(xiàn)IDropTarget接口
為此建立一個基類為IDropTarget的類:
class CDropTargetEx : public IDropTarget
IDropTarget接口在OLEIDL.h里定義,為純虛接口。
在CDropTargetEx里依次聲明接口所包含的7個函數(shù),原形為:
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);
(為了實現(xiàn)Addref計數(shù),還有一個ULONG tb_RefCount成員變量是必須的。QueryInterface,AddRef,Release這3個函數(shù)的實現(xiàn)是COM知識中最基本的,請參見附例)
在講解IDropTarget其他函數(shù)的具體實現(xiàn)之前,有必要介紹一下一個你可能永遠不會直接調(diào)用但是確實存在的函數(shù):DoDragDrop函數(shù).此函數(shù)在某數(shù)據(jù)源的數(shù)據(jù)被拖動的時候就被調(diào)用,它負責(zé)
檢測目標窗口是否支持拖放,發(fā)現(xiàn)目標窗口的IDropTarget接口
隨時跟蹤鼠標和鍵盤的狀態(tài),根據(jù)狀態(tài)決定調(diào)用其DrageEnter,DragMove,Drop或DragLeave接口
從這些接口獲取客戶
程序的返回值,根據(jù)這些值和用戶界面以及數(shù)據(jù)源進行交互。
可以說DoDragDrop控制拖放的整個過程,我們要做的,只是將這個過程里發(fā)生的事件,接管下來并得到相應(yīng)的信息,和DoDragDrop進行交互而已。了解了這一點有助于我們理解為什么通過區(qū)區(qū)一個接口4個函數(shù)就可以實現(xiàn)了拖放的效果,因為
系統(tǒng)為我們已經(jīng)做了很多。
另一個非常重要的API是RegisterDragDrop,這個函數(shù)的原形是這樣的:
WINOLEAPI RegisterDragDrop(
HWND hwnd,
IDropTarget * pDropTarget
);
不用被WINOLEAPI嚇到,這是一個宏:
#define STDAPI EXTERN_C HRESULT STDAPICALLTYPE
也就是表示一個標準的WIN API函數(shù),返回一個HRESULT的值。
函數(shù)RegisterDragDrop的作用是告訴
系統(tǒng):某個窗口(hwnd參數(shù)指定)可以接受拖放,接管拖放的接口是pDropTarget。
記住在調(diào)用RegisterDragDrop之前,一定要先調(diào)用OleInitialize初始化OLE環(huán)境。
在類CDropTargetEx里設(shè)計了一個函數(shù)
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; }
}
在這個函數(shù)里調(diào)用RegisterDragDrop,將this指針傳入,表示本類實現(xiàn)了IDropTarget.,由本類接管拖放事件。另外順便定義了一下拖放鼠標和鍵盤特性常數(shù),對這個類來說,我希望默認的只接受鼠標左鍵的拖放,所以,默認的AcceptKeyState值是MK_LBUTTON。相關(guān)的鍵盤鼠標常數(shù)還有MK_SHIFT,MK_ALT,MK_RBOTTON,MK_MBUTTON,MK_BOTTON等幾個,我想這個幾個常數(shù)從字面上就可以理解它的意思了。這些常數(shù)可以用“位與”的操作組合。
以下具體討論IDropTarget的拖放相關(guān)接口函數(shù)(4個),這里的拖放對象以文本和
文件為主。
DragEnter
當你用鼠標選中了某一個
文件或一段文本,并且將鼠標移到某個可以接受拖放(已經(jīng)調(diào)用過RegisterDragDrop)的窗口里,DragEnter將第一時間被調(diào)用。再看一下其原形:
HRESULT DragEnter( IDataObject * pDataObject,
DWORD grfKeyState,
POINTL pt,
DWORD * pdwEffect )
pDataobject 是從拖放的原數(shù)據(jù)中傳遞過來的一個IDataObject接口實例,包含數(shù)據(jù)對象的一些相關(guān)方法,可以通過此接口獲得數(shù)據(jù)。
grfKeyState 為DragEnter被調(diào)用時當前的鍵盤和鼠標的狀態(tài),包含上面介紹過的鍵盤鼠標狀態(tài)常數(shù)。
pt 表示鼠標所在的點。是以整個屏幕為參考坐標的。
pdwEffect 是DoDragDrop提供的一個DWORD指針,客戶
程序通過這個指針給DoDragDrop返回特定的狀態(tài)。有效的狀態(tài)包括:
DROPEFFECT_NONE=0 表示此窗口不能接受拖放。
DROPEFFECT_MOVE=1 表示拖放的結(jié)果將使源對象被刪除
DROPEFFECT_COPY=2 表示拖放將引起源對象的復(fù)制。
DROPEFFECT_LINK =4 表示拖放源對象創(chuàng)建了一個對自己的連接
DROPEFFECT_SCROLL=0x80000000表示拖放目標窗口正在或?qū)⒁M行卷滾。此標志可以和其他幾個合用
對于拖放對象來說,一般只要使用DROPEFFECT_NONE和DROPEFFECT_COPY即可。
在DragEnter里要做什么呢?主要是告知拖放已經(jīng)進入窗口區(qū)域,并判斷是否支持某具體類型的拖放。
首先,要判斷鍵盤的狀態(tài)。在調(diào)用DragDropRegister時我傳入了一個AcceptKeyState并將其保存在m_AcceptKeyState成員變量里,現(xiàn)在可以拿它跟這里得到的grfKeyState比較:
if(grfKeyState!=m_AcceptKeyState )
{
*pdwEffect = DROPEFFECT_NONE;
return S_OK;
}
如果鍵盤和鼠標的狀態(tài)和我期望的不一樣,那么pdwEffect里返回DROPEFFECT_NONE表示不接受拖放。
然后,判斷拖放過來的IDataObject對象里有沒有我感興趣的數(shù)據(jù)。
這里要介紹的是兩個關(guān)鍵的結(jié)構(gòu)體FORMATETC和STDMEDIUM
FORMATETC是OLE數(shù)據(jù)交換的一個關(guān)鍵結(jié)構(gòu),對某種設(shè)備,數(shù)據(jù),和相關(guān)媒體做了格式上的描述。
其定義為
typedef struct tagFORMATETC
{
CLIPFORMAT cfFormat;
DVTARGETDEVICE *ptd;
DWORD dwAspect;
LONG lindex;
DWORD tymed;
}FORMATETC, *LPFORMATETC;
在這里我們最感興趣的是cfFormat和tymed兩個數(shù)據(jù)。cfFormat是標準的“粘帖板”數(shù)據(jù)類型比如CF_TEXT之類。tymed表示數(shù)據(jù)所依附的媒介,比如內(nèi)存,磁盤
文件,存儲對象等等。其他的成員可以參見MSDN。
一個典型的FORMATETC結(jié)構(gòu)變量定義如下:
FORMATETC cFmt = {(CLIPFORMAT) CF_TEXT, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
IDataObject提供了一個GetData接口來獲取其實例里包含的數(shù)據(jù),比如:
STGMEDIUM stgMedium;
ret = pDataObject->GetData(&cFmt, &stgMedium);
GetData傳入cFmt,以指出所感興趣的數(shù)據(jù),并將返回在stgMedium結(jié)構(gòu)里。
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;
看起來頗為復(fù)雜,其實主要是一系列句柄或數(shù)據(jù)對象接口的聯(lián)合,根據(jù)數(shù)據(jù)具體的類型,使用其中之一即可。tymed和FORMATETC里一樣,指出數(shù)據(jù)的載體類型(遺憾的是它不能指出具體的標準類型比如CF_TEXT或者其他)。至于pUnkForRelease,是源數(shù)據(jù)指定的一個接口,用來傳遞給ReleaseStgMedium函數(shù),如果它不為NULL,則ReleaseStgMedium函數(shù)使用這個接口釋放數(shù)據(jù)。如果為NULL,則ReleaseStgMedium函數(shù)使用默認的IUnknown接口。對于常規(guī)的拖放來說,這個對象指針應(yīng)該為NULL.
得到了句柄或數(shù)據(jù)對象接口,也相當于得到了拖放的數(shù)據(jù)。
定義一個特定的FORMATETC結(jié)構(gòu)實例傳遞給IDataObject的GetData,可以直接詢問和獲取某一種特定的數(shù)據(jù)。如果我們對我們想要的數(shù)據(jù)是非常確定的,這是比較有效率的方法。但是如果我們期望能夠?qū)ν戏诺膶ο筮M行自適應(yīng)的話,我們可以采取枚舉IDataObject里包含的所有數(shù)據(jù)類型的方案。這就要用到IEnumFORMATETC 接口了。
IEnumFORMATETC接口從IDataObject接口里獲?。?div style="height:15px;">
IEnumFormatETC *pEnumFmt = NULL;
ret = pDataObject->EnumFormatEtc (DATADIR_GET,&pEnumFmt);
如果獲取成功,則可以通過IEnumFORMATETC接口的Next方法,來枚舉所有的數(shù)據(jù)格式:
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;
}
}
第一個參數(shù)表示一次獲取的FORMATETC結(jié)構(gòu)數(shù)據(jù)的數(shù)量,cFmt是一個FORMATETC指針,指向一個數(shù)據(jù)緩沖,用來返回FORMATETC數(shù)據(jù)。,Fetched是Next調(diào)用后得到的FORMATETC數(shù)據(jù)個數(shù)。一般一次獲取一個,直到Next返回不為S_OK。
我們可以對每個得到cFmt調(diào)用IDataObject->GetData方法,但是一般來說,一個數(shù)據(jù)對象包含的數(shù)據(jù)不止一種,而且一般有一些自定義的數(shù)據(jù)類型(關(guān)于自定義數(shù)據(jù)類型,參見:RegisterClipboardFormat,如果要自己實現(xiàn)Drag/Drop源數(shù)據(jù),這個函數(shù)是有用的),對此我們不感興趣,因為這里只要求處理文本和
文件的拖動,為此,只處理cfFormat為CF_TEXT和CF_HROP的數(shù)據(jù):
GetDragData為CDropTargetEx類的一個成員函數(shù):
///////////////////////////////////////////////////
//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;
}
在這個成員函數(shù)里,根據(jù)cFmt,調(diào)用IDataObject->GetData函數(shù)獲得數(shù)據(jù)(對于CF_TEXT和CF_HROP來說,數(shù)據(jù)的媒介載體tymed都是HGLOBAL類型的)。
在具體實現(xiàn)的時候,我定義了一個結(jié)構(gòu):
typedef struct _DRAGDATA
{
int cfFormat;
STGMEDIUM stgMedium;
}DRAGDATA,*LPDRAGDATA;
將STGMEDIUM和數(shù)據(jù)類型(比如CF_TEXT,記錄在cfFormat)都記錄在DRAGDATA里。并且使用了一個vector數(shù)組,將這個結(jié)構(gòu)保存在數(shù)組里。對于不是我們想要的STGMEDIUM數(shù)據(jù),我們馬上調(diào)用ReleaseStgMedium函數(shù)進行釋放,免得造成內(nèi)存泄露。
這樣,DragEnter的工作就基本完成了,最后需要做的就是給DoDragDrop返回相應(yīng)的狀態(tài):如果我們獲得了想要的數(shù)據(jù)就給* pdwEffect賦值為DROPEFFECT_COPY,否則,就是DROPEFFECT_NONE;
如果支持拖放,鼠標形狀將變成一個有接受意義的圖標,否則,是一個拒絕意義的圖標。
DragOver
鼠標拖動對象進入窗口之后,將會在窗口范圍內(nèi)移動,這時DoDragDrop就會調(diào)用IDropTarget的DragOver接口。其原形為:
HRESULT DragOver(
DWORD grfKeyState
POINTL pt,
DWORD * pdwEffect
)
相對來說對于這個接口方法的實現(xiàn)可以簡單的多:只要根據(jù)grfKeyState判斷鍵盤和鼠標的狀態(tài)是否符合要求,根據(jù)pt傳入的鼠標點判斷該點是否支持拖放(比如將拖放區(qū)域限制在窗口的一部分的話),然后為*pdwEffect賦值為DROPEFFECT_COPY或DROPEFFECT_NONE.當然,還可以做一些你喜歡的事情,比如把鼠標坐標打印到屏幕上。不過為了性能和安全起見,建議不要做延時明顯的操作。
DragLeave:
這個方法沒有傳入?yún)?shù),相當簡單。
當拖動的鼠標離開了窗口區(qū)域,這個方法將被調(diào)用,你可以在這里寫一些清理內(nèi)存的代碼。在CDropTargetEx類里,由于在DragEnter里new了一些數(shù)據(jù)結(jié)構(gòu),并加到一個指針數(shù)組里,所以我必須在這里對此數(shù)據(jù)進行清理,對此結(jié)構(gòu)里的STDMEDIUM調(diào)用ReleaseStgMedium然后Delete該結(jié)構(gòu)。
另外,如果需要的話,可以通知用戶鼠標指針已經(jīng)離開了拖放區(qū)域。
Drop
如果鼠標沒有離開窗口,而是在窗口內(nèi)釋放按紐,那么拖放時間的“放”就在這時發(fā)生,IDropTarget接口的Drop方法被調(diào)用。其原形為
HRESULT Drop(
IDataObject * pDataObject,
DWORD grfKeyState,
POINTL pt,
DWORD * pdwEffect
)
有些資料建議在這里才調(diào)用pDataObject->GetData方法獲取數(shù)據(jù),在CDropTargetEx類里,數(shù)據(jù)實際上已經(jīng)在DragEnter里獲取了。這樣做的理由是我希望一開始就獲得數(shù)據(jù),從它本身進行判斷是否支持拖放,而不是在“放”的時候才判斷是否合法數(shù)據(jù)。
既然數(shù)據(jù)已經(jīng)獲得,那么我就可以從保存數(shù)據(jù)的指針數(shù)組里提取出STGMEDIUM數(shù)據(jù)來,并根據(jù)數(shù)據(jù)的具體格式進行處理(最后一定要記住對STGMEDIUM進行ReleaseStgMedium)
對于CF_TEXT類型的數(shù)據(jù),STGMEDIUM的成員hGlobal里包含的是一段全局內(nèi)存數(shù)據(jù)。獲取這些數(shù)據(jù)的方法是:
TCHAR *pBuff = NULL;
pBuff=(LPSTR)GlobalLock(hText);
GlobalUnlock(hText);
則得到一個指向內(nèi)存數(shù)據(jù)的指針pBuff。在我這個例子里一般是一段"\0"結(jié)尾的文本
字符串。這樣就實現(xiàn)了文本的拖放。
對于CF_HDROP類型的數(shù)據(jù),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<Count;i++)
{
if(::DragQueryFile(hDrop,i,Buffer,sizeof(Buffer)))
{
//Got the FileName in Buffer
}
}
::DragFinish(hDrop);
return true;
}
獲得的Buffer是就是拖放的
文件名,如果拖放的是多個
文件,在for循環(huán)里可以依次獲取這些
文件的
文件名。這樣就實現(xiàn)了
文件的拖放。
CDropTargetEx類使用非常簡單:
在客戶窗口的相關(guān)
文件中,定義一個CDropTargetEx實例:CDropTargetEx DropTarget;
在窗口創(chuàng)建之后,將窗口句柄進行拖放注冊:
DropTarget.DragDropRegister(hWnd);
或者
DropTarget.DragDropRegister(hWnd,MK_CONTROL|MK_LBUTTON);
表示鼠標左鍵按下并且按住Ctrl鍵的拖放有效;
對于獲取拖放的結(jié)果,我使用的是回調(diào)函數(shù)方式:
回調(diào)原形 typedef VOID (_stdcall *DROPCALLBACK)(LPCSTR Buffer,int type);
在適當?shù)牡胤剑ū热绱翱诘膶崿F(xiàn)CPP里)定義函數(shù)DropCallback:
void _stdcall DropCallBack(LPCSTR Buffer,int type)
并且將其地址賦于DropTarget實例:
DropTarget.SetCallBack(DropCallBack);
這樣,拖放文本到客戶窗口,回調(diào)函數(shù)將被調(diào)用,參數(shù)Buffer為拖放的文本,format為CF_TEXT。而拖放
文件的時候,對每個被拖放的
文件都調(diào)用一次回調(diào)函數(shù),參數(shù)Buffer為
文件全路徑名,format為CF_HDROP。
示例的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;
}
}
總結(jié):使用IDropTarget實現(xiàn)通用的拖放,只要實現(xiàn)其7個接口,并且對得到的IDataObject用正確的格式(FORMATETC)調(diào)用正確的GetData獲取數(shù)據(jù),返回DROPEFFECT決定拖放的特征和結(jié)果,并處理拖放結(jié)果即可。
要注意的小問題是:
要調(diào)用OleInitialize而不是CoInitialize或CoInitializeEx對COM進行初始,否則RegisterDragDrop將不會成功,返回的錯誤是E_OUTOFMEMORY--內(nèi)存不夠,無法進行該操作。
調(diào)用ReleaseStgMedium釋放STGMEDIUM里的數(shù)據(jù),而不是直接對其hGlobal成員調(diào)用CloseHandle.
拖放操作關(guān)系到兩個進程的數(shù)據(jù)交換,會將兩個進程都堵塞,直到拖放完成為止,所以,在接管拖放的接口方法中,不要進行過于耗時的運算。
這個例子相當簡單,還可以簡化,比如取消vector,將獲得HGLOBAL句柄作為成員變量存儲,或者將獲取數(shù)據(jù)的操作全部放到Drop方法里。
對于拖放
文件,還有一個更簡單的方法:響應(yīng)WM_DROPFILES 消息。步驟是:
對客戶窗口調(diào)用DropAccepFiles,使該窗口可以接受
文件拖放。
響應(yīng)WM_DROPFILES消息,其wParam就是HDROP句柄
對此句柄調(diào)用DropQueryFiles獲取拖放
文件列表并結(jié)束拖放,參見上面關(guān)于ProcessDrop的代碼
對于拖放的全面闡述,請參見MSDN->PlatformSDK Document->User Interface Services->Windows Shell里關(guān)于“Transferring Shell Objects with Drag-and-Drop and the Clipboard”一章。Windows Shell
系統(tǒng)提供了很多接口,讓用戶利用和擴充這些接口,很方便的開發(fā)和使用豐富的shell服務(wù),確實是一種很聰明的設(shè)計。