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,但为什么要这么麻烦来?