置顶随笔
2007年5月10日
OLE Drap/Drop(6)
欢迎你到OLE拖放操作的第六章!这里将着重于一个实现了drop-target的小程序,这就意味着我们的程序能够接收拖到它上面的对象(文件、图片、文本)了。
我们实现一个IDropTarget的COM接口允许OLE程序拖动数据到我们的程序上;这里仅仅是一个简单的EDIT控件,所以他将CF_TEXT数据作为目标。
成为一个“Drop Target”
为了时窗口可以接收拖放操作的数据,窗口必须注册为drop目标;有一个OLE的API调用RegisterDragDrop来完成这个事情,函数的原型是:
WINOLEAPI RegisterDragDrop (HWND hwnd, IDropTarget * pDropTarget);
第一个参数是窗口的HANDLE,这个窗口是拖动的目标窗口;第二个参数是一个指向IDropTarget COM对象的指针,COM/OLE运行时将在拖放操作的过程中调用这个方法。
同样有一个OLE API调用来将window从拖放操作中删除:
WINOLEAPI RevokeDragDrop(HWND hwnd);
我们所要做的就是在窗口创建的时候调用RegisterDragDrop,在窗口销毁的时候调用RevokeDragDrop。在我们调用RegisterDragDrop之前,我们需要构造一个COM对象来支持IDropTarget接口。
IDropTarget接口
IDropTarget接口相对比较简单,有四个函数需要实现,当然,也要实现IUnknown接口,不过我们前面已经介绍了。
IDropTarget 方法 |
描述 |
DragEnter |
判断是否可以接受一个拖操作,以及接受之后的效果 |
DragOver |
提供通过DoDragDrop函数执行的目标反馈 |
DragLeave |
导致一个drop目标挂起它的返回行为 |
Drop |
数据放进目标窗口 |
这些函数都由COM/OLE运行时在一个对象被拖到我们注册窗口的时候来调用。就象上表显示的一样,每个函数都有不同的任务,我们需要做的就是实现这些函数。
实现IDropTarget
以我的经验,IDropTarget接口非常难以写为不涉及特定程序的代码,例如:写成可以在所有程序都使用的通用IDropTarget COM对象是很难的。
这是因为IDropTarget要求在一个对象拖过你的目标窗口时显示图形效果,且也只有特定程序代码才可以访问这些数据对象内容。
在我们的拖放接口之外,IDropTarget是最容易被集成到你窗口类的对象。例如:假定你已经用C++类实现了一个自定义的窗口,为这个窗口添加一个多drop目标支持的最好方法就是从IDropTarget直接继承,而不需要单独定义一个CDropTarget类;这意味着你的drop-target代码能够访问所有你的窗口状态。
然而,我们这里提供完整的CDropTarget类:
class CDropTarget : public IDropTarget
{
public:
// IUnknown implementation
HRESULT __stdcall QueryInterface (REFIID iid, void ** ppvObject);
ULONG __stdcall AddRef (void);
ULONG __stdcall Release (void);
// IDropTarget implementation
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, POINTL pt, DWORD * pdwEffect);
// Constructor
CDropTarget(HWND hwnd);
~CDropTarget();
private:
// internal helper function
DWORD DropEffect (DWORD grfKeyState, POINTL pt, DWORD dwAllowed);
bool QueryDataObject(IDataObject *pDataObject);
// Private member variables
long m_lRefCount;
HWND m_hWnd;
bool m_fAllowDrop;
// Other internal window members
};
除引用记数器外,我们需要存储另外两个变量:m_hWnd变量是drop-target窗口的HANDLE,这个在提供可见效果的时候需要;m_fAllowDrop用来指示被拖动的数据对象是否包含我们需要的有用数据。因此我们没有连续查询数据对象,这是一个最优的办法。
IDropTarget::DragEnter方法
让我们首先看一下IDropTarget函数,因为这是在一个对象被拖过我们窗口时最先被COM调用的函数:
HRESULT DragEnter (
IDataObject * pDataObject,//指向源数据对象的接口指针
DWORD grfKeyState, // 当前键盘修饰符的状态
POINTL pt, // 当前鼠标的坐标
DWORD * pdwEffect // 指向拖放操作的效果指针
);
仔细看一下上面函数的原型,因为这对于理解每个参数怎么样使用很重要:
l IDataObject-第一个参数是拖放操作的源对象通过COM传递来的数据对象指针。IDataObject是拖放操作带来数据的传输媒体,我们在DragEnter的时候查看数据对象来看是否有我们想要的任何数据。
l grfKeyState-保留键盘修饰符的状态,例如:Control、Alt、和Shift以及鼠标按键的状态。是有一到多个MK_CONTROL、MK_SHIFT、MK_ALT、MK_BUTTON、MK_LBUTTON等组成的简单DWORD变量
l pt-一个POINTL结构体,包含了鼠标进入我们窗口的坐标;在许多程序中,这个参数用来检查鼠标是否放置在允许的drop区域上,或者用来简单的放置某些插入光标来指示drop数据放在那里。
l pdwEffect-一个DWORD的指针,指出drop源允许的drop效果。这个值和DoDragDrop的dwOKEffect值相同。
我们的DragEnter实现需要做几个通常的工作,另外画一个图形的反馈:
1. 检查提供的数据对象,然后判断它是否包含任何有用的数据
2. 检查存储在grfKeyState的键盘状态,并且计算应该是什么样的drop效果,例如:如果Control键按下,drop效果应该是复制,如果Shift被按下,drop效果应该是移动。
3. 验证这些效果是否与drop源的效果相兼容
4. 存储最终的drop效果到pdwEffect的DWORD指针。
不要如此复杂吧!DragEnter的目的就是简单的对拖放操作说“yes还是NO”,指定采用什么drop效果以便于OLE更新鼠标光标。
HRESULT __stdcall CDropTarget::DragEnter(IDataObject *pDataObject, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect)
{
// does the dataobject 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;
}
除了设置光标下的窗口和设置EDIT位置外,DragEnter的功能已经由两个内部协助函数代理而简化了:
bool CDropTarget::QueryDataObject(IDataObject *pDataObject)
{
FORMATETC fmtetc = { CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
// does the data object support CF_TEXT using a HGLOBAL?
return pDataObject->QueryGetData(&fmtetc) == S_OK ? true : false;
}
QueryDataObject是一个私有函数,纯粹用来检查提供的数据,然后决定它是否包含对我们的drop目标有用的数据。在我们的例子中,我们仅仅接受CF_TEXT数据存储为HGLOBAL,因此这是我们请求的类型。一个私有成员变量m_fAllowDrop用来记住这个决定。
DWORD CDropTarget::DropEffect (DWORD grfKeyState, POINTL pt, DWORD dwAllowed)
{
DWORD dwEffect = 0;
// 1. 检查pt来看是否允许drop操作在某个位置
// 2. 计算出基于grfKeyState的drop效果
if(grfKeyState & MK_CONTROL)
{
dwEffect = dwAllowed & DROPEFFECT_COPY;
}
else if(grfKeyState & MK_SHIFT)
{
dwEffect = dwAllowed & DROPEFFECT_MOVE;
}
// 3. 非键盘修饰符指定(或drop效果不允许),因此基于drop源的效果
if(dwEffect == 0)
{
if(dwAllowed & DROPEFFECT_COPY) dwEffect = DROPEFFECT_COPY;
if(dwAllowed & DROPEFFECT_MOVE) dwEffect = DROPEFFECT_MOVE;
}
return dwEffect;
}
DropEffect协助函数用来计算基于键盘状态的drop效果,并且这个效果是达到源允许的。
首先grfKeyState变量用来检查看是否使用了Control或Shift键;这些键的标准的OLE行为是Control应该是复制数据,shift应该是移动数据。如果两个都按下,数据 应该是连接(例如:源应该建立一个到目标的快捷方式),但我们不支持这个功能。
主要的事情是使用位与操作符来对dwEffect赋drop效果值的时候:
dwEffect = dwAllowed & DROPEFFECT_COPY;
这个分配的结构很简单-dwEffect将拥有DROPEFFECT_COPY,但只有在dwAllowed变量中仅仅包含这个值的时候起作用;这种逻辑用法防止我们强制执行一个源不允许的drop效果。
下面是看一下在没有键盘修饰符的时候怎么做,例如:Control和Shift没有使用。在这种情况我,我们检查拖放的源对象允许的drop效果,以及选择使用哪个效果;在我们的实现中,我们是移动数据而不是复制。
IDropTarget::DragOver方法
这个函数在拖放操作的整个生命周期中被多次调用,因此,高效的写这个函数很重要;DragOver在键盘修饰符改变(shift/control等)或当鼠标移动的时候被调用。告诉OLE采用什么样基于键盘状态和鼠标位置的drop效果是这个函数的责任:
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;
}
DragOver写的很简单,逻辑上与DragEnter相同,我们使用前面计算过的m_fAllowDrop和DropEffect协助函数来通过pdwEffect指针返回drop效果。
IDropTarget::DragLeave函数
这个函数在鼠标光标移到drop目标窗口外面的时候调用,或者按下Escape键来取消拖放操作时。它的原型如下:
HRESULT __stdcall CDropTarget::DragLeave (void)
{
return S_OK;
}
这是这个函数的基本写法;这个函数存在的唯一原因是便于程序在鼠标移到窗口外面的时候使用图形返回效果来得到一个机会清理。例如:想象下面的场景,无论什么东西都拖过目标对象,DragEnter函数用来改变窗口边界的颜色;在这种情况下,DragLeave函数用来恢复窗口边界的颜色。
IDropTarget::Drop函数
Drop函数的原型与DragEnter函数相同:
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的接口指针,我们可以从中得到数据并粘贴到我们的编辑窗口中。
DropData协助函数用来访问数据对象内部的CF_TEXT数据,并插入到edit控件中;这个程序是是纯理论的,我们已经知道怎么样访问一个数据对象了,这里不在不厌其烦的介绍,你可以看源代码。
OLE Drap/Drop(5)
欢迎来到OLE拖放旅程系列的第五部分,我们机会到了OLE拖放实现的最后阶段,现在需要做的事情就是实现IDropSource和IDropTarget接口;一般我们完成这些,我们就可以在任何程序中添加拖放操作了。
本部分的目的在于实现一个用作拖放源的简单程序,它不能接收任何拖放的数据,但这不要紧,因为我们能使用任何平常支持拖放操作的windows程序(例如:WordPad)来测试,程序就是一个windows的Edit控件,它是子类化的,且支持拖操作。
这个子类的细节在这里不讨论,但源码可以很清晰的说明这个任务。
成为一个拖放的源对象
初始化一个拖放操作很简单,只要调用DoDragDrop这个API就足够了。
WINOLEAPI DoDragDrop (
IDataObject * pDataObject, // Pointer to the data object
IDropSource * pDropSource, // Pointer to the source
DWORD dwOKEffect, // Effects allowed by the source
DWORD * pdwEffect // Pointer to effects on the source
);
一旦你调用这个AIP,OLE运行时就代表你的程序来接管并处理所有必要的鼠标和键盘windows消息,因此你基本上将控制权在调用这个函数的时候交给了OLE。
前两个参数是COM接口,一个是IDataObject-我们前面已经介绍了这个接口。
第三个参数是一个DWORD值,它表示源允许的拖动效果,其以位掩码的方式给出。这些效果是DROPEFFECT_XXX值,通常是DROPEFFECT_MOVE和DROPEFFECT_COPY的联合。如果你想仅仅允许从我们的源复制数据,那么我们应该就指定DROPEFFECT_COPY。
最后一个参数是指向DWORD的指针。该值在DoDragDrop返回的时候可以访问,包含OLE期望源对象执行的效果和动作,例如:用户选择移动还是复制数据?
执行拖放操作的代码实际上分成三步:首先我们需要写一个小的功能函数叫做StringToHandle,它转换一个char*字符为HGLOBAL,从而我们可以在OLE中使用:
HANDLE StringToHandle (char *szText, int nTextLen)
{
void *ptr;
// if text length is -1 then treat as a nul-terminated string
if(nTextLen == -1)
nTextLen = lstrlen (szText) + 1;
// allocate and lock a global memory buffer. Make it fixed
// data so we don't have to use GlobalLock
ptr = (void *)GlobalAlloc(GMEM_FIXED, nTextLen);
// copy the string into the buffer
memcpy (ptr, szText, nTextLen);
return ptr;
}
StringToHandle完全不执行错误简单,因此这是你的责任;下一步是准备拖放操作使用的数据:
FORMATETC fmtetc =
{
CF_TEXT, // we will be dropping some text
0,
DVASPECT_CONTENT,
-1,
TYMED_HGLOBAL // stored as a HGLOBAL
};
STGMEDIUM stgmed =
{
TYMED_HGLOBAL,
{0},
0
};
// Create a HGLOBAL inside the storage medium
stgmed.hGlobal = StringToHandle ("Hello, World", -1);
紧接着就是创建两个拖放操作的COM接口:IDropSource和IDataObject。我们在前面的旅程中实现了CreateDataObject,马上会实现CreateDropSource:
IDropSource *pDropSource;
IDataObject *pDataObject;
CreateDropSource (&pDropSource);
CreateDataObject (&pDataObject, &fmtetc, &stgmed, 1);
在创建好IDataObject和IDropSource之后就可以调用DoDragDrop了:
DWORD dwResult;
// do the drag-drop!
dwResult = DoDragDrop(pDataObject, pDropSource, DROPEFFECT_COPY, &dwEffect);
// finished. Check the return values to see if we need to do anything else
if(dwResult == DRAGDROP_S_DROP)
{
if(dwEffect == DROPEFFECT_MOVE)
{
// remove the data we just dropped from active document
}
}
最后一件事情就是清除所有我们使用过的资源,首先删除我们使用的两个COM接口,然后删除包含我们文本的HGLOBAL内存缓冲区。
// release the COM interfaces
pDropSource->Release();
pDataObject->Release();
ReleaseStgMedium(&stgmed);
什么时候调用DoDragDrop方法来
知道了怎么样初始化拖放操作非常好,但真正重要的是理解将上面的代码放到你程序的什么位置?
因为拖放操作是基于鼠标的,它通常在处理windows鼠标消息的时候被初始化,如果你在支持拖放操作的程序中测试(例如WordPad),你会观察到RichEdit控件有下面的行为:
1. 当鼠标移动过选中的文本,它的光标形状变成箭头
2. 当按下左键的时候,被选择的不会被删除,且设置内部状态来指示可能要开始拖放操作
3. 当鼠标被第一次移动(并且内部状态指示左键一直按在被选中的文本区域中),拖动操作开始。
4. 在这个时候,OLE接管并处理所有鼠标消息,直到操作完成
5. 然而,如果左键被释放了,或者鼠标根本没有移动,RichEdit选择部分被清除。
这些行为在C或C++中实现非常简单,对于我们的子类化的控件,可能是这样:
case WM_LBUTTONDOWN:
if(MouseInSelection(hwndEdit))
{
fMouseDown = TRUE;
return 0;
}
break;
case WM_MOUSEMOUVE:
if(fMouseDown == TRUE)
{
DoDragDrop (...);
}
fMouseDown = FALSE;
break;
case WM_LBUTTONUP:
fMouseDown = FALSE;
break;
IDropSource接口
IDropSource是拖放操作中最简单的接口,除了IUnknown函数外,它仅仅包含两个需要实现的函数:
IDropSource 方法 描述
QueryContinueDrag 决定是否应该继续、取消或完成一个拖放操作,基于鼠标和<Escape>、 <Control> 和 <Shift> 键的状态。
GiveFeedback 为拖放操作的源提供一个用来给出可视化效果的方法,基于鼠标键、escape、control等键的状态。
两个函数由COM/OLE运行时在拖放操作的修饰键状态改变的时候调用,需要做的工作就是实现这个接口-实际上,在准备拖放的时候已经做了很多编码了。
实现IDropSource
同样我们用一个源文件来实现drop源。在dropsource.cpp中是类的定义,下面就是这些代码:
class CDropSource : public IDropSource
{
public:
//
// IUnknown members
//
HRESULT __stdcall QueryInterface (REFIID iid, void ** ppvObject);
ULONG __stdcall AddRef (void);
ULONG __stdcall Release (void);
//
// IDropSource members
//
HRESULT __stdcall QueryContinueDrag (BOOL fEscapePressed, DWORD grfKeyState);
HRESULT __stdcall GiveFeedback (DWORD dwEffect);
//
// Constructor / Destructor
//
CDropSource();
~CDropSource();
private:
//
// private members and functions
//
LONG m_lRefCount;
};
该类的构造函数除了执行初始化对象引用记数的操作外不执行任何其他的工作。
IDropSource::QueryContinueDrag
下面是QueryContinueDrag函数的定义:
HRESULT QueryContinueDrag(
BOOL fEscapePressed, // Is the <Escape> key being pressed?
DWORD grfKeyState, // Current state of keyboard modifier keys
);
函数返回三个值的一个:
1. S_OK,拖动操作应该继续;如果没有检查到错误就是这个结果,开始拖放操作的鼠标没有被释放,也没有ESC键
2. DRAGDROP_S_DROP,放操作发生来完成拖操作;如果grfKeyState指出启动拖放操作的键松开了就会发生这个结果。
3. DRAGDROP_S_CANCEL;没有放操作发生时拖操作被放弃,这是fEscapePressed是真的表示ESC键被按下的结果。
作为COM的习惯,下面的两个行为在拖放操作中应该遵守:
1. 按下Escape键时取消拖放操作
2. 松开鼠标左键的时候,应该执行放操作
为了符合这些规则,QueryContinueDrag如下实现:
HRESULT __stdcall CDropSource::QueryContinueDrag(BOOL fEscapePressed, DWORD grfKeyState)
{
// if the Escape key has been pressed since the last call, cancel the drop
if(fEscapePressed == TRUE)
return DRAGDROP_S_CANCEL;
// if the LeftMouse button has been released, then do the drop!
if((grfKeyState & MK_LBUTTON) == 0)
return DRAGDROP_S_DROP;
// continue with the drag-drop
return S_OK;
}
IDropSource::GiveFeedback
这个函数通常在每个程序中都不同,因为没有一个程序是相同的。然而除非我们提供一个图形反馈效果,否则我们的是很简单:
HRESULT __stdcall CDropSource::GiveFeedback (DWORD dwEffect){ return DRAGDROP_S_USEDEFAULTCURSORS;}
参数dwEffect(它告诉我们哪个鼠标键被按下,哪个键盘修饰在使用)在许多拖放操作程序中都可以忽略,简单的DRAGDROP_S_USEDEFAULTCURSORS来告诉COM在发生任何修饰符改变的时候更新鼠标光标。
当然,我们可以检查dwEffect的DROPEFFECT_XXX标志来PAINT我们的源窗口并返回S_OK,但为什么要这么麻烦来?
OLE Drap/Drop(4)
本章注重于实现一个暴露IEnumFORMATETC接口的COM对象,这里有两部分代码可以下载。第一包含一个完整的通用的IEnumFORMATETC实现,你可以将它用到你的程序中。另一部分代码是一个叫做IDataObject Viewer的所有代码。这是PlatformSDK同名程序的替代品,它是一个怎么样使用IEnumFORMATETC接口的基本介绍,而不是写这个接口。更重要的是,它在调式OLE拖放代码是非常有用,你可以拖动任何格式的IDataObject到它上面,它会显示显示数据包含的可用格式。
IEnumFORMATETC接口在开始拖放时经常不会注意到,在许多情况下它是不必要的,但为了你的IDataObject可以在所有条件下保证100%工作,提供该接口的完整实现是必要的。
IEnumFORMATETC方法 |
描述 |
Next |
返回枚举中的下一个FORMATETC结构体 |
Skip |
跳过指定数量的FORMATETC structures (例如,不返回他们). |
Reset |
返回枚举的开始状态 |
Clone |
返回与当前结构相同的IEnumFORMATETC 接口, 并且有相同的低层状态 |
下图应该可以能够帮助你描述IEnumFORMATETC接口:
枚举包含3项,枚举索引初始化在第一项(索引是0)。
1. Next方法在索引0时返回第一个FORMATETC结构,并且枚举指针指向索引1。
2. Skip方法以参数2来调用,跳过两个位置,到达枚举的尾部(索引3)。
3. Reset方法返回到索引的开始(索引0)。
IEnumFORMATETC实际上非常简单,仅仅需要实现四个方法:
class CEnumFormatEtc : public IEnumFORMATETC
{
public:
//
// IUnknown members
//
HRESULT __stdcall QueryInterface (REFIID iid, void ** ppvObject);
ULONG __stdcall AddRef (void);
ULONG __stdcall Release (void);
//
// IEnumFormatEtc members
//
HRESULT __stdcall Next (ULONG celt, FORMATETC * rgelt, ULONG * pceltFetched);
HRESULT __stdcall Skip (ULONG celt);
HRESULT __stdcall Reset (void);
HRESULT __stdcall Clone (IEnumFORMATETC ** ppEnumFormatEtc);
//
// Construction / Destruction
//
CEnumFormatEtc(FORMATETC *pFormatEtc, int nNumFormats);
~CEnumFormatEtc();
private:
LONG m_lRefCount; // Reference count for this COM interface
ULONG m_nIndex; // current enumerator index
ULONG m_nNumFormats; // number of FORMATETC members
FORMATETC * m_pFormatEtc; // array of FORMATETC objects
};
构造一个IEnumFORMATETC对象
IEnumFORMATETC最复杂的事情是创建对象,在这时候实现COM方法真的非常简单,好了,创建一个对象是非茶馆内容易的,因为我所需要的就是使用C++操作符new来做这件事情:
IEnumFORMATETC *pEnumFormatEtc = new CEnumFormatEtc (fmtetc, numfmts);
CEnumFormatEtc::CFormatEtc (FORMATETC *pFormatEtc, int nNumFormats)
{
m_lRefCount = 1;
m_nIndex = 0;
m_nNumFormats = nNumFormats;
m_pFormatEtc = new FORMATETC[nNumFormats];
// make a new copy of each FORMATETC structure
for(int i = 0; i < nNumFormats; i++)
{
DeepCopyFormatEtc (&m_pFormatEtc[i], &pFormatEtc[i]);
}
}
我们来看以下这个C++构造函数做了什么,它有两个参数:一个指向FORMATETC结构的数组,另外一个是表示数组中有多少元素的整数。
第一行初始化对象引用记数,这是所有COM对象的标准,我们应该非常熟悉它,因此我这里不在做更多的介绍。
下一步就是初始化枚举状态,成员变量m_nIndex表示枚举中的当前位置,因此它以0开始是很自然的,同样,m_nNumFormats变量用来表示枚举的结尾,有了这两个变量,我们可以跟踪枚举当前的位置和结束位置。
最重要的一步是分配参数中的FORMATETC结构体的一个新数组副本。一个数据被分配(m_pFormatEtc)其保存所有要被枚举的结构体,每个枚举需要有自己的私有FORMATETC结构的缓存,关键细节是复制FORMATETC结构的方法,这里,我们引入一个叫DeepCopyFormatEtc新的函数。
void DeepCopyFormatEtc(FORMATETC *dest, FORMATETC *source)
{
// copy the source FORMATETC into dest
*dest = *source;
if(source->ptd)
{
// allocate memory for the DVTARGETDEVICE if necessary
dest->ptd = (DVTARGETDEVICE*)CoTaskMemAlloc(sizeof(DVTARGETDEVICE));
// copy the contents of the source DVTARGETDEVICE into dest->ptd
*(dest->ptd) = *(source->ptd);
}
}
函数的第一行非常简单:
*dest = *source;
这个实际上是一个标准的C函数memcpy。实际上,这几乎在所有的情况都是需要的,因为他能正确的执行一个二进制的结构体到结构体的复制,问题是当源FORMATETC::ptd成员的已经被初始化为指向一个DVTAGETDEVIDE结构,就不能正确复制了。
仅仅在FORMATETC上执行memcpy是不够的,因为两个FORMATETC结构体都指向原来的DVTARGETDEVICE;因此我们私有的结构体复制函数是需要的。
IEnumFORMATETC::Next文档声明调用这使用CoTaskMemFree这个API来释放DVTARGETDEVICE结构体,逻辑上意味着这个结构必须首先已经使用CoTaskMemAlloc来分配了棵,因此这就是深度复制所做的,使用CoTaskMemAlloc来分配一个新的DVTARGETDEVICE结构体,并且设置dest->ptd指向原来的,那么source->DVTARGETDEVICE结构体就被复制到新的指针上了。
清理一个IEnumFORMATETC对象
CEnumFormatEtc类的C++析构函数必须清理所有在构造函数分配的内存:
CEnumFormatEtc::~CEnumFormatEtc()
{
// first free any DVTARGETDEVICE structures
for(ULONG i = 0; i < m_nNumFormats; i++)
{
if(m_pFormatEtc[i].ptd)
CoTaskMemFree(m_pFormatEtc[i].ptd);
}
// now free the main array
delete[] m_pFormatEtc;
}
这是一个简单的任务,调用CoTaskMemFree来释放所有在构造函数中分配的DVTAGETDEVICE结构体,一旦这些已经释放完了,m_pFormatEtc数组也应该被释放。
取代SHCreateStdEnumFmtEtc
你可能会问,在该指南中,我们为什么会一直这么烦心来,因为SHCreateStdEnumFmtEtc API调用可以用来创建IEnumFORMATETC接口,但不幸的是,它只能在WINDOS2000以上的版本使用,看原型:
HRESULT SHCreateStdEnumFmtEtc(UINT cfmt, const FORMATETC afmt[], IEnumFORMATETC **ppenumFormatEtc);
因此如果你不准备向下兼容老的版本Windows的拖放操作,否则我们总是需要实现IEnumFORMATETC。我们需要做的就是写一个SHCreateStdEnumFmtEtc的drop-in替代版本,我们可以在仅仅支持windows2000的时候很容易切换,我们的版本是这样的:
HRESULT CreateEnumFormatEtc (UINT cfmt, FORMATETC *afmt, IEnumFORMATETC **ppEnumFormatEtc)
{
if (cfmt == 0 || afmt == 0 || ppEnumFormatEtc == 0)
return E_INVALIDARG;
*ppEnumFormatEtc = new CEnumFormatEtc (afmt, cfmt);
return (*ppEnumFormatEtc) ? S_OK: E_OUTOFMEMORY;}
函数非常简单,因为所有的工作都在CEnumFormatEtc构造函数中调用,我们需要做的就是创建一个类的实例,然后以最后一个参数返回;其余的代码是错误检查。
使用API是也很简单:
FORMATETC fmtetc = {CF_TEXT, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
IEnumFORMATETC *pEnumFormatEtc;
CreateEnumFormatEtc (1, &fmtetc, &pEnumFormatEtc);
这似乎是枚举一些简单FORMATETC结构的许多工作,但这是值得的,因为我们的COM枚举器现在真正的独立了,剩下的接口就非常简单了。
IEnumFORMATETC::Reset
这个成员非常简单,设置枚举到开始的位置:
HRESULT CEnumFormatEtc::Reset (void){
m_nIndex = 0;
return S_OK;}
上面的实现可以自解释。
IEnumFORMATETC::Skip
该实现直接向前移动,简直不需要解释:
HRESULT IEnumFORMATETC::Skip (ULONG celt){
m_nIndex += celt;
return (m_nIndex <= m_nNumFormats) ? S_OK : S_FALSE;}
该函数仅仅向前移动枚举指定单元。注意,尽管这里没有保证索引在枚举范围内,但一个返回值用来指示是否前进的太多。
IEnumFORMATETC::Clone
Clone函数起先看起来优点神秘;尽管我很少看到这个函数调用,它实际上很容易实现:
HRESULT IEnumFORMATETC::Clone(IEnumFORMATETC **ppEnumFormatEtc)
{
HRESULT hResult;
// make a duplicate enumerator
hResult = CreateEnumFormatEtc(m_nNumFormats, m_pFormatEtc, ppEnumFormatEtc);
if(hResult == S_OK)
{
// manually set the index state
((CEnumFormatEtc *)*ppEnumFormatEtc)->m_nIndex = m_nIndex;
}
return hResult;
}
上面代码很简单地创建了一个IEnumFORMATETC接口的实例,使用我们前面写的CreateEnumFormatEtc函数;使用当前的枚举内部状态,因此结果就是复制接口的当前内部状态。
在if从句中的转型看起来有点复杂,其用来保留枚举的索引位置,转型是必须的,因为IEnumFORMATETC接口并可以访问内部变量,然而,我们知道ppEnumFormatEtc实际上就是一个CEnumFormatEtc,所以这个转换能安全的执行。转换操作看起来复杂的原因是我们必须引用ppEnumFormatEtc参数来访问指向IEnumFORMATETC的指针。
IEnumFORMATETC::Next
Next成员函数比其他的稍微棘手一点:
HRESULT CEnumFormatEtc::Next(ULONG celt, FORMATETC *pFormatEtc, ULONG *pceltFetched)
{
ULONG copied = 0;
// copy the FORMATETC structures into the caller's buffer
while (m_nIndex < m_nNumFormats && copied < celt)
{
DeepCopyFormatEtc (&pFormatEtc [copied], &m_pFormatEtc [m_nIndex]);
copied++;
m_nIndex++;
}
// store result
if(pceltFetched != 0)
*pceltFetched = copied;
// did we copy all that was requested?
return (copied == celt) ? S_OK : S_FALSE;
}
这个函数看起来有点复杂,但可以被分解成三个重要的操作;主要的部分是while循环部分,它负责复制FORMATETC结构(使用深度复制程序),循环仅仅复制范围内的元素到提供的缓冲区中。
第二部分是返回实际复制的相数,且返回一个错误值来指示是否所有需要复制的元素都被复制了。
最后一部分就是错误代码来指示复制指定数量的项数的操作成功和失败
OLE Drap/Drop(3)
上一张我们着重介绍了怎么样使用OLE和IDataObject来访问windows粘贴板。本章主要实现一个IDataObject接口,然后使用我们完成的数据对象来存储文本“Hello World”到粘贴板中。
创建一个COM接口-IDataObject
为了创建一个COM对象,我们需要定义一个实现所有这些函数的C++类,并且让COM的虚函数表为我们自动包含,我们使用C++类继承:
class CDataObject : public IDataObject
{
Public:
// IUnknown members
HRESULT __stdcall QueryInterface (REFIID iid, void ** ppvObject);
ULONG __stdcall AddRef (void);
ULONG __stdcall Release (void);
// IDataObject members
HRESULT __stdcall GetData (FORMATETC *pFormatEtc, STGMEDIUM *pmedium);
HRESULT __stdcall GetDataHere (FORMATETC *pFormatEtc, STGMEDIUM *pmedium);
HRESULT __stdcall QueryGetData (FORMATETC *pFormatEtc);
HRESULT __stdcall GetCanonicalFormatEtc (FORMATETC *pFormatEct, FORMATETC *pFormatEtcOut);
HRESULT __stdcall SetData (FORMATETC *pFormatEtc, STGMEDIUM *pMedium, BOOL fRelease);
HRESULT __stdcall EnumFormatEtc (DWORD dwDirection, IEnumFORMATETC **ppEnumFormatEtc);
HRESULT __stdcall DAdvise (FORMATETC *pFormatEtc, DWORD advf, IAdviseSink *, DWORD *);
HRESULT __stdcall DUnadvise (DWORD dwConnection);
HRESULT __stdcall EnumDAdvise (IEnumSTATDATA **ppEnumAdvise);
stgmed
// Constructor / Destructor
CDataObject (FORMATETC *fmtetc, STGMEDIUM *, int count);
~CDataObject ()
private:
LONG m_lRefCount;
int LookupFormatEtc(FORMATETC *pFormatEtc);
};
上面列出了所有IDataObject成员,包括IUnknown接口成员,这是因为我们现在需要实现整个COM对象,因此每个成员必须正确的包含。
由于IUnknown函数我们在前面已经介绍了,我们继续介绍IDataObject函数。有些好的消息,同时也有些坏的消息;好的消息是,不是所有饿函数都需要实现,在IDataObject的9个函数中,我们仅仅需要实现3个来支持OLE的拖放操作,因此显著节省了我们的工作量。
坏的消息是:一般我们已经实现了IDataObject方法,我们需要实现完全独立的COM接口-IEnumFORMATETC接口。然而到这步还有很大的距离,因此让我们以一个简单分配新IDataObject的实例作为一个开始。
构造IDataObject
IDataObject的主要任务是允许一个消费者查询数据,这些查询从QueryData或EnumFormatEtc调用来发起的,因此,IDataObject需要知道存储什么样的数据格式,并且在消费者需要数据的时候,它能够提供。
我们因此需要找到一些办法来以FORMATETC结构的形式用真正的数据片来组装IDataObject且说明数据是什么。
IDataObject在C++类构造函数的时候组装,为了更弹性,可能需要添加内部帮助程序来执行这个任务,但对于我们简单实现仅在构造函数中使用。
CDataObject::CDataObject (FORMATETC *fmtetc, STGMEDIUM *stgmed, int count){
// reference count must 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。我看到过许多不正确的COM代码,他们初始化记数为0,COM规约明确地声明,一个COM对象必须以“1”作为生命周期的开始,如果你记得,一个记数为0的COM对象应该被删除,因此它应该从不应该被初始化为这个值。
第二个任务是在类构造函数中做一个私有的FORMATETC和STGMEDIUM的副本。数据对象不是每个STGMEDIUM结构体内部的所有者,它纯粹是引用并且在请求调用GetData的时候复制数据。
创建IDataObject对象
现在我们有一个定义良好的IDataObject构造函数,我可以写一个包装函数来隐藏类的细节:
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;
}
现在创建一个IDataObject变的非常简单:
FORMATETC fmtetc = {CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
STGMEDIUM stgmed = {TYMED_HGLOBAL, {0}, 0};
stgmed.hGlobal = StringToHandle ("Hello, World!");
IDataObject *pDataObject;
CreateDataObject (&fmtetc, &stgmed, 1, &pDataObject);
许多IDataObject的实现包含许多接口内部执行内存分配的程序指定编码;在这个实现后面的思想是可以提供一个用于各种程序的通用IDataObject。好了,在创建数据对象之前有点工作需要做就是创建FORMATETC和STGMEDIUM结构,但这很容易被隔离,并且不会污染接口编码。
IDataObject::QueryGetData
该成员函数在某程序想检查IDataObject看是否包含指定类型的数据时候调用。一个指向FORMATETC结构的指针作为一个参数,且IDataObject::QueryGetData来检查这个结构且返回一个值来指示请求的数据是否可用。
HRESULT __stdcall IDataObject::QueryGetData(FORMATETC *pFormatEtc)
{
return (LookupFormatEtc(pFormat) == -1) ? DV_E_FORMATETC : S_OK;
}
这个例子中的QueryGetData函数非常简单,我们放弃私有协助函数-LookupFormatEtc的所有工作:
int CDataObject::LookupFormatEtc(FORMATETC *pFormatEtc)
{
// 轮流检查格式看是否能找到匹配的格式
for(int i = 0; i < m_nNumFormats; i++)
{
if((m_pFormatEtc[i].tymed & pFormatEtc->tymed) &&
m_pFormatEtc[i].cfFormat == pFormatEtc->cfFormat &&
m_pFormatEtc[i].dwAspect == pFormatEtc->dwAspect)
{
// return index of stored format
return i;
}
}
// error, format not found
return -1;
}
上面的函数尽量在我们数据对象的可用结构中查找一个与指定FORMATETC结构匹配的对象,如果找到一个匹配的,就简单的返回相应m_pFormatEtc数组的索引,如果找不到,返回-1表示一个错误。
注意,在if从句中的位与操作符:
if( m_pFormatEtc[i].tymed & pFormatEtc->tymed )
AND操作符用在这里是因为FORMATETC::tymed成员实际上是一个位标志,它能够包含不止一个值;例如:QueryGetData的调用者可以完全指定一个FORMATETC::tymed值(TYMED_HGLOBAL|TYMED_ISTREAM)就意味着你支持HGLOBAL或IStream吗?
IDataObject::GetData
GetData函数和QueryGetData有许多相似之处,除了如果支持请求的数据格式,它必须返回指定的存储类型。
HRESULT __stdcall CDataObject::GetData (FORMATETC *pFormatEtc, STGMEDIUM *pStgMedium)
{
int idx;
// try to match the specified 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 into the 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;
}
同样要调用内部协助函数LookupFormatEtc来检查是否支持请求的数据格式,如果支持,相应的STGMEDIUM数据被复制到调用者提供的结构。
注意,现在调用DupGlobalMem程序,这是一个协助函数,它返回指定HGLOBAL内存的HANDLE的副本,并且必须返回部分,因为每个GetData调用都要求一个新的数据副本。
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;
}
我们需要同样的程序来支持TYMED_xxx存储类型,但现在我们设想实现的支持格式是IStream。
IDataObject::EnumFormatEtc
这是最后需要自己动手的成员,不幸的是这个成员函数实现如此简单,但也要求我们写IEnumFORMATETC对象。
HRESULT __stdcall CDataObject::EnumFormatEtc (DWORD dwDirection, IEnumFORMATETC **ppEnumFormatEtc)
{
// OLE仅仅支持得到方向成员
if(dwDirection == DATADIR_GET)
{
// 在WIN2K下,你可以调用AIP函数SHCreateStdEnumFmtEtc来完成,但为了支持//所有的window平台,我们需要实现IEnumFormatEtc。
return CreateEnumFormatEtc(m_NumFormats, m_FormatEtc, ppEnumFormatEtc);
}
else
{
// the direction specified is not supported for drag+drop
return E_NOTIMPL;
}
}
看到上面的代码,你会提到SHCreateStdEnumFmtEtc这个API调用,它能够代表我们创建IEnumFORMATETC接口,不幸的是,这个API仅仅在WIN2K上可用,因此,我们需要提供其他创建IEnumFORMATETC对象。
因此下面的旅程中,我们将提供一个CreateEnumFormatEtc的完整实现,来代替Shell API调用。
不支持的IDataObject函数
仍然有一些IDataObject函数需要实现,而同时每个函数必须是一个有效的程序,有个简单的办法可以指定给OLE,我们不支持这些拖放操作以外的函数。
IDataObject::DAdvise、IDataObject::EnumDAdvise和IDataObject::DUnadivise函数简单的返回OLE_E_ADVISENOTSUPPORTED。
HRESULT CDataObject::DAdvise (FORMATETC *pFormatEtc, DWORD advf, IAdviseSink *pAdvSink, DWORD *pdwConnection)
{
return OLE_E_ADVISENOTSUPPORTED;
}
HRESULT CDataObject::DUnadvise (DWORD dwConnection)
{
return OLE_E_ADVISENOTSUPPORTED;
}
HRESULT CDataObject::EnumDAdvise (IEnumSTATDATA **ppEnumAdvise)
{
return OLE_E_ADVISENOTSUPPORTED;
}
GetDataHere只需要实现IStream和IStorage接口来支持数据对象,在我们的例子中,我们只支持HGLOBAL数据,因此返回DATA_E_FORMATETC是一个明智的选择。
HRESULT CDataObject::GetDataHere (FORMATETC *pFormatEtc, STGMEDIUM *pMedium)
{
return DATA_E_FORMATETC;
}
SetData和GetCanonicalFormatEtc也只要简单的实现,本例中可以返回E_NOTIMPL值,即使我们返回错误的值,一个GetCanonicalFormatEtc记名票据,输出的FORMATETC结构ptd成员应该是0。
HRESULT CDataObject::GetCanonicalFormatEtc (FORMATETC *pFormatEct, FORMATETC *pFormatEtcOut)
{
// Apparently we have to set this field to NULL even though we don't do anything else
pFormatEtcOut->ptd = NULL;
return E_NOTIMPL;
}
HRESULT CDataObject::SetData (FORMATETC *pFormatEtc, STGMEDIUM *pMedium, BOOL fRelease)
{
return E_NOTIMPL;
}
添加数据到粘贴板
好了,这里有一个简单那的程序用来通过OLE和数据对象来添加“Hello World”到Windows的粘贴板。
#include <windows.h>
int main(void)
{
OleInitialize (0);
IDataObject *pDataObject;
FORMATETC fmtetc = {CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
STGMEDIUM stgmed = {TYMED_HGLOBAL, {0}, 0};
stgmed.hGlobal = StringToHandle ("Hello, World!”, -1);
// create the data object
if (CreateDataObject(&fmtetc, &stgmed, 1, &pDataObject) == S_OK)
{
// add data to the clipboard
OleSetClipboard (pDataObject);
OleFlushClipboard ();
pDataObject->Release();
}
// cleanup
ReleaseStgMedium (&stgmed);
OleUninitialize ();
return 0;
}
不幸的是这个程序不能工作,因为我们还没有实现IEnumFORMATETC和CreateEnumFormatEtc函数。
OLE Drap/Drop(2)
欢迎来到OLE拖放指南第二部分;本部分的目的在于解释在OLE环境中,程序之间怎么样表示和传输数据。
OLE数据传输的核心是IDataObject COM接口,一个IDataObject提供从一个程序到另一个程序传输和访问数据的方法。最通用的OLE数据传输是窗口粘贴板,当然也有拖放。IDataObject是一到多个数据的有效的COM包装。
在我们调查IDataObject任何细节之前,两个重要的数据结构你必须熟悉:FORMATETC和STGMEDIUM接口,他们用来描述和存储OLE数据。
描述OLE数据
FORMATETC接口(发音“format et cetera”)用来表示IDataObject提供(或接收)的数据类型,是标准window粘贴板格式(CF_TEXT等)的扩展,因此除了基本的粘贴板格式之外,还包含了数据怎么样rendered和存储。
typedef struct
{
CLIPFORMAT cfFormat; // 粘贴板格式
DVTARGETDEVICE *ptd; // (NULL) rendering的目标设备
DWORD dwAspect; // (DV_CONTENT) rendering的详细程度
LONG lindex; // (-1) 在数据通过页面边界分割的时候使用
DWORD tymed; // 用于数据传输的存储媒体(HGLOBAL,IStream)
} FORMATETC;
FORMATETC结构的成员如下描述:
cfFormat:粘贴板格式,用来表示FORMATETC结构。可以是内建的格式(例如:CF_TEXT或CF_BITMAP)或者用RegisterClipboardFormat注册的自定义格式。
Ptd:指向DVTARGETDEVICE结构,提供已经rendered数据的设备信息。正常的粘贴板操作和拖放操作都是NULL。
dwAspect:描述用户怎么样render数据的大量细节。通常这个是DVASPECT_CONTECT,表示全内容,但也可以描述较少的信息,例如:图标。
Lindex:仅仅在当数据通过页面边界被分割的时候使用,它不用于简单的OLE传输,因此该值几乎总是-1。
Typemed:这是一个有趣的成员;因为其描述了用于存储数据的存储媒体类型。该成员名字自词组“Type of Medium”;该值在window.h中定义的TYMED_XXX等值。
因此有了这个数据结构,OLE已经提供了一个描述消费者什么样的数据已经怎么样render这个数据。
存储OLE数据
结构体STGMEDIUM(STORAGE MEDIUM的缩写)提供一个用来存储数据的容器,因此叫存储媒体:
typedef struct
{
DWORD tymed;
union
{
HBITMAP hBitmap;
HMETAFILEPICT hMetaFilePict;
HENHMETAFILE hEnhMetaFile;
HGLOBAL hGlobal;
LPWSTR lpszFileName;
IStream *pstm;
IStorage *pstg;
};
IUnknown *pUnkForRelease;
} STGMEDIUM;
这个结构定义看起来比较复杂,但是有用的仅仅三个成员,因为未命名联合合并了所有内容作为一个实体共享同样的存储空间。
1. tymed:这个成员必须和FORMATETC结构相同,这个成员指定已经存储的媒体类型,例如,全局数据(TYMED_HGLOBAL),IStream(TYPED_ISTREAM)等等。相应的联合中的元素是数据的句柄。
2. hBitmap/hGlobal等:实际的数据,仅仅他们中的一个是有效的,这依赖于tymed的值。
3. pUnkForRelease:一个可选的指针,指向IUnknown接口,数据的接收方应该调用其Release方法。当这个字段是NULL时,接收方有责任释放内存句柄。ReleaseStgMedium API调用在这里非常有用,它负责释放STGMEDIUMS的数据内容,因此实际上我们不需要做什么。
STGMEDIUM结构是传统的windows HGLOBAL内存句柄的扩展,同时支持HGLOBAL(且一直是最常用的),同时支持许多其他的类型,最有用的是IStream和IStorage通用COM接口。
总之,结构体FORMATETC和STGMEDIUM一起用来描述和存储OLE数据实体的。FORMATETC通常用来从IDataObject请求指定类型的数据,同时STGMEDIUM结构用来接收和保存请求的数据。
传输OLE数据
IDataObject接口提供了从一个程序到另一个程序传递数据的方法,IDataObject在两个情况下非常有用:粘贴板和拖放。如果设计精细,可以用一个COM对象来同时实现粘贴板和拖放操作。
下面的表列出了IDataObject成员函数,按照他们在接口虚表中出现的顺序。为了简化的原因,IUnknown方法(AddRef,Release和QueryInterface)没有列出。
IDataObject方法 |
描述 |
GetData |
Render在FORMATETC结构体中描述的数据,并通过STGMEDIUM结构体来传递数据 |
GetDataHere |
Render在FORMATETC结构体中的数据,并通过调用者分配的STGMEDIUM结构体传输数据。 |
QueryGetData |
判断数据对象是否可以render在FORMATETC结构中描述的数据 |
GetCanonicalFormatEtc |
提供一个潜在不同的但逻辑上相同的FORMATETC结构体。 |
SetData |
提供一个通过FORMATECT结构和STGMEDIUM结构描述的源数据对象。 |
EnumFormatEtc |
创建并返回一个IEnumFORMATETC接口的指针来枚举数据对象支持的FORMATETC对象。 |
DAdvise |
创建一个在数据对象和通知接收器之间的连接,因此通知接收器能接收到数据对象中通知的改变 |
DUnadvise |
销毁一个前面使用DAdvise方法安装的通知。 |
EnumDAdvise |
创建和返回一个指向枚举当前通知连接的接口指针。 |
这个表看起来很漂亮,我们也看到EnumFormatEtc方法并发现我们不得不同时实现IEnumFORMATETC接口,它有13个成员函数,不包括IUnknown方法,到这里我们还没有考虑IDropSource和IDropTarget。
庆幸的是,为了简化OLE拖放,我们仅仅需要实现GetData、QueryGetData和EnumFormatEtc,因此这节省了我们许多工作。
使用IDataObject来访问粘贴板
为了使在我们的OLE旅程中放松一下,下面我们来看一个简单的通过OLE来访问粘贴板的例子:
WINOLEAPI OleGetClipboard(IDataObject ** ppDataObj);
这个简单的Windows API调用用来返回一个IDataObject,它提供用来一个干净地访问WINDOWS粘贴板内容的好接口。注意,我们在本例中不需要实现IDataObject接口,我们仅仅需要知道接口怎么样工作的,一个简单的访问粘贴板内容的程序如下:
#include <windows.h>
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;
}
OLE API调用非常简单,且它是直接来访问IDataObject对象:
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 a HGLOBAL
if(pDataObject->GetData(&fmtetc, &stgmed) == S_OK)
{
// 我们必须锁定HGLOBAL句柄,因为我们不能确信这是否是一个GEM_FIXED数据
char *data = GlobalLock(stgmed.hGlobal);
printf("%s\n", data);
// cleanup
GlobalUnlock(stgmed.hGlobal);
ReleaseStgMedium(&stgmed);
}
}
上面的代码演示了最常用的访问IDataObject的方法,数据通过调用IDataObject::GetData来请求,我们构造一个FORMATETC对象,它指定了我们想要访问的数据的类型,在这个例子中,标准的CF_TEXT数据缓冲区以HGLOBAL内存对象来存储。
数据返回到我们提供的STGMEDIUM结构体中,一旦我们锁定并显示数据,清理和调用标准的ReleaseStgMedium API来释放存储在STGMEDIUM结构中的数据就简单了。
注意,代码中仅仅当文本被选择到粘贴板的时候才工作,也就是说,如果没有CF_TEXT被存储到粘贴板,粘贴板的IDataObject::GetData程序调用会失败,我们什么也不打印。
OLE Drap/Drop(1)
转自:http://www.handytech.cn/main/ArticleShow.asp?ArtID=244
该指南的目的在于读者能够在他们自己的程序实现完整的拖拽功能。自Window95以来,Drag和Drop已经成为Window程序的一个标准功能,随着COM和OLE成为主流技术,程序能和Window Shell甚至其他Window程序无缝交互。这个弹性是以高昂的代价为基础的,说的婉转点,写一个COM或OLE支持的程序完全是一个噩梦。
本指南目的在于帮助你轻松克服写一个OLE接口支持的拖拽程序的困难。通常,我们使用纯WIN32 API基础。然而,我会使用C++而不是C,因为C++是写COM接口程序的首选语言;我也会解释怎么样以简单的方式转换成C语言。
我有意以几个部分来写这个指南,主要的原因是太多的信息。另外,Drag-and-drop组件也使他们有各自不同的主题,因此我采用了这种方法。指南的第一部分(实际上就是该部分)简单介绍OLE 拖拽,后面的指南着重于拖拽;第2和3部分介绍OLE数据传输IDataObject接口。第4部分看一下IEnumFORMATETC接口,第5和6部分介绍drag源和drop目标。
推荐阅读
我强烈推荐你研究一下下面的信息,因为我是从那里学习COM、OLE拖拽的。
1. msdn.microsoft.com
每个win32相关的起始之处。
2. Inside OLE 2nd edition
该书中有许多有用的信息,被作为OLD的圣经。它有点老了,但包含每个你需要知道的东西。MSDN中包含了该书的一个软COPY,也许一直再那里;因特耐特上也有许多PDF和CHM的版本。
3. ftp://ftp.microsoft.com/softlib/msfiles
微软的FTP服务器包含几百个以前的资源,到目前为止我发现的最有用的东西是两个小文件:drgdrps.exe和drgdrpt.exe。他们是自解压的ZIP文件,包含了简单的drop源和drop目标程序的完整代码,为了可以简单的访问这些文件,你仅仅需要输入下面的命令:
ftp ftp.microsoft.com
username "ftp"
password "ftp"
1. cd softlib/mslfiles
bin
get drgdrps.exe
get drgdrpt.exe
bye
4. 微软技术论文-OLE for Idiots系列,What OLE is Really about等,这些论文虽然很老了,但他们在今天依然有用。在GOOGLE中可以轻松查询到。
OLE Drag和Drop
拖放是用来描述使用鼠标将数据从一个地方传输到另一个地方的短语。
每个拖放操作包含三个元素,当然这些元素是COM对象,需要支持拖放功能的程序都必须实现这三个元素。
1. IDropSource接口表示拖放操作的源。IDropSource包含产生可视化的方法,取消或完成拖放操作的方法。
2. IDropTarget接口用来表示拖放操作的目标对象。
3. IDataObject接口用来表示拖放操作过程中传输的数据。
注意,一个程序不需要支持所有的COM接口;如果你想定义一个drop目标,那么仅仅实现IDropTarget接口,同样,如果一个需要支持作为数据源的程序应该支持IDropSource和IDataObject接口。当然,程序也可以实现三个接口,从而在同一个程序中支持拖放操作。
上面的图描述了拖放操作中需要支持的关键组件;花点时间来理解一下图的内容。左边的方块是拖放操作的出发点,它已经创建了两个COM对象,每个暴露一个接口(IDataObject和IDropSource),OLE通过他们来执行拖放操作。
右边的方块表示拖放操作的目标,其创建一个COM对象(IDropTarget接口)。当鼠标被拖动过目标窗口时,OLE传递一个IDataObject接口到目标对象,这是源暴露给目标的数据对象。对象不能以任何方式得到一个副本,仅仅COM接口变为可用。当目标从数据对象提取数据时,OLE/COM运行时负责函数调用已经通过进程边界的数据传输。
上面的例子中,源和目标可以是同一个进程,也可以是不同的进程。在那里实现并不重要,因为OLE运行时(实际上是COM)负责数据对象在目标进程中激活。
开始拖放
任何程序想要使用OLE函数时首先需要做的是在启动的时候调用OleInitialize并且在结束的时候调用OleUninitialize。这么说不是很准确,最好说想要使用OLE的线程必须调用这些函数,因为COM和OLE必须在每个线程中被初始化和释放。
WINOLEAPI OleInitialize (LPVOID pvReserved);
WINOLEAPI OleUninitialize ();
非常核心的OLE拖放是一个API调用DoDragDrop,函数原型如下:
WINOLEAPI DoDragDrop(
IDataObject * pDataObject, // Pointer to the data object
IDropSource * pDropSource, // Pointer to the source
DWORD dwOKEffect, // Effects allowed by the source
DWORD * pdwEffect // Pointer to effects on the source
);
一个程序想初始化拖放操作,他必须首先调用这个函数,但在调用DoDragDrop之前,两个重要的步骤必须完成。
在调用DoDragDrop ,IDataObject和IDropSource对象被拖放操作的发起者创建。创建这两个对象并不是琐碎的事情,因此我们在下一部分介绍。另外我们这里一直没有提到创建任何GUI相关的对象(例如窗口),实际上一个drop源是独立于任何窗口的单独实体,即使拖放操作在窗口程序处理WM_MOUSEMOVE消息的时候初始化的。
当DoDragDrop被调用时,进入一个摸态的消息循环,用来监视鼠标和简单的消息。
接收Drag和Drop数据
一个程序想作为拖放操作的接收方,它必须调用RegisterDragDrop函数,当然,这个程序也必须调用与源程序一样调用OleInitialize/OleUninitialize函数。
WINOLEAPI RegisterDragDrop(
HWND hwnd, // Handle to a window that can accept drops
IDropTarget * pDropTarget // Pointer to object that is to be target of drop
);
看一下上面函数原型显示了最后一个拖放操作组件-IDropTarget COM接口。RegisterDragDrop同时要求一个窗口的句柄。该窗口被OLE运行时注册,因此,当鼠标拖过该窗口时,OLE能调用IDropTarget接口的方法来通知拥有该窗口的程序正在进行一个拖放操作。
当这个窗口被销毁时,应该调用RevokeDragDrop API:
WINOLEAPI RevokeDragDrop(
HWND hwnd // Handle to a window that can accept drops
);
这个API用OLE来反初始化指定的窗口,并且释放注册时使用的IDropTarget接口和进程中的DropTarget对象。
2007年4月3日