.NET中的Drag and Drop操作(一)
在上一篇文章介绍了在.NET中进行Drag和Drop操作的方法,以及底层的调用实现过程。实际是通过一个DoDragDrop的WIN32 API来监视拖拽过程中的鼠标,根据鼠标的位置获得IDropTraget和IDropSource接口,对拖拽源和目标进行操作。但是拖拽的目的是进行数据的交换,在上一篇文章中对于发送和接受数据都是一笔带过,所以这一篇主要介绍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中最方便的数据传送方式就是剪贴板了,从一个程序复制数据,然后通过剪贴板传送到另一个程序中。剪贴板可以在应用程序间交换各种不同类型的数据,也就是程序之间发送和接受数据时都遵循了同一套规则,他们共同是用的这个对象叫做Shell Data Object。
.
.
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
.
.
Shell Data Object是一个Windows程序使用剪贴板和Drop and Drag操作的基础。当我们Source创建一个Data Object时,他并不知道Target能接受什么类型的数据。而对于Target来说他可能可以接受多种数据。因此Data Object中往往包含一些传送的数据的格式信息;除此之外他还包含一些对数据的操作信息,比如是移动数据还是复制数据。而一个Data Object中可以包含多个项目的切不同类型的数据。
.
.
前面说了,Data Object中需要包含发送数据的格式信息,所以对于Data Object对象中存放的每一项数据,都会分配一个数据格式,这个类型就叫做Clipboard Formats。WINDOIWS中定义了一些Clipboard Formats。他们名称通常都是CF_XXX的格式。比如CF_TEXT就表示ANSI格式的文本数据。我们在Source和Target之间使用这些类型数据时,需要使用RegisterClipboardFormat来注册这个格式。但是有一个比较特殊的类型CF_HDROP是不需要注册的,因为他是系统的私有格式。当Target接受到Drop操作时,会枚举发送来的数据的这些格式,以决定使用那一种格式去解析这些数据。
.
.
但是实际中,并不是直接使用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 。而联合体中的对象则指向了数据存储的位置。
.
.
以上两个结构体就是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
.
.
.
上面介绍了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 以获得预定义的格式。
.
.
.
通过上面的分析,我们知道了,我们程序中和程序间实现拖拽或是使用剪贴板传递数据时,使用了一个实现了IDataObject的对象。其中包含的传递的数据格式和数据的内存地址等信息。而.NET中通过一个DataObject类封装了数据操作。上面c++的代码也掩饰了WINDOWS下最原始的构造IDataObject对象的方法,下面我们就看看.NET下是如何封装的。
首先我们来看看,在数据源中拖动一个对象时,是如何构造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的调用。
.
.
前一篇介绍了,调用了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个结构体,所以我们可以获得指定格式的数据,然后对数据进行操作。
.
.
我们在.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接口对象。
1:对于无参构造函数,内部构造一个实现了IDataObject(.NET)接口的DataStroe对象,从名字看出是存放数据的;
2:对于有参的构造函数,如果传入的是对象是实现了IDataObject(.NET)接口的对象,直接保存到innerData字段,如若是IDataObject(COM)接口的对象,则使用一个OleConverter对象对数据进行以下包装;如果是没有实现这2个接口的对象,则还是利用DataStroe对象,并Setdata。
3:对于制定了数据格式的对象,调用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
.
.
前面花了很多时间介绍了DataObject的构造函数和内部的数据结构。下面就具体看看它自己的SetData和GetData方法吧。首先我们要明确一个问题,就是DataObject在内部维护的innerData存放的数据类型是2种:DataStore和OleConverter,知道这个非常重要。
我们这里只去关注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类中。
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