在上一篇文章介绍了在.NET中进行Drag和Drop操作的方法,以及底层的调用实现过程。实际是通过一个DoDragDrop的WIN32 API来监视拖拽过程中的鼠标,根据鼠标的位置获得IDropTraget和IDropSource接口,对拖拽源和目标进行操作。但是拖拽的目的是进行数据的交换,在上一篇文章中对于发送和接受数据都是一笔带过,所以这一篇主要介绍Drag和Drop操作中的数据。
一 .NET中Drag和Drop时的数据传输
Drag和Drop的过程其实就是一个数据交换的过程,比如我们把ListView中的一条数据拖放到另一个ListView中;或者是把一个MP3拖放到播放器中;或者是拖动一段文字到输入框;甚至windows的资源管理器中,从C盘拖动一个文件到D盘,其实都是这样一个Drag and Drop的过程。
我们先来看看我们上一篇文章中ListView直接拖动的例子
//ListView1 拖动
private void listView1_ItemDrag(object sender, ItemDragEventArgs e)
{
ListViewItem[] itemTo = new ListViewItem[((ListView)sender).SelectedItems.Count];
for (int i = 0; i < itemTo.Length; i++)
{
itemTo[i] = ((ListView)sender).SelectedItems[i];
}
((ListView)(sender)).DoDragDrop(itemTo, DragDropEffects.Copy);
}
//ListView2 接收
private void listView2_DragDrop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(typeof(ListViewItem[])))
{
ListViewItem[] files = (ListViewItem[])e.Data.GetData(typeof(ListViewItem[]));
foreach (ListViewItem s in files)
{
ListViewItem item = s.Clone() as ListViewItem;
listView2.Items.Add(item);
}
}
}
我们看到ListView1动数据时,DoDragDrop方法的第一个参数就是一个Object型的,用来传送任何类型的数据;而listView2_DragDrop方法则用来接收数据,我们注意到typeof(ListViewItem[]),接收时指定了要接收的数据类型。可以看到我们例子中,DataSource和DataTarget之间传送和接受的数据时都是Object型。如果我们发送时的原始类型和接收时指定的类型不相符,就无法得到数据。
上面是比较好理解的,和我们定义方法中,使用Object类型传递各种类型的数据,方法中在进行数据类型的转换道理是一样的。不过这只是在我们自己的程序中,我们清楚数据源和数据目标之间要传递的数据类型,所以不存在问题。而对于两个程序之间进行数据交换就没有这么简单了,首先系统并不认识Object这样一个类型,其实就是即便有了一种通用的类型,接收方并不知道传送的数据原始类型,如果对仍和数据都进行转换,并不是一个好的办法。
二 Windows中程序间的数据传输
windows中最方便的数据传送方式就是剪贴板了,从一个程序复制数据,然后通过剪贴板传送到另一个程序中。剪贴板可以在应用程序间交换各种不同类型的数据,也就是程序之间发送和接受数据时都遵循了同一套规则,他们共同是用的这个对象叫做Shell Data Object。
1. COM和OLE对象
Shell Data Object是一个COM对象。也可以说她是一个OLE对象。OLE的全称是Object Linking and Embedding,对象连接与嵌入,早期用于复合文档。而COM是一种技术规范,来源于OLE,但是后来的OLE2和ACTIVEX都是遵循了COM的规范进行开发的。比如我们在Word中嵌入Excel,并且不用打开就能编辑。但不仅仅是这个应用,整个WINDOWS平台都大量的运用到了COM技术开发平台组件。包括我们的.NET平台,也是一个COM组件,在运行.NET程序是加载这个组件。我们使用Class是在代码级别进行重用,而是用COM是在二进制级别进行重用。 我们这里我打算介绍COM(我也介绍不来,哈哈),只需要大概有个了解。有兴趣的话可以看看《COM技术内幕》,好像还有本《COM本质论》我没看过。更多内容可以参见OLE and Data Transfer:http://msdn.microsoft.com/en-us/library/ms693425(v=VS.85).aspx
2. Shell Data Oject
Shell Data Object是一个Windows程序使用剪贴板和Drop and Drag操作的基础。当我们Source创建一个Data Object时,他并不知道Target能接受什么类型的数据。而对于Target来说他可能可以接受多种数据。因此Data Object中往往包含一些传送的数据的格式信息;除此之外他还包含一些对数据的操作信息,比如是移动数据还是复制数据。而一个Data Object中可以包含多个项目的切不同类型的数据。
3.Clipboard Formats
前面说了,Data Object中需要包含发送数据的格式信息,所以对于Data Object对象中存放的每一项数据,都会分配一个数据格式,这个类型就叫做Clipboard Formats。WINDOIWS中定义了一些Clipboard Formats。他们名称通常都是CF_XXX的格式。比如CF_TEXT就表示ANSI格式的文本数据。我们在Source和Target之间使用这些类型数据时,需要使用RegisterClipboardFormat来注册这个格式。但是有一个比较特殊的类型CF_HDROP是不需要注册的,因为他是系统的私有格式。当Target接受到Drop操作时,会枚举发送来的数据的这些格式,以决定使用那一种格式去解析这些数据。
4. 两个关键的数据结构
但是实际中,并不是直接使用Clipboard Formats描述传送的数据,而是对他进行了一些扩展。FORMATETC就是用来描述数据格式的一个结构体。具体定义参见:http://msdn.microsoft.com/en-us/library/ms682177(v=VS.85).aspx
typedef struct tagFORMATETC {
CLIPFORMAT cfFormat;
DVTARGETDEVICE *ptd;
DWORD dwAspect;
LONG lindex;
DWORD tymed;
} FORMATETC, *LPFORMATETC;
cfFormat字段指定的就是一个Clipboard Formats;
tymed是指定传输机制,也就是数据的存储介质:A global memory object;An IStream interface;An IStorage interface.
而其他参数并不是太重要就不详细介绍了。这个数据结构作用就是指定传输的数据信息,并不包含实际的数据。继续看下一个结构体
typedef struct tagSTGMEDIUM {
DWORD tymed;
union {
HBITMAP hBitmap;
HMETAFILEPICT hMetaFilePict;
HENHMETAFILE hEnhMetaFile;
HGLOBAL hGlobal;
LPOLESTR lpszFileName;
IStream *pstm;
IStorage *pstg;
} ;
IUnknown *pUnkForRelease;
} STGMEDIUM, *LPSTGMEDIUM;
STGMEDIUM结构体可以理解为用来存放具体数据的全局内存句柄的结构。具体可以参见:http://msdn.microsoft.com/en-us/library/ms683812(v=VS.85).aspx
结构看上去比较复杂,tymed是指示传送数据的机制。在封送和解析过程中,会使用这个字段去决定联合体中使用哪一种数据类型。因为和FORMATETC中必须标示相同,所以这里对于TYMED枚举来说,只有3个枚举可用: TYMED_HGLOBAL ,TYMED_ISTREAM, TYMED_ISTORAGE 。而联合体中的对象则指向了数据存储的位置。
5. 传送数据的例子
以上两个结构体就是Shell Data Object传送的核心部分,分别指定了数据的格式和位置。下面看一下MSDN上使用2个结构体传送数据的例子。
STDAPI DataObj_SetDWORD(IDataObject *pdtobj, UINT cf, DWORD dw)
{
FORMATETC fmte = {(CLIPFORMAT) cf,
NULL,
DVASPECT_CONTENT,
-1,
TYMED_HGLOBAL};
STGMEDIUM medium;
HRESULT hres = E_OUTOFMEMORY;
DWORD *pdw = (DWORD *)GlobalAlloc(GPTR, sizeof(DWORD));
if (pdw)
{
*pdw = dw;
medium.tymed = TYMED_HGLOBAL;
medium.hGlobal = pdw;
medium.pUnkForRelease = NULL;
hres = pdtobj->SetData(&fmte, &medium, TRUE);
if (FAILED(hres))
GlobalFree((HGLOBAL)pdw);
}
return hres;
}
首先初始化了一个FORMATECT的结构,使用的数据格式是传入的cf,而tymed则表示数据存放在全局内存区域,其他参数按例子设置。然后建立了一个STGMEDIUM结构,并且使用GlobalAlloc分配了一块内存区域,并指向传入的参数dw,也就是数据实际的存放地址。然后设置了TYMED字段和FORMATECT结构相同,并吧hGlobal字段指向了数据的地址。最后将这2个数据结构保存到了一个是IDataObject类型的对象中,完成了Data Object的创建。
STDAPI DataObj_GetDWORD(IDataObject *pdtobj, UINT cf, DWORD *pdwOut)
{ STGMEDIUM medium;
FORMATETC fmte = {(CLIPFORMAT) cf, NULL, DVASPECT_CONTENT, -1,
TYMED_HGLOBAL};
HRESULT hres = pdtobj->GetData(&fmte, &medium);
if (SUCCEEDED(hres))
{
DWORD *pdw = (DWORD *)GlobalLock(medium.hGlobal);
if (pdw)
{
*pdwOut = *pdw;
GlobalUnlock(medium.hGlobal);
}
else
{
hres = E_UNEXPECTED;
}
ReleaseStgMedium(&medium);
}
return hres;
}
而在接受时,首先还是构造了一个和发送时一样的FORMATETC结构,以表示我要接受此种类型的数据。注意,这里的cf前面说过是必须注册过的。而后又构造了一个空的STGMEDIUM结构,并使用了IDataObject的GetData方法,来获得到了数据源的STGMEDIUM结构信息。这个方法会根据FORMATETC中的tymed来设置的。然后就是获得数据的内存地址,并得到数据,完成了整个数据的传递。
以上就是WINDOWS下我们传送数据时的格式和方式,具体内容参见Shell Data Object:http://msdn.microsoft.com/en-us/library/bb776903(v=VS.85).aspx
三 IDataObject接口
上面介绍了Windows中传送数据时低层的数据结构,但是我们注意到,我们并不是直接使用的2个结构体,而是使用了一个类型为IDataObject对象来传送数据,并且使用了他提供的SetData和GetData的方法来设置和获取数据。前面我们说过了,要想2个程序能传递各种类型的数据,必须遵循同一套规则,比如都是用Object类型的对象。而对于Windows来说,使用的就是Shell Data Object,但是它只是一个概念上的对象。只有实现了IDataObject接口的对象才具备有这样的功能。
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("0000010E-0000-0000-C000-000000000046")]
public interface IDataObject
{
void GetData([In] ref FORMATETC format, out STGMEDIUM medium);
void GetDataHere([In] ref FORMATETC format, ref STGMEDIUM medium);
[PreserveSig]
int QueryGetData([In] ref FORMATETC format);
[PreserveSig]
int GetCanonicalFormatEtc([In] ref FORMATETC formatIn, out FORMATETC formatOut);
void SetData([In] ref FORMATETC formatIn, [In] ref STGMEDIUM medium, [MarshalAs(UnmanagedType.Bool)] bool release);
IEnumFORMATETC EnumFormatEtc(DATADIR direction);
[PreserveSig]
int DAdvise([In] ref FORMATETC pFormatetc, ADVF advf, IAdviseSink adviseSink, out int connection);
void DUnadvise(int connection);
[PreserveSig]
int EnumDAdvise(out IEnumSTATDATA enumAdvise);
}
以上是IDataObjet接口的定义,它为传送数据提供与格式无关的机制。由类实现之后,IDataObject 方法使单一数据对象能够以多种格式提供数据。与仅支持单一数据格式的情况相比,如果以多种格式提供数据,则往往可使更多的应用程序能够使用该数据。这里的IDataObject是一个COM接口,而在.NET平台中,也存在一个IDataObject接口。
[ComVisible(true)]
public interface IDataObject
{
// Methods
object GetData(string format);
object GetData(Type format);
object GetData(string format, bool autoConvert);
bool GetDataPresent(string format);
bool GetDataPresent(Type format);
bool GetDataPresent(string format, bool autoConvert);
string[] GetFormats();
string[] GetFormats(bool autoConvert);
void SetData(object data);
void SetData(string format, object data);
void SetData(Type format, object data);
void SetData(string format, bool autoConvert, object data);
}
我们看到这2个接口和核心部分就是SetData和GetData方法,以及查询Format的方法。我们在.NET平台上想要使用OLE对象传递数据时需要实现这2个接口。在.NET平台上,DataObject类就实现了这2个接口,使得我们可以使用他进行程序间的拖拽,当然程序内部实际也是通过他来传递的。
MSDN对DataObject类的描述如下:
DataObject 通常用于 Clipboard和拖放操作。DataObject 类提供 IDataObject 接口的建议实现。建议使用 DataObject 类,而不用自己实现 IDataObject。可将不同格式的多种数据存储在 DataObject 中。可通过与数据关联的格式从 DataObject 中检索这些数据。因为目标应用程序可能未知,所以通过将数据以多种格式放置在 DataObject 中,可使数据符合应用程序的正确格式的可能性增大。请参见 DataFormats 以获得预定义的格式。
四 .NET中Drag and Drop数据传输的分析
通过上面的分析,我们知道了,我们程序中和程序间实现拖拽或是使用剪贴板传递数据时,使用了一个实现了IDataObject的对象。其中包含的传递的数据格式和数据的内存地址等信息。而.NET中通过一个DataObject类封装了数据操作。上面c++的代码也掩饰了WINDOWS下最原始的构造IDataObject对象的方法,下面我们就看看.NET下是如何封装的。
1. DataSource中创建DataObject
首先我们来看看,在数据源中拖动一个对象时,是如何构造DataObject对象的。我们记得,我们DoDragDrop方法接受一个Object的数据对象,上一篇我们介绍过这个方法,但是跳过了数据部分。我们还是看Control对象中的方法。
[UIPermission(SecurityAction.Demand, Clipboard=UIPermissionClipboard.OwnClipboard)]
public DragDropEffects DoDragDrop(object data, DragDropEffects allowedEffects)
{
int[] finalEffect = new int[1];
UnsafeNativeMethods.IOleDropSource dropSource = new DropSource(this);
IDataObject dataObject = null; //COM Interface
if (data is IDataObject) //COM Interface
{
dataObject = (IDataObject) data;
}
else
{
DataObject obj3 = null;
if (data is IDataObject)//.NET Interface
{
obj3 = new DataObject((IDataObject) data);
}
else
{
obj3 = new DataObject();
obj3.SetData(data);
}
dataObject = obj3;
}
try
{
SafeNativeMethods.DoDragDrop(dataObject, dropSource, (int) allowedEffects, finalEffect);
}
catch (Exception exception)
{
if (ClientUtils.IsSecurityOrCriticalException(exception))
{
throw;
}
}
return (DragDropEffects) finalEffect[0];
}
因为在.NET平台上有两个IDataObject接口,一个是COM接口一个是.NET接口,所以我在上面标示出来了。分析上面的代码很简单,如果传递进来的是一个IDataObject(COM)接口的对象,则直接保存到dataObject中;如果是IDataObject(.NET)接口的对象则吧对象保存在到DataObject对象中(这里只是吧data复给了DataObject的内部对象);如果不是这2种数据类型,那么就调用SetData方法把这个对象设置到DataObject中。完成数据的设置,最终得到的都是COM接口的dataObject对象,用于API的调用。
2. DataTarget接收DataObject
前一篇介绍了,调用了DoDragDrop方法后,系统会跟踪鼠标动作。当我们把拖拽的对象释放到一个Target Window中的时候,target就能够接受到我们创建的IDataObject(COM)对象。而在.NET中,如果是用DataObject,那么我们就会接受到这个对象。我们回头在来看看接受的代码。
private void listView1_DragDrop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
String[] files = (String[])e.Data.GetData(DataFormats.FileDrop);
foreach (String s in files)
{
ListViewItem item = new ListViewItem(s);
listView1.Items.Add(item);
}
}
}
在.NET中,这些数据被封装到了DragEventArgs对象中,通过e.Data我们级获得了一个实现了IDataObject(.NET)接口的对象。我们知道这个对象实际包含的内容就是我们前面提到过的那2个结构体,所以我们可以获得指定格式的数据,然后对数据进行操作。
3 DataObject内部结构
我们在.NET平台上已经能很好的完成拖拽操作了,但是对于底层的动作我们还是一无所知。其实我们已经知道了SetDta和GetData就是对两个结构体的操作,但是在.NET上我们无法看到这样的操作,因为他已经被DataObject内部实现了。我们可以大概分析一下他内部的工作情况。通过Reflector我们发现,DataObject的结构相当的复杂,虽然提供给外界的方法功能很简单,但是内部却进行了很多操作。
图截取了DataObject的一部分,可以看到在他里面还包含有三个内嵌类。因为这个类代码有接近2000行,所以就不全部贴出来了。
我们首先来看看它的构造函数:
static DataObject()
{
CF_DEPRECATED_FILENAME = "FileName";
CF_DEPRECATED_FILENAMEW = "FileNameW";
ALLOWED_TYMEDS = new TYMED[] { TYMED.TYMED_HGLOBAL, TYMED.TYMED_ISTREAM, TYMED.TYMED_ENHMF, TYMED.TYMED_MFPICT, TYMED.TYMED_GDI };
serializedObjectID = new Guid("FD9EA796-3B13-4370-A679-56106BB288FB").ToByteArray();
}
这个是他的静态构造函数,其中ALLOWED_TYMEDS字段很让人熟悉,没错真是我们前面介绍的FORMATETC和STGMEDIUM结构体中tymed字段的值。这里存放在数组中,而并没有引进整个Nataive枚举。
public DataObject()
{
this.innerData = new DataStore();
}
public DataObject(object data)
{
if ((data is IDataObject) && !Marshal.IsComObject(data)) //.NET
{
this.innerData = (IDataObject) data;
}
else if (data is IDataObject)//COM
{
this.innerData = new OleConverter((IDataObject) data);
}
else
{
this.innerData = new DataStore();
this.SetData(data);
}
}
internal DataObject(IDataObject data)
{
if (data is DataObject) //COM
{
this.innerData = data as IDataObject;
}
else
{
this.innerData = new OleConverter(data);
}
}
internal DataObject(IDataObject data)
{
this.innerData = data; //.NET
}
public DataObject(string format, object data) : this()
{
this.SetData(format, data);
}
这几个构造函数基本都在做一件事,那就是把数据保存到内部的innerData对象,他是一个.NET的IDataObject接口对象。
- 对于无参构造函数,内部构造一个实现了IDataObject(.NET)接口的DataStroe对象,从名字看出是存放数据的;
- 对于有参的构造函数,如果传入的是对象是实现了IDataObject(.NET)接口的对象,直接保存到innerData字段,如若是IDataObject(COM)接口的对象,则使用一个OleConverter对象对数据进行以下包装;如果是没有实现这2个接口的对象,则还是利用DataStroe对象,并Setdata。
- 对于制定了数据格式的对象,调用SetData方法,设置数据。
这个地方感觉是曾相识,是的。DoDragDrop方法在内部也对数据进行了一系列类似的转化,不同的时她是把数据转换为IDataObject(COM)对象,因为WINDOWS只认识这种数据结构;而这里我们是吧数据转换为IDataObject(.NET)存储,因为我们是在.NET平台内部使用。
内嵌的类
在前面我们看到了2个内嵌的类,DataStore和OleConverter,他们都实现了IDataObject(.NET),作用就是把数据转换为内部的存储类型。
private class DataStore : IDataObject
{
// Fields
private Hashtable data;
// Methods
public DataStore();
public virtual object GetData(string format);
public virtual object GetData(Type format);
public virtual object GetData(string format, bool autoConvert);
public virtual bool GetDataPresent(string format);
public virtual bool GetDataPresent(Type format);
public virtual bool GetDataPresent(string format, bool autoConvert);
public virtual string[] GetFormats();
public virtual string[] GetFormats(bool autoConvert);
public virtual void SetData(object data);
public virtual void SetData(string format, object data);
public virtual void SetData(Type format, object data);
public virtual void SetData(string format, bool autoConvert, object data);
// Nested Types
private class DataStoreEntry
{
// Fields
public bool autoConvert;
public object data;
// Methods
public DataStoreEntry(object data, bool autoConvert);
}
}
我们看到DataStore中data是一个HashTable类型,也就是说一个DataStroe对象中可以存储多种数据。在前面DataObject中我们看到,是建立DataStroe对象后通过SetData来保存数据:
public virtual void SetData(string format, bool autoConvert, object data)
{
if ((data is Bitmap) && format.Equals(DataFormats.Dib))
{
if (!autoConvert)
{
throw new NotSupportedException(SR.GetString("DataObjectDibNotSupported"));
}
format = DataFormats.Bitmap;
}
this.data[format] = new DataStoreEntry(data, autoConvert);
}
我们可以看到最终的数据是保存到了它内部的一个DataStoreEntry对象中,而是以format作为KEY值,也就是说我我们能存储多种数据类型,但是不能吧同一种数据SetData多次。而GetData时,就是去hashtable中取得value。
相比而言OleConverter就要复杂许多,因为要要进行的是COM到.NET对象之间的转换。
private class OleConverter : IDataObject
{
// Fields
internal IDataObject innerData; //COM
// Methods
public OleConverter(IDataObject data);
public virtual object GetData(string format);
public virtual object GetData(Type format);
public virtual object GetData(string format, bool autoConvert);
private object GetDataFromBoundOleDataObject(string format);
private object GetDataFromHGLOBLAL(string format, IntPtr hglobal);
private object GetDataFromOleHGLOBAL(string format);
private object GetDataFromOleIStream(string format);
private object GetDataFromOleOther(string format);
public virtual bool GetDataPresent(string format);
public virtual bool GetDataPresent(Type format);
public virtual bool GetDataPresent(string format, bool autoConvert);
private bool GetDataPresentInner(string format);
public virtual string[] GetFormats();
public virtual string[] GetFormats(bool autoConvert);
private int QueryGetData(ref FORMATETC formatetc);
private int QueryGetDataInner(ref FORMATETC formatetc);
private Stream ReadByteStreamFromHandle(IntPtr handle, out bool isSerializedObject);
private string[] ReadFileListFromHandle(IntPtr hdrop);
private object ReadObjectFromHandle(IntPtr handle);
[SecurityPermission(SecurityAction.Assert, Flags=SecurityPermissionFlag.SerializationFormatter)]
private static object ReadObjectFromHandleDeserializer(Stream stream);
private string ReadStringFromHandle(IntPtr handle, bool unicode);
public virtual void SetData(object data);
public virtual void SetData(string format, object data);
public virtual void SetData(Type format, object data);
public virtual void SetData(string format, bool autoConvert, object data);
// Properties
public IDataObject OleDataObject { get; }
}
实际也算不上之转换,只能说是.NET对象对COM对象的一个包装,因为她的内部还是维护了一个COM接口对象。那我们看看他SetData方法。
public virtual void SetData(string format, bool autoConvert, object data)
{
}
悲剧,竟然什么都看不到。应该是DataObject并没有实现COM接口的SetData方法。我们在看看GetData方法。我们发现,向外部公开的GetData方法都是调用了这样一个内部的方法:
private object GetDataFromBoundOleDataObject(string format)
{
object dataFromOleOther = null;
try
{
dataFromOleOther = this.GetDataFromOleOther(format);
if (dataFromOleOther == null)
{
dataFromOleOther = this.GetDataFromOleHGLOBAL(format);
}
if (dataFromOleOther == null)
{
dataFromOleOther = this.GetDataFromOleIStream(format);
}
}
catch (Exception)
{
}
return dataFromOleOther;
}
有点眼熟,这里正好对应了我们前面介绍FORMATETC结构体时的tymed字段的3种情况.看看从HGLOBAL是如何获取数据的吧。
private object GetDataFromOleHGLOBAL(string format)
{
FORMATETC formatetc = new FORMATETC();
STGMEDIUM medium = new STGMEDIUM();
formatetc.cfFormat = (short) DataFormats.GetFormat(format).Id;
formatetc.dwAspect = DVASPECT.DVASPECT_CONTENT;
formatetc.lindex = -1;
formatetc.tymed = TYMED.TYMED_HGLOBAL;
medium.tymed = TYMED.TYMED_HGLOBAL;
object dataFromHGLOBLAL = null;
if (this.QueryGetData(ref formatetc) == 0)
{
try
{
IntSecurity.UnmanagedCode.Assert();
try
{
this.innerData.GetData(ref formatetc, out medium);
}
finally
{
CodeAccessPermission.RevertAssert();
}
if (medium.unionmember != IntPtr.Zero)
{
dataFromHGLOBLAL = this.GetDataFromHGLOBLAL(format, medium.unionmember);
}
}
catch
{
}
}
return dataFromHGLOBLAL;
}
哈哈,这里就很清楚了;和我们前面C++的那个获取数据的例子基本上是一样的。.NET中也引入了这2个数据结构。只不过对于cfFormat进行了一下转换,如果进入到GetFormat方法中可以看到 int id = SafeNativeMethods.RegisterClipboardFormat(format);我们前面介绍过,对于非CF_HDROP类型,都需要进行注册。然后同样是调用COM接口的GetData方法来获得STGMEDIUM结构的数据,然否通过GetDataFromHGLOBLAL获得数据:
private object GetDataFromHGLOBLAL(string format, IntPtr hglobal)
{
object obj2 = null;
if (hglobal != IntPtr.Zero)
{
if ((format.Equals(DataFormats.Text) || format.Equals(DataFormats.Rtf)) || (format.Equals(DataFormats.Html) || format.Equals(DataFormats.OemText)))
{
obj2 = this.ReadStringFromHandle(hglobal, false);
}
else if (format.Equals(DataFormats.UnicodeText))
{
obj2 = this.ReadStringFromHandle(hglobal, true);
}
else if (format.Equals(DataFormats.FileDrop))
{
obj2 = this.ReadFileListFromHandle(hglobal);
}
else if (format.Equals(DataObject.CF_DEPRECATED_FILENAME))
{
obj2 = new string[] { this.ReadStringFromHandle(hglobal, false) };
}
else if (format.Equals(DataObject.CF_DEPRECATED_FILENAMEW))
{
obj2 = new string[] { this.ReadStringFromHandle(hglobal, true) };
}
else
{
obj2 = this.ReadObjectFromHandle(hglobal);
}
UnsafeNativeMethods.GlobalFree(new HandleRef(null, hglobal));
}
return obj2;
}
读取的是全局内存区域的数据,不过还差那么一点,要根据格式读取,那就看看我们用的最多的DataFormats.FileDrop。
private string[] ReadFileListFromHandle(IntPtr hdrop)
{
string[] strArray = null;
StringBuilder lpszFile = new StringBuilder(260);
int num = UnsafeNativeMethods.DragQueryFile(new HandleRef(null, hdrop), -1, null, 0);
if (num > 0)
{
strArray = new string[num];
for (int i = 0; i < num; i++)
{
int length = UnsafeNativeMethods.DragQueryFile(new HandleRef(null, hdrop), i, lpszFile, lpszFile.Capacity);
string path = lpszFile.ToString();
if (path.Length > length)
{
path = path.Substring(0, length);
}
string fullPath = Path.GetFullPath(path);
new FileIOPermission(FileIOPermissionAccess.PathDiscovery, fullPath).Demand();
strArray[i] = path;
}
}
return strArray;
}
当我们拖拽的是文件时,内部调用了一个DragQueryFile的API方法,来获得所有文件的路径,并存放到数组中。这里涉及到FileDrop这个类型,在windows中应该是对应我们前面提到过的CF_HDROP的剪贴板类型。他的 STGMEDIUM结构体的hGlobal字段指向一个名为DROPFILES的结构体,这个结构体中保存了文件路径列表,每个路径之间是用double-null间隔的。而DragQueryFile方法就是读取次结构中的文件路径信息的。具体参见:http://msdn.microsoft.com/en-us/library/bb776902(VS.85).aspx#CF_HDROP
4. DataObject内部实现
前面花了很多时间介绍了DataObject的构造函数和内部的数据结构。下面就具体看看它自己的SetData和GetData方法吧。首先我们要明确一个问题,就是DataObject在内部维护的innerData存放的数据类型是2种:DataStore和OleConverter,知道这个非常重要。
IDataObject COM接口的实现
我们这里只去关注SetData和GetData方法:
[SecurityPermission(SecurityAction.Demand, Flags=SecurityPermissionFlag.UnmanagedCode)]
void IDataObject.GetData(ref FORMATETC formatetc, out STGMEDIUM medium)
{
if (this.innerData is OleConverter)
{
((OleConverter) this.innerData).OleDataObject.GetData(ref formatetc, out medium);
}
else
{
medium = new STGMEDIUM();
if (this.GetTymedUseable(formatetc.tymed))
{
if ((formatetc.tymed & TYMED.TYMED_HGLOBAL) != TYMED.TYMED_NULL)
{
medium.tymed = TYMED.TYMED_HGLOBAL;
medium.unionmember = UnsafeNativeMethods.GlobalAlloc(0x2042, 1);
if (medium.unionmember == IntPtr.Zero)
{
throw new OutOfMemoryException();
}
try
{
((IDataObject) this).GetDataHere(ref formatetc, ref medium);
return;
}
catch
{
UnsafeNativeMethods.GlobalFree(new HandleRef((STGMEDIUM) medium, medium.unionmember));
medium.unionmember = IntPtr.Zero;
throw;
}
}
medium.tymed = formatetc.tymed;
((IDataObject) this).GetDataHere(ref formatetc, ref medium);
}
else
{
Marshal.ThrowExceptionForHR(-2147221399);
}
}
}
如果DataObject内部对象是一个OleConverter对象,我们就调用它的GetData方法去获得数据,这个我们在上面已经看到了具体的实现了。如果不是,我们在使用GetDataHere去获得,从调用的对象可以发现,最终的实现都是在OleConverter对象之中。
[SecurityPermission(SecurityAction.Demand, Flags=SecurityPermissionFlag.UnmanagedCode)]
void IDataObject.SetData(ref FORMATETC pFormatetcIn, ref STGMEDIUM pmedium, bool fRelease)
{
if (!(this.innerData is OleConverter))
{
throw new NotImplementedException();
}
((OleConverter) this.innerData).OleDataObject.SetData(ref pFormatetcIn, ref pmedium, fRelease);
}
SetData比较简单,就是调用OleConverter中那个我们看不到实现的方法。所以说,DataObject对象对COM接口的具体实现,其实全部在OleConverter类中。
IDataObject .NET接口实现
public virtual object GetData(string format, bool autoConvert)
{
return this.innerData.GetData(format, autoConvert);
}
public virtual void SetData(string format, bool autoConvert, object data)
{
this.innerData.SetData(format, autoConvert, data);
}
public virtual string[] GetFormats(bool autoConvert)
{
return this.innerData.GetFormats(autoConvert);
}
实现都是调用内部对象innerData的方法,前面我们就说了,innerData指向对象的实际类型只是DataStore和OleConverter。所以运行是,这些方法的实现,都在我们前面所介绍的这2个内嵌类之中了。
五 总结
在Windows系统中,程序之间拖拽和使用实现了IDataObject COM接口对象作为数据实体。他实际是包装了2个结构体。而我们所做的Get和Set数据的操作本质就是操作这两个结构体。在.NET中DataObject实现了这个COM接口,但是为了.NET平台使用,还制定了一个同名的.NET接口。但是最终在系统传送数据时全部转换为了COM接口。从这里也能看出.NET为了我们方便的使用时,内部封装了很多东西,甚至是牺牲了一定的速度作为代价。DataObject只是一个.NET和COM对象之间的一个枢纽。
这是自己第一次涉及到.NET和COM交互的知识,所以还有很多地方不是很明白,就只能避重就轻。而且有很多地方也介绍的可能不太清楚。比如剪贴板的format 和.NET中的Fromats对象是如何转换的。所以就没有去具体分析,也是因为时间有限。后面如果弄明白了就补上。下一篇打算简单介绍一下应用程序往Windwows资源管理器拖拽对象,已经如果实现自定义拖拽效果。
参考:
MSDN : Transferring Shell Objects with Drag-and-Drop and the Clipboard
你好,我想请教一下,如果我在把VC的一个对象(例如CPerson)拖拽到.NET中,怎么样才能获得VC对象中的数据呢?.NET中没有相应的对象,类似的问题应该如何处理呢?谢谢了!
太牛了,有点黑客的感觉。