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

CsConsoleFormat:简介

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2018 年 3 月 2 日

CPOL

4分钟阅读

viewsIcon

17386

.NET 的 CsConsoleFormat 库 - 使用现代技术在控制台中进行格式化

引言

.NET Framework 仅包含非常基础的控制台格式化功能。如果你需要输出几个字符串,那没问题。如果你想输出表格,你必须手动计算列宽,常常是硬编码。如果你想给输出着色,你必须在写入字符串时穿插设置和恢复颜色。如果你想正确地进行单词换行或结合以上所有功能……

代码很快就会变成一团糟,难以阅读。这根本不好玩!在 GUI 中,我们有 MV*、绑定和各种酷炫的东西。编写控制台应用程序感觉像是回到了石器时代。

CsConsoleFormat 来拯救你!

示例

想象一下你通常的 OrderOrderItemCustomer 类。让我们创建一个打印 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

  1. 使用包管理器安装 NuGet 包 Alba.CsConsoleFormat

    PM> Install-Package Alba.CsConsoleFormat

    或 .NET CLI

    > dotnet add package Alba.CsConsoleFormat
  2. 在你的 .cs 文件中添加 using Alba.CsConsoleFormat;

  3. 如果你打算在 Windows 上使用 ASCII 图形,请设置 Console.OutputEncoding = Encoding.UTF8;

  4. 如果你想使用 XAML

    1. 将 XAML 文件添加到你的项目中。将其生成操作设置为“嵌入的资源”。
    2. 使用 ConsoleRenderer.ReadDocumentFromResource 加载 XAML。
  5. 如果你想使用纯 C#

    1. 在代码中构建一个文档,以 Document 元素作为根节点。
  6. 在生成的文档上调用 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 方法而不是构造函数参数:原始值被转换为 stringnull 被忽略,枚举(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 方法会使代码更加冗长。XElements 没有这个问题,因为它们完全通过 params object[] 参数构建,这些参数可以包含其子元素和“属性”(XML术语中的属性)。

我目前使用的是第三种选择,它结合了这些方法,但这是一种折衷,既不简洁也不一致。你更喜欢哪种语法?

历史

  • 1.0 — 第一个版本

许可证

  • 库 — Apache 许可证 2.0。文章 — CC-BY 4.0
© . All rights reserved.