65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 文件浏览器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (167投票s)

2006年8月5日

28分钟阅读

viewsIcon

2504516

downloadIcon

82103

用 C# 编写的文件浏览器,非常类似于 Windows Explorer。

目录

引言

这个项目引入了一个早期版本的 Windows Explorer 克隆。它包含了对您计算机上所有文件和文件夹的浏览,包括虚拟文件夹。它使用与 Windows Explorer 相同的 ContextMenu,并支持拖放。

我使用 Visual Studio 2005 (.NET 2.0) 创建了这个项目,并且没有尝试过 .NET 1.1。我很确定它可以在 .NET 1.1 上运行,但需要进行一些更改。例如,我使用了 ToolBarMenuStrip,它在 1.1 中不可用。如果有人想要 1.1 版本并且无法转换,我很乐意自己转换并提供代码。

更新 V1.3

这次更新中有很多不同的新内容。最重要的一个新功能是插件。您现在可以为该应用程序添加自己的插件,用于在详细信息视图中添加列以及添加特殊视图。更多关于这方面的信息请参见“插件”部分。另一个重要的更新是添加了本文档的一个部分,解释了如何在您自己的应用程序中使用这个 Control 以及如何使用它的功能,请参见此更新的“使用控件”部分。

此外,还有一些小的更新、添加和错误修复。有关这些更新,请参见“历史记录”部分。

更新 V1.2

这只是一个很小的更新,但非常实用。我向 ListView 的标准 ContextMenu 添加了 "新建" 菜单。因此,现在可以在程序内添加新的文件夹和文件。

此外,还进行了一些错误修复,并且更新线程中也进行了一些更改,这还带来了一些速度上的改进。

更新 V1.1

这是自第一个版本以来一个相当大的改进。它不包含很多新功能,但有很多修复和速度提升。最重要的修复是内存泄漏修复,这是由更新线程引起的。在解决此问题时,我还添加了另一种更新方法。此方法使用 SHChangeNotifyRegister 函数来检索 Shell 通知消息。这些消息用于进行更多更新,例如更改图标、插入媒体和重命名。因此,现在当您将光盘插入光驱时,驱动器的图标和文本将更改为光盘的图标和文本。

需要注意的一个新功能是重命名功能。您现在可以通过从右键菜单中选择重命名项或按 F2 来重命名项目。但请注意,这也将更改文件的扩展名,但如果您这样做,它会警告您。重命名多个项目时,它将采用您输入的名称并在其后添加一个数字,与 Windows Explorer 类似,每个项目都不同。我建议您在一些测试文件上尝试重命名功能,以确切了解它的作用,然后再用于其他文件。

有关其他更改,请参见“历史记录”部分。

背景

我当时在寻找一些有趣的东西来编程,然后就有了制作自己的 Windows Explorer 的想法。我开始这个项目时,想法是制作一个具有插件支持的增强版 Windows Explorer。但在能够增强 Windows Explorer 之前,您需要有一个像 Windows Explorer 一样工作的程序。所以我开始在互联网上搜索解决方案。

在网上搜索时,我发现了大量关于 Windows Explorer 和 Shell 扩展的编程资料。但没有一个真正拥有我需要的一切,而且大多数程序都是用 C++ 编写的,而我真的很想要一个 C# 的。最终,我找到了开始我的程序所需的文章:一个全 VB.NET Explorer 树控件,带ImageList管理。尽管它是用 VB 写的,但我从中获得了大量信息,并且这个项目的大部分内容都依赖于这篇文章。因此,更多信息或 VB 版本请参见该文章。

现在唯一的问题是我以前从未接触过 Windows Shell。所以,首先,我搜索了关于 Shell 如何工作以及您可以用它做什么的文章。嗯,您可以做很多事情,太多在这里无法解释。如果您以前从未使用过 Shell,或者不太了解 Shell 的工作原理,我推荐这篇文章:C# 实用 Shell。这篇文章还提供了一些指向 MSDN 文章的链接。阅读它们需要一些时间,但这绝对帮助我很多,使我能够制作这个程序。您也可以在 MSDN 上找到关于此程序中使用的所有 Shell 方法、结构和枚举的大量信息。

在创建了程序的基础之后,我开始实现 Shell ContextMenu、拖放支持和类似 Windows Explorer 的 ComboBox 等功能。我并没有在 CodeProject 上找到一篇关于此的优秀文章,但在互联网上有很多可用的。我编写的一切都围绕着 Windows API 中的 Shell 函数构建了一个包装器,并惊讶于它的效果如此之好。

