该页面可以包含自动翻译的文本。

表格容器

使用 Table 容器来布局最复杂的数据。表格在 PDF 文档中起着关键作用,可提高文档的清晰度和效果。

表格为展示数据提供了结构化格式。它们可以让你以简洁且具有视觉吸引力的方式呈现复杂细节。与冗长的段落相比,你可以使用表格简明地展示事实、数字和趋势。

设计良好的表格能提升可读性。通过对齐列和行,你可以创建一种引导读者视线的结构。你可以使用背景、边框和字体样式来突出特定单元格或标题。

表格容器

通过调用 LayoutContainer.Table 方法获取表格容器。然后必须在容器中定义至少一列。在最简单的情况下,你可以通过多次调用 Cell 方法来填充所有列和行。

请继续阅读,了解如何定义列,以及如何为表格添加页眉和/或页脚。本文也介绍了 Table 容器的其他所有功能。

本文是关于用于 PDF 生成的 Layout API 系列的一部分。如果你刚接触该 API,请先阅读 Layout API 入门 部分。

列和行

每个表格都必须定义至少一列。使用 Table.Columns 方法定义列。该方法接受类型为 Action<TableColumnContainer> 的委托。在委托代码中,通过调用提供对象的方法向表格添加列。

ConstantColumn 方法添加一个宽度等于精确点数的列。

RelativeColumn 方法添加一个相对宽度的列。该方法接受列应占据的份数。总份数是本表中所有 RelativeColumn 调用所使用数字的总和。大多数情况下,该方法的工作方式与 Row containerRelativeItem 相同。请阅读链接以了解详细说明。

无需显式定义行。行数取决于列和单元格属性。单元格数量也会影响行数。表格的高度可以大于其所有行高度之和。每一行的单元格数量可以少于列数。列中的单元格数量也可以少于行数。

PdfDocumentBuilder.Create().Generate("table-basic.pdf", doc =>
{
    doc.Pages(page =>
    {
        page.Size(300, 100);
        page.Content()
            .Padding(10)

            //.MinimalBox()

            .Border(b => b.Thickness(1).Color(new PdfRgbColor(250, 123, 5)))
            .Table(table =>
            {
                table.Columns(columns =>
                {
                    columns.ConstantColumn(100);
                    columns.RelativeColumn(1);
                    columns.RelativeColumn(3);
                });

                for (int i = 0; i < 10; i++)
                {
                    table.Cell()
                        .Border(b => b.Thickness(0.1))
                        .AlignCenter()
                        .Text($"Cell {i + 1}");
                }
            });
    });
});

你可以在 table-basic.pdf 中查看代码结果。

在生成的 PDF 中,表格占据了父容器的全部空间。即使单元格数量不足以覆盖该区域。取消对 MinimalBox 调用的注释,可将表格高度限制为其各行高度之和。

代码定义了三列,并向表格添加了 10 个单元格。表格按顺序放置单元格,在需要时添加新行。只有一个单元格的行完全没有问题。

如果当前页面空间不足,表格会将单元格放到下一页。通过增加添加的单元格数量,你可以观察到这一点。

单元格

下面说明 Layout API 为处理表格单元格提供了什么。

位置

创建表格单元格有两种方式:

  • 不显式指定位置信息
  • 至少部分指定位置

我在上一节的代码中使用了第一种方式。你调用不接受参数的 Cell 方法重载。该方法会在上一个已添加单元格之后的列中添加一个新单元格。如果右侧没有空间,单元格会移到下一行。在 RTL 模式下,该方法会检查上一个已添加单元格左侧是否有空间。

如果你知道新单元格的确切位置,请使用 Cell 方法的另一个重载,即接受类型为 Action<TableCell> 的委托。在委托代码中,通过调用所提供对象的 RowIndexColumnIndex 方法来指定单元格的行和索引。

你也可以只为新单元格指定行索引或列索引。只指定行索引时,列索引为 0。反之亦然。

创建单元格时,可以以任意顺序混用这两种 Cell 重载调用。

