.NET中的Drag and Drop操作(二)

来源:互联网 发布:单片机课程设计目的 编辑:程序博客网 时间:2024/05/17 16:57

在上一篇文章介绍了在.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接口对象。

1:对于无参构造函数,内部构造一个实现了IDataObject(.NET)接口的DataStroe对象,从名字看出是存放数据的;

2:对于有参的构造函数,如果传入的是对象是实现了IDataObject(.NET)接口的对象,直接保存到innerData字段,如若是IDataObject(COM)接口的对象,则使用一个OleConverter对象对数据进行以下包装;如果是没有实现这2个接口的对象,则还是利用DataStroe对象,并Setdata。

3:对于制定了数据格式的对象,调用SetData方法,设置数据。

 

 

这个地方感觉是曾相识,是的。DoDragDrop方法在内部也对数据进行了一系列类似的转化,不同的时她是把数据转换为IDataObject(COM)对象,因为WINDOWS只认识这种数据结构;而这里我们是吧数据转换为IDataObject(.NET)存储,因为我们是在.NET平台内部使用。

 

 

  • 内嵌的类

在前面我们看到了2个内嵌的类,DataStoreOleConverter,他们都实现了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中我们看到,是建立DataStore对象后通过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



转载自:http://blog.csdn.net/cc_net/article/details/5801563

0 0
原创粉丝点击