使用控件

要在您自己的程序中使用此控件,请向您的项目添加对 dll 的引用。之后,您可以将 Browser 控件添加到工具箱并将其添加到您自己的项目中。我创建了一些属性,允许您在设计时更改控件的行为和外观。

ShowNavigationBar 显示或隐藏导航栏
ShowFolders 显示或隐藏文件夹 TreeView
ShowFoldersButton 显示或隐藏文件夹 TreeView 的按钮
StartUpDirectory 枚举,指示启动时将打开哪个目录
StartUpDirectoryOther String,指示启动时将打开哪个目录(StartUpDirectory 必须是 "Other")
ShellBrowser 设置用于检索 ShellItemShellBrowser,如果设置为 null,Browser 将创建自己的
PluginWrapper 设置用于检索插件的 PluginWrapper,如果设置为 null,Browser 将创建自己的
SplitterDistance 设置 TreeViewListView 之间的分隔线距离>/TD>

StartUpDirectory 是一个特殊文件夹的枚举,用于确定 Browser 的启动位置。如果要提供自己的位置,必须将此值设置为 "Other",并在 StartUpDirectoryOther 属性中提供自己的位置。

当您想在程序中添加多个 Browser 控件时,可以使用 ShellBrowserPluginWrapper 属性。您可以将这些 Browser 控件链接起来,方法是将 ShellBrowserPluginWrapper 设置为同一个对象。这将使程序运行得比为控件使用不同的 ShellBrowserPluginWrapper 更快、更有效。在演示项目中,您将看到一个如何将两个 Browser 控件添加到项目中的示例。

此外,还有一些属性只能在运行时使用