在本文后面的代码示例中,我将使用下面的 扩展方法。这样可以让示例更简洁、更易读。

static class LayoutHelpers
{
    public static TextSpan BoxWithText(this LayoutContainer cell, string caption)
        => cell.Border(b => b.Thickness(0.5))
            .Background(new PdfGrayColor(75))
            .ShowOnce()
            .MinWidth(50)
            .MinHeight(50)
            .Padding(10)
            .AlignCenter()
            .AlignMiddle()
            .Text(caption);
}

下面的示例创建了一个同时包含显式和隐式放置单元格的表格。我使用了扩展方法为单元格应用样式和文本。单元格中的数字反映了单元格的创建顺序。

PdfDocumentBuilder.Create().Generate("table-cells.pdf", doc =>
{
    doc.Pages(page =>
    {
        page.Size(300, 200);
        page.Content()
            .Padding(10)
            .MinimalBox()
            .Border(b => b.Thickness(0.1))
            .Table(table =>
            {
                table.Columns(columns =>
                {
                    for (int i = 0; i < 4; i++)
                        columns.RelativeColumn();
                });

                table.Cell().BoxWithText("1");
                table.Cell(c => c.RowIndex(1).ColumnIndex(1)).BoxWithText("2");
                table.Cell().BoxWithText("3");
                table.Cell(c => c.RowIndex(2).ColumnIndex(2)).BoxWithText("4");
                table.Cell(c => c.RowIndex(0).ColumnIndex(3)).BoxWithText("5");
            });
    });
});

你可以在 table-cells.pdf 中查看代码结果。

行和列跨度

单元格可以跨越多行和/或多列。使用接受委托的 Cell 方法重载来指定单元格跨越的行数和列数。

PdfDocumentBuilder.Create().Generate("table-cellspans.pdf", doc =>
{
    doc.Pages(page =>
    {
        page.Size(300, 150);
        page.Content()
            .Padding(10)
            .MinimalBox()
            .Border(b => b.Thickness(0.1))
            .Table(table =>
            {
                table.Columns(columns =>
                {
                    for (int i = 0; i < 4; i++)
                        columns.RelativeColumn();
                });

                table.Cell(c => c.RowSpan(2).ColumnSpan(2)).BoxWithText("1");
                table.Cell(c => c.ColumnSpan(2)).BoxWithText("2");
                table.Cell().BoxWithText("3");
                table.Cell().BoxWithText("4");
            });
    });
});

你可以在 table-cellspans.pdf 中查看代码结果。

重叠

单元格可能会相互覆盖。当发生这种情况时,创建顺序决定哪个单元格显示在上层。

当单元格跨越多列或多行时,可能会出现一些重叠单元格。或者,当你同时显式和隐式地定位单元格时,也会出现这种情况。

PdfDocumentBuilder.Create().Generate("table-overlapping.pdf", doc =>
{
    doc.Pages(page =>
    {
        page.Size(300, 300);
        page.Content()
            .Padding(10)
            .MinimalBox()
            .Border(b => b.Thickness(0.1))
            .Table(table =>
            {
                table.Columns(columns =>
                {
                    for (int i = 0; i < 4; i++)
                        columns.RelativeColumn();
                });

                for (int i = 0; i < 16; i++)
                    table.Cell().BoxWithText($"{i + 1}");

                table.Cell(c => c.RowIndex(2).ColumnIndex(1).ColumnSpan(3))
                    .Background(new PdfRgbColor(250, 123, 5), 50);
            });
    });
});

你可以在 table-overlapping.pdf 中查看代码结果。

延伸到底部

如果表格布局较为复杂,分页机制可能会导致某些列底部出现空白。若要消除空白,请使用 ExtendCellsToBottom 方法。该方法会延伸每列中的最后一个单元格,使单元格在表格底部结束。

在下面的示例中,第一页展示默认行为。第二页展示 ExtendCellsToBottom 调用如何影响各列。

