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

两篇文件介绍了.NET平台下Drag and Drop操作的原理以及整个拖拽的过程,还分析了拖拽过程中的数据的格式。本篇是这个小系列的最后一篇,主要是通过列子介绍.NET程序如何与Windows Shell之间进行双向的文件传递,以及如何修改拖动时的图标样式。

 

一 Windows Shell

 

可能有点奇怪,介绍Drag and Drop 怎么介绍到Shell上去了。虽然拖拽的数据对象可以是任意格式的,但是我们平时拖拽的最多的还是文件,文件夹这样的对象。打开文件,发送文件,移动文件,这样的操作我们在Windows中使用的太多了。而这些都和Shell有着密切的关系。这里就简单介绍一下,详细可以参见MSDN :Windows Shell

 

1.什么是Shell

 

Shell其实也是一种程序,如果接触过unix或Linux或许比较好理解。准确的说Shell是一个命令解析器,在Windows上我们输入Cmd,在出来的窗体中可以进行一些列的系统操作,启动程序、管理文件、设置系统服务等等;而同样我们也可以在Windows提供的图形界面中操作,比如打开我的电脑管理文件、打开控制面板设置计算机。这就是我们常见的两种Shell:图形界面Shell和命令行Shell。 Shell实际是介于操作系统内核与用户之间的一个接口。

 

 

2.Windwos Shell

 

这里我们主要了解的是图形界面的Shell。Windows UI为用户提供了访问各种对象、运行程序以及管理系统的能力。在访问的众多对象中,我们最熟悉的就是文件和目录,他们都是存放在硬盘上的;但是还有一些并不是真实存在的对象,比如远程打印机和回收站,他们并不是真正的存在于硬盘之上。Shell把这些对象组织为一套层次结构,提供给用户和程序使用和管理。

 

 

3. Shell编程

 

Windows Shell最常见的部分就是桌面和任务栏,Shell所管理的对象我们可以称之为Shell Object。我们前面提到过Shell Object,但是他并不是仅仅包含文件和目录,还包含那些虚拟的对象。桌面是所有Shell Object的根,也就是层次结构中最顶层的。

对于桌面来说,它也是一个窗体,实际就是一个ListView控件,所以在窗口拖动文件,和我们在自己的程序中拖动是没有本质区别的。而资源管理器Explorer也是一个程序,通过API获得Shell 的层次结构并显示,然后提供给用户进行操作。所以我们完全可以通过使用API,在自己的程序中实现简单的Sehll功能。也可以通过对Shell编程,实现自己的功能。

关于Shell编程可以参见:Windows Shell 编程

而在CodeProject上有一个C#实现的资源管理器:http://www.codeproject.com/KB/miscctrl/FileBrowser.aspx

 

 

二 .NET和Shell文件相互拖拽

 

 

其实前面文章的例子已经有了.NET程序接受文件拖拽的列子,但是平日却很少见到从程序中拖拽对象到资源管理器中。我们知道了资源管理器其实也是一个程序,她自身能够实现拖拽操作就说明她实现了IDragSource和IDropTarget接口,既然是这样,她就和我们程序一样,能够接受其他程序拖拽来的对象。所以我们程序在生成数据时必须满足使用IDataObject对象,并且传送的类型是双反都能使用的。

通过前面文章介绍,我们知道了.NET平台上的DataObject对象实现了IDataObject(COM)接口,并且CF_HDROP是私有的,不需要注册的。在.NET中对用的是DropFiles。所以我们在生成对象时需要满足这2个条件就能和Shell之间进行交互了。

        private void listView1_ItemDrag(object sender, ItemDragEventArgs e)
        {
            string[] files = new string[listView1.SelectedItems.Count];
            int i = 0;
            foreach (ListViewItem item in listView1.SelectedItems)
            {
                files[i++] = item.Tag.ToString();
            }

            if (files != null)
            {
                System.Windows.Forms.IDataObject pObj = new System.Windows.Forms.DataObject(DataFormats.FileDrop, files);
                pObj.SetData(DataFormats.FileDrop, files);
                listView1.DoDragDrop(pObj, DragDropEffects.Copy);
            }

        }

上面是我拖拽程序中的文件时的代码。和前两篇列子同,这里发送的数据类型不在是ListViewItem,因为这个类型Shell是不认识的,而且要使用这个类型时,Source和Target都需要注册,但是我们是没办法去控制Target的。所以这里传递的类型是DataFormats.FileDrop,而数据部分是ListView中选择的文件的路径。前面介绍过,路径会组成CF_HDROP结构,然后通过IDataObject来传递到Shell。

