使用 IShellFolder 重写 DirectoryInfo






4.86/5 (17投票s)
本文介绍 DirectoryInfoEx 如何使用 IShellFolder 来使用 C# 列出特殊/虚拟目录。
引言
DirectoryInfo
是一个表示磁盘上文件夹的类,它适用于列出文件系统条目,但不能用于表示特殊文件夹(例如,不存在于磁盘上的虚拟文件夹)。您可能需要使用 IShellFolder
来枚举这些目录。DirectoryInfoEx
是为了支持这些文件夹而编写的。
该项目基于 Steven Roebert 的 C# 文件浏览器代码和文章进行重写。他的代码比我的项目做得更多,他的文章强调了如何充分利用 shell,一旦您有了完整的实现(例如,上下文菜单、shell 拖放、预览处理程序等),但缺乏关于如何创建一个的文档。我重写了他代码的一部分(核心部分)来学习它是如何工作的,本文将解释如何在 C# 中使用 IShellFolder
接口执行基本的文件操作。
由于我知识的不足,以及 DirectoryInfo
/ FileInfo
的性质,新的类比 CShellItem
简单得多。
如何使用
基本上,它与 System.IO.DirectoryInfo
的接口相同,它仅支持 x86/x64 桌面应用程序
(不支持 WinRT)
您可以通过new DirectoryInfoEx() 来构建一个条目,支持的参数包括:
- Path - 磁盘路径(例如 C:\)或 Shell guid 路径(例如 Desktop = ::{00021400-0000-0000-C000-000000000046})
- CSIDL - 用于标识特殊目录,已废弃,因为它是不可更改的。
- Environment.SpecialFolder - CSIDL 的 .Net 枚举,已废弃。
- KnownFolder - 已知文件夹标识符,可由软件供应商更改。
- KnownFolderIds - DirectoryInfoEx 的 KnownFolderIds 枚举。
要获取子项,DirectoryInfoEx
包含用于所有文件系统条目的 GetFileSystemInfos()
、GetFileSystemInfosAsync()
和 EnumerateFileSystemInfos()
方法,以及仅用于目录或文件的 Get/EnumerateDirectories/Files(Async) 方法。
FileInfoEx
支持 Open()
来打开和/或写入 FileStreamEx
,这是一个 Stream
,其他辅助方法包括 OpenText()
、OpenRead()
和 AppendText()
。
对于基于静态的操作,有 DirectoryEx
和 FileEx
静态类,并且由于路径(例如 GUID)的变化,请使用 PathEx
代替。
目录
- 获取 PIDL
IShellFolder
接口IStorage
接口IStream
接口- 我的
DirectoryInfoEx
实现 - 演示
获取 PIDL
Shell 命名空间中的文件夹和文件可以通过 PIDL
(ITEMIDLIST
)定位,就像文件夹路径(或 DisplayName
)一样,有相对 PIDL
和完整 PIDL
,就像相对路径(abc.txt)和完整路径(c:\abc.txt)一样。
要从字符串路径获取 PIDL
,您可以使用 Desktop 的 IShellFolder.
ParseDisplayName()(见下文)。
ShellAPI.SFGAO pdwAttributes = 0;
DesktopShellFolder.ParseDisplayName(IntPtr.Zero, IntPtr.Zero,
path, ref pchEaten, out pidlPtr, ref pdwAttributes);
PIDL pidl = new PIDL(pidlPtr, false);
从 CSIDL 获取 PIDL
对于特殊目录(例如,DRIVES 等虚拟目录,或 PROFILE 等特殊文件系统目录),CSIDL 枚举中有一个列表,您可以使用 SHGetSpecialFolderLocation() 来获取其 PIDL。
int RetVal = ShellAPI.SHGetSpecialFolderLocation(IntPtr.Zero, csidl, out ptrAddr);
if (ptrAddr != IntPtr.Zero)
{
pidl = new PIDL(ptrAddr, false);
return pidl;
}
从 KnownFolder 获取 PIDL
由于新增了目录,基于 int 的 CSIDL 在 Windows Vista 中被 IKnownFolder 取代,
public interface IKnownFolder
{
void GetId(...); //Return Guid of KnownFolder, see here.
void GetCategory(...); //Return Fixed, Virtual, Common and PerUser
//Get IShellItem which replaced PIDL,
//but Current version DirectoryInfoEx (1.0.27) still uses PIDL.
void GetShellItem(..., Guid interfaceGuid, out object shellItem);
//Get PIDL, so this is used instead.
int GetIDList(...,out IntPtr itemIdentifierListPointer);
//Get and SetPath
void GetPath(...);
void SetPath(...);
//Return a definition structure with more information of that KnownFolder.
void GetFolderDefinition(...);
...
}
IKnownFolderManager 可以将 IKnownFolder 与路径、pidl、csidl 相互转换。
public interface IKnownFolderManager
{
void FolderIdFromCsidl(int Csidl, out Guid knownFolderID);
void FolderIdToCsidl(Guid id, out int Csidl);
void GetFolderIds(out IntPtr folders, out UInt32 count);
void GetFolder(Guid id, IKnownFolder knownFolder);
void GetFolderByName(string canonicalName, out IKnownFolder knowFolder);
void FindFolderFromPath(string path,KnownFolderFindMode mode, out IKnownFolder knownFolder);
void FindFolderFromIDList(IntPtr pidl, out IKnownFolder knownFolder);
...
}
DirectoryInfoEx 基于 pidl,因此如果使用 IKnownFolder 或 KnownFolderIds 构建,它将使用 IKnownFolder.GetIDList() 方法来获取 PIDL 作为条目的 ID。
KnownFolderIds 是一个手动准备的枚举,包含在 此页面(最高至 Windows 8.1)上找到的所有 200 个 KnownFolders。因此,您可以使用以下方式初始化 DirectoryInfoEx:
string[] txtFiles = new DirectoryInfoEx(KnownFolderIds.DocumentsLibrary)
.EnumerateFiles("*.txt", SearchOption.TopDirectoryOnly)
.Select(fi => fi.FullName)
.ToArray();
获取 IShellFolder 接口
Desktop 是所有 shell 命名空间文件夹的根,您可以使用 SHGetDesktopFolder() 来获取 **Desktop** 的 IShellFolder
接口。
IntPtr ptrShellFolder = IntPtr.Zero;
if (ShellAPI.SHGetDesktopFolder(out ptrShellFolder) == ShellAPI.S_OK)
iShellFolder = (IShellFolder)Marshal.GetTypedObjectForIUnknown(ptrShellFolder,
typeof(IShellFolder));
至于其他目录(包括非文件目录,例如 **MyComputer**),您可以使用 BindToObject()。
if (Parent.ShellFolder.BindToObject(pidl.Ptr, IntPtr.Zero,
ref ShellAPI.IID_IShellFolder, out ptrShellFolder) == ShellAPI.S_OK)
iShellFolder = (IShellFolder)Marshal.GetTypedObjectForIUnknown(ptrShellFolder,
typeof(IShellFolder));
IShell Interface
它包含管理文件夹的方法,例如:
- 获取子项列表(子文件夹/子文件),例如:
static ShellAPI.SHCONTF folderflag = ShellAPI.SHCONTF.FOLDERS | ShellAPI.SHCONTF.INCLUDEHIDDEN; /* Specify to include folders only */ if (iShellFolder.EnumObjects(IntPtr.Zero, folderflag, out ptrEnum) == ShellAPI.S_OK) //return the pointer of IEnumIDList { IEnumIDList IEnum = (IEnumIDList)Marshal.GetTypedObjectForIUnknown(ptrEnum, typeof(IEnumIDList)); IntPtr pidlSubItem; int celtFetched; while (IEnum.Next(1, out pidlSubItem, out celtFetched) == ShellAPI.S_OK && celtFetched == 1) dirList.Add(new PIDL(pidlSubItem, false)); /* Add PIDL of each subdirectory to dirList, noted that in normal case you * should release pidlSubItem but Steven Roebert's PIDL class is a IDisposable * class which will dispose it for you. * Also noted that the pidl is relative instead of full. * Use GetDisplayNameOf() (see below) to get it's name. */ if (IEnum != null) //Release resource { Marshal.ReleaseComObject(IEnum); Marshal.Release(ptrEnum); } }
- 使用 GetDisplayNameOf() 将
PIDL
转换回可读路径。IntPtr ptrStr = Marshal.AllocCoTaskMem(ShellAPI.MAX_PATH * 2 + 4); Marshal.WriteInt32(ptrStr, 0, 0); StringBuilder buf = new StringBuilder(ShellAPI.MAX_PATH); try { /* uflags is a SHGNO enum that allow you to get different folder names, * e.g. "My Documents"(SHGNO.NORMAL) folder is named as * "Documents"(SHGNO.FORPARSING) in file system. * StrRetToBuf() convert STRRET structure to * buffer usable by StringBuilder */ if (iShellFolder.GetDisplayNameOf(pidl, uFlags, ptrStr) == ShellAPI.S_OK) ShellAPI.StrRetToBuf(ptrStr, pidl, buf, ShellAPI.MAX_PATH); } finally { if (ptrStr != IntPtr.Zero) Marshal.FreeCoTaskMem(ptrStr); ptrStr = IntPtr.Zero; } Console.WriteLine(buf.ToString());
- 检索子文件夹的
IShellFolder
/IStorage
接口。使用 SHBindToParent():
IntPtr pidlLast = IntPtr.Zero; retVal = ShellAPI.SHBindToParent(dir.PIDLRel.Ptr, ShellAPI.IID_IStorage, out storagePtr, ref pidlLast);
retVal = dir.Parent.ShellFolder.BindToStorage( dir.PIDLRel.Ptr, IntPtr.Zero, ref ShellAPI.IID_IStorage, out storagePtr); /* Beside IID_IStorage interface, there is IID_IStream and IID_IPropertySetStorage as well. */ if ((retVal == ShellAPI.S_OK)) { IStorage storage = (IStorage)Marshal.GetTypedObjectForIUnknown(storagePtr, typeof(IStorage)); /* Your work here, free the pointer and interface when done. */ }
IStorage Interface
它包含用于在文件夹中创建或管理项/子项的方法,例如:
- 重命名、移动或复制文件,使用
PIDL
作为参数。例如:SrcStorage.MoveElementTo(SourceFileName, DestStorage, DestFilename, ShellAPI.STGMOVE.MOVE) != ShellAPI.S_OK)
- 删除文件
ParentStorage.DestroyElement(name);
- 读/写文件内容
/* FileStreamEx class is a customized IDisposable Stream class, * which uses IStream interface of a file. */ FileStreamEx stream = new FileStreamEx(path, mode, access); StreamReader sr = new StreamReader(stream); Console.WriteLine(sr.ReadToEnd());
IStream Interface
它允许您读/写数据到流对象。
- 要获取
IStream
接口,请使用 OpenStream()(读/写)或 CreateStream()(创建新)。if (parentStorage.OpenStream(filename, IntPtr.Zero, grfmode, 0, out streamPtr) == ShellAPI.S_OK) stream = (IStream)Marshal.GetTypedObjectForIUnknown(streamPtr, typeof(IStream));
IStream
包含seek()
、read()
、write()
等方法。- 如果流对象被释放,则认为它已关闭。
所有接口对象在使用完毕后都应被释放。
我的 DirectoryInfoEx 实现
该实现比 CShellItem
简单,但是,由于 FileSystemInfoEx
的 PIDL 被公开(通过 PIDLRel
和 PIDL
属性),您可以实现自定义操作(例如,提取图标、上下文菜单)。IShellFolder
和 IStorage
(按需生成)也在 DirectoryInfoEx
中公开,它们在处置时会自动销毁,请勿自行释放它们。
DirectoryInfoEx
和 FileInfoEx
都继承自 FileSystemInfoEx
,它们用于枚举(列出)子项,DirectoryInfoEx
包含 GetFiles()
和 GetDirectories()
方法用于此目的。要修改文件,请使用 FileEx
类来管理文件(DirectoryEx
尚未实现,目前请使用 System.IO.Directory
),并使用 FileStreamEx
类来读写文件。
DirectoryInfoEx
(和 FileInfoEx
)的构造函数接受 Path 或 PIDL
。DirectoryInfoEx
中定义了许多特殊目录,包括 DesktopDirectory
、MyComputerDirectory
、CurrentUserDirectory
、SharedDirectory
和 NetworkDirectory
。对于其他特殊目录,您可以通过调用 DirectoryInfoEx.CSIDLtoPIDL()
来获取其 PIDL
。
以及一个演示
此演示是一个简单的 WPF 应用程序,它列出了桌面下的子目录。图标是使用 SHGetFileInfo() 获取的,该函数接受一个完整的 PIDL
参数。
1.0.27 - 此演示已删除,目前请查看 FileExplorer。
问题
- 需要花费时间来实现
IShellItem
来替换ItemIDList
(PIDL)。
参考文献
- C# 文件浏览器(Steven Roebert)
- VB Explorer 树(Jim Parsells)
- 对:简单 WPF Explorer Tree 的过度反应(Karl on WPF)
- MSDN
历史记录
- 09-08-23 版本 0.2
- 更新了演示。
- 09-11-01 版本 0.3
- 演示不再加载网络内容,编辑转换器以禁用此更改。
- 添加了
DirectoryEx
(静态类)。 - PIDL 类现在是
IDisposable
并且自动释放。还添加了新的内部类ShellFolder
和Storage
,它们执行相同的功能。 - 性能得到改进,不再从桌面目录构造。(见上文)
DirectoryInfoEx
和FileInfoEx
现在可序列化。
- 09-11-01 版本 0.4
- 修复了缓存未生效的问题。
- 09-11-04 版本 0.5
- 即使指定的路径不存在,DirectoryInfoEx/FileInfoEx 也能正常工作(Exists == false,您必须在尝试使用它之前调用
Create()
或Refresh()
)。 - 添加了
Refresh()
、Create()
、MoveTo()
、Delete()
、CreateSubdirectory()
Open()
和相关的实例方法。 - 构造函数支持环境变量(例如 %temp%)
- 测试项目。
- 即使指定的路径不存在,DirectoryInfoEx/FileInfoEx 也能正常工作(Exists == false,您必须在尝试使用它之前调用
- 09-11-07 版本 0.6
- 添加了上下文菜单支持(ContextMenuWrapper)。
- 更新了演示(上下文菜单)。
- 添加了
FileSystemWatcherEx
类。 - 修复了由
EnumFiles()
(由GetFiles()
和GetFileSystemInfos()
使用)创建的FileInfoEx
返回错误的父目录。
- 09-11-08 版本 0.7
- 修复了所有
FileInfoEx
的根目录等于 c:\Users\{User}\Desktop 而不是 GUID 的问题。 - 更新了演示(上下文菜单多选)。
- 修复了所有
- 09-11-16 版本 0.8
- 修复了在同一目录中无法重命名项的问题。
- 修复了
ContextMenuWrapper
在弹出时未返回OnHover
消息的问题。 - 添加了
QueryMenuItemsEventArgs.Command
,以便正确返回用户查询项。 - 更新了演示(添加了状态栏)。
- 09-12-06 版本 0.9
- 修复了
DirectoryInfoEx.EmuFiles
中的小拼写错误(if (iShellFolder != null)
)。 - 修复了
DirectoryEx.Copy
未递归复制目录的问题(它目前只复制一个空文件夹)。 - 修复了
DIrectoryEx.Move
(以及可能的 FileEx)无法正常工作的问题。 - 修复了
DirectoryInfoEx.Delete()
中的错误运算符 (new),应为 override。
- 修复了
- 10-01-04 版本 0.10
- 修复了
FileSystemInfoEx.getParentIShellFolder()
方法在 Desktop 目录中直接项的 pidl 生成 ArgumentException 的问题(由_desktopShellFolder.BindToObject({Desktop's PIDL},...);
引起)。 - 修复了
FileSystemInfoEx.Delete()
调用时返回NotImplementException
的问题。 - 修复了
DirectoryEx/FileEx.Exists
未检查它是目录/文件的问题。 - 修复了
FileSystemInfoEx.refresh()
方法未更新属性的问题。 - 在
FileSystemInfoEx
、DirectoryInfoEx
和FileInfoEx
类中实现了IClonable
接口。 - 向
ContextMenuWrapper
类添加了BeforeInvoke
事件。 - 添加了双击文件列表时的运行行为。
- 添加了
FileSystemWatcherEx.Filter
。
- 修复了
- 10-01-20 版本 0.11
- 添加:演示中的
DirectoryTree
;现在可以正确刷新更改。(在GetDirectoriesConverterEx
类中使用FileSystemWatcherEx
类实现了一个ObservableCollection
。) - 添加:
ContextMenuWrapper.OnQueryMenuItems.QueryContextMenu2 / QueryContextMenu3
属性。 - 添加:
ContextMenuWrapper.OnBeforePopup
事件。 - 添加:
ContextMenuWrapper.OnQueryMenuItems
事件;现在支持多级菜单(例如:@"Tools\Add")。 - 添加:
ContextMenuWrapper.OnQueryMenuItems
事件;现在支持 GrayedItems / HiddenItems。
- 添加:演示中的
- 10-02-15 版本 0.12
- 修复:桌面下的用户/共享目录的全名现在是其 GUID 而不是其文件路径。
- 修复:PIDL、PIDLRel、ShellFolder、Storage 属性按需生成,以避免跨线程问题。
- 添加:
PathEx
类,用于处理 PIDL 相关的路径。
- 10-03-14 版本 0.13
- 修复:
FileSystemWaterEx
忽略了删除目录事件。 - 修复:删除了 PIDL 中的
IDisposable
,因为它导致了AccessViolationException
,用户必须通过调用Free()
方法来释放。
- 修复:
- 10-03-16 版本 0.14
- 修复:
FileSystemInfoEx
现在存储 PIDL/Rel 的副本,在调用属性时返回其副本(以避免AccessViolation
)。 - 修复:
FileSystemInfoEx
在构造时记录 PIDL,因为某些路径无法解析(例如,EntireNetwork)。 - 添加:允许文件夹列表,以便列出非文件祖先目录(例如,回收站)。
- 修复:
- 10-03-18 版本 0.15
- 修复:在几个位置未释放 ShellFolder/PIDL。
- (请注意,PIDL/ShellFolder/Store 不再存储在
FileSystemInfoEx
中 => 用户必须自行释放。)
- 10-03-19 版本 0.16
- 添加:
IShellFolder2
接口和ShellFolder2
类。 - 添加:
ExtraPropertiesProvider
,它可以列出额外的文件属性/列表列(例如,ExtraPropertiesProvider.GetProperty(file, ref ImageSummaryInformation.BitDepth);
)。 - 修复:
getRelPIDL()
在使用字符串构造的File/DirInfoEx
中无法返回正确的值。(尝试返回一个已释放的指针。) - 修复:在两个位置未释放
ShellFolder
。
- 添加:
- 2010-04-26 版本 0.17
- 在
DirectoryInfoEx
中添加了this
运算符。 - 在
BeforePopup
中添加了DefaultItem
和DefaultCommand
。 - 添加了
WorkSpawner
,它可以生成ListWork
、CopyWork
、MoveWork
和DeleteWork
来执行响应式的多线程操作。 - 修复了一些 XP 系统无法创建共享目录的问题。(由 cwharmon)
- 删除了
DirectoryInfoEx
文件/目录的列表缓存(_cachedFileList),因为它在文件过多时会变慢(以及旧的 EnumDirs/EnumFiles 实现)。 - 添加了
DirectoryInfoEx/DirectoryEx.EnumerateFiles/EnumerateDirectories/EnumerateFileSystemInfos
() 方法,其功能类似于 .Net4 中的方法(添加了 CancelDelegate 以使其可取消)。 - 添加了 FileEx.
ReadLines
/ReadAllLines
() 方法。 - 添加了 IOTools.
CopyFile
() 方法,支持取消。 - 添加了
FileSystemInfoEx.FromString
() 方法。
- 在
- 2010-05-25 版本 0.18
- 现在可以使用 WPF 文件浏览器用户控件(
DirectoryTree
和FileList
)。 - 修复了
DirectoryInfoEx.EnumerateDirectories
在列出网络目录时返回文件的错误。 - 添加了
ExComparer
类,它允许对FileSystemInfoEx
条目数组进行排序。 - 修复了
DriveInfoEx
返回错误的TotalSize
。DriveInfoEx
构造函数现在接受完整的驱动器名称("C" 和 "C:\" 都接受)。 - (已移除)添加了一个检查以忽略未枚举的项,这样
DirectoryInfoEx.EnumerateDirectories
就不会返回某些系统目录(例如 C:\MSOCache)。 - 添加了 IOTools.
GetRelativePath
。记录了 IOTools 的静态方法。 - 添加了 IOTools.
ShellFolderToCSIDL
() 和CSIDLToShellFolder
() 静态方法。 - 添加了
DirectoryInfoEx
的构造函数,它支持Environment.ShellFolder
。 - 修复了 DirectoryInfoEx.
EnumerateFiles
忽略 SearchPattern 的问题。 - 修复了在某些情况下上下文菜单消失的问题。(由 cwharmon)
- 更新了
DirectoryInfoEx
/FileInfoEx
列表代码以提高速度。 - 为所有 Work、WorkBase 添加了进度对话框。
IsProgressDialogEnabled
和ProgressDialog
。
- 现在可以使用 WPF 文件浏览器用户控件(
- 2010-07-17 版本 0.19
- 添加了
FileTypeInfoProvider
- 修复了获取 C:\{User}\Desktop 的存储时出现的
ArgumentException
。
- 添加了
- 2010-08-22 版本 0.20
- 修复了
ShellProgressDialog
关闭后仍然运行的问题。 - 在
ExtraPropertiesProvider
中添加了LinkSummaryInformation
。
- 修复了
- 2010-11-04 版本 0.21
- 对
CustomMenuStructure
类进行了小更新。 - 修复了 FileSystemInfoEx.
getRelativePIDL
() 和getParentPIDL
() 返回的relPIDL
不是克隆,如果尝试释放将导致崩溃。(例如,在 DirectoryEx.Exists
() 中)。
- 对
- 2011-03-01 版本 0.22
- 添加了
ImageExtractor
,它使用IExtractImage
来生成缩略图。 - 添加了
PreviewHelper
和PreviewControl
,它们是IPreviewHandler
。 - 修复了 Library.ms 目录下的目录的非法 PIDL。
- 添加了
- 2013-12-16 版本 0.24
- 将 FileSystemInfoEx.PIDL/PidlRel 的访问权限更改为扩展方法(
RequestPIDL
/RelPIDL
)。 - 修复了列表时的内存泄漏。
- 将 FileSystemInfoEx.PIDL/PidlRel 的访问权限更改为扩展方法(
- 2013-12-24 版本 0.25
- 修复了 OpenWithInfo.
GetExecutablePath
()
- 修复了 OpenWithInfo.
- 2014-11-10 版本 1.0.26
- 修复了与 x64 模式相关的两个崩溃。
- 添加了 DirectoryInfoEx.ShellFolderType (
Environment.SpecialFolder
)。
- 2014-11-19 版本 1.0.27
- 添加了 DirectoryInfoEx.
GetFileSystemInfosAsync
()、GetDirectoriesAsync
() 和GetFilesAsync
() 异步方法。 - 添加了 KnownFolder 支持(请参阅 MSDN)。
- 添加了 DirectoryInfoEx.