.NET中的Drag and Drop操作(一)
.NET中的Drag and Drop操作(二)
前两篇文件介绍了.NET平台下Drag and Drop操作的原理以及整个拖拽的过程,还分析了拖拽过程中的数据的格式。本篇是这个小系列的最后一篇,主要是通过列子介绍.NET程序如何与Windows Shell之间进行双向的文件传递,以及如何修改拖动时的图标样式。
.
可能有点奇怪,介绍Drag and Drop 怎么介绍到Shell上去了。虽然拖拽的数据对象可以是任意格式的,但是我们平时拖拽的最多的还是文件,文件夹这样的对象。打开文件,发送文件,移动文件,这样的操作我们在Windows中使用的太多了。而这些都和Shell有着密切的关系。这里就简单介绍一下,详细可以参见MSDN :Windows Shell
Shell其实也是一种程序,如果接触过unix或Linux或许比较好理解。准确的说Shell是一个命令解析器,在Windows上我们输入Cmd,在出来的窗体中可以进行一些列的系统操作,启动程序、管理文件、设置系统服务等等;而同样我们也可以在Windows提供的图形界面中操作,比如打开我的电脑管理文件、打开控制面板设置计算机。这就是我们常见的两种Shell:图形界面Shell和命令行Shell。 Shell实际是介于操作系统内核与用户之间的一个接口。
.
.
这里我们主要了解的是图形界面的Shell。Windows UI为用户提供了访问各种对象、运行程序以及管理系统的能力。在访问的众多对象中,我们最熟悉的就是文件和目录,他们都是存放在硬盘上的;但是还有一些并不是真实存在的对象,比如远程打印机和回收站,他们并不是真正的存在于硬盘之上。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程序接受文件拖拽的列子,但是平日却很少见到从程序中拖拽对象到资源管理器中。我们知道了资源管理器其实也是一个程序,她自身能够实现拖拽操作就说明她实现了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。
.
不同于IDragSource和IDropTarget,.NET并没有提供这样一个COM对象的包装类供我们使用,所以我们必须自己在.NET中使用这个对象。搜索时发现在.NET4的System.Activities.Presentation 命名空间下提供了一个DragDropHelper,但是我们使用Winform,应该不能使用。DragDropHelper提供了两个接口来实现在Drag和Drop操作中显示图标,这两个接口是IDragSourceHelper 和IDropTargetHelper。
.
.
// 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中进行传递。以便接收方能显示。
.
.
//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++实现的。
因为程序需要和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; }
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进行操作。到此为止,一切准备完毕。
.
.
相对于在自己程序中拖拽文件显示图标,接受文件时显示图标显得更加简单。
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调用的代码见下面的例子。
.
.
在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盘,我也不知道。。。
.
.
好,不该做的都做了,该做的还没做。最后就看看如何让程序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,说明失败了。
.
.
找到了问题的原因,就要来解决。既然是没有实现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的发布,图形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(中文翻译)