PdfDocumentBuilder.Create().Generate("table-extendcells.pdf", doc =>
{
    for (int take = 0; take < 2; take++)
    {
        doc.Pages(page =>
        {
            page.Size(300, 200);
            page.Content()
                .Padding(10)
                .MinimalBox()
                .Border(b => b.Thickness(0.1))
                .Table(table =>
                {
                    if (take != 0)
                        table.ExtendCellsToBottom();

                    table.Columns(columns =>
                    {
                        for (int i = 0; i < 4; i++)
                            columns.RelativeColumn();
                    });

                    table.Cell(c => c.RowIndex(0).ColumnIndex(0)).BoxWithText("1");
                    table.Cell(c => c.RowIndex(2).ColumnIndex(0)).BoxWithText("2");
                    table.Cell(c => c.RowIndex(1).ColumnIndex(1)).BoxWithText("3");
                    table.Cell(c => c.RowIndex(2).ColumnIndex(2)).BoxWithText("4");
                    table.Cell(c => c.RowIndex(1).ColumnIndex(3)).BoxWithText("5");
                });
        });
    }
});

你可以在 table-extendcells.pdf 中查看代码结果。

表格可以包含页眉和页脚。使用 HeaderFooter 方法访问并设置对应的 TableCellContainer。该容器提供 Cell 方法来创建单元格。这些方法与主表格内容的对应方法完全相同。

页眉、页脚和主表格单元格使用相同的列定义。但这三组单元格集合彼此独立,不会互相影响。

当表格无法放在一页中时,页眉和页脚会在表格占用的每一页上重复显示。

我在前面的示例中使用了 BoxWithText 扩展方法。接下来这段代码中还会使用 WhiteBoxWithText 扩展方法。

static class LayoutHelpers
{
    public static TextSpan WhiteBoxWithText(
        this LayoutContainer cell, string caption, bool center = true)
    {
        return cell.Border(b => b.Thickness(0.5))
            .ShowOnce()
            .MinWidth(50)
            .MinHeight(50)
            .Padding(10)
            .Container(l => center ? l.AlignCenter() : l)
            .AlignMiddle()
            .Text(caption);
    }
}

下面的示例展示了如何创建带页眉的表格。我使用一个包含五种关键化学元素的表格。每个元素都有名称,以及摄氏度和开尔文温度下的熔点。

var elements = new (string Name, double Celsius, double Kelvin)[] {
    ("Oxygen", -218.79, 54.36),
    ("Carbon", double.NaN, double.NaN),
    ("Hydrogen", -259.16, 13.99),
    ("Nitrogen", -209.86, 63.23),
    ("Sulfur", 115.21, 388.36),
};

static string formatDouble(double val)
{
    return double.IsNaN(val)
        ? string.Empty
        : val.ToString(CultureInfo.InvariantCulture);
}

PdfDocumentBuilder.Create().Generate("table-header.pdf", doc =>
{
    doc.Pages(page =>
    {
        page.Size(400, 500);
        page.Content()
            .Padding(10)
            .MinimalBox()
            .Border(b => b.Thickness(0.1))
            .Table(table =>
            {
                table.Columns(columns =>
                {
                    columns.RelativeColumn();
                    columns.ConstantColumn(100);
                    columns.ConstantColumn(100);
                });

                table.Header(header =>
                {
                    header.Cell(c => c.RowSpan(2)).BoxWithText("Chemical element");
                    header.Cell(c => c.ColumnSpan(2)).BoxWithText("Melting point");

                    header.Cell().BoxWithText("Celsius");
                    header.Cell().BoxWithText("Kelvin");
                });

                foreach (var (Name, Celsius, Kelvin) in elements)
                {
                    table.Cell().WhiteBoxWithText(Name, false);

                    table.Cell().WhiteBoxWithText(formatDouble(Celsius));
                    table.Cell().WhiteBoxWithText(formatDouble(Kelvin));
                }
            });
    });
});

你可以在 table-header.pdf 中查看代码结果。

示例代码

请花些时间查看 将表格添加到 PDF 文档 示例。它还展示了如何创建带页眉的表格,但没有使用任何扩展方法。并且还有一个 VB.NET 版本。