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

14 篇文章 0 订阅

前两篇文件介绍了.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操作中显示图标,这两个接口是IDragSourceHelper 和IDropTargetHelper。

 

.

.

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(中文翻译)



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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
selenium的dragElement是指使用Selenium库的Actions类的方法来实现拖放操作。在给定的示例代码,使用了Actions类的ClickAndHold()方法来点击并按住要拖动的元素,然后使用DragAndDropToOffset()方法将该元素拖动到指定的偏移量位置。这个方法可以用于拖动滑块等元素,例如大麦网登录界面验证码滑块。这个方法需要传入要拖动的元素和拖动的偏移量作为参数,以实现元素在页面的拖动操作。这样可以模拟用户在页面上拖动元素的行为。<span class="em">1</span><span class="em">2</span> #### 引用[.reference_title] - *1* [selenium:如何模拟鼠标拖放(drag and drop)](https://blog.csdn.net/m0_59490221/article/details/118501243)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [Python爬虫示例代码,使用Selenium和BeautifulSoup处理静态网页.txt](https://download.csdn.net/download/weixin_44609920/88225738)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值