完成一个功能,运行我们的程序后,每打开一个explorer 窗口,向该窗口拷贝我们的文件。
在资源管理器中进行拖拽操作,没有键盘操作的话。不同分区间默认为拷贝,同分区间默认为move操作。
如果加上键盘操作,则会规定操作的类型如下:
Shift move
Ctrl copy
Alt link
另外:
Drag系统文件 link
Drop 开始菜单和toobar link
Drag、Drop 字面上很好理解,拖和放。
WM_DROPFILES 这个消息我们现在几乎不用了,应用程序几乎都是用的OLE 技术。
OLE 技术中每个拖放的操作都由三个元素组成,这些元素都是COM 对象,必须由任何想要支持拖放的应用程序来实现。
1. 源 :IDropSource接口,包含生成视觉反馈以及取消或完成拖放操作的方法。
2. 目标 :IDropTarget 接口
3. 正在传输的数据 :由IDataObject接口表示
作为源,需要1,3两个接口
作为目标,仅仅需要2一个接口即可。
使用OLE 的线程必须调用OleInitialize,完成时调用OleUninitialize。COM和 OLE 必须在每个线程的基础上被初始化和销毁。
拖放过程的核心:
WINOLEAPI DoDragDrop(
IDataObject *pDataObject, // Pointer to the data object
IDropSource *pDropSource, // Pointer to the source
DWORD dwOKEffect, // Effectsallowed by the source
DWORD * pdwEffect // Pointer to effects on the source
);
当调用该函数,进入一个模态的消息循环,这时候,鼠标和键盘消息将会被监视。
如果目标应用程序想要作为拖放操作的接收者,调用WINOLEAPIRegisterDragDrop(
HWND hwnd, // Handle to a window that can accept drops
IDropTarget *pDropTarget // Pointer to object that is to be target of drop
);
该函数将该窗口注册到OLE 运行,当鼠标移动到该窗口上,OLE 调用IDropTarget 界面中的方法通知应用程序拥有该窗口,拖放操作正在进行。当窗口关闭的时候,必须调用
WINOLEAPI RevokeDragDrop(
HWND hwnd // Handle to a window that can accept drops
);
该API 使用COM 的函数注销指定的窗口,并释放注册用的IDropTarget对象及其接口函数。
OLE 数据传输的核心是IDataObjectCOM 接口,IDataObject 提供了一种从一个应用程序传输和访问数据到另一个应用程序的方法。核心还是使用的剪贴板。
首先介绍两个结构体
typedef struct
{
CLIPFORMAT cfFormat; // Clipboard format
DVTARGETDEVICE*ptd; //(NULL) Target device for rendering
DWORD dwAspect; // (DV_CONTENT) How much detail is requiredfor data rendering
LONG lindex; //(-1) Used when data is splitacross page boundaries
DWORD tymed; // Storage medium used fordata transfer (HGLOBAL, IStream etc)
} FORMATETC;
该结构体描述了一个IDdataObject 可以提供或接收的数据的类型,基本上是标准Windows 剪贴板格式(CF_TEXT)的扩展。
用于标识FORMATETC结构的剪贴板格式。这可以是内置的格式,如CF_TEXT或CF_BITMAP,或者使用RegisterClipboardFormat注册的自定义格式。
指向DVTARGETDEVICE结构的指针,它提供有关数据呈现的设备的信息。对于正常的剪贴板操作和拖放操作,通常为NULL。
描述用于呈现数据的细节数量。通常这将是DVASPECT_CONTENT,意思是“全部内容”,但可以描述较小的细节,如缩略图或图标。
仅在数据跨页边界分割时使用,不用于简单的OLE传输。
它描述了用于保存数据的“存储介质”的类型。这个成员从“类型的媒介” 对应于windows.h 中的 TYMED_XXX值
存储OLE数据
typedef struct
{
DWORD tymed;
union
{
HBITMAP hBitmap;
HMETAFILEPICT hMetaFilePict;
HENHMETAFILE hEnhMetaFile;
HGLOBAL hGlobal;
LPWSTR lpszFileName;
IStream *pstm;
IStorage *pstg;
};
IUnknown*pUnkForRelease;
} STGMEDIUM;
tymed:
这必须与FORMATETC结构中的相同
hBitmap / hGlobal等:实际数据。只有其中之一将是有效的,这取决于价值TYMED。
pUnkForRelease:可选的指向IUnknown接口的指针,数据的接收者应在其上调用Release。当此字段为NULL时,收件人有责任释放内存句柄。该ReleaseStgMedium API调用在这里很有用,因为它需要释放STGMEDIUM的数据内容。
IDataObject 定义的接口函数
接口定义
IDataObject : public IUnknown
{
public:
virtual /* [local] */ HRESULT STDMETHODCALLTYPE GetData(
/* [unique][in] */ FORMATETC *pformatetcIn,
/* [out] */ STGMEDIUM *pmedium) = 0;
virtual /* [local] */ HRESULT STDMETHODCALLTYPE GetDataHere(
/* [unique][in] */ FORMATETC *pformatetc,
/* [out][in] */ STGMEDIUM *pmedium) = 0;
virtual HRESULT STDMETHODCALLTYPE QueryGetData(
/* [unique][in] */ __RPC__in_opt FORMATETC *pformatetc) = 0;
virtual HRESULT STDMETHODCALLTYPE GetCanonicalFormatEtc(
/* [unique][in] */ __RPC__in_opt FORMATETC *pformatectIn,
/* [out] */ __RPC__out FORMATETC *pformatetcOut) = 0;
virtual /* [local] */ HRESULT STDMETHODCALLTYPE SetData(
/* [unique][in] */ FORMATETC *pformatetc,
/* [unique][in] */ STGMEDIUM *pmedium,
/* [in] */ BOOL fRelease) = 0;
virtual HRESULT STDMETHODCALLTYPE EnumFormatEtc(
/* [in] */ DWORD dwDirection,
/* [out] */ __RPC__deref_out_opt IEnumFORMATETC **ppenumFormatEtc) = 0;
virtual HRESULT STDMETHODCALLTYPE DAdvise(
/* [in] */ __RPC__in FORMATETC *pformatetc,
/* [in] */ DWORD advf,
/* [unique][in] */ __RPC__in_opt IAdviseSink *pAdvSink,
/* [out] */ __RPC__out DWORD *pdwConnection) = 0;
virtual HRESULT STDMETHODCALLTYPE DUnadvise(
/* [in] */ DWORD dwConnection) = 0;
virtual HRESULT STDMETHODCALLTYPE EnumDAdvise(
/* [out] */ __RPC__deref_out_opt IEnumSTATDATA **ppenumAdvise) = 0;
};
对于简单的OLE 拖放来说,仅仅需要GetData QueryGetData和 EnumFormatEtc三个成员函数
GetData |
得到在FORMATETC结构中描述的数据,并通过STGMEDIUM结构进行传输。 |
QueryGetData |
确定数据对象是否能够呈现FORMATETC结构中描述的数据 |
EnumFormatEtc |
创建并返回一个指向IEnumFORMATETC接口的指针,以枚举数据对象产生的FORMATETC对象 |
#include
#include
void DisplayDataObject(IDataObject *pDataObject)
{
FORMATETC fmtetc= { CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM stgmed;
// ask the IDataObject for some CF_TEXT data, stored as aHGLOBAL
if(pDataObject->GetData(&fmtetc,&stgmed) == S_OK)
{
// We need to lock the HGLOBAL handle because we can't
// be sure if this is GMEM_FIXED (i.e. normal heap) data ornot
char *data = (char*)GlobalLock(stgmed.hGlobal);
printf("%s\n",data);
// cleanup
GlobalUnlock(stgmed.hGlobal);
ReleaseStgMedium(&stgmed);
}
}
int main(void)
{
IDataObject *pDataObject;
// Initialize COM and OLE
if(OleInitialize(0)!= S_OK)
return 0;
// Access the data on the clipboard
if(OleGetClipboard(&pDataObject) == S_OK)
{
// access the IDataObject using a separate function
DisplayDataObject(pDataObject);
pDataObject->Release();
}
// Cleanup
OleUninitialize();
return 0;
}
为了创建我们自己的COM对象,我们按照函数顺序继承IDataObject对象的所有函数。以及IUnknown类的成员函数。
其中我们仅仅需要实现三个函数,上面我们已经提到过了。
另外,我们需要提供一个IEnumFORMATETC接口。
为了之后的消费者“IDropTarget”能够得到我们的数据,我们需要提供其需要的数据,Drop对象会调用QueryData或EnumFormatEtc获得数据。我们需要找到一些使用实际数据填充IDataObject的方法,以FORMATETC结构的形式告诉它数据是什么。
CDataObject::CDataObject(FORMATETC *fmtetc,STGMEDIUM *stgmed, int count)
{
// reference countmust ALWAYS start at 1
m_lRefCount = 1;
m_nNumFormats =count;
m_pFormatEtc = new FORMATETC[count];
m_pStgMedium = new STGMEDIUM[count];
for(int i = 0; i < count; i++)
{
m_pFormatEtc[i]= fmtetc[i];
m_pStgMedium[i]= stgmed[i];
}
}
COM 对象的引用计数初始化为1,并将传入的两个结构体数组保存。
HRESULT CreateDataObject(FORMATETC *fmtetc, STGMEDIUM*stgmeds, UINT count, IDataObject**ppDataObject)
{
if(ppDataObject == 0)
return E_INVALIDARG;
*ppDataObject = new CDataObject(fmtetc, stgmeds, count);
return (*ppDataObject) ? S_OK : E_OUTOFMEMORY;
}
HRESULT __stdcall CDataObject::QueryGetData(FORMATETC*pFormatEtc)
{
return (LookupFormatEtc(pFormat) == -1) ?DV_E_FORMATETC : S_OK;
}
int CDataObject::LookupFormatEtc(FORMATETC*pFormatEtc)
{
// check each of ourformats in turn to see if one matches
for(int i = 0; i tymed) &&
m_pFormatEtc[i].cfFormat== pFormatEtc->cfFormat &&
m_pFormatEtc[i].dwAspect== pFormatEtc->dwAspect)
{
//return index of stored format
returni;
}
}
// error, format notfound
return -1;
}
HRESULT __stdcall CDataObject::GetData (FORMATETC*pFormatEtc, STGMEDIUM *pStgMedium)
{
int idx;
// try to match thespecified FORMATETC with one of our supported formats
if((idx =LookupFormatEtc(pFormatEtc)) == -1)
return DV_E_FORMATETC;
// found a match -transfer data into supplied storage medium
pMedium->tymed = m_pFormatEtc[idx].tymed;
pMedium->pUnkForRelease = 0;
// copy the data intothe caller's storage medium
switch(m_pFormatEtc[idx].tymed)
{
case TYMED_HGLOBAL:
pMedium->hGlobal = DupGlobalMem(m_pStgMedium[idx].hGlobal);
break;
default:
return DV_E_FORMATETC;
}
return S_OK;
}
HGLOBAL DupGlobalMemMem(HGLOBAL hMem)
{
DWORD len = GlobalSize(hMem);
PVOID source = GlobalLock(hMem);
PVOID dest = GlobalAlloc(GMEM_FIXED,len);
memcpy(dest, source,len);
GlobalUnlock(hMem);
return dest;
}
HRESULT __stdcall CDataObject::EnumFormatEtc (DWORD dwDirection, IEnumFORMATETC**ppEnumFormatEtc)
{
// only the getdirection is supported for OLE
if(dwDirection == DATADIR_GET)
{
//for Win2k+ you can use the SHCreateStdEnumFmtEtc API call, however
//to support all Windows platforms we need to implement IEnumFormatEtc ourselves.
return SHCreateStdEnumFmtEtc (m_NumFormats,m_FormatEtc, ppEnumFormatEtc);
}
else
{
//the direction specified is not supported for drag+drop
return E_NOTIMPL;
}
}
我们直接使用SHCreateStdEnumFmtEtc就好了
这样是可以的。对的。
首先来看上面介绍过的函数
WINOLEAPI DoDragDrop(
IDataObject *pDataObject, // Pointer to the data object
IDropSource *pDropSource, // Pointer to the source
DWORD dwOKEffect, // Effectsallowed by the source
DWORD * pdwEffect // Pointer to effects on the source
);
前两个参数是COM 接口,一个是IDataObject我们已经在上面介绍过,第三个参数是一个DWORD 值,取自DROPEFFECT_xxx,通常为:DROPEFFECT_COPY和DROPEFFECT_MOVE的组合,如果只允许我们的源复制数据,只指定DROPEFFECT_COPY。
最后一个参数包含OLE 希望源执行的“效果”或动作,即用户是否选择移动或复制数据。
IDropSource只需要实现两个功能:
QueryContinueDrag 根据鼠标按钮和
GiveFeedback 提供一种拖放源的方法,根据上面列出的修饰键的状态(即鼠标按钮,转义,控制等)给出视觉反馈。
当拖动修改键的状态发生变化,这两个函数都将由COM/OLE 运行时调用。
class CDropSource : public IDropSource
{
public:
//
// IUnknown members
//
HRESULT __stdcallQueryInterface (REFIID iid, void ** ppvObject);
ULONG __stdcallAddRef (void);
ULONG __stdcall Release (void);
//
// IDropSourcemembers
//
HRESULT __stdcallQueryContinueDrag (BOOL fEscapePressed, DWORD grfKeyState);
HRESULT __stdcallGiveFeedback (DWORD dwEffect);
//
// Constructor /Destructor
//
CDropSource();
~CDropSource();
private:
//
// private membersand functions
//
LONG m_lRefCount;
};
对象的构造函数除了初始化对象的引用计数之外,不执行任何任务。
HRESULT QueryContinueDrag(
BOOL fEscapePressed, // Is the key being pressed?
DWORD grfKeyState, // Current state of keyboard modifier keys
);
函数的三个可能返回值
S_OK 拖放操作继续
DRAGDROP_S_DROP 应该完成拖放操作
DRAGDROP_S_CANCEL 拖放操作被取消,不会出现拖放操作。
Drag And Drop操作的一些习惯操作:
1. 按下:Escape键时,取消拖放操作。
2. 当鼠标左键释放,执行Drop
HRESULT __stdcallCDropSource::QueryContinueDrag(BOOL fEscapePressed, DWORD grfKeyState)
{
// if the Escape keyhas been pressed since the last call, cancel the drop
if(fEscapePressed ==TRUE)
returnDRAGDROP_S_CANCEL;
// if the LeftMousebutton has been released, then do the drop!
if((grfKeyState &MK_LBUTTON) == 0)
returnDRAGDROP_S_DROP;
// continue with thedrag-drop
return S_OK;
}
GiveFeedback 函数对于每个应用程序通常是不同的,但这里仅仅是返回一个同样的结果,当然我们也可以丰富这个函数。
HRESULT __stdcall CDropSource::GiveFeedback(DWORD dwEffect)
{
return DRAGDROP_S_USEDEFAULTCURSORS;
}
dwEffect 告诉我们按下了哪些鼠标按钮,使用哪些键盘修饰符。通过简单返回DRAGDROP_S_USEDDEFAULTCURSORS,指示COM 在修饰符更改时自动更新鼠标光标。
WINOLEAPI RegisterDragRrop(HWNDhWnd,IDropTarget* pDropTarget);
第一个窗口句柄,将注册为放置目标,第二个参数IDropTarget 指向IDropTarget COM 对象的指针,在拖放过程中,COM/OLE 运行时将调用此接口上的方法。
WINOLEAPI RevokeDragDrop(HWND hWnd);
IDropTarget接口相对简单,四个功能加IUnknown接口
DragEnter 确定下降是否可以接受,并且如果被接受则影响它
DragOver 通过DoDragDrog 功能向用户提供目标反馈
DragLeave 导致Drop端暂停其反馈操作
Drop 将数据放入目标窗口
class CDropTarget : public IDropTarget
{
public:
// IUnknownimplementation
HRESULT __stdcall QueryInterface (REFIID iid, void ** ppvObject);
ULONG __stdcall AddRef (void);
ULONG __stdcall Release (void);
// IDropTargetimplementation
HRESULT __stdcall DragEnter(IDataObject *pDataObject, DWORD grfKeyState,POINTL pt, DWORD *pdwEffect);
HRESULT __stdcall DragOver(DWORD grfKeyState, POINTL pt, DWORD * pdwEffect);
HRESULT __stdcall DragLeave(void);
HRESULT __stdcall Drop(IDataObject * pDataObject,DWORD grfKeyState, POINTLpt, DWORD * pdwEffect);
// Constructor
CDropTarget(HWND hwnd);
~CDropTarget();
private:
// internal helperfunction
DWORD DropEffect(DWORD grfKeyState, POINTL pt, DWORD dwAllowed);
bool QueryDataObject(IDataObject *pDataObject);
// Private membervariables
long m_lRefCount;
HWND m_hWnd;
bool m_fAllowDrop;
// Other internalwindow members
};
M_hWnd 为接受Drop 消息的窗口的句柄,m_fAllowDrop 用于指示是否被丢弃的数据对象对我们包含有用的数据。
当一个对象被拖动到我们的窗口,这是COM 调用的第一个函数:
HRESULT DragEnter(
IDataObject *pDataObject, // Pointer to the interface of the source dataobject
DWORD grfKeyState, // Currentstate of keyboard modifier keys
POINTL pt, //Current cursor coordinates
DWORD * pdwEffect // Pointer to the effect of thedrag-and-drop operation
);
第一个参数是通过拖放操作的源传递给我们的数据对象的另一个指针(通过COM)。IDataObject只是要删除的数据的“传输介质”。我们可以在DragEnter中查询数据对象,查看是否有任何我们想要的数据。
保存键盘修饰键的状态,例如Control,Alt和Shift以及鼠标按钮的状态。这是一个简单的DWORD变量,包含以下一个或多个位标志:MK_CONTROL,MK_SHIFT,MK_ALT,MK_BUTTON,MK_LBUTTON等。
一个POINTL结构,包含鼠标进入窗口时的坐标。在某些应用中,此参数将用于检查鼠标是否位于允许的下降区域之上,或者简单地用于定位某种“插入”光标来指示丢弃的数据将在哪里。
指向DWORD值的指针,用于指定drop-source 允许的drop-effect 。此值与提供者调用DoDragDrop时指定的dwOKEffect相同(因为这个参数的忽视,浪费了好多时间,疯狂调试)。
HRESULT __stdcall CDropTarget::DragEnter(IDataObject*pDataObject, DWORD grfKeyState,
POINTLpt, DWORD *pdwEffect)
{
// does thedataobject contain data we want?
m_fAllowDrop =QueryDataObject(grfKeyState, pdwEffect, pDataObject);
if(m_fAllowDrop)
{
//get the dropeffect based on keyboard state
*pdwEffect= DropEffect(grfKeyState, pt, *pdwEffect);
SetFocus(m_hWnd);
PositionCursor(m_hWnd,pt);
}
else
{
*pdwEffect= DROPEFFECT_NONE;
} return S_OK;
}
bool CDropTarget::QueryDataObject(IDataObject *pDataObject)
{
FORMATETC fmtetc = {CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
// does the dataobject support CF_TEXT using a HGLOBAL?
return pDataObject->QueryGetData(&fmtetc) ==S_OK ? true : false;
}
DWORD CDropTarget::DropEffect(DWORDgrfKeyState, POINTL pt, DWORD dwAllowed)
{
DWORD dwEffect = 0;
// 1. check"pt" -> do we allow a drop at the specified coordinates?
// 2. work out thatthe drop-effect should be based on grfKeyState
if(grfKeyState &MK_CONTROL)
{
dwEffect= dwAllowed & DROPEFFECT_COPY;
}
else if(grfKeyState& MK_SHIFT)
{
dwEffect= dwAllowed & DROPEFFECT_MOVE;
}
// 3. nokey-modifiers were specified (or drop effect not allowed), so
// base the effect on those allowed by the dropsource
if(dwEffect == 0)
{
if(dwAllowed& DROPEFFECT_COPY) dwEffect = DROPEFFECT_COPY;
if(dwAllowed& DROPEFFECT_MOVE) dwEffect = DROPEFFECT_MOVE;
}
return dwEffect;
}
在这个实现中,我们让数据移动覆盖数据副本。
在拖放的声明周期内,当键盘修饰符的状态改变或鼠标移动,被调用。该函数指示基于键盘和鼠标位置的状态返回值。
HRESULT __stdcall CDropTarget::DragOver(DWORD grfKeyState, POINTL pt, DWORD * pdwEffect)
{
if(m_fAllowDrop)
{
*pdwEffect= DropEffect(grfKeyState, pt, *pdwEffect);
PositionCursor(m_hWnd,pt);
}
else
{
*pdwEffect= DROPEFFECT_NONE;
}
return S_OK;
}
鼠标光标移动到我们的放置目标窗口之外,或者按下Escape来取消拖放操作。给我们的程序一个释放资源或者改变界面的机会,这里我们仅仅返回S_OK
HRESULT __stdcall CDropTarget::DragLeave(void)
{
return S_OK;
}
HRESULT __stdcall CDropTarget::Drop(IDataObject*pDataObject, DWORD grfKeyState,POINTL pt, DWORD *pdwEffect)
{
PositionCursor(m_hWnd,pt);
if(m_fAllowDrop)
{
DropData(m_hWnd,pDataObject);
*pdwEffect= DropEffect(grfKeyState, pt, *pdwEffect);
}
else
{
*pdwEffect= DROPEFFECT_NONE;
}
return S_OK;
}
OLE 确定拖放将继续时,调用这个函数,我们得到与DragEnter中接收的IDataObject相同的接口指针,我们可以从中获取数据。
完整源码及参考:
http://www.catch22.net/tuts/ole-drag-and-drop