把桌面的文件拖拽到C盘下,可惜截图无线截取鼠标的状态。因为这里是复制,在拖动时候在也不是显示静止的图标了,而是一个小框一个+,在Win7上会显示复制2个字。成功了。

可见在C#下代码非常的简单,比设置不需要做什么工作就能和Shell交互了。这里需要注意的就是DoDragDrop(pObj, DragDropEffects.Copy); 这里我们制定了传输的行为是Copy这样,我们从把文件从程序拖拽到C盘时,是复制;如果我们选择为Move,那么在移动后Shell会把桌面的文件删除掉。这里你也可以选择多种方式,比如ALL,这样他就会根据Target放设定的Effects来表现。Shell默认是设置为Move。

 

 

 

 

三 显示拖拽图标

 

 

到目前为止,我们已经实现了程序与Shell之间相互拖拽的操作,当然和其他程序之间相互拖拽也是一样的道理了。但是我们发现,在Windwos中拖拽对象时,都会显示对象本省的图标,但是我们程序拖拽文件到Shell,或者Sehll拖拽文件到程序中,都没有显示。但是Windows为我们程序显示DragImage提供了一个COM辅助对象:DragDropHelper。

 

1.DragDropHelper

 

不同于IDragSource和IDropTarget,.NET并没有提供这样一个COM对象的包装类供我们使用,所以我们必须自己在.NET中使用这个对象。搜索时发现在.NET4的System.Activities.Presentation 命名空间下提供了一个DragDropHelper,但是我们使用Winform,应该不能使用。DragDropHelper提供了两个接口来实现在Drag和Drop操作中显示图标,这两个接口是IDragSourceHelperIDropTargetHelper

 

 

2 IDragSourceHelper

 

// Initializes the drag-image manager for a windowless control.

HRESULT InitializeFromBitmap(
  [in]  LPSHDRAGIMAGE pshdi,
  [in]  IDataObject *pDataObject
);

// Initializes the drag-image manager for a control with a window.

HRESULT InitializeFromWindow(
  [in]  HWND hwnd,
  [in]  POINT *ppt,
  [in]  IDataObject *pDataObject
);

IDragSourceHelper 接口提供了2个方法来设置我们在拖拽时的图标。 需要注意的是IDragSourceHelper 接口已经由Shell的drag-image manager 实现了,所以我们程序只需要调用接口的方法,而不用负责实现。

上面的2个方法分别是针对不同的情况:

对于有窗体的控件来说,应该调用InitializeFromWindow方法,因为窗体可以注册一个DI_GETDRAGIMAGE 的消息,而我们程序在调用这个方法时,会把对象的图标存入到一个SHDRAGIMAGE结构体中,通过消息的lParam参数发送到对应的窗体中。这样通过windows paint就能正常的显示这个图标了。

而对于非窗体控件来说,应该调用InitializeFromBitmap方法,通过参数可以看到,这个方法有一个LPSHDRAGIMAGE类型参数,她是指向SHDRAGIMAGE结构体的指针,所以我们必须手动指定图标,以便显示。

通过上面我们大概可以知道,IDragSourceHelper 的作用就是把图标的数据,加入到DataObject中进行传递。以便接收方能显示。

 

 

3 IDropTargetHelper

 

//Notifies the drag-image manager that the drop target's IDropTarget::DragEnter method has been called.

HRESULT DragEnter(
  [in]  HWND hwndTarget,
  [in]  IDataObject *pDataObject,
  [in]  POINT *ppt,
  [in]  DWORD dwEffect
);

//Notifies the drag-image manager that the drop target's IDropTarget::DragLeave method has been called.

HRESULT DragLeave();

//Notifies the drag-image manager that the drop target's IDropTarget::DragOver method has been called.

HRESULT DragOver(
  [in]  POINT *ppt,
  [in]  DWORD dwEffect
);

//Notifies the drag-image manager that the drop target's IDropTarget::Drop method has been called.

HRESULT Drop(
  [in]  IDataObject *pDataObject,
  [in]  POINT *ppt,
  [in]  DWORD dwEffect
);

//Notifies the drag-image manager to show or hide the drag image.

HRESULT Show(
  [in]  BOOL fShow
);

我们发现 IDropTargetHelper提供的5个方法中,有4个我们都很熟悉,和IDropTarget提供方法完全一样。只不过这里IDropTargetHelper提供的方法也是已经由Shell的drag-image manager 实现,我们不需要自己去实现。这里4个方法,是用来和IDropTarget提供的方法协同合作的。通过调用这几个方法,我们可以在Target中显示Drop image。而Show方法怎是指示是否显示image。

