我正在写一个DirectoryTreeDrawer
类附带的库。它的主要目的是根据提供的目录路径或TextWriter
实例绘制树结构(到底层的DirectoryInfo
)。
以下是使用DirectoryTreeDrawer
类的.NET Core控制台应用程序示例:
public class Program
{
public static void Main()
{
using (var drawer = new DirectoryTreeDrawer(System.Console.Out))
{
var workingDirectoryPath = System.IO.Directory.GetCurrentDirectory();
drawer.DrawTree(workingDirectoryPath);
}
}
}
运行上面的命令会产生类似于下面几行的输出(为简洁而截断):
ConsoleDemo\
├────ConsoleDemo.csproj.nuget.cache
├────ConsoleDemo.csproj.nuget.g.props
├────ConsoleDemo.csproj.nuget.g.targets
├────project.assets.json
├──ConsoleDemo.csproj
├──Program.cs
DrawTree()
将PrintDirectoryContent()
称为递归魔法开始的地方。从提供的路径开始,程序以递归方式遍历子目录,并以反映原始目录的相对深度的方式打印文件和目录的名称。
public void DrawTree(DirectoryInfo directoryInfo)
{
var searchPattern = "*";
var searchOption = SearchOption.TopDirectoryOnly;
PrintDirectoryName(directoryInfo, depth: 0);
PrintDirectoryContent(directoryInfo, searchPattern, searchOption, depth: 0);
}
private void PrintDirectoryContent(DirectoryInfo currentDirectory, string searchPattern, SearchOption searchOption, int depth)
{
var directories = currentDirectory.GetDirectories(searchPattern, searchOption);
var directoriesCount = directories.GetLength(0);
for (var directoryIndex = 0; directoryIndex < directoriesCount; directoryIndex++)
{
var directoryInfo = directories[directoryIndex];
PrintDirectoryName(directoryInfo, depth + 1);
PrintDirectoryContent(directoryInfo, searchPattern, searchOption, depth + 1);
}
var files = currentDirectory.GetFiles(searchPattern, searchOption);
var filesCount = files.GetLength(0);
for (var fileIndex = 0; fileIndex < filesCount; fileIndex++)
{
var fileInfo = files[fileIndex];
PrintFileName(fileInfo, depth + 1);
}
}
文件(或目录)前缀由单个├
符号组成,后面分别重复─
符号到当前深度。
private void PrintDirectoryName(DirectoryInfo directoryInfo, int depth)
{
_textWriter.WriteLine($"{CreateDepthPrefix(depth)}{directoryInfo.Name}{Path.DirectorySeparatorChar}");
}
private void PrintFileName(FileInfo fileInfo, int depth)
{
_textWriter.WriteLine($"{CreateDepthPrefix(depth)}{fileInfo.Name}");
}
private string CreateDepthPrefix(int depth)
{
return $"{'├'}{new string('─', 2 * depth)}";
}
我想标记最后一个条目(文件或目录)的前缀不同于通常的前缀。而不是用├
标志开始前缀,我想用└
标志开始它。所以最后一行输出而不是:
├──Program.cs
......看起来像这样:
└──Program.cs
对我来说,问题是如何知道哪个文件或目录是最后一个要打印的文件或目录。如果我能够知道它,我可以在打印前缀时简单地运行检查。
有没有更好的解决方案,然后将所有条目(文件和目录名称和深度)保存到集合,然后执行检查“最后进入”条件?或者也许它是唯一的一个?
该库是开源的,可在GitLab上找到。在这里您还可以找到原始的DirectoryTreeDrawer
类。请注意,为了简洁起见,我对它进行了大量编辑。
我想说清楚,我不是要求代码审查,就像某人看起来一样。我正面临一个问题,我正在寻求解决方案。
我的简短回答是我在评论中写的,彼得已经提供了答案,但是这里有一种可以被认为更具可读性的替代格式,它为每个目录和文件提供缩进,这样就可以更容易地看到哪个父级它属于。有关示例输出,请参见最后一张图
这是通过跟踪父项中的最后一个文件或文件夹并将其传递给PrintItem
方法(这是我在您的问题的评论中提出的答案)来完成的。另一个变化是前缀从父传递到子传递,因此我们可以包含嵌套项的连接器。要跟踪“嵌套”的项目(意味着项目的父项具有显示在当前项目之后的兄弟项目),我们将IsNested
参数传递给PrintDirectory
方法,以便可以相应地更新前缀。
我还将它修改为static
类,它将TextWriter
传递给不同的方法。不确定这是否真的更好,但除了TextWriter
之外,其他一切看起来都应该是静态的。
public static class DirectoryTreeDrawer
{
public static void DrawTree(string directoryPath, TextWriter textWriter)
{
DrawTree(new DirectoryInfo(directoryPath), textWriter);
}
public static void DrawTree(DirectoryInfo directoryInfo, TextWriter textWriter)
{
PrintDirectory(directoryInfo, textWriter);
}
private static void PrintDirectory(DirectoryInfo directory, TextWriter textWriter,
string prefix = " ", string searchPattern = "*", SearchOption searchOption =
SearchOption.TopDirectoryOnly, bool isLast = true, bool isNested = false)
{
PrintItem(directory.Name, prefix, isLast, textWriter, true);
var subDirs = directory.GetDirectories(searchPattern, searchOption);
var files = directory.GetFiles(searchPattern, searchOption);
// If this is a "nested" directory, add the parent's connector to the prefix
prefix += isNested ? "│ " : " ";
for (var directoryIndex = 0; directoryIndex < subDirs.Length; directoryIndex++)
{
var isLastChild = directoryIndex == subDirs.Length - 1 && files.Length == 0;
// If the parent has files or other directories, mark this as "nested"
var isNestedDir = files.Length > 0 || !isLastChild;
PrintDirectory(subDirs[directoryIndex], textWriter, prefix, searchPattern,
searchOption, isLastChild, isNestedDir);
}
for (var fileIndex = 0; fileIndex < files.Length; fileIndex++)
{
var isLastFile = fileIndex == files.Length - 1;
PrintItem(files[fileIndex].Name, prefix, isLastFile, textWriter);
}
}
private static void PrintItem(string name, string prefix, bool isLastItem,
TextWriter textWriter, bool isDirectory = false)
{
var itemConnector = isLastItem ? "└─" : "├─";
var suffix = isDirectory ? Path.DirectorySeparatorChar.ToString() : "";
textWriter?.WriteLine($"{prefix}{itemConnector}{name}{suffix}");
}
}
用法
private static void Main()
{
DirectoryTreeDrawer.DrawTree(Environment.CurrentDirectory, Console.Out);
GetKeyFromUser("\nDone! Press any key to exit...");
}
产量
而且,从输出结果来看,很明显我多年来一直在重复使用同一个项目,它在Debug
文件夹中有一堆无关的文件...... :)
我会做什么,我会列出所有,然后更改光标位置,然后更改打印字符:
public void DrawTree(string directoryPath)
{
if (string.IsNullOrWhiteSpace(directoryPath))
{
throw new ArgumentException(
"Provided directory path is null, emtpy " +
"or consists of only whitespace characters.",
nameof(directoryPath));
}
DrawTree(new DirectoryInfo(directoryPath));
//remember current position because we need to return position to it
int currentCursorPositionTop = Console.CursorTop;
//set position to the last row
Console.SetCursorPosition(0, Console.CursorTop-1);
//change the first charachter
Console.Write("└");
//return cursor position to the previous one so our "Press any key to continue" can apear below our list.
Console.SetCursorPosition(0, currentCursorPositionTop);
}
更新:为了保持更高级别的抽象,我建议将数据写入List,然后只需更改最后一个的前缀:
class ContentItem {
public string Prefix {get;set; }
public int Depth {get;set; }
public string Name {get;set; }
public override string ToString() {
return $"{Prefix}{(new String("-", Depth))} {Name}";
}
}
因此,改变控制台光标位置,您可以更改列表项前缀:
var items[items.Count()-1].Prefix = "└";
然后你循环遍历项目并将它们传递给TextWriter,StreamWriter或其他任何东西。
问题是如何知道哪个文件或目录是最后一个要打印的文件或目录。如果我能够知道它,我可以在打印前缀时简单地运行检查。
鉴于你发布的代码,知道你是否在最后一个条目是微不足道的,因为你的递归方法有一个depth
参数,并且每个项目的循环都被编入索引。这意味着你只需要查看你的int
值,例如:
private void PrintDirectoryContent(DirectoryInfo currentDirectory, string searchPattern, SearchOption searchOption, int depth)
{
var directories = currentDirectory.GetDirectories(searchPattern, searchOption);
var directoriesCount = directories.GetLength(0);
for (var directoryIndex = 0; directoryIndex < directoriesCount; directoryIndex++)
{
var directoryInfo = directories[directoryIndex];
PrintDirectoryName(directoryInfo, depth + 1);
PrintDirectoryContent(directoryInfo, searchPattern, searchOption, depth + 1);
}
var files = currentDirectory.GetFiles(searchPattern, searchOption);
var filesCount = files.GetLength(0);
for (var fileIndex = 0; fileIndex < filesCount; fileIndex++)
{
var fileInfo = files[fileIndex];
PrintFileName(fileInfo, depth + 1, depth == 0 && fileIndex == filesCount - 1);
}
}
private void PrintDirectoryName(DirectoryInfo directoryInfo, int depth)
{
_textWriter.WriteLine($"{CreateDepthPrefix('├', depth)}{directoryInfo.Name}{Path.DirectorySeparatorChar}");
}
private void PrintFileName(FileInfo fileInfo, int depth, bool isLast)
{
_textWriter.WriteLine($"{CreateDepthPrefix(isLast ? '└' : '├', depth)}{fileInfo.Name}");
}
private string CreateDepthPrefix(char initialChar, int depth)
{
return $"{initialChar}{new string('─', 2 * depth)}";
}
即表达式depth == 0 && fileIndex == filesCount - 1
用作我添加到isLast
方法的PrintFileName()
参数的精确值。
请注意,缺少一个好的Minimal, Complete, and Verifiable example,我不得不对你发布的代码进行一些调整,以便编译和运行它。您发布的代码也不会产生您的问题所说的输出;具体来说,顶级目录名称也与'├'
字符结束。我想在你的实际代码中,你就是特殊情况。
我没有花时间试图让输出与你说的完全匹配,而是只关注手头的问题。我假设您可以根据实际拥有的代码调整上面的代码,其中包含该问题的答案。