使用IDropTarget介面同時支援文字和檔案拖放 (轉)

worldblog發表於2007-12-11
使用IDropTarget介面同時支援文字和檔案拖放 (轉)[@more@] 

使用IDropTarget介面同時支援文字和拖放

to:vcbear@163.com">vcbear

關於的外殼擴充套件,拖放是比較簡單的一種,在網上可以找到不少介紹這個技巧的文章。大部分是介紹使用MFC的COleDropTarget實現的,我覺得一般使用COleDropTarget已經很好了,但是我習慣在一些模組中,完全的不使用MFC,比如純SDK程式設計,還有用在ATL的時候,MFC是相當累贅的。所以COleDropTarget在這個意義上講不夠完美。

參考了MSDN以及的相關文章和程式碼(by Thomas Blenkers)之後,我發現拖放實際上主要使用了IDropTarget的介面方法,非常簡單,不妨直接面對原始IDropTarget實現自己的拖放類。

作為學習筆記,就有了這麼一篇文字,以拋磚引玉:

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 ** ppv);

ULONG STDMETHODCALLTYPE AddRef(void);

ULONG STDMETHODCALLTYPE Release(void);

HRESULT STDMETHODCALLTYPE DragOver(D 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個函式就可以實現了拖放的效果,因為系統為我們已經做了很多。

另一個非常重要的是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_NONEDROPEFFECT_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 dwect;

LONG lindex;

DWORD tymed;

}FORMATETC, *LPFORMATETC;

在這裡我們最感興趣的是cfFormattymed兩個資料。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_COPYDROPEFFECT_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。在我這個例子裡一般是一段""結尾的文字字串。這樣就實現了文字的拖放。

對於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

}

}

::DragFinish(hDrop);

return true;

}

獲得的Buffer是就是拖放的檔名,如果拖放的是多個檔案,在for迴圈裡可以依次獲取這些檔案的檔名。這樣就實現了檔案的拖放。


 

CDropTargetEx類使用非常簡單:

在客戶視窗的相關檔案中,定義一個CDropTargetEx例項:CDropTargetEx DropTarget;

在視窗建立之後,將視窗控制程式碼進行拖放註冊:

DropTarget.DragDropRegister(hWnd);

或者

DropTarget.DragDropRegister(hWnd,MK_CONTROL|MK_LBUTTON);

表示滑鼠左鍵按下並且按住Ctrl鍵的拖放有效;

對於獲取拖放的結果,我使用的是回撥函式方式:

回撥原形 typedef VOID (_stdcall *DROPCALLBACK)(LPCSTR Buffer,int type);

在適當的地方(比如視窗的實現CPP裡)定義函式DropCallback:

void _stdcall DropCallBack(LPCSTR Buffer,int type)

並且將其地址賦於DropTarget例項:

DropTarget.SetCallBack(DropCallBack);

這樣,拖放文字到客戶視窗,回撥函式將被呼叫,引數Buffer為拖放的文字,format為CF_TEXT。而拖放檔案的時候,對每個被拖放的檔案都呼叫一次回撥函式,引數Buffer為檔案全路徑名,formatCF_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 : is Drag and Drop to this Windows ,Open it?",Buffer);

if(MessageBox(hMainWnd,Buf,"Question",MB_YESNO)==IDYES)

{

Execute(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的程式碼

對於拖放的全面闡述,請參見MSDN->PlatformSDK Document->User Interface Services->Windows Shell裡關於“Tranerring Shell Objects with Drag-and-Drop and the Clipboard”一章。Windows Shell系統提供了很多介面,讓使用者利用和擴充這些介面,很方便的開發和使用豐富的shell服務,確實是一種很聰明的設計。

 

附/downfile.asp?fileid=61">例子和程式碼

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-991273/,如需轉載,請註明出處,否則將追究法律責任。

相關文章