所以如果我们想要在target上显示image,只需要在IDropTarget提供的方法内部调用相应的IDropTargetHelper方法就能完成。我们看到DropEnter方法需要传入的参数包括IDataObject对象,因为对象的图标也是保存在对象中的,所以这里需要传递给它,用来显示。

 

 

 

 

 

四 代码实现

 

 

下面主要介绍如何在.NET中实现显示图标的功能,因为涉及到与COM交互,在.NET中使用起来就没有C++那么方便了。不过能用C#实现的还是尽量用C#实现,网络上虽然有一些例子,但是大部分都是用C++实现的。

 

1.准备工作

 

因为程序需要和COM交互,所以在调用接口之前,我们必须做一些准备工作,才能正常的使用这些接口。关于COM组件,可以参见前面提到过的《COM技术内幕》。简单说,我们使用COM组件提供的功能,首先必须获得这个组件对象,然后通过唯一的接口,查询到我们要使用的接口,并使用。这里我们首先要获得DragDropHelper对象,然后获得IDragSourceHelper和IDropTargetHelper接口。获得接口后就能进行方法的调用了。

对于COM组件和对象来说,都有唯一标识他们的GUID。在.NET中使用COM组件时,我们也需要用到,关于组件和接口的GUID可以通过MSDN查询到。

        public static Guid CLSID_DragDropHelper = new Guid("{4657278A-411B-11d2-839A-00C04FD918D0}");

        public static Guid IID_IDropTargetHelper = new Guid("{4657278B-411B-11d2-839A-00C04FD918D0}");

        public static Guid IID_IDragSourceHelper = new Guid("{DE5BF786-477A-11d2-839D-00C04FD918D0}");

