DRAG & DROP

使用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(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該結構。

另外,如果需要的話,可以通知用戶鼠標指針已經離開了拖放區域。


--------------------------------------------------------------------------------


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<Count;i++)

{

if(::DragQueryFile(hDrop,i,Buffer,sizeof(Buffer)))

{

//Got the FileName in Buffer

}

}

::DragFinish(hDrop);

return true;

}

獲得的Buffer是就是拖放的文件名,如果拖放的是多個文件,在

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爲文件全路徑名,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;

}

}

 

總結:使用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; nCur<nFileCount; ++nCur) //遍歷取得每個文件名

                     {

                            ZeroMemory(chTemp, _MAX_PATH+1);

                DragQueryFile(hDropFiles, nCur, (LPTSTR)chTemp, _MAX_PATH+1);

                            AddAllFiles(chTemp);

                     }

              }

              GlobalUnlock(hMemData);

              return TRUE;

       }

       else

       {

              return FALSE;

       }

}

在第二個類COleDropTarget中添加如下對應的函數:

    virtual DROPEFFECT OnDragEnter(CWnd* pWnd, COleDataObject* pDataObject, DWORD dwKeyState, CPoint point);

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

它們的動作都差不多:先用RTTI判斷窗口指針pWnd的類型,如果是CListCtrlEx,則調用CListCtrlEx中對應的處理函數,否則調用基類的處
理函數。以OnDrop爲例:

BOOL COleDropTargetEx::OnDrop(CWnd* pWnd, COleDataObject* pDataObject, DROPEFFECT dropEffect, CPoint point)

{

       CListCtrlEx*     pListCtrlEx = NULL;

     

       ASSERT_VALID(this);

       ASSERT(IsWindow(pWnd->m_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|DROPEFFECT_COPY);

最後一點要注意的是,在Windows NT 4.0以上的系統中,即使實際產生的是DROPEFFECT_MOVE動作,DoDragDrop函數也只返回DROPEFFECT_NONE。
產生這個問題的原因在於,Windows NT 4.0的Shell會直接移動文件本身來對移動操作進行優化。返回值DROPEFFECT_MOVE最初的含義,就是通知
執行拖放操作的應用程序去刪除原位置上的文件。但是因爲Shell已經替應用程序完成了這個(刪除)動作,所以,函數返回DROPEFFECT_NONE。
要想知道文件是否真的被移動了也很簡單,只要在函數返回之後檢查一下原位置上的文件是否存在就可以了

發佈了138 篇原創文章 · 獲贊 9 · 訪問量 102萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章