ListViewMode 设置 ListView 的初始视图(不能是 "Small Icons")
SelectedItem 设置当前目录(ShellItem
SelectedNode 设置当前目录(TreeNode
ShowFoldersButton 显示或隐藏文件夹 TreeView 的按钮

最后,还有一些方法可以以编程方式执行 Browser 的某些操作

SelectPath 设置当前目录
BrowserBack 与单击 Browser 的后退按钮相同
BrowserForward 与单击 Browser 的前进按钮相同
BrowserUp 与单击 Browser 的向上按钮相同
CreateNewFolder 在当前目录中创建一个新目录,如果可能的话

SelectPath 接受 3 种不同的 Object 来设置当前目录。可以是用于选择的文件夹的 ShellItem,可以是目录路径的 string(也可以是 "我的文档\我的音乐"),或者可以是 SpecialFolders 枚举的值。

类概述

主类

这些是提供实际控件的类。

浏览器 实际的文件浏览器控件
BrowserTreeViewBrowserListViewBrowserComboBox Browser 中使用的控件
BrowserTreeSorterBrowserListSorter 用于对 TreeNodesListViewItems 进行排序的类
BrowserComboItem BrowserComboBox 提供项

这些类非常简单,不需要太多解释。要将我的控件用于您的项目,您实际上只需要使用 Browser 类。只需将此控件添加到窗体中,一切都应该正常工作。有关更多信息,请查看我代码中的注释。不幸的是,目前我的代码注释不多,但我会尽快添加更多注释。

Shell 类

这些类提供对 Shell 函数的简单访问。

ShellAPI 包括 Windows API 导入、常量、结构和枚举
ShellBrowser 用于检索表示文件系统的 ShellItems
ShellItem 表示一个文件系统对象、文件夹或文件(可以是虚拟文件夹)
ShellImageList 检索 Shell ImageList 并使其可供 Browser 使用
PIDL 围绕 PIDL 结构指针构建,用于标识文件系统对象

ShellAPIShellImageList 非常类似于我之前提到的 Jim Parsells 项目中的类。它们分别类似于 ShellDllSystemImageListManager。有关这些类的更多信息,请先尝试他的文章。ShellItem 来自他的 CShItem 类,但我已完全重写了它,以满足我的需求。我不会详细介绍这个类,但如果很多人确实需要更多关于它的信息,我可能会写一篇文章。

包装器类

这些类提供了用于控件的拖放操作和 ContextMenu 的包装器。

BrowserTVContextMenuWrapperBrowserLVContextMenuWrapper

TreeViewListView 提供 ContextMenu

ContextMenuHelper 负责执行 Shell ContextMenu 命令
BrowserTVDropWrapperBrowserLVDropWrapper TreeViewListView 提供放置操作
BrowserTVDragWrapperBrowserLVDragWrapper TreeViewListView 提供拖动操作

这些类是最重要的,也是我将在本文其余部分解释的。

Shell 右键菜单

检索它

当您尝试寻找一篇关于如何在程序中获取 Shell ContextMenu 的好文章时,您会注意到的第一件事是,几乎所有的文章都是关于制作菜单的扩展,而不是关于为自己的程序检索它。幸运的是,我找到了一篇对此进行过非常彻底解释的博客:如何托管 IContextMenu。它全是用 C++ 写的,所以我不得不将其翻译成 C#。由于这是一篇包含 11 部分的文章,我将在这里尝试从 C# 的角度解释一切。我将假设您熟悉 Shell 命名空间和 pidls,因为在这里解释它们需要花费很多时间,而且本文旨在涵盖 ContextMenu。因此,如果您不熟悉这些术语,请先查找有关这些内容的文章。我在本文开头提到的那篇(C# 实用 Shell)是我所需要的一切。

在解释显示 ContextMenu 的过程之前,我将简要描述我们将要使用的接口

IShellFolder 用于管理文件夹,所有 Shell 命名空间文件夹对象都公开它
IContextMenu Shell 调用它来创建或合并与 Shell 对象关联的快捷菜单
IContextMenu2 当菜单包含所有者绘制的菜单项时,用于创建或合并与特定对象关联的快捷菜单
IContextMenu3 当菜单实现需要处理 WM_MENUCHAR 消息时,用于创建或合并与特定对象关联的快捷菜单

现在让我们开始处理 ContextMenu 的事情。要检索您想要的菜单,您需要一些东西

  • 父目录的 IShellFolder 接口
  • 您想要获取 ContextMenu 的项目的 pidls(相对于父目录)
  • 同一项目的 IContextMenu

获取 IShellFolder 接口不需要太多操作。ShellItem 类为每个目录提供了 IShellFolder,所以您只需要获取父目录的 ShellItem 类,然后您就可以获得 IShellFolder 接口。pidls 也可以从 ShellItem 类中检索。在我的控件中,每个 TreeNodeListViewItem 都在其 Tag 属性中拥有自己的 ShellItem,因此获取所需的 pidls 也非常容易。完成这些之后,您就有了一切,可以获取 IContextMenu 接口了。IShellFolder 接口有一个方法,可以为它的子项提供许多不同的接口,这些接口包括 IContextMenu。我们需要像下面这个例子一样调用 IShellFolderGetUIObjectOf 方法。

public static bool GetIContextMenu(
          IShellFolder parent,
          IntPtr[] pidls,
          out IntPtr icontextMenuPtr,
          out IContextMenu iContextMenu)
{
    if (parent.GetUIObjectOf(
                IntPtr.Zero,
                (uint)pidls.Length,
                pidls,
                ref ShellAPI.IID_IContextMenu,
                IntPtr.Zero,
                out icontextMenuPtr) == ShellAPI.S_OK)
    {
        iContextMenu =
            (IContextMenu)Marshal.GetTypedObjectForIUnknown(
                icontextMenuPtr, typeof(IContextMenu));

        return true;
    }
    else
    {
        icontextMenuPtr = IntPtr.Zero;
        iContextMenu = null;

        return false;
    }
}

如您所见,您需要一个 IntPtr 数组。此数组包含要为其检索 IContextMenu 的项目的 pidls。数量可以是任意的,在我们的程序中,这个数量取决于选择了多少项目。使用 GetUIObjectOf,您将获得一个指向 IContextMenu 的指针,要获得实际接口,您需要使用 Marshal 类。

现在我们需要一个 ContextMenu,因为我们只调用 Windows API 方法,我们只需要一个 ContextMenuHandle。要以 Windows API 的方式创建新的 ContextMenu,我们只需要调用 ShellAPI.CreatePopupMenu(),它将返回一个新的 ContextMenu 的指针。现在,您可以调用 IContextMenu 接口的 QueryContextMenu 方法来添加所有 Shell ContextMenu 的菜单项。

contextMenu = ShellAPI.CreatePopupMenu();
  
iContextMenu.QueryContextMenu(
  contextMenu,
  0,
  ShellAPI.CMD_FIRST,
  ShellAPI.CMD_LAST,
  ShellAPI.CMF.EXPLORE |
  ShellAPI.CMF.CANRENAME |
  ((Control.ModifierKeys & Keys.Shift) != 0 ? 
    ShellAPI.CMF.EXTENDEDVERBS : 0));

调用选定的命令

现在 contextMenu 指针指向我们需要的 ContextMenu。调用此方法后,您可以以任何您想要的方式更改菜单。要更改菜单,您可以使用 ShellAPI 类中的 API 函数 AppendMenuInsertMenu。之后,就该向用户显示我们的菜单了。我们通过调用 ShellAPI.TrackPopupMenuEx 来做到这一点。此方法将等待用户选择一个项,并返回所选项的 id。这个 id 不仅仅是列表中的项的索引,它是一个特殊的 id。要执行与所选项相关的命令,我们需要一个 CMINVOKECOMMANDINFOEX 结构。我们可以使用它调用 IContextMenu 中的 InvokeCommand 方法来执行所选命令。有关此结构的更多信息,请参阅 MSDN

ShellAPI.CMINVOKECOMMANDINFOEX invoke = 
        new ShellAPI.CMINVOKECOMMANDINFOEX();
invoke.cbSize = ShellAPI.cbInvokeCommand;
invoke.lpVerb = (IntPtr)cmd;
invoke.lpDirectory = parentDir;
invoke.lpVerbW = (IntPtr)cmd;
invoke.lpDirectoryW = parentDir;
invoke.fMask = ShellAPI.CMIC.UNICODE | ShellAPI.CMIC.PTINVOKE |
    ((Control.ModifierKeys & Keys.Control) != 0 ? ShellAPI.CMIC.CONTROL_DOWN : 0) |
    ((Control.ModifierKeys & Keys.Shift) != 0 ? ShellAPI.CMIC.SHIFT_DOWN : 0);
invoke.ptInvoke = new ShellAPI.POINT(ptInvoke.X, ptInvoke.Y);
invoke.nShow = ShellAPI.SW.SHOWNORMAL;

iContextMenu.InvokeCommand(ref invoke);

在上面的示例中,cmd 变量是选定的索引。我们所需要做的就是将其转换为指针,Shell 函数就知道如何处理它。如您所见,我还包含了一些关于 ModfierKeys 的代码。您可能知道,当您使用 Windows Explorer 删除文件时,有两种方法:将其移至回收站,或永久删除。当您只按删除键时,所选文件将被移至回收站,但当您按住 Shift 并按删除键时,文件将被永久删除。这就是为什么您必须将 ModifierKeys 添加到结构中。

另一个需要注意的地方是,我们向结构添加了一个 POINT。这个 POINT 代表您单击鼠标右键的屏幕位置。您是否注意到,当您在 Windows Explorer 的 ContextMenu 中单击属性时,属性窗口会显示在您单击鼠标右键的位置?是的,它会的,要在您的程序中获得相同效果,您必须设置这个 POINT

"打开方式" 和 "发送到" 子菜单

当这一切都奏效后,我非常高兴,但很快就发现了一些奇怪的事情。当您选择 "打开方式" 或 "发送到" 子菜单时,您看不到其中的其他菜单项。既然我们也希望这些菜单起作用,我们就需要获取更多接口。IContextMenu 有两个子类,它们对于使菜单正常工作是必需的:IContextMenu2IContextMenu3。要获取这些接口,我们只需像这样使用 Marshal 类。

Marshal.QueryInterface(
    icontextMenuPtr, ref ShellAPI.IContextMenu2_IID, out context2Ptr);
    
Marshal.QueryInterface(
    icontextMenuPtr, ref ShellAPI.IContextMenu3_IID, out context3Ptr);
    
iContextMenu2 =
    (IContextMenu2)
        Marshal.GetTypedObjectForIUnknown(context2Ptr, typeof(IContextMenu2));

iContextMenu3 =
    (IContextMenu3)
        Marshal.GetTypedObjectForIUnknown(context3Ptr, 
        typeof(IContextMenu3));

这些接口将为我们绘制菜单,但它们需要知道何时进行绘制。为此,我们需要重写 WndProc 方法并检查发送给它的消息。当这些消息与创建、测量或绘制 ContextMenu 项有关时,我们将分别调用 IContextMenu2IContextMenu3 接口的 HandleMenuMsgHandleMenuMsg2 方法,这些方法将完成其余的必要工作。

正如您在 MSDN 上看到的,IContextMenu2 接口将处理 WM_INITMENUPOPUPWM_MEASUREITEMWM_DRAWITEM 消息,而 IContextMenu3 接口将处理 WM_MENUCHAR 消息。因此,当您在显示 ContextMenu 时遇到这些消息之一,请调用 HandleMenuMsgHandleMenuMsg2 方法来处理特定消息。

protected override void WndProc(ref Message m)
{
    if (iContextMenu2 != null &&
        (m.Msg == (int)ShellAPI.WM.INITMENUPOPUP ||
         m.Msg == (int)ShellAPI.WM.MEASUREITEM ||
         m.Msg == (int)ShellAPI.WM.DRAWITEM))
    {
        if (iContextMenu2.HandleMenuMsg(
            (uint)m.Msg, m.WParam, m.LParam) == ShellAPI.S_OK)
            return;
    }
    
    if (iContextMenu3 != null &&
        m.Msg == (int)ShellAPI.WM.MENUCHAR)
    {
        if (iContextMenu3.HandleMenuMsg2(
            (uint)m.Msg, m.WParam, m.LParam, IntPtr.Zero) == ShellAPI.S_OK)
            return;
    }
    
    base.WmdProc(ref Message m);
}

一旦您实现了这一点,您就会发现子菜单现在也能正常工作了。

这是使 ContextMenu 起作用的主要思路。我的程序还像 Windows Explorer 一样,在 TreeNodeContextMenu 上添加了 "折叠" 和 "展开" MenuItem。当鼠标悬停在菜单项上时,它还会为显示 ContextMenuItem 的帮助 String 触发一个事件。如果您需要知道如何做到这一点,请检查我的代码。

拖放支持

在我开始为我的程序实现拖放支持之前,我阅读了 Jim Parsells 的这篇文章:为 Explorer 树控件添加拖放,以及 Michael Dunn 的另一篇文章:如何实现您的程序与 Explorer 之间的拖放。这些文章让我走上了正确的道路,也让我觉得更有挑战性。在 Jim Parsells 的文章结尾,他提到了用他那种方式实现时遇到的一些问题。我认为我通过不使用 .NET 拖放方法来实现它,解决了这些问题。所以,忘记 .NET 所有漂亮的实现吧,我们将使用 Windows API 来实现。

拖放支持需要三个新接口

IDropTarget 包含在任何可以成为拖放操作数据目标的应用程序中使用的函数
IDropSource 包含用于向最终用户生成视觉反馈以及取消或完成拖放操作的函数
IDataObject Clipboard 类和拖放操作使用,用于存储被拖动对象的数据

幸运的是,我们有 Shell 命名空间

Shell 命名空间的优点是它会为您处理所有繁琐的工作,唯一的问题是如何让 Shell 完成它的工作。一旦您开始使用 Shell 并对它越来越熟悉,这一切就会变得容易得多,您会很快找到解决问题的方法。一旦我掌握了 ContextMenu 的诀窍,实现拖放对我来说实际上是一项相当容易的任务。

将项目拖放到您的控件上

要注册您的程序以进行放置操作,您需要一个实现了 IDropTarget 接口的类。在我的程序中,BrowserTVDropWrapperBrowserLVDropWrapper 都是 IDropTargets。在我们的类中引发必要的事件之前,我们需要注册它们。您可以通过调用 ShellAPI.RegisterDragDrop 方法来实现这一点,该方法接受两个参数。一个参数是注册拖动操作的控件的句柄,另一个参数是要接收有关拖动消息的 IDropTarget。当您的程序不再使用它时,您还需要使用 ShellAPI.RevokeDragDrop 方法撤销您的注册。一旦您注册了 IDropTarget,您的类将收到 4 个需要更多注意的消息。

第一个消息是 DragEnter,当有人将一个对象拖入您的控件时会调用它。您将收到一个指向被拖动的 IDataObject 的指针、修改键和鼠标按钮的当前状态、鼠标指针的位置以及 DragDropEffects 枚举的一个实例的引用。这有很多信息,但我们不必全部使用它。Shell 为我们提供了一个特定 Shell 对象的 IDropTarget,它将完成所有工作。我们唯一需要做的就是检查哪个项目正在被拖动,获取该项目的 IDropTarget,然后将所有信息传递给该接口。要从一个项目获取 IDropTarget,我们必须再次调用父 IShellFolder 接口的 GetUIObjectOf 方法(就像 IContextMenu 接口一样)。因此,代码中的基本思想如下。

private ShellDll.IDropTarget GetIDropTarget(ShellItem item, 
                             out IntPtr dropTargetPtr)
{
    ShellItem parent = item.ParentItem != null ? item.ParentItem : item;

    if (parent.ShellFolder.GetUIObjectOf(
            IntPtr.Zero,
            1,
            new IntPtr[] { item.PIDLRel.Ptr },
            ref ShellAPI.IID_IDropTarget,
            IntPtr.Zero,
            out dropTargetPtr) == ShellAPI.S_OK)
    {
        ShellDll.IDropTarget target =
            (ShellDll.IDropTarget)Marshal.GetTypedObjectForIUnknown(
                dropTargetPtr, typeof(ShellDll.IDropTarget));

        return target;
    }
    else
    {
        dropTargetPtr = IntPtr.Zero;
        return null;
    }
}

public int DragEnter(
    IntPtr pDataObj, 
    ShellAPI.MK grfKeyState, 
    ShellAPI.POINT pt, 
    ref DragDropEffects pdwEffect)
{
    Point point = br.FolderView.PointToClient(new Point(pt.x, pt.y));
    TreeViewHitTestInfo hitTest = br.FolderView.HitTest(point);

    dropNode = hitTest.Node;

    if (dropNode != null)
    {
        ShellItem item = (ShellItem)dropNode.Tag;
        parentDropItem = item;
    
        dropTarget = GetIDropTarget(item, out dropTargetPtr);
    
        if (dropTarget != null)
        {
            dropTarget.DragEnter(pDataObj, grfKeyState, pt, ref pdwEffect);
        }
    }
    
    return ShellAPI.S_OK;
}

调用 DragEnter 方法后,当拖动的项目在您的控件上时,DragOver 方法会被调用很多次。这样,您可以提供关于拖动项目可以放置或不能放置的具体信息。与 DragEnter 方法一样,我们可以再次让 Shell 完成所有繁琐的工作。

现在剩下两个方法,要么拖动操作在您的控件上被取消,要么它成功并且项目被放置在您的控件上。对于操作被取消的情况,有 DragLeave 方法。没有提供额外信息,只是通知拖动已在您的控件上结束。现在没有什么需要做的了,除了准备好您的类以接收另一个拖动操作。

如果放置成功,DragDrop 方法将被调用,并提供与 DragEnterDragOver 方法几乎相同的信息。唯一的区别在于需要采取一些行动。同样,这个行动将由 Windows Shell 执行。当我们调用之前检索到的 IDropTargetDragDrop 方法时,Shell 会为我们完成所有工作。当您使用 Windows Explorer 时,会显示所有相同的通知和进程窗口。

好吧,放置操作差不多就是这样了。在我的类中,我还做了一些额外的工作来让它们看起来更漂亮。这包括选择您正在拖动对象的节点,并显示 Windows Explorer 绘制的您正在拖动的内容的漂亮虚影图像。但这些东西并不是让它全部工作的必需品。

从您的控件拖动项目

放置部分完成后,就可以开始实现拖动操作了。这部分与 VB Explorer 略有不同。在 Jim Parsell 的 Explorer 中,他使用一个特殊的类来创建被拖动项目的 IDataObject。他通过使用 .NET 的 IDataObject 接口以 .NET 的方式进行,但实际上您除了传递它之外,并不需要对 IDataObject 接口做任何事情。也就是说,如果您以 Shell 的方式进行,这在我看来比 .NET 的方式容易得多。

因为我们将使用 API 方法拖动项目,所以我们需要一个 IDropSource 接口。这个接口将负责拖动时的任何绘制或取消操作。BrowserTVDragWrapperBrowserLVDragWrapper 类实现了这个接口,它们将确保支持拖动。

我们需要做的第一件事是在一个项目被拖动时收到通知。TreeViewListView 都有一个用于此的事件(ItemDrag),所以我们只需注册它。一旦拖动初始化,我们就需要调用一个 API 方法来注册包装器为 IDropSource 并触发拖动。要调用的方法是 ShellAPI.DoDragDrop,它有两个输入参数和一个输出。两个输入参数是被拖动项目的 IDataObject 和一个 DragDropEffects 枚举的实例,用于告诉该方法哪些拖放效果是允许的。输出参数也是一个 DragDropEffects 枚举的实例,指定了已执行的效果。

DragDropEffects 很容易提供,但 IDataObject 需要更多工作。幸运的是,我们已经两次看到了获取这个接口的流程。我们可以再次使用 GetUIObjectOf 方法(这个方法确实非常有用)。请注意,当您拖动多个项目时,ItemDrag 事件只会引发一次,所以您必须检查选择了哪些项目以获取正确的 IDataObject

public ShellDll.IDataObject GetIDataObject(ShellItem[] items, 
                            out IntPtr dataObjectPtr)
{
    ShellItem parent = 
        items[0].ParentItem != null ? items[0].ParentItem : items[0];

    IntPtr[] pidls = new IntPtr[items.Length];
    for (int i = 0; i < items.Length; i++)
        pidls[i] = items[i].PIDLRel.Ptr;

    if (parent.ShellFolder.GetUIObjectOf(
            IntPtr.Zero,
            (uint)pidls.Length,
            pidls,
            ref ShellAPI.IID_IDataObject,
            IntPtr.Zero,
            out dataObjectPtr) == ShellAPI.S_OK)
    {
        ShellDll.IDataObject dataObj =
            (ShellDll.IDataObject)
                Marshal.GetTypedObjectForIUnknown(
                    dataObjectPtr, typeof(ShellDll.IDataObject));

        return dataObj;
    }
    else
    {
        dataObjectPtr = IntPtr.Zero;
        return null;
    }
}

一旦拖动初始化,您的 IDropSource 接口将收到两条与拖动相关的消息。第一条是 QueryContinueDrag,它询问如何处理某种情况:执行放置、取消放置或继续拖动。您会收到一些信息来确定该怎么做。您会得到一个布尔值,表示是否按下了 Escape 键,如果是,则必须取消操作。您还会得到修改键和鼠标按钮的状态。在这里,您必须检查是继续拖动还是执行放置。如果用于初始化拖动的鼠标按钮仍然按下,则继续拖动,否则执行放置。然后,该方法将如下所示。

public int QueryContinueDrag(bool fEscapePressed, ShellAPI.MK grfKeyState)
{
    if (fEscapePressed)
        return ShellAPI.DRAGDROP_S_CANCEL;
    else
    {
        if ((startButton & MouseButtons.Left) != 0 && 
            (grfKeyState & ShellAPI.MK.LBUTTON) == 0)
            return ShellAPI.DRAGDROP_S_DROP;
        else if ((startButton & MouseButtons.Right) != 0 && 
                 (grfKeyState & ShellAPI.MK.RBUTTON) == 0)
            return ShellAPI.DRAGDROP_S_DROP;
        else
            return ShellAPI.S_OK;
    }
}

您的接口将收到的另一条消息是 GiveFeedback。您将获得当前适用于被拖动对象的 DragDropEffect。这条消息允许您更改 Cursor 以匹配这个特定的 DragDropEffect。因为我们只需要普通的 Cursor,所以这个方法只有一行代码。Shell 为我们提供了一个选项,即只使用标准的 Cursor 进行拖放操作,这正是我们想要的。所以这个方法看起来会是这样。

public int GiveFeedback(DragDropEffects dwEffect)
{
    return ShellAPI.DRAGDROP_S_USEDEFAULTCURSORS;
}

好了,我们完成了。这就是拖动操作需要做的所有事情。我没有谈论我控件的浏览部分,只是因为这会花费太多时间,并使本文过于冗长。如果有人真的想知道,我可能会再写一篇文章。

插件

什么样的插件

随着 1.3 更新,我添加了为程序添加插件的选项。这些插件用于获取有关文件和文件夹的额外信息。目前我有两个不同的插件,还有一个正在制作中。前两个插件中的一个是一个插件,用于检索 ListView 的详细信息视图的额外列。没有插件,您只有 "名称" 列,这对于详细信息视图来说太少了。在演示项目中,我添加了一个这样的插件,它添加了 "大小"、"创建日期" 和 "修改日期" 列。第二个插件更高级一些,它是一个用于 ListView 的特殊视图。在演示项目中,我添加了一个这样的演示插件,它将在 ListView 的视图选项中添加 "图像视图"。如果您选择此视图,当您选择图像时,您将看到图像的预览。请参见下图以更好地了解我的意思。

Sample image

如何制作它们

要制作您自己的插件,您需要创建一个项目并引用 FileBrowser.dll。完成此操作后,我提到的插件有两个接口。一个是 IColumnPlugin,另一个是 IViewPlugin。您需要创建一个公共类来实现其中一个或两个接口。完成之后,将您的项目构建为类库,这将创建一个 DLL 文件。将此 dll 文件添加到您启动程序所在文件夹中的一个名为 "plugins" 的文件夹中,然后启动您的程序。现在应该已经加载了您的插件,您就可以使用它了。有关演示项目,请参见上面可下载的插件演示项目。

所有方法的作用

但在您构建自己的插件之前,显然需要知道两个接口的每个方法的作用以及何时调用它们。所以我将简要解释它们的作用。两个接口都实现了基本的 IBrowserPlugin 接口,我将首先解释它。

  • IBrowserPlugin

    名称 插件的名称。
    Info(信息) 插件的简要描述

    这些属性目前没有使用,但我将来会用它们来列出已加载的插件,并允许用户选择要使用的插件。

  • IColumnPlugin

    ColumnNames 一个数组,包含此插件提供的所有列的名称
    GetAlignment 返回特定列的 HorizontalAlignment
    GetFolderInfo 返回文件夹特定列的信息
    GetFileInfo 返回文件特定列的信息

    当当前目录更改时,会调用 GetFolderInfoGetFileInfo 方法,它们返回将放入插件列的信息。调用此方法时,插件将收到两个参数。如果项目是目录,则为 IDirInfoProvider,如果项目是文件,则为 IFileInfoProvider。这些接口将提供一个包含文件或文件夹信息的结构,对于文件,它还将提供一个指向该文件的 Stream。第二个参数是要提供信息的特定项目的 ShellItem。通过这两个参数,插件应该检索所需的信息并为列提供一个 string。要更好地了解可能性,请参见演示项目。

  • IViewPlugin

    ViewName 在选择 ListView 视图选项时显示的名称
    ViewControl 选择该视图时将显示的 Control
    FolderSelected 当选择一个文件夹时将被调用
    FileSelected 当选择一个文件时将被调用
    Reset 当打开新目录时将被调用

    ViewControl 可以是您想要的任何东西,所以您可以制作各种各样的视图插件。只需确保在插件的构造函数被调用时初始化 Control,否则您将遇到跨线程问题。当选择一个项目时,会调用 FolderSelectedFileSelected 方法,它们与 GetFolderInfoGetFileInfo 方法具有相同的参数。在我的演示插件中,我使用从 IFileInfoProvider 获得的 Stream 来读取一张图片并在 Control 上显示它。

如果您需要有关插件的任何其他信息,请在下方发布消息,我会尽快回复。在下一次更新中,我希望包含第三个插件,您可以使用它将命令添加到 ContextMenu。现在您可以尝试这两个插件。

致谢

在编写我的程序时,我在网上使用了许多资源,因为关于这个主题写了很多内容,所以我无法为您提供所有为我的工作做出贡献的网站,但我将为您提供主要文章,这些是我无法完成的文章。

改进

我的程序肯定还有改进的空间。我需要做的第一件事是为我的代码添加更多的注释,因为几乎没有注释,然后需要完善一些主要内容。

  • 从我的控件拖动时制作一个漂亮的拖动图像
  • 提高浏览包含大量对象的文件夹时的速度
  • ListView 的标准 ContextMenu 添加更多菜单项
  • 实现撤销和重做功能
  • 可能还有很多我现在想不起来的事情,任何来自读者的其他想法都欢迎提出。

历史

2006年8月23日:V1.3.3

  • SHNotifyRegisterSHNotifyDeregister 中添加了入口点,以防止调用它们时出现任何问题

2006年8月22日:V1.3.2

  • 更新了 PIDL 类,添加了 IL 函数
  • 添加了仅用于浏览文件夹的 TreeView 的演示项目

2006年8月21日:V1.3

  • Browser 添加了插件功能
  • 使用双 pane 浏览器更新了演示项目
  • 为插件添加了演示项目
  • 向导航栏添加了后退和前进按钮
  • Browser 添加了新属性和公共方法
  • ListViewItem 添加了工具提示(目前仅限文件)
  • 为创建新文件夹添加了快捷方式(Ctrl + N)
  • 导航栏输入地址时的错误修复
  • 放置操作的错误修复
  • 创建了 ShellHelper 类,其中包含经常使用的方法
  • 改进了 Browser 的启动
  • 其他小的错误修复
  • 用 "使用控件" 和 "插件" 部分更新了文章

2006年8月14日:V1.2

  • ListViewContextMenu 添加了 "新建" 菜单
  • 更新线程的改进
  • 小的错误修复

2006年8月11日:V1.1

  • 修复了更新线程中的内存泄漏
  • 添加了 ShellNotify 函数
  • 在浏览包含大量项目的文件夹时,速度大幅提升
  • 添加了媒体未插入时的对话框
  • 导航栏错误修复
  • 添加了重命名功能
  • 重写了 PIDL 类以使用 Shell32 导入的方法
  • ListView ContextMenu 添加了 "粘贴" 和 "粘贴快捷方式" 菜单
  • 向详细信息视图添加了 "名称" 列
  • 其他小的错误修复

2006年8月5日:V1.0

  • 本文的初始版本
  • 程序的初始版本
© . All rights reserved.