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

 系列文章:

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

 

.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之间进行交互了。

 

 上面是我拖拽程序中的文件时的代码。和前两篇列子同,这里发送的数据类型不在是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

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

 

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

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

 

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

 

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

.

.

3 IDropTargetHelper

 

 

 我们发现 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查询到。

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

 

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

 

 

 

2.获得接口对象

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

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

 

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

 

 

  

 

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

 

.

.

 

3 Drop Image

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

 

 

 

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

 

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

 

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

 

.

.

4.程序中显示文件

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

 

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

 

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

 

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

 

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

 

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

 

.

.

 

5 Drag Image

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

 

 

先获得选中的文件的路径,存放到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++编写的。

 

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

 

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

 

所以这里我还是使用了他提供的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写的。只是显现了拖拽,没有实现图标的现实。

 

 

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

 

我们看到这个方法是从实现了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(中文翻译)

 

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值