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

在上一篇文章介绍了在.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中我们看到,是建立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


如果本文对您有帮助,可以扫描下方二维码打赏!您的支持是我的动力!
微信打赏 支付宝打赏

2 评论

  1. 你好,我想请教一下,如果我在把VC的一个对象(例如CPerson)拖拽到.NET中,怎么样才能获得VC对象中的数据呢?.NET中没有相应的对象,类似的问题应该如何处理呢?谢谢了!

eddy3进行回复 取消回复

您的电子邮箱地址不会被公开。 必填项已用*标注