向TreeView添加文件夹结构需要数小时

为什么这段代码需要数小时才能完成:

public void SetRoot(string path) { Tag = path; BeginUpdate(); AddFolderRecursive(Nodes, path); EndUpdate(); } private void AddFolderRecursive(TreeNodeCollection nodes, string path) { try { var dirs = Directory.EnumerateDirectories(path).OrderBy(d => d).Select(d => d.Split(Path.DirectorySeparatorChar).Last()); TreeNode node; ShellFileGetInfo.FolderIcons fi; foreach (var d in dirs) { node = nodes.Add(Path.Combine(path, d), d, ImageList.Images.Count); node.Tag = Path.Combine(path, d); node.SelectedImageIndex = ImageList.Images.Count + 1; fi = ShellFileGetInfo.GetFolderIcon((string)node.Tag, false); // ImageList.Images.Add(fi.closed); // ImageList.Images.Add(fi.open); AddFolderRecursive(node.Nodes, (string)node.Tag); } } catch (UnauthorizedAccessException) { } } 

我已经让这段代码运行了14个小时,并且在调用SetRoot( @"c:\" );时调用它仍然没有完成获取所有文件夹的列表SetRoot( @"c:\" ); 。 代码正在运行,它正在添加到树中,但这只是荒谬的。

124,157个文件夹

基本上,我想在我的驱动器上使用所有文件夹来浏览树视图(因此树视图是可搜索的)并使用实际的文件夹图标(我使用SHGetFileInfo p / invoke和必要的参数来执行此操作)。 即使没有获得图标(我有另一个问题是,文件夹的图标在文件夹的基础上是唯一的,即使图标图像本身可能是相同的。我无法确定 – 很快 – 如果我已经将图像保存在我的TreeView的ImageList – 也就是说,’c:\ windows’的文件夹图标与’c:\ windows \ system32’相同,但句柄等都返回不同的信息,所以那里似乎没有什么可以继续唯一索引它们。)

在我的代码中可以调整什么来使这个过程更快,同时仍保留系统中的文件夹图标? 请记住,我也想要所有文件夹而不跳过空文件夹

(我甚至无法显示TreeView的图片,因为我在完成显示之前的14小时后停止了循环运行)。

为了测试TreeView控件的速度,我编写了以下代码:

 DateTime start = DateTime.UtcNow; treeView1.BeginUpdate(); await Task.Run(() => { int x = 0; while (x < 100000) { x++; if (treeView1.InvokeRequired) { treeView1.Invoke((MethodInvoker)delegate { treeView1.Nodes.Add("Node - " + x); } ); } } }); treeView1.EndUpdate(); Text = start.ToLongTimeString() + " - " + DateTime.UtcNow.ToLongTimeString(); 

以下是结果的屏幕截图:

TreeView中有100,000个项目

如您所见,如果您使用BeginUpdateEndUpdate来阻止控件在每个项目上绘制或刷新,则在大约2分钟内快速填充TreeView以及100,000个项目。 这也表明它绝对不是阻止我的TreeView控件–14小时过度 – 即使从1996年驱动器,14个小时来枚举100,000个文件夹,也太长了。

Windows树视图控件根本不是为了容纳这么多节点而设计的。 您无法在实际时间内使用数千个节点填充此控件。 此外,即使在短时间内列举所有这些项目也是不切实际的。 更尴尬的是试图提前为树中的每个对象提取图标。

前进的方法是不要尝试使用所有项目填充控件。 只需填充父节点。 然后当它们打开枚举并添加孩子。 这就是所有shell程序的运行方式。

经过进一步研究,我发现问题在于可用于枚举文件和文件夹的方法非常慢,并且当文件夹不可访问时,会抛出UnauthorizedAccessException ,每次事件的延迟大约为200ms。 这些exception会叠加并导致大的延迟。

此外,关于在向TreeView添加项目时的二次指数的Davids语句导致进一步延迟也是如此,但是在这种情况下,附加延迟仅与TreeView节点添加的缩放不成比例。

为了解决这个问题,我能够将其缩小到3个问题,其中两个我已经完全解决了,因此控制function的这些部分在合理的时间范围内。 打破它,这是导致OP问题延迟的3个问题:

  • TreeView节点添加在树越深时呈指数级增长,并且添加的节点越多。
  • 文件系统访问不访问可用于NTFS的本机日记系统,因此每个文件或目录都是每次调用单独获取的。 此外,如果文件夹被标记为受限制,则UnauthorizedAccessException在每次遭遇时施加大约200ms的人为延迟。
  • 检索自定义文件夹图标会请求多个IO操作(具有各自的延迟),并且上述用于存储每个图标对的方法效率低,导致其自身的额外延迟,即使在此范围内较小,延迟也是相关的。

在缩小延迟范围后,我能够针对这些因素逐一减少它们。


加快获取文件夹列表

我必须做的第一件事是用更可行的东西替换文件系统访问方法 – 直接访问NTFS Journal系统,我可以通过从StCroixSkipper的USN Journal Explorer v1.3和VB中的MFT Scanner中获取一些代码来完成 .NET制作以下类NtfsUsnJournal.cs ,我把它放在pastebin上,因为这超出了我在StackOverflow上发布的内容。

此更改允许我在4秒内以递归方式检索C驱动器上的所有文件夹

注意:到目前为止,我还没有找到一种方法来访问日记,而不需要应用程序的提升(管理员)权限。 所有尝试在没有提升的情况下访问日记会导致拒绝访问exception。


提高大型嵌套节点集的TreeView性能

接下来,我需要提高TreeView的性能,以便在加载当前文件夹结构时添加超过100,000个嵌套节点。 为了做到这一点,花了一些谷歌,并修改了一些修改代码,以适应上述类的Usn格式。

结果是扩展TreeView的客户usercontrol的以下添加:

 #region TreeViewFast private readonly Dictionary _treeNodes = new Dictionary(); ///  /// Load the TreeView with items. ///  /// Item type /// Collection of items /// Function to parse Id value from item object /// Function to parse parentId value from item object /// Function to parse display name value from item object. This is used as node text. public void LoadItems(IEnumerable items, Func getId, Func getParentId, Func getDisplayName) { // Clear view and internal dictionary Nodes.Clear(); _treeNodes.Clear(); // Load internal dictionary with nodes foreach (var item in items) { var id = getId(item); var displayName = getDisplayName(item); var node = new TreeNode { Name = id.ToString(), Text = displayName, Tag = item }; _treeNodes.Add(getId(item), node); } // Create hierarchy and load into view foreach (var id in _treeNodes.Keys) { var node = GetNode(id); var obj = (T)node.Tag; var parentId = getParentId(obj); if (parentId.HasValue) { var parentNode = GetNode(parentId.Value); if(parentNode == null) { Nodes.Add(node); } else { parentNode.Nodes.Add(node); } } else { Nodes.Add(node); } } } ///  /// Get a handle to the object collection. /// This is convenient if you want to search the object collection. ///  public IQueryable GetItems() { return _treeNodes.Values.Select(x => (T)x.Tag).AsQueryable(); } ///  /// Retrieve TreeNode by Id. /// Useful when you want to select a specific node. ///  /// Item id public TreeNode GetNode(ulong id) { try { return _treeNodes[id]; } catch (KeyNotFoundException) { return null; } } ///  /// Retrieve item object by Id. /// Useful when you want to get hold of object for reading or further manipulating. ///  /// Item type /// Item id /// Item object public T GetItem(ulong id) { return (T)GetNode(id).Tag; } ///  /// Get parent item. /// Will return NULL if item is at top level. ///  /// Item type /// Item id /// Item object public T GetParent(ulong id) where T : class { var parentNode = GetNode(id).Parent; return parentNode == null ? null : (T)Parent.Tag; } ///  /// Retrieve descendants to specified item. ///  /// Item type /// Item id /// Number of generations to traverse down. 1 means only direct children. Null means no limit. /// List of item objects public List GetDescendants(ulong id, int? deepLimit = null) { var node = GetNode(id); var enumerator = node.Nodes.GetEnumerator(); var items = new List(); if (deepLimit.HasValue && deepLimit.Value <= 0) return items; while (enumerator.MoveNext()) { // Add child var childNode = (TreeNode)enumerator.Current; var childItem = (T)childNode.Tag; items.Add(childItem); // If requested add grandchildren recursively var childDeepLimit = deepLimit.HasValue ? deepLimit.Value - 1 : (int?)null; if (!deepLimit.HasValue || childDeepLimit > 0) { var childId = ulong.Parse(childNode.Name); var descendants = GetDescendants(childId, childDeepLimit); items.AddRange(descendants); } } return items; } #endregion 

为了使用,我创建了一个新方法,它充当一个简单的“加载器”,如下所示:

 public void PopulateTree(string path) { Tag = path; using (NtfsUsnJournal ntfs = new NtfsUsnJournal(new DriveInfo(path))) { List folders; ntfs.GetNtfsVolumeFolders(out folders); Func getId = (x => x.FileReferenceNumber); Func getParentId = (x => x.ParentFileReferenceNumber); Func getDisplayName = (x => x.Name); LoadItems(folders, getId, getParentId, getDisplayName); } } 

对此进行测试,现在只需6秒即可将所有100,000多个文件夹完全加载到TreeView中,并且用户体验即时扩展


自定义图标的文件夹

这是我目前最后关注的地方,我仍然在寻找一种方法来彻底改善这一点。

到目前为止我所做的是检查文件夹中是否存在desktop.ini ,如果存在, 调用SHGetFileInfo pinvoke获取自定义文件夹图标。 然后我将正在扩展的文件夹添加到内部列表,表明我已经检查了该文件夹并获得了任何关联的图标,这些图标都发生在OnBeforeExpand事件中。 虽然这些调用很便宜,但它仍然会给进程增加一个显着的延迟(扩展c:\ windows需要12秒)。

这是代码(也在自定义TreeView中)

 private List _expandedCache; protected override void OnBeforeExpand(TreeViewCancelEventArgs e) { if (!_expandedCache.Contains(e.Node.FullPath)) { BeginUpdate(); ShellFileGetInfo.FolderIcons fi; _expandedCache.Add(e.Node.FullPath); string curPath; foreach(TreeNode n in e.Node.Nodes) { curPath = Path.Combine((string)Tag, n.FullPath.Replace('/', Path.DirectorySeparatorChar)); if (File.Exists(Path.Combine(curPath, "desktop.ini")) == true) { fi = ShellFileGetInfo.GetFolderIcon(curPath, false); if(fi.closed != null || fi.open != null) { ImageList.Images.Add(fi.closed); ImageList.Images.Add(fi.open); n.SelectedImageIndex = ImageList.Images.Count - 1; n.ImageIndex = ImageList.Images.Count - 2; } } } EndUpdate(); } base.OnBeforeExpand(e); } 

这是最后一次主要的File.Exists() ,我认为有一种方法可以比传统的File.Exists()方法和SHGetFileInfo pinvoke更快地访问,以便以便宜的方式获取自定义文件夹图标

更新 :经过更多测试后,我能够将最后一个问题缩小到将图标添加到ImageList时。 每次将图像添加到连接到TreeView的ImageList时,都会刷新整个节点TreeView。 如果有人知道如何将所有这些工作放在一起同时保持高性能的图像,请告诉我。 或者,如果此内部刷新可以某种方式以不锁定UI的方式推送到后台。

我愿意打赌代码不是原因,它可能是两件事之一:

  1. 你的硬盘很乱,因为你可以尝试碎片方法。

  2. (也发生在我身上)你的文件夹没有被windows索引(索引 – https://en.m.wikipedia.org/wiki/Indexing_Service )来修复它你需要去你正在处理的主文件夹并询问Windows索引文件夹及其所有子文件夹(文件夹框中的某个位置),此过程将花费大约一天,但之后您的程序应该正常(并且快速)。