CsConsoleFormat:简介
.NET 的 CsConsoleFormat 库 - 使用现代技术在控制台中进行格式化
引言
.NET Framework 仅包含非常基础的控制台格式化功能。如果你需要输出几个字符串,那没问题。如果你想输出表格,你必须手动计算列宽,常常是硬编码。如果你想给输出着色,你必须在写入字符串时穿插设置和恢复颜色。如果你想正确地进行单词换行或结合以上所有功能……
代码很快就会变成一团糟,难以阅读。这根本不好玩!在 GUI 中,我们有 MV*、绑定和各种酷炫的东西。编写控制台应用程序感觉像是回到了石器时代。
CsConsoleFormat 来拯救你!
示例
想象一下你通常的 Order
、OrderItem
和 Customer
类。让我们创建一个打印 order
的文档。有两种语法,你可以选择任何一种。
XAML (类似于 WPF)
<Document xmlns="urn:alba:cs-console-format"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Span Background="Yellow" Text="Order #"/>
<Span Text="{Get OrderId}"/>
<Br/>
<Span Background="Yellow" Text="Customer: "/>
<Span Text="{Get Customer.Name}"/>
<Grid Color="Gray">
<Grid.Columns>
<Column Width="Auto"/>
<Column Width="*"/>
<Column Width="Auto"/>
</Grid.Columns>
<Cell Stroke="Single Wide" Color="White">Id</Cell>
<Cell Stroke="Single Wide" Color="White">Name</Cell>
<Cell Stroke="Single Wide" Color="White">Count</Cell>
<Repeater Items="{Get OrderItems}">
<Cell>
<Span Text="{Get Id}"/>
</Cell>
<Cell>
<Span Text="{Get Name}"/>
</Cell>
<Cell Align="Right">
<Span Text="{Get Count}"/>
</Cell>
</Repeater>
</Grid>
</Document>
// Assuming Order.xaml is stored as an Embedded Resource in the Views folder.
Document doc = ConsoleRenderer.ReadDocumentFromResource(GetType(), "Views.Order.xaml", Order);
ConsoleRenderer.RenderDocument(doc);
C# (类似于 LINQ to XML)
using static System.ConsoleColor;
var headerThickness = new LineThickness(LineWidth.Single, LineWidth.Wide);
var doc = new Document()
.AddChildren(
new Span("Order #") { Color = Yellow },
Order.Id,
"\n",
new Span("Customer: ") { Color = Yellow },
Order.Customer.Name,
new Grid { Color = Gray }
.AddColumns(
new Column { Width = GridLength.Auto },
new Column { Width = GridLength.Star(1) },
new Column { Width = GridLength.Auto }
)
.AddChildren(
new Cell { Stroke = headerThickness }
.AddChildren("Id"),
new Cell { Stroke = headerThickness }
.AddChildren("Name"),
new Cell { Stroke = headerThickness }
.AddChildren("Count"),
Order.OrderItems.Select(item => new[] {
new Cell()
.AddChildren(item.Id),
new Cell()
.AddChildren(item.Name),
new Cell { Align = HorizontalAlignment.Right }
.AddChildren(item.Count),
})
)
);
ConsoleRenderer.RenderDocument(doc);
特点
- 类 HTML 元素:段落、跨度、表格、列表、边框、分隔符
- 布局:网格、堆叠、对接、换行、绝对定位
- 文本格式化:前景色和背景色、字符换行、单词换行
- Unicode 格式化:连字符、软连字符、不间断连字符、空格、不间断空格、零宽度空格
- 多种语法(参见上面的示例)
- 类似于 WPF:支持 XAML,具有一次性绑定、资源、转换器、附加属性,可以从程序集资源加载文档
- 类似于 LINQ to XML:支持 C#,具有对象初始化器,通过扩展方法或索引器设置附加属性,通过折叠枚举并将对象和字符串转换为元素来添加子元素
- 绘图:使用框线字符绘制几何图形(线条、矩形),颜色转换(变暗、变亮),文本,图像
- 国际化:尊重各个级别的区域性,并且可以为每个元素自定义
- 导出到多种格式:ANSI 文本、无格式文本、HTML;RTF、XPF、WPF FixedDocument、WPF FlowDocument
- JetBrains ReSharper 注释:CanBeNull、NotNull、ValueProvider、Pure 等。
- WPF 文档控件,文档转换器。
Using the Code
-
使用包管理器安装 NuGet 包 Alba.CsConsoleFormat
PM> Install-Package Alba.CsConsoleFormat
或 .NET CLI
> dotnet add package Alba.CsConsoleFormat
-
在你的 .cs 文件中添加
using Alba.CsConsoleFormat;
。 -
如果你打算在 Windows 上使用 ASCII 图形,请设置
Console.OutputEncoding = Encoding.UTF8;
。 -
如果你想使用 XAML
- 将 XAML 文件添加到你的项目中。将其生成操作设置为“嵌入的资源”。
- 使用
ConsoleRenderer.ReadDocumentFromResource
加载 XAML。
-
如果你想使用纯 C#
- 在代码中构建一个文档,以
Document
元素作为根节点。
- 在代码中构建一个文档,以
-
在生成的文档上调用
ConsoleRenderer.RenderDocument
。
实际示例
该库的 GitHub 存储库中有一个名为 Alba.CsConsoleFormat.Sample.ProcessManager
的示例项目,它可以列出当前进程、启动新进程并显示帮助。项目的逻辑很简单,它使用 CommandLineParser
库来解析命令行,然后使用 System.Diagnostics.Process
类来执行其主要操作。
这是用于生成视图的代码:显示错误和信息性消息,以表格形式显示进程列表,显示帮助。Process
类包含进程信息,BaseOptionAttribute
类包含 CommandLineParser
中的单个动词或参数的信息。请注意,C# 6 的一些特性被使用,包括 using static
。
用于构建文档树的 API 类似于 LINQ to XML (System.Xml.Linq
),但使用 AddChildren
方法而不是构造函数参数:原始值被转换为 string
,null
被忽略,枚举(IEnumerable
)被折叠并插入其元素。
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using CommandLine;
using static System.ConsoleColor;
internal class View
{
private static readonly LineThickness StrokeHeader =
new LineThickness(LineWidth.None, LineWidth.Wide);
private static readonly LineThickness StrokeRight =
new LineThickness(LineWidth.None, LineWidth.None, LineWidth.Single, LineWidth.None);
public Document Error (string message, string extra = null) =>
new Document { Background = Black, Color = Gray }
.AddChildren(
new Span("Error\n") { Color = Red },
new Span(message) { Color = White },
extra != null ? $"\n\n{extra}" : null
);
public Document Info (string message) =>
new Document { Background = Black, Color = Gray }
.AddChildren(message);
public Document ProcessList (IEnumerable<Process> processes) =>
new Document { Background = Black, Color = Gray }
.AddChildren(
new Grid { Stroke = StrokeHeader, StrokeColor = DarkGray }
.AddColumns(
new Column { Width = GridLength.Auto },
new Column { Width = GridLength.Auto, MaxWidth = 20 },
new Column { Width = GridLength.Star(1) },
new Column { Width = GridLength.Auto }
)
.AddChildren(
new Cell { Stroke = StrokeHeader, Color = White }
.AddChildren("Id"),
new Cell { Stroke = StrokeHeader, Color = White }
.AddChildren("Name"),
new Cell { Stroke = StrokeHeader, Color = White }
.AddChildren("Main Window Title"),
new Cell { Stroke = StrokeHeader, Color = White }
.AddChildren("Private Memory"),
processes.Select(process => new[] {
new Cell { Stroke = StrokeRight }
.AddChildren(process.Id),
new Cell { Stroke = StrokeRight, Color = Yellow,
TextWrap = TextWrapping.NoWrap }
.AddChildren(process.ProcessName),
new Cell { Stroke = StrokeRight, Color = White,
TextWrap = TextWrapping.NoWrap }
.AddChildren(process.MainWindowTitle),
new Cell { Stroke = LineThickness.None, Align = HorizontalAlignment.Right }
.AddChildren(process.PrivateMemorySize64.ToString("n0")),
})
)
);
public Document HelpOptionsList (IEnumerable<BaseOptionAttribute> options, string instruction) =>
new Document { Background = Black, Color = Gray }
.AddChildren(
new Div { Color = White }
.AddChildren(instruction),
"",
new Grid { Stroke = LineThickness.None }
.AddColumns(GridLength.Auto, GridLength.Star(1))
.AddChildren(options.Select(OptionNameAndHelp))
);
public Document HelpAllOptionsList (ILookup<BaseOptionAttribute,
BaseOptionAttribute> verbsWithOptions, string instruction) =>
new Document { Background = Black, Color = Gray }
.AddChildren(
new Span($"{instruction}\n") { Color = White },
new Grid { Stroke = LineThickness.None }
.AddColumns(GridLength.Auto, GridLength.Star(1))
.AddChildren(
verbsWithOptions.Select(verbWithOptions => new object[] {
OptionNameAndHelp(verbWithOptions.Key),
new Grid { Stroke = LineThickness.None, Margin = new Thickness(4, 0, 0, 0) }
.Set(Grid.ColumnSpanProperty, 2)
.AddColumns(GridLength.Auto, GridLength.Star(1))
.AddChildren(verbWithOptions.Select(OptionNameAndHelp)),
})
)
);
private static object[] OptionNameAndHelp (BaseOptionAttribute option) => new[] {
new Div { Margin = new Thickness(1, 0, 1, 1), Color = Yellow, MinWidth = 14 }
.AddChildren(GetOptionSyntax(option)),
new Div { Margin = new Thickness(1, 0, 1, 1) }
.AddChildren(option.HelpText),
};
private static object GetOptionSyntax (BaseOptionAttribute option)
{
if (option is VerbOptionAttribute)
return option.LongName;
else if (option.ShortName != null) {
if (option.LongName != null)
return $"--{option.LongName}, -{option.ShortName}";
else
return $"-{option.ShortName}";
}
else if (option.LongName != null)
return $"--{option.LongName}";
else
return "";
}
}
结果看起来像这样
API 设计问题
我现在在 LINQ-to-XML 类型语法的可能 API 之间犹豫不决
new Document(new Div("Hello"))
— 所有元素的构造函数中的params object[]
参数new Document().AddChildren(new Div().AddChildren("Hello"))
— 所有元素的AddChildren
扩展方法中的params object[]
参数new Document().AddChildren(new Div("Hello"))
— 两者混合:大多数情况使用AddChildren
,但也为经常只包含文本的类的构造函数提供了string text
参数
我不愿使用带 params object[]
的构造函数的原因是,元素可以(并且经常)有设置其属性的初始化器,因此在设置属性之前添加子元素虽然可行,但会破坏逻辑顺序(new Div( ... 几行元素 ... ) { Color = Yellow }
)。一些类也可能拥有有意义的非默认构造函数。但是,仅使用 AddChildren
方法会使代码更加冗长。XElement
s 没有这个问题,因为它们完全通过 params object[]
参数构建,这些参数可以包含其子元素和“属性”(XML术语中的属性)。
我目前使用的是第三种选择,它结合了这些方法,但这是一种折衷,既不简洁也不一致。你更喜欢哪种语法?
历史
- 1.0 — 第一个版本
许可证
- 库 — Apache 许可证 2.0。文章 — CC-BY 4.0