对于组件来说,她的ID称为CLSID,而对于组件的接口,使用IID,以上就是需要使用到的组件和接口的GUID。因为要在.NET中使用这些接口,所以必须在.NET中声明这些接口:

        [ComImport]
        [GuidAttribute("4657278B-411B-11d2-839A-00C04FD918D0")]
        [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
        public interface IDropTargetHelper
        {
            // Notifies the drag-image manager that the drop target's IDropTarget::DragEnter method has been called
            [PreserveSig]
            Int32 DragEnter(IntPtr hwndTarget, System.Runtime.InteropServices.ComTypes.IDataObject pDataObject, ref POINT ppt, DragDropEffects dwEffect);

            // Notifies the drag-image manager that the drop target's IDropTarget::DragLeave method has been called
            [PreserveSig]
            Int32 DragLeave();

            // Notifies the drag-image manager that the drop target's IDropTarget::DragOver method has been called
            [PreserveSig]
            Int32 DragOver(ref POINT ppt, DragDropEffects dwEffect);

            // Notifies the drag-image manager that the drop target's IDropTarget::Drop method has been called
            [PreserveSig]
            Int32 Drop(System.Runtime.InteropServices.ComTypes.IDataObject pDataObject, ref POINT ppt, DragDropEffects dwEffect);

            // Notifies the drag-image manager to show or hide the drag image
            [PreserveSig]
            Int32 Show(bool fShow);
        }



        [ComImport]
        [GuidAttribute("DE5BF786-477A-11d2-839D-00C04FD918D0")]
        [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
        public interface IDragSourceHelper
        {
            [PreserveSig]
            Int32 InitializeFromBitmap(SHDRAGIMAGE pshdi, System.Runtime.InteropServices.ComTypes.IDataObject pDataObject);


            [PreserveSig]
            Int32 InitializeFromWindow(IntPtr hwnd, ref POINT ppt, System.Runtime.InteropServices.ComTypes.IDataObject pDataObject);
        }

接口定义如上,其中【ComImport】标识,这个对象是在COM中定义的,而【GuidAttribute】指定了对象的GUID,也就指定了是COM中的那个对象(对象和GUID之间的关系是保存在注册表中的)。【PreserveSig】是标识当方法返回的HRESULT不为S_OK时是否引发异常。默认为True,表示不引发异常。对于参数类型,也已经转换为了.NET下对应的类型。还构造了SHDRAGIMAGE和POINT两个结构体.

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        public struct POINT
        {
            public POINT(int x, int y)
            {
                this.x = x;
                this.y = y;
            }
            public int x;
            public int y;
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        public struct SIZE
        {
            public SIZE(int cx, int cy)
            {
                this.cx = cx;
                this.cy = cy;
            }
            public int cx;
            public int cy;
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        public struct SHDRAGIMAGE
        {
            SIZE sizeDragImage;
            POINT ptOffset;
            IntPtr hbmpDragImage;
            Int32 crColorKey;
        }

 

 

2.获得接口对象

 

OK,到现在为止准备工作已经做的差不多了,下面就是来获得接口对象了。以下两个方法就是获得IDropTargetHelper和IDragSourceHelper接口。代码基本是一样的。

        public static bool GetIDropTargetHelper(out IntPtr helperPtr, out IDropTargetHelper dropHelper)
        {
            if (CoCreateInstance(
                    ref CLSID_DragDropHelper,
                    IntPtr.Zero,
                    CLSCTX.INPROC_SERVER,
                    ref IID_IDropTargetHelper,
                    out helperPtr) == 0)
            {
                dropHelper =
                    (IDropTargetHelper)Marshal.GetTypedObjectForIUnknown(helperPtr, typeof(IDropTargetHelper));

                return true;
            }
            else
            {
                dropHelper = null;
                helperPtr = IntPtr.Zero;
                return false;
            }
        }

        public static bool GetIDragSourceHelper(out IntPtr helperPtr, out IDragSourceHelper dropHelper)
        {
            if (CoCreateInstance(
                    ref CLSID_DragDropHelper,
                    IntPtr.Zero,
                    CLSCTX.INPROC_SERVER,
                    ref IID_IDragSourceHelper,
                    out helperPtr) == 0)
            {
                dropHelper =
                    (IDragSourceHelper)Marshal.GetTypedObjectForIUnknown(helperPtr, typeof(IDragSourceHelper));

                return true;
            }
            else
            {
                dropHelper = null;
                helperPtr = IntPtr.Zero;
                return false;
            }
        }

首先调用API的方法CoCreateInstance获得CLSID指定的对象,我们看到这里是DragDropHelper对象,但是和我们获取一般对象不一样,并没有一个对象的引用,反倒是只有一个IDropTargetHelper dropHelper对象获得了IDropTargetHelper 接口的地址。其实COM的特点就是这样,提供一组接口给外部使用,而且你只能通过一个接口去查询其他接口,并且任意的接口都能查询其他IID指定的接口。你在使用一个组件功能时,需要去查询,她是否实现了你需要的接口,所以这里获得组件对象是没有意义的。

得到接口地址以后,我么通过Marshal.GetTypedObjectForIUnknown方法,通过接口地址,获得了一个托管的COM接口对象,这样在程序中就能通过这个引用来调用接口的方法了。对于GetIDragSourceHelper方法实现是完全一样的。

        API.IDropTargetHelper dropHelper;
        API.IDragSourceHelper dragHelper;
        IntPtr dropHelperPtr;
        IntPtr dragHelperPtr;

        public Form1()
        {
            InitializeComponent();
            API.GetIDropTargetHelper(out dropHelperPtr, out dropHelper); //获取IDropTargetHelper接口对象
            API.GetIDragSourceHelper(out dragHelperPtr, out dragHelper); //获取IDragSourceHelper接口对象
            this.FormClosed += delegate { Marshal.Release(dropHelperPtr); Marshal.Release(dragHelperPtr); }; //释放COM
        }

在窗体中调用相应的方法,把接口保存到dropHelper和dragHelper对象中。因为我们是调用CoCreateInstance的API函数创建了COM对象,所以我们必须手动释放掉这些对象,可以使用Marshal.Release进行操作。到此为止,一切准备完毕。

 

 

3 Drop Image

 

相对于在自己程序中拖拽文件显示图标,接受文件时显示图标显得更加简单。

        private void listView1_DragEnter(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
                e.Effect = DragDropEffects.Copy;

            API.POINT pt = new API.POINT(e.X, e.Y);
            dropHelper.DragEnter(this.Handle, (System.Runtime.InteropServices.ComTypes.IDataObject)e.Data, ref pt, e.Effect);
        }

        private void listView1_DragOver(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
                e.Effect = DragDropEffects.Copy;

            API.POINT pt = new API.POINT(e.X, e.Y);
            dropHelper.DragOver(ref pt, e.Effect);
        }

        private void listView1_DragLeave(object sender, EventArgs e)
        {
            dropHelper.DragLeave();
        }

在IDropTraget相应的方法中调用接口dropHelper接口的方法,传递的参数也很简单,下面就看看效果吧。

好了,图标出来了。成功拖入。

对于Drop调用的代码见下面的例子。

 

 

4.程序中显示文件

 

在Drag Image之前,先来看看如何让文件象在资源管理器中一样显示。当我们把文件拖动到程序中时,只是把文件信息显示在ListView中,而文件实际还是在硬盘上。当然我们在window shell中看到的,其实和我们程序中一样。只不过shell通过一种层次的方式,显示出来。我们完全可以使用自己的资源管理器,完全可以让C盘D盘显示在一起,E盘F盘显示在一起。只是组织方式不用,一切都是幻觉。

在ListView中显示以上的信息不难,以为我们知道文件的路径很容易得到FileInfo对象。但是问题是图标是如何显示呢?如何去获得文件的图标呢。.NET好像并没有这个功能,这个时候还是得自己调用API了。在API中有一部分已SH开头的表示是SHELL API。这里我们需要用大的是SHGetFileInfo。

DWORD_PTR SHGetFileInfo(
  __in   LPCTSTR pszPath,
  __in   DWORD dwFileAttributes,
  __out  SHFILEINFO *psfi,
  __in   UINT cbFileInfo,
  __in   UINT uFlags
);

        [DllImport("shell32.dll", CharSet = CharSet.Auto)]
        public static extern IntPtr SHGetFileInfoW(string pszPath, uint dwFileAttributes, ref SHFILEINFO psfi, uint cbSizeFileInfo, uint uFlags);

他的原型和.NET中引进的类型如上。后面有W表示使用Unicode编码。具体参数参见http://msdn.microsoft.com/en-us/library/bb762179(VS.85).aspx这个方法我们需要指定文件的路径和提供一个SHFILEINFO结构用来保存文件信息,在就是指示需要获得信息的FLAG。这些也能从MSDN上找到。所以我们的Drop方法修改为以下代码:

private void listView1_DragDrop(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                string[] files = (string[])e.Data.GetData(DataFormats.FileDrop); //获取拖拽进来的文件路径(H_DROP)
                foreach (string file in files)
                {
                    //如果文件已经存在不在显示
                    foreach (ListViewItem it in listView1.Items)
                    {
                        if (file == it.Tag.ToString())
                        {
                            listView1_DragLeave(this, null);
                            return;
                        }
                    }

                    int img = -1;
                    ArrayList items = new ArrayList();
                    if (Directory.Exists(file))
                    {
                        //通过API获得目录信息
                        API.SHFILEINFO sfi = new API.SHFILEINFO();
                        API.SHGetFileInfoW(file, 0, ref sfi, (uint)Marshal.SizeOf(sfi), API.FILE_FLAG.SHGFI_DISPLAYNAME | API.FILE_FLAG.SHGFI_TYPENAME | API.FILE_FLAG.SHGFI_ICON);

                        DirectoryInfo di = new DirectoryInfo(file);
                        items.Add(sfi.szDisplayName);
                        items.Add(sfi.szTypeName);
                        items.Add("");//目录没有大小 
                        items.Add(di.LastWriteTime.ToString("g"));
                        items.Add(file);
                        img = sfi.iIcon;
                    }
                    else if (File.Exists(file))
                    {
                        //通过API获得文件信息
                        API.SHFILEINFO sfi = new API.SHFILEINFO();
                        API.SHGetFileInfoW(file, 0, ref sfi, (uint)Marshal.SizeOf(sfi), API.FILE_FLAG.SHGFI_USEFILEATTRIBUTES | API.FILE_FLAG.SHGFI_DISPLAYNAME | API.FILE_FLAG.SHGFI_TYPENAME | API.FILE_FLAG.SHGFI_ICON);

                        FileInfo fi = new FileInfo(file);
                        items.Add(sfi.szDisplayName);
                        items.Add(sfi.szTypeName);
                        long l = 0;
                        try
                        {
                            l = fi.Length;
                        }
                        catch
                        {
                        }
                        double test = (double)l / 1000;
                        long fs = l / 1000;
                        fs += test > fs ? 1 : 0;
                        items.Add(fs.ToString() + " KB");
                        items.Add(fi.LastWriteTime.ToString("g"));
                        items.Add(file);
                        img = sfi.iIcon;
                    }
                    ListViewItem listviewItem = new ListViewItem((string[])items.ToArray(typeof(string)));
                    listviewItem.Tag = file;
                    listviewItem.ImageIndex = img;
                    listviewItem.Selected = true;
                    listView1.Items.Add(listviewItem);
                }

            }

            API.POINT point = new API.POINT(e.X, e.Y);
            dropHelper.Drop((System.Runtime.InteropServices.ComTypes.IDataObject)e.Data, ref point, e.Effect);
        }

我们在遍历对象时区分了文件和目录,通过SHGFI_ICON flag我们得到了显示的图片,但是我们发现SHFILEINFO的iIcon字段是一个int行,而不是一个IntPrt,也就是它存放的指示图片的序号,而不是地址。我们知道ListView中显示图片一般都是放在一个ImageList中然后指定序号,而我们现在只有序号却没有ImageList。

我们这里使用的Image是Shell提供的系统ImageList,我们需要通知ListView,使用系统的ImageList,这样通过序号就能找到图片了。

        private void Form1_Load(object sender, EventArgs e)
        {
            //获得系统的ImageList
            int LVM_SETIMAGELIST = 0x1003;

            API.SHFILEINFO sfi = new API.SHFILEINFO();
            IntPtr Small = API.SHGetFileInfoW(@"c:/", 0, ref sfi, (uint)Marshal.SizeOf(sfi), API.FILE_FLAG.SHGFI_ICON | API.FILE_FLAG.SHGFI_SYSICONINDEX | API.FILE_FLAG.SHGFI_SMALLICON);
            int SmallInt = Small.ToInt32();

            API.SendMessage(listView1.Handle, LVM_SETIMAGELIST, (int)API.FILE_FLAG.SHGFI_SMALLICON, SmallInt);

            IntPtr Large = API.SHGetFileInfoW(@"c:/", 0, ref sfi, (uint)Marshal.SizeOf(sfi), API.FILE_FLAG.SHGFI_ICON | API.FILE_FLAG.SHGFI_SYSICONINDEX | API.FILE_FLAG.SHGFI_LARGEICON);
            int LargeInt = Large.ToInt32();

            API.SendMessage(listView1.Handle, LVM_SETIMAGELIST, (int)API.FILE_FLAG.SHGFI_LARGEICON, LargeInt);
        }

我们在加载窗体时,象ListView发送了一个LVM_SETIMAGELIST 消息,而获得系统图标句柄,就是通过上面的方法,指定路径为C盘,falg增加SHGFI_SYSICONINDEX。为什么是C盘,我也不知道。。。

 

 

5 Drag Image

 

好,不该做的都做了,该做的还没做。最后就看看如何让程序drag image。或许和drop image一样简单吧,在DoDragaDrop之前,我们调用InitializeFromWindow方法:

        private void listView1_ItemDrag(object sender, ItemDragEventArgs e)
        {
            string[] files = new string[listView1.SelectedItems.Count];
            int i = 0;
            foreach (ListViewItem item in listView1.SelectedItems)
            {
                files[i++] = item.Tag.ToString();
            }

            if (files != null)
            {
                System.Windows.Forms.IDataObject pObj = new System.Windows.Forms.DataObject(DataFormats.FileDrop, files);
                API.POINT pt = new API.POINT(PointToClient(MousePosition).X, PointToClient(MousePosition).Y);
                dragHelper.InitializeFromWindow(listView1.Handle, ref pt, (System.Runtime.InteropServices.ComTypes.IDataObject)pObj);
                listView1.DoDragDrop(pObj, DragDropEffects.All);
            }
        }

先获得选中的文件的路径,存放到files数组中。把ListView的句柄,已经IDataObject对象传递给他,并且传递当前鼠标的位置。(原来Control对象提供了MousePostion方法来获得鼠标位置,我竟然一直都不知道,泪奔啊@_@!)看下效果:

O了啊!正常显示。把文件拖拽到桌面,悲剧发生了,竟然不显示图标了。什么情况。百思不得其解啊。google,我勒个去。竟然什么相关资料。一开始不是提到过CodeProject上的一个C#写的资源管理器吗。一看只使用了IDropTargetHelper,没有用IDragSourceTarget。在 CodeProject上找了半天,终于找到了一篇文章:Windows Explorer style ghost drag image in a C# application 不错,解决了这个问题。我的界面和图标也是参照他来做的。我简单增加了判断,这样在自己窗体中释放已存在的对象时不会在进行添加。

看了他的文章,其中一句话是:

an IDataObject implementation that has its SetData implemented to take and store any format “set” by external objects,

我在看看MSDN发现:

Note   The drag-and-drop helper object calls IDataObject::SetData to load private formats—used for cross-process support—into the data object. It later retrieves these formats by calling IDataObject::GetData. To support the drag-and-drop helper object, the data object’s SetData and GetData implementations must be able to accept and return arbitrary private formats.

也就是说,要使用drag and drop helper object ,传递的数据必须可以设置和读取任意格式的数据。回想上一篇,.NET的DataObject对象是没有实现COM接口的SetData方法的。所以,我们的DataObject中并没有带有图标信息,当然就不能显示了。为什么在自己的ListView中可以显示呢,这个。。。也不是太清楚了。。。。然后我看了下InitializeFromWindow方法的返回值,确实不是0,说明失败了。

 

 

6 解决问题

 

找到了问题的原因,就要来解决。既然是没有实现SetData方法,那么不如我们自己来实现一个DataObject对象吧。不过在.NET中实现,饿,那是相当的麻烦,我是在是懒了,而且这对我来说也不算是个简单的活,所以,我还是使用了上面那个介绍drag image的人写的代码。他是用的托管C++编写的。

       private void listView1_ItemDrag(object sender, ItemDragEventArgs e)
        {
            string[] files = new string[listView1.SelectedItems.Count];
            int i = 0;
            foreach (ListViewItem item in listView1.SelectedItems)
            {
                files[i++] = item.Tag.ToString();
            }

            if (files != null)
            {
                //System.Windows.Forms.IDataObject pObj = new System.Windows.Forms.DataObject(DataFormats.FileDrop, files);
                //API.POINT pt = new API.POINT(PointToClient(MousePosition).X, PointToClient(MousePosition).Y);
                //dragHelper.InitializeFromWindow(listView1.Handle, ref pt, (System.Runtime.InteropServices.ComTypes.IDataObject)pObj);
                //listView1.DoDragDrop(pObj, DragDropEffects.All);

                DataObjectEx data = new DataObjectEx();
                data.SetData(DataFormats.FileDrop, files);
                DragDropEffects res = ShellUtils.DragSource.DoDragDrop(data, listView1, DragDropEffects.All, PointToClient(MousePosition));
            }
        }

我们注释掉之前的代码,修改为使用DataObjectEx,这个是继承与DataObjet,内部维护一个实现了IDataObject(COM)接口的CDataObject对象。然后调用了他提供的 DoDragDrop方法,此方法内部调用了InitializeFromWindow和API的DoDragDrop。我尝试使用Control的DoDragDrop,传递对象是DataObjectEx,但是失败了。

              DataObjectEx data = new DataObjectEx();
              data.SetData(DataFormats.FileDrop, files)
              API.POINT pt = new API.POINT(PointToClient(MousePosition).X, PointToClient(MousePosition).Y);
                dragHelper.InitializeFromWindow(listView1.Handle, ref pt, (System.Runtime.InteropServices.ComTypes.IDataObject)data );
                listView1.DoDragDrop(data , DragDropEffects.All);

因为DataObjectEx继承与DataObject,而DataObject并没有实现COM接口的SetData方法,这里我们应该传递的是他内部维护的CDataObject对象,但是她返回的是指针类型

::IDataObject* GetDataObject() { return _pDataObject; } 

所以这里我还是使用了他提供的DoDragDrop方法。如果想使用Control的方法,应该还是有办法的,我们可以自己构建CDataObject对象。传递此对象。但是也会有些复杂,所以我没有尝试,这里只是弄明白如何正确显示drag image。看看效果吧。

至此我们的任务已经完成了。使用了ShellUtils.dll这个DLL,使用时还发生了点问题,他的DLL是使用.NET1.1编译的,我的程序是在VS2010下用.NET4.0写的,但是调用DLL函数时程序却死掉了,按道理来说.NET4开始支持In Process Side By Side,1.1编译的DLL,应该是以.NET1.1版本运行,而EXE是以.NET4.0运行,但是因为本机上没有安装.NET1.1,而安装了,NET2.0和.NET4.0(3.0?3.5呢?这2个版本只是加入了新的库,CLR还是2.0,这里讨论的是运行时CLR的版本),这个时候DLL是在2.0版本下运行的,可能是因为兼容性的原因导致的吧,毕竟1.1到2.0变化还是比较大的。于是我把源码在4.0下重新编译了一次,OK了。

传送门:.NET 4.0新功能介绍:In Process Side By Side

 

 

 

 

五 Windows7的拖拽

 

 

随着Windows7的发布,图形Shell也变的越来越炫了,Windows中也增加了一些和Shell有关的API。除此之外还提供了一个WindowsAPICodePack 的源码包,里面包括了一些.NET发布时没有包括的库。比如Shell库、DirectX库、电源管理、Windows7任务栏,这些都允许我们在.NET中用托管代码进行操作,确实大大方便了.NET开发。那么在Windows7中的拖拽怎么实现,在代码包的/Samples/Shell/DragAndDrop目录下有一个拖拽的例子,不过他是用WPF写的。只是显现了拖拽,没有实现图标的现实。

        void OnDrop( object sender, DragEventArgs e )
        {
            if( !inDragDrop )
            {
                string[ ] formats = e.Data.GetFormats( );
                foreach( string format in formats )
                {
                    // Shell items are passed using the "Shell IDList Array" format. 
                    if( format == "Shell IDList Array" )
                    {
                        // Retrieve the ShellObjects from the data object
                        DropDataList.ItemsSource = ShellObjectCollection.FromDataObject( e.Data );
                        
                        e.Handled = true;
                        return;
                    }
                }
            }

            e.Handled = false;
        }

在OnDrop事件中,获得并显示数据和我们不太一样,这里调用了一个 ShellObjectCollection.FromDataObject的方法

        /// <summary>
        /// Creates a ShellObjectCollection from an IDataObject passed during Drop operation.
        /// </summary>
        /// <param name="dataObject">An object that implements the IDataObject COM interface.</param>
        /// <returns>ShellObjectCollection created from the given IDataObject</returns>
        public static ShellObjectCollection FromDataObject(object dataObject)
        {
            System.Runtime.InteropServices.ComTypes.IDataObject iDataObject = dataObject as
                System.Runtime.InteropServices.ComTypes.IDataObject;

            IShellItemArray shellItemArray;
            Guid iid = new Guid(ShellIIDGuid.IShellItemArray);
            ShellNativeMethods.SHCreateShellItemArrayFromDataObject(iDataObject, ref iid, out shellItemArray);
            return new ShellObjectCollection(shellItemArray, true);
        }

我们看到这个方法是从实现了IDataObject(COM)接口的对象中获得数据.此方法并没有使用IDataObject的GetData方法,而是调用了一个api函数SHCreateShellItemArrayFromDataObject。并且数据类型是ShellIIDGuid.IShellItemArray。

MSDN上显示,这个API是在VISTA上新增了,也就是在XP上不能使用,并且有这么一段话:

This API lets you convert the data object into a Shell item that the handler can consume. It is recommend that handlers use a Shell item array rather than clipboard formats like CF_HDROP and CFSTR_SHELLIDLIST (also known as HIDA) as it leads to simpler code and allows some performance improvements.

建议我们使用Shell item array,而不是我们之前使用的CF_HDROP,也就是FileDrop。因为这个用起来使得代码更简单效率更高。

 

 

 

六 总结

 

至此有关.NET平台上的使用Drag和Drop操作就已经介绍完了,不管是XP还是WIN7,低层的实现原理应该是一样的。写这三篇文章完全是机缘巧合。因为新的项目,被问到是否了解windows的拖拽操作。特别是从程序向windows拖拽。所以写这三篇文章也是从一无所知开始的,花费了一周多的时间看MSDN和CodeProject上的列子以及写BLOG,肯定有很多不正确的地方。欢迎指正。本文使用的例子已经上传,下载地址:http://download.csdn.net/source/2617949

 

 

 

参考资料:

 

Transferring Shell Objects with Drag-and-Drop and the Clipboard

How to Implement Drag and Drop Between Your Program and Explorer

C# File Browser

C# does Shell, Part 1

Windows Explorer style ghost drag image in a C# application

OLE Drag and Drop

OLE Drag and Drop(中文翻译)


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

10 评论

  1. 网上关于拖拽图标的问题介绍的很少,都只是实现拖拽功能就完了,很少关注拖拽图标这种细节的。
    楼主文章写得真好,很详细,感谢分享!

  2. 写的真好 !!!
    我想问一下两个问题1.怎么实现只有指定的文件类型可以拖拽到控件(例如.txt或.xml)。2.通过往控件外拖拽的方式取消其控件中已有的项

  3. 回复 a8352081:
    我是在listView1_ItemDrag 中把要拖动的文件获得当前路径,你要做的就是在这个之前下载这个文件到本地,然后在包装成dataobject,然后调用DoDragDrop。不过这个可能有点问题,因为listView1_ItemDrag这个事件触发后要等待你下载。

  4. 大哥,我现在做的一个功能是,从ListView中拖几个项到shell中,然后从远程数据库下载被拖动的项对应的文件保存到那个shell。
    现在问题是,我不知道如何得到那个shell的路径。我看你的博文,是复制文件,而且复制操作还是由系统完成的,和我的不太一样,不知道该怎么办呢?

  5. 回复 cc_net:谢谢你才对。我会细读,学好技术。哈哈……等到公司再学就晚了

ofdata进行回复 取消回复

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