数据对象是所有Shell数据传输的要点。数据对象主要用作数据容器,用以存储要传输的数据。然而,传输目标也可以与数据对象通信,以方便处理像优化的移动这样特定类型的Shell数据传输。本文提供对Shell数据对象如何工作、传输源如何创建数据对象、传输目标如何处理数据对象的一般讨论。关于如何使用数据对象传输各种不同类型Shell数据的详细讨论,见Handling Shell Data Transfer Scenarios。
1.数据对象如何工作
数据对象是由数据源创建,用以传输数据到传输目标的COM对象。数据对象通常携带多项数据,原因是:
1.1 剪贴板格式
数据对象中的每个数据项都有一个相关的格式,通常称作剪贴板格式。Winuser.h中声明了多种对应通常使用的数据类型的标准剪贴板格式。剪贴板格式是个整数值,但通常用其形式为CF_XXX的等价名字来表示。比如说,ANSI文本的剪贴板格式是CF_TEXT。
应用程序可以通过定义私有格式来扩展可用的剪贴板格式。要定义私有格式,调用RegisterClipboardFormat,传入标识格式的字符串。函数返回一个表示剪贴板格式的无符号整数值,可以像标准剪贴板格式那样使用这个值。但使用这个格式之前,传输源和目标都必须注册这个格式。但用于传输Shell数据的剪贴板格式CF_HDROP是个例外(不需要注册),它也被定义为私有格式。关于可用剪贴板格式的描述,见Shell Clipboard Formats。
虽然有一些例外,但通常来说,对于所支持的每种剪贴板格式,数据对象只包含一个数据项。这种格式与数据项的一对一关系,使得格式值可以用于标识相关数据项。实际上,在讨论数据对象内容的时候,通常用数据的格式来指代数据本身。比如说,在讨论数据对象的ANSI文本数据项的时候,通常会说“取出CF_TEXT格式……”。
放置目标收到数据对象指针时,会枚举可用的格式以确定什么类型的数据可用,然后请求一种或者多种可用的格式,取出数据。从数据对象中取出数据的方式随着格式的不同而不同,相关详细讨论见本文的How a Target Handles a Data Object节。
对于简单的剪贴板数据传输,数据放置在全局存储对象中。对象的地址及其格式被放置在剪贴板中。剪贴板格式告诉放置目标可以从相关地址处取得什么类型的数据。虽然简单的剪贴板数据传输很容易实现,但是:
基于上述理由,所有Shell数据传输都使用数据对象。数据对象不直接使用剪贴板格式,而是由一种通用的剪贴板格式,即FORMATETC结构体标识。
1.2 FORMATETC结构体
FORMATETC结构体是剪贴板格式的扩展版本。为了能用于Shell数据传输,它具有下列特征:
1.3 STGMEDIUM结构体
STGMEDIUM结构体提供对被传输数据的访问。Shell数据支持三种传输机制:
STGMEDIUM结构体的tymed成员值TYMED_XXX表示数据传输机制。结构体的第二个成员是一个指针,传输目标用它取出数据。指针可以是多种类型中的一种,这决定于tymed成员值。下表总结了用于Shell数据传输的三种tymed成员值以及它们相关的STGMEDIUM成员名字:
tymed 值 | 成员名 | 描述 |
---|---|---|
TYMED_HGLOBAL | hGlobal | 指向全局存储对象的指针。这种指针类型通常用于传输少量数据。比如说,Shell使用全局存储对象传输像文件名或者URL这样较短的字符串。 |
TYMED_ISTREAM | pstm | 指向IStream接口的指针。这种指针类型对于大多数Shell数据传输是最佳选择,因为与TYMED_HGLOBAL相比,它使用相对少的内存。而且,TYMED_ISTREAM不要求数据源以任何特殊方式存储其数据。 |
TYMED_ISTORAGE | pstg | 指向IStorage接口的指针。传输目标调用IStorage接口方法取出数据。与TYMED_ISTREAM类似,这种指针类型需求的内存也比较少。然而,它不如TYMED_ISTREAM灵活,所以使用得较少 |
2. 传输源如何创建数据对象
用户发起Shell数据传输时,传输源需要创建数据对象并载入数据,过程如下:
经过这几步之后,数据对象就可以被传递给目标了。对于剪贴板数据传输,直到目标通过OleGetClipboard请求数据对象前,对象都被简单地保持在剪贴板中。对于拖放数据传输,数据对象必须创建代表数据的图标,并且在用户移动光标时相应移动图标。在拖动循环中,传输源通过数据对象的IDropSource接口获取其状态信息。更深入的讨论,见本文的Implementing IDropSource节。
如果目标从剪贴板取得数据对象,传输源不会收到通知。而对于拖放操作,数据对象被放置到目标中时,发起拖动循环的DoDragDrop函数调用会返回。
2.1 如何向数据对象添加全局存储对象
很多Shell数据格式以全局存储对象的形式存在。创建包含全局存储对象的剪贴板格式,并载入到数据对象中的过程如下:
下面的例子函数创建一个含有DWORD值的全局存储对象,并且载入到数据对象中。pdtobj参数是到数据对象IDataObject接口的指针,cf是剪贴板格式值,dw是数据值。
<…… 省略示例代码 ……>
2.2 实现IDataObject
IDataObject是数据对象的主要接口,所有数据对象都必须实现它。传输源和目标把IDataObject用于各种目的:
IDataObject支持很多方法。本节讨论如何实现IDataObject的三个最重要方法:SetData、EnumFormatEtc和GetData。关于IDataObject其他方法的讨论,见IDataObject的参考文档。
2.2.1. SetData 方法
IDataObject::SetData方法的主要功能是让数据源把数据装载到数据对象中。对于要包含的每种格式,数据源会创建用以标识格式的FORMATETC结构体和用以存储数据指针的STGMEDIUM结构体,随后数据源会在调用IDataObject::SetData时会传入这两个结构体。IDataObject::SetData方法必须保存这两个结构体信息,这样传输目标调用IDataObject::GetData时才能从数据对象中取出数据。
然而,在传输文件的时候,Shell通常把要传输的每个文件的信息放到单独的CFSTR_FILECONTENTS格式的数据项中。为了区分不同的文件,每个文件的FORMATETC结构体的lIndex成员被设置为标识特定成员的索引值。IDataObject::SetData实现必须能够存储多个只是lIndex成员有所不同的CFSTR_FILECONTENTS格式的数据项。
光标位于目标窗口上时,目标可以使用拖放辅助对象来指定拖动图像。拖放辅助对象调用IDataObject::SetData把私有格式装载到用于跨进程支持的数据对象中。为支持拖放辅助对象,IDataObject::SetData实现必须能够接受和存储任何私有格式的数据项。
某些类型的Shell数据传输要求在放下数据后,目标调用IDataObject::SetData为数据对象提供关于放下操作的反馈。比如说,在优化的移动操作中,移动文件后,目标通常会删除原文件,但这不是必须的。目标调用IDataObject::SetData,传入CFSTR_LOGICALPERFORMEDDROPEFFECT(LogicalPerformedDropEffect)格式,从而通知数据对象它是否该删除了原文件。目标还使用其他一些Shell剪贴板格式来把信息传递给数据对象。IDataObject::SetData实现必须能够识别这些格式并分别作出不同响应。更深入的讨论,见Handling Shell Data Transfer Scenarios。
2.2.2. EnumFormatEtc方法
目标收到一个数据对象后,通常通过FORMATETC来确定对象包含什么格式的数据。IDataObject::EnumFormatEtc方法会创建OLE枚举对象并返回其IEnumFORMATETC接口指针,然后目标可以使用这个接口指针来枚举可用的格式。
枚举对象应该按照质量从高到低的次序枚举可用的格式。格式的相对质量高低由放置源决定。一般来说,质量最高的格式包含最丰富完整的数据。比如说,通常认为24位色图像比灰度级图像质量高。按照质量高低次序枚举格式的原因是:目标通常枚举可用格式,直到找到一个它所支持的格式,然后使用这个格式取出数据。为使目标能够取得最佳的可用格式,必须以质量高低次序来枚举可用格式。
用于Shell数据的枚举对象的实现通常跟用于其它类型数据的相同,除了一个例外。因为通常只为每种格式包含一个数据项,数据对象通常会枚举通过IDataObject::SetData传入的每种格式。然而,正如在SetData方法节讨论的那样,Shell数据对象可以包含多个CFSTR_FILECONTENTS格式的数据项。因为IDataObject::EnumFormatEtc的目的是让目标可以确定存在哪些类型的数据,所以没有必要枚举多个CFSTR_FILECONTENTS格式。如果需要知道数据对象包含多少个这种格式的数据项,可以从相关的CFSTR_FILEDESCRIPTOR格式数据项中获取。关于IDataObject::EnumFormatEtc的更深入讨论,见方法的参考文档。
2.2.3. GetData 方法
目标调用IDataObject::GetData取出特定格式的数据。格式是用FORMATETC结构体指定的。方法会返回格式的STGMEDIUM结构体。
目标可以把FORMATETC结构体的tymed成员设置为TYMED_XXX值来指定要使用哪种数据传输机制。然而,目标也可以发出更通用的请求,让数据对象决定使用哪种数据传输机制。如果要让数据对象选择使用的数据传输机制,把tymed设置为支持的所有TYMED_XXX值。这样IDataObject::GetData会从这些数据传输机制中选择一种,返回其STGMEDIUM结构体。比如说,通常把tymed设置为TYMED_HGLOBAL | TYMED_ISTREAM | TYMED_ISTORAGE 来请求三种Shell数据传输机制中的任何一种。
注意:因为可以有多个CFSTR_FILECONTENTS格式的数据项,所以FORMATETC结构体的cfFormat和tymed成员是不足以指示IDataObject::GetData应该返回哪个STGMEDIUM结构体的。对于CFSTR_FILECONTENTS格式,IDataObject::GetData还应该检查FORMATETC结构体的lIndex成员,以保证返回正确的STGMEDIUM结构体。
数据对象中放置有CFSTR_INDRAGLOOP格式的数据,让目标可以检查拖动循环的状态,而避免密集地从存储器提取对象数据。这种格式的数据是一个DWORD值,非零表示数据对象在拖动循环中;数据被放下时,这个值会被设置为零。如果目标请求这种格式的数据,但源没有载入这种数据,IDataObject::GetData应该表现为已经载入值为零的这种数据。
光标位于目标窗口上时,目标可以使用拖放辅助对象来指定拖动图像。拖放辅助对象调用IDataObject::SetData把私有格式装载到用于跨进程支持的数据对象中。后续它会调用IDataObject::GetData来获取这种私有数据。为支持拖放辅助对象,Shell数据对象实现应该可以返回任何私有格式的数据项。
2.3 实现IDropSource
拖放源必须创建展示IDropSource接口的对象。这个对象让拖放源可以更新用以指示当前光标位置的拖动图像,以及为系统提供如何终止拖放操作的反馈。IDropSource有两个方法:GiveFeedback和QueryContinueDrag。
2.3.1. GiveFeedback 方法
在拖动循环中,由放置源负责跟踪光标位置并显示合适的拖动图像。然而,在一些情况下,光标位于放置目标窗口上时,可能需要改变拖动图像。光标进入和离开目标窗口,以及滑过目标窗口时,系统会不时地调用放置目标的IDropTarget接口。目标会以DROPEFFECT值响应调用,这个值会通过GiveFeedback方法转发给拖动源。拖动源可以根据这个值来修改拖动图像的外观。更深入的讨论,见GiveFeedback和DoDragDrop方法的参考文档。
2.3.2. QueryContinueDrag 方法
数据对象处于拖放循环中时,如果鼠标或者键盘状态发生改变,这个方法会被调用。方法会通知拖动源ESC键是否被按下,并且提供CTRL或者SHIFT等修饰键状态。方法的返回值指定下列三种动作之一:
更深入的讨论,见GiveFeedback和DoDragDrop方法的参考文档
3. 目标如何处理数据对象
目标可以从剪贴板接收数据对象,或者当数据对象在目标窗口上放下时接收它。然后目标就可以从数据对象中取出数据。如果必要的话,目标还会通知数据对象操作的结果。在开始Shell数据传输前,放置目标必须做下列准备操作:
对于剪贴板传输,目标不会在数据对象被放置到剪贴板中时收到任何通知。通常由一个用户操作,比如说,点击程序工具栏上的粘贴按钮,来通知程序剪贴板中有数据对象可用。然后目标调用OleGetClipboard从剪贴板中获取数据对象的IDataObject指针。而对于拖放数据传输,系统使用目标的IDropTarget接口为目标提供数据传输进度的相关信息:
关于如何实现这些方法的更深入讨论,见本文的Implementing IDropTarget节。
数据被放下时,IDropTarget::Drop为目标提供数据对象的IDataObject接口指针,目标使用这个指针从数据对象中取出数据。
3.1 从数据对象中取出Shell数据
数据对象被放下,或者从剪贴板接收到数据对象后,目标会取出所需的数据。取出数据操作的第一步通常是枚举对象所包含的格式:
要获取特定格式的数据,调用IDataObject::GetData,传入相关的FORMATETC结构体。这个方法会返回用以访问数据的STGMEDIUM结构体。要指定特定的数据传输机制,把FORMATETC结构体的tymed成员设置为相应的TYMED_XXX值。要让数据对象选择数据传输机制,把FORMATETC结构体的tymed成员设置为能够处理的所有数据传输机制的相关TYMED_XXX值。这时数据对象会从这些数据传输机制中选择一种,返回合适的STGMEDIUM结构体。
对于大多数格式,传入枚举可用格式时取得的FORMATETC结构体就可以取得相应的数据了,但CFSTR_FILECONTENTS格式是个例外。因为数据对象可以包含多个这种格式的数据项,枚举格式时返回的FORMATETC结构体可能并不对应想要取出的特定数据项。除了cfFormat和tymed成员外,还必须设置lIndex成员为文件索引值。更深入的讨论,见Using the CFSTR_FILECONTENTS Format to Extract Data from a File。
数据取出过程决定于返回的STGMEDIUM结构体所包含的指针。如果结构体包含IStream或者IStorage接口指针,则使用相关的接口方法取出数据。从全局存储对象中取出数据的过程在下一节讨论。
3.2 从数据对象中取出全局存储对象
很多Shell数据格式以全局存储对象的形式存在。从数据对象中的全局存储对象里取出数据,赋值给局部变量的过程如下:
注意:必须用ReleaseStgMedium释放全局存储对象,而不是GlobalFree。
下面的示例展示了如何从数据对象中的全局存储对象里取出DWORD值。pdtobj是数据对象的IDataObject接口指针,cf是标识所需数据的剪贴板格式值,pdwOut用以返回数据值。
<…… 省略示例代码 ……>
3.3 实现IDropTarget
光标位于目标窗口上时,系统使用IDropTarget接口与目标进行通信。目标的响应通过拖放源的IDropSource接口转发给拖放源。根据响应,拖放源可以修改代表数据的图标。如果放置目标需要指定数据图标,则可以通过创建拖放辅助对象实现。
按照惯例,目标设置IDropTarget::Drop的pdwEffect参数为合适的DROPEFFECT值,向数据对象通知操作的结果。对于Shell数据对象,目标也可能需要调用IDataObject::SetData。关于目标应该如何处理不同的数据传输情形,见Handling Shell Data Transfer Scenarios。
下面的几节将简要讨论如何实现IDropTarget::DragEnter、IDropTarget::DragOver和IDropTarget::Drop方法。更多的细节见相关参考文档。
3.3.1. DragEnter 方法
光标进入目标窗口时系统调用IDropTarget::DragEnter方法。方法参数为目标提供光标位置、CTRL等键盘修饰键状态和数据对象的IDataObject接口指针。目标应该使用IDataObject接口确定是否可以接受数据对象中某种格式的数据。如果可以,通常保持pdwEffect值不变。如果不能接受数据对象中的任何数据,则设置pdwEffect参数为DROPEFFECT_NONE。系统会把这个值传递给数据对象的IDropSource接口,使对象可以显示合适的拖动图像。
在放下数据前,目标不应该使用IDataObject::GetData方法获取Shell数据并进行绘制,这可能会使拖动光标停滞。为避免这个问题,有些Shell对象包含CFSTR_INDRAGLOOP格式的数据。通过取出这种格式的数据,目标可以检查拖动循环的状态,而避免密集地绘制存储器中的对象数据。这种格式的数据是一个DWORD值,非零表示对象处于拖动循环中;数据被放下时,这个值被设置为零。
如果目标可以接受数据对象中的数据,则应该检查grfKeyState来确定是否按下了任何改变放置行为的修饰键。比如说,通常默认的操作是移动,但是按下CTRL键则表示复制。
光标位于目标窗口上时,目标可以使用拖动辅助对象来替换数据对象的拖动图像。如果需要替换的话,IDropTarget::DragEnter应该调用IDropTargetHelper::DragEnter,传入IDropTarget::DragEnter参数所包含的信息到拖放辅助对象中。
3.3.2. DragOver方法
光标位于目标窗口上时,系统会不时地调用IDropTarget::DragOver方法。方法参数会为目标提供光标位置和CTRL等修饰键状态。IDropTarget::DragOver的职责跟IDropTarget::DragEnter几乎相同,因而通常它们的实现也非常类似。
如果目标使用了拖放辅助对象,IDropTarget::DragOver应该调用IDropTargetHelper::DragOver方法,把IDropTarget::DragOver参数包含的信息转发给拖放辅助对象。
3.3.3. Drop方法
用户放下数据时,系统会调用IDropTarget::Drop方法通知目标。放下操作通常是通过释放鼠标键进行的。IDropTarget::Drop的参数跟IDropTarget::DragEnter相同。目标对方法的响应通常是从数据对象中取出一种或者多种格式的数据。取出数据操作完成后,目标应该设置pdwEffect参数为某个DROPEFFECT值来指示操作的结果。对于某些类型的Shell数据传输,目标还应该调用IDataObject::SetData,把带有操作结果相关信息的数据传递给数据对象。更详尽的讨论,见Handling Shell Data Transfer Scenarios。
如果目标使用了拖放辅助对象,IDropTarget::Drop应该调用IDropTargetHelper::Drop,把IDropTargetHelper::DragOver参数所包含的信息转发给拖放辅助对象。
3.4 使用拖放辅助对象
Shell导出了拖放辅助对象(CLSID_DragDropHelper),使得拖放图像位于目标窗口上时,目标可以指定拖放图像。要创建拖放辅助对象,调用CoCreateInstance,传入CLSID_DragDropHelper以创建一个进程内服务器对象。拖放辅助对象暴露两个接口,它们的使用方法如下:
3.4.1.使用IDragSourceHelper接口
拖放辅助对象暴露的IDropSourceHelper接口让拖放目标可以提供光标位于目标窗口上时要显示的图像。 IDragSourceHelper提供了两种可供选择的,用以指定用作拖放图像的位图的方法:
3.4.2. 使用IDropTargetHelper接口
这个接口让放置目标可以在光标进入或者离开目标窗口时通知拖放辅助对象。光标位于目标窗口上时,目标用IDropTargetHelper以为拖放辅助对象提供自己从IDropTarget接口接收到的信息。
IDropTargetHelper的四个方法:IDropTargetHelper::DragEnter、IDropTargetHelper::DragLeave、IDropTargetHelper::DragOver和IDropTargetHelper::Drop与IDropTarget的同名方法相关联。要使用拖放辅助对象,IDropTarget方法应该调用相应的IDropTargetHelper方法转发信息给拖放辅助对象。IDropTargetHelper的第五个方法,IDropTargetHelper::Show,用以通知拖放辅助对象显示或者隐藏拖放图像。这个方法在滑过处于低色深视频模式的目标窗口时使用。它让目标在绘制窗口时可以隐藏拖动图像。