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

容器的大小、位置和渲染

内容是文档中最重要的部分。这一点毋庸置疑。另一个关键部分是格式,它能创建清晰、专业且有效的沟通。格式正确的文档在视觉上更具吸引力、可读,并且易于导航。

你可能已经知道如何使用容器组织内容,以及如何为它们应用背景色。本文介绍如何为容器指定大小和位置。它还涵盖了条件渲染内容等高级功能,并介绍了对从右到左内容方向的支持。

容器定位

LayoutContainer 类提供了专业排布容器所需的一切。通过应用内边距和对齐,你可以创建一份给用户留下积极印象的文档。

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

大小

默认情况下,容器占据其内容所需的最小区域。换句话说,容器的大小等于其内容的固有大小。

图像的固有大小由图像文件本身的尺寸决定。文本片段的固有大小是覆盖该片段中所有字形的区域大小。

ColumnTable 这样的复合容器的大小取决于容器各部分的大小。

Width & Height

可以使用 WidthHeight 方法为容器指定精确的宽度和高度。这对占位容器非常方便。

精确大小也很适合图像。这是因为 Layout API 会根据其 ImageContentMode 将图像缩放到适合或填充容器。

对于复合容器和包含文本的容器,你需要谨慎使用精确大小。当无法将内容放入提供的大小时,会抛出 LayoutException

有些情况下,你只想对宽度或高度设置约束。可以使用 MinWidthMinHeightMaxWidthMaxHeight 方法设置这些约束。

请注意,当无法满足这些约束时,库会抛出 LayoutException

Extend

容器可以扩展自身以占用可用的最大空间。当你不知道确切大小或大小约束时,这会很有用。

当你只希望容器在水平方向占用所有可用空间时,使用 ExtendHorizontal 方法。当你只希望容器在垂直方向扩展时,使用 ExtendVertical 方法。Extend 方法会使容器在两个方向上占用所有可用空间。

var gray = new PdfGrayColor(75);
var text = "Content goes here";
var size = new PdfSize(150, 50);

PdfDocumentBuilder.Create().Generate("positioning-extend.pdf", doc =>
{
    for (int i = 0; i < 4; i++)
    {
        doc.Pages(page =>
        {
            page.Size(size);
            page.Content().Row(r =>
            {
                var container = r.AutoItem().Background(gray);
                switch (i)
                {
                    case 0:
                        container.Text(text);
                        break;

                    case 1:
                        container.ExtendHorizontal().Text(text);
                        break;

                    case 2:
                        container.ExtendVertical().Text(text);
                        break;

                    case 3:
                        container.Extend().Text(text);
                        break;
                }
            });
        });
    }
});

上述代码的结果见 positioning-extend.pdf。如你所见,四个页面中的每一页都在灰色背景上包含相同的文本,但容器在每一页上的大小不同。

MinimalBox

LayoutContainer 类提供了 MinimalBox 方法。它与 Extend 方法正好相反。MinimalBox 方法会生成嵌套容器,只为内容使用所需的最小空间。

var gray = new PdfGrayColor(75);
var size = new PdfSize(150, 50);

PdfDocumentBuilder.Create().Generate("positioning-minimalbox.pdf", doc =>
{
    doc.Pages(page =>
    {
        page.Size(size);
        page.Content().MinimalBox().Background(gray).Text("I don't want more space");
    });

    doc.Pages(page =>
    {
        page.Size(size);
        page.Content().Background(gray).Text("I'll take everything");
    });
});

上述代码的结果见 positioning-minimalbox.pdf。由于调用了 MinimalBox,第一页上的文本只占用所需空间。第二页上,文本覆盖整个页面。

Scale

可以对容器中的任何内容进行缩放。Scale 方法会影响水平方向和垂直方向的内容。使用 ScaleHorizontalScaleVertical 方法,只在一个方向上更改内容。后两个方法不会保留内容的宽高比。

小于 1 的缩放值会减小容器占用的区域。大于 1 的值会增大区域。要翻转容器中的内容,请使用负缩放值。例如,ScaleVertical(-1) 会生成原始内容的上下颠倒版本。

PdfDocumentBuilder.Create().Generate("positioning-scale.pdf", doc =>
{
    doc.Pages(page =>
    {
        page.Content()
            .MinimalBox()
            .Column(column =>
            {
                var scales = new[] { 0.5f, 0.75f, 1, 1.3f, 1.5f };

                foreach (var scale in scales)
                {
                    var percent = (int)(scale * 100);
                    column.Item()
                        .Scale(scale)
                        .Text(FormattableString.Invariant($"Scale equals {scale} ({percent}%)."))
                            .FontSize(20);

                    column.Item().LineHorizontal(0.5);
                }
            });
    });
});

上述代码的结果见 positioning-scale.pdf

ScaleToFit

你可以将内容缩小到适合可用空间。例如,当你有一块固定区域用于输出某人的姓名或地址时。当然,如果姓名较长,也可以增大区域。但更简单的方法可能是稍微缩小文本。使用 ScaleToFit 方法缩小内容。

ScaleToFit 会保留内容的宽高比。该方法不会放大内容。请注意,此方法会执行迭代计算,这可能会减慢 PDF 生成过程。

PdfDocumentBuilder.Create().Generate("positioning-scaletofit.pdf", doc =>
{
    doc.Pages(page =>
    {
        page.Content().Column(column =>
        {
            for (int i = 0; i < 5; i++)
            {
                column.Item()
                    .Width(230 - 20 * i)
                    .Height(20)
                    .ScaleToFit()
                    .Border(b => b.Thickness(0.5))
                    .Text(" This text should fit into the changing width.");
            }
        });
    });
});

上述代码的结果见 positioning-scaletofit.pdf

AspectRatio

宽高比定义了容器宽度和高度之间的比例关系。要确定容器的宽高比,请用其宽度除以高度。

使用 AspectRatio 方法为容器指定宽高比。当你为不同布局或页面大小设计可复用容器时,它很有帮助。

PdfDocumentBuilder.Create().Generate("positioning-aspectratio.pdf", doc =>
{
    var ratios = new double[] { 0.25, 0.5, 1, 2 };
    foreach (var ratio in ratios)
    {
        var ratioText = ratio.ToString(CultureInfo.InvariantCulture);
        doc.Pages(page =>
        {
            page.Size(200, 200);
            page.Content().Column(column =>
            {
                column.Item()
                    .AspectRatio(ratio)
                    .Background(new PdfGrayColor(75))
                    .Text($"Width / Heigth = {ratioText}");
            });
        });
    }
});

上述代码的结果见 positioning-aspectratio.pdf

该方法有一个类型为 AspectRatioMode 的可选参数。使用此参数可以指定在保留宽高比的同时如何调整内容大小。

指定了宽高比的容器会尽可能占用更多空间。根据模式不同,容器会尝试占用整个可用区域(默认)、宽度或高度。

请注意,库可能会抛出 LayoutException。当它无法满足大小、宽高比和宽高比模式要求时就会发生这种情况。

Unconstrained

容器也可以完全没有大小约束。使用 Unconstrained 方法移除容器的所有大小约束。

无约束容器中的内容会占据与内容固有大小相等的空间。无约束容器本身不占用空间。因此,同级容器可以覆盖无约束容器的内容。

PdfDocumentBuilder.Create().Generate("positioning-unconstrained.pdf", doc =>
{
    doc.Pages(page =>
    {
        page.Content().MinimalBox()
            .Border(b => b.Thickness(0.5))
            .Column(column =>
            {
                column.Item().Text("First item");

                column.Item().Unconstrained()
                    .Text("Second item ignores all size constraints");

                // 为第三项使用一行空格
                column.Item().Text(new string(' ', 20))
                    .BackgroundColor(new PdfRgbColor(187, 237, 237), 50);

                column.Item().Text("Fourth item");
            });
    });
});

上述代码的结果见 positioning-unconstrained.pdf。在代码中,我为列中的第三项使用了一行空格,并加了半透明背景。如你所见,第三项部分覆盖了第二项(无约束)内容。

位置

容器的位置取决于多个因素。其中一些因素包括对齐、内边距、父容器的位置以及内容方向。默认情况下,任何容器都会贴靠到最左侧和最上方的可用位置。

Padding

最常见的需求之一是在容器内容周围添加一些空间。LayoutContainer 类提供了一组方法来设置容器的内边距区域。内边距区域是内容与边框之间的空间。换句话说,内边距表示围绕内容的内部空间。

Padding 方法一次性设置容器四个边的内边距。使用 PaddingHorizontal 方法只指定左右两侧的内边距。PaddingVertical 仅对上下两侧执行相同操作。要单独设置某一侧的内边距,请使用 PaddingTop/Bottom/Left/Right 方法之一。

Align

要更改容器的位置,请使用对齐方法。AlignLeft/AlignCenter/AlignRight 方法应用水平对齐并返回一个嵌套容器。AlignTop/AlignMiddle/AlignBottom 方法返回一个带有相应垂直对齐的嵌套容器。

显式应用对齐的容器会占用所需的最小宽度和/或高度区域。下面的代码创建了一个包含两个项的列,其中一个项显式应用了对齐。

PdfDocumentBuilder.Create().Generate("positioning-alignment.pdf", doc =>
{
    var color = new PdfRgbColor(187, 237, 237);
    var text = "Hello";
    doc.Pages(page =>
    {
        page.Size(200, 100);
        page.Content().Column(c =>
        {
            c.Item().Extend().Background(color).Text(text);
            c.Item().Extend().AlignLeft().Background(color).Text(text);
        });
    });
});

调用 Extend 会使两个项都占满整页。我在第二项上调用了 AlignLeft 方法。这个调用不会改变位置,因为该项的容器默认已将文本左对齐。但是,显式应用的对齐改变了第二项所占用的区域。

上述代码的结果见 positioning-alignment.pdf

Translate

要在水平方向和/或垂直方向重新定位容器,请使用 Translate/TranslateX/TranslateY 方法。第一个方法同时在水平和垂直方向移动容器。后两个方法只在一个方向上移动容器。

所有这些方法都会覆盖位置,但保留大小约束。移动后的容器可能与其他容器重叠。使用负参数值可向左和/或向上移动。正值会导致向右和/或向下偏移。

PdfDocumentBuilder.Create().Generate("positioning-translate.pdf", doc =>
{
    doc.Pages(page =>
    {
        page.Size(200, 100);
        page.Content().Row(r =>
        {
            r.ConstantItem(50)
                .Background(new PdfRgbColor(187, 237, 237))
                .Text("Left");

            r.ConstantItem(50)
                // 将此项向左移动 10 磅并向下移动 5 磅
                .Translate(-10, 5)
                .Background(new PdfRgbColor(15, 130, 9))
                .Text("Right");
        });
    });
});

上述代码的结果见 positioning-translate.pdf

Rotate

旋转后的内容,尤其是文本,可以从多个方面改善文档。例如,它可以节省空间,并使文档更具吸引力和美感。

LayoutContainer 类提供了两种旋转内容的方法。无论使用哪种方法,包含旋转内容的容器都会遵守位置和大小约束。

旋转 90 度

RotateRightRotateLeft 方法分别将内容顺时针和逆时针旋转 90 度。

下面的代码展示了如何创建一个在主页面内容旁边带有竖排文本的文档。

PdfDocumentBuilder.Create().Generate("positioning-rotate.pdf", doc =>
{
    var lightGray = new PdfGrayColor(90);
    doc.Pages(page =>
    {
        page.Size(298, 210);
        page.Content().Row(r =>
        {
            r.AutoItem()
                .RotateLeft()
                .Background(lightGray)
                .Text("This content goes up");

            r.RelativeItem(1)
                .ExtendVertical()
                .PaddingHorizontal(10)
                .Column(t =>
                {
                    for (int i = 0; i < 15; i++)
                        t.Item().Text("The main content line goes here");
                });

            r.AutoItem()
                .RotateRight()
                .Background(lightGray)
                .Text("This content goes down");
        });
    });
});

上述代码的结果见 positioning-rotate.pdf

旋转到任意角度

Rotate 方法可以将内容旋转任意角度。正值表示顺时针旋转,负值表示逆时针旋转。

旋转的原点是容器的左上角。旋转后的内容可能与其他容器重叠。

PdfDocumentBuilder.Create().Generate("positioning-rotate2.pdf", doc =>
{
    doc.Pages(page =>
    {
        page.Size(298, 210);
        page.Content()
            .Padding(25)
            .Background(new PdfGrayColor(70)) // 灰色
            .AlignCenter()
            .AlignMiddle()

            .Background(new PdfGrayColor(100)) // 白色

            .Rotate(30)

            .Width(100)
            .Height(100)
            .Background(new PdfRgbColor(187, 237, 237)); // 蓝色
    });
});

上述代码的结果见 positioning-rotate2.pdf

要更改旋转原点,请在调用 Rotate 之前调用一个 Translate 方法。不要忘记在调用后将原点移回。

// 平移旋转原点
.TranslateX(50)
.TranslateY(50)

.Rotate(30)

// 平移回来
.TranslateX(-50)
.TranslateY(-50)

条件布局

PDF 文档的布局可以依赖于某个条件。例如,你可以为列中的偶数行和奇数行使用不同的对齐方式或背景色。

使用 Container 方法插入一个布局依赖于条件的嵌套容器。调用此方法不会打断方法调用链。

PdfDocumentBuilder.Create().Generate("positioning-container.pdf", doc =>
{
    doc.Pages(page =>
    {
        page.Content().Column(c =>
        {
            for (int i = 0; i < 15; i++)
            {
                c.Item()
                    .TextStyle(TextStyle.Parent.FontSize(14))
                    .Container(x => i % 2 == 0 ? x.Background(new PdfGrayColor(70)) : x)
                    .Text($"Row {i + 1}");
            }
        });
    });
});

上述代码的结果见 positioning-container.pdf

DSL

文档的某些部分可以使用相同的布局。例如,它们可以设置外观相同的边框,或使用相同的格式。根据“不要重复自己”原则,我建议将公共代码提取到一个方法中。

为公共代码使用扩展方法有两个好处:

  • 你可以在方法调用链中使用该方法
  • 你可以为一组方法调用提供有意义的名称

基于这些优点,你可以构建一个领域特定语言(DSL)。使用 DSL,你的布局代码可以更短,也更容易理解。

static class LayoutHelpers
{
    public static LayoutContainer NumberCell(this Table table)
        => table.Cell().Border(b => b.Thickness(0.5)).PaddingHorizontal(10);
}

PdfDocumentBuilder.Create().Generate("positioning-dsl.pdf", doc => doc.Pages(page =>
{
    page.Content().Table(t =>
    {
        t.Columns(c =>
        {
            for (int i = 0; i < 4; ++i)
                c.ConstantColumn(50);
        });

        for (int i = 0; i < 16; i++)
            t.NumberCell().Text($"{i + 1}");
    });
}));

渲染过程

Layout 附加组件会根据大小和位置约束排列你放入容器中的任何内容。有些内容可以跨越多个页面。内容的渲染顺序是严格确定的。内容流是这一顺序的另一个名称。

LayoutContainer 类提供了一些用于微调内容流的方法。你不一定每次都需要它们,但在某些情况下,没有其他方式可以实现所需布局。

PageBreak

当你需要从新页面开始一块内容时,请使用 PageBreak 方法。例如,你可以使用该方法让 Column 项从新页面开始。

下面是一个将列拆分为每个页面仅包含两行的示例代码。

PdfDocumentBuilder.Create().Generate("positioning-pagebreak.pdf", doc => doc.Pages(page =>
{
    page.Size(200, 100);
    page.Content().Column(c =>
    {
        for (int i = 1; i <= 10; ++i)
        {
            c.Item().Text($"Item {i}");

            if (i % 2 == 0)
                c.Item().PageBreak();
        }
    });
}));

上述代码的结果见 positioning-pagebreak.pdf

ShowIf

根据条件,你可能需要显示/隐藏某个容器。ShowIf 方法本质上就是针对这种条件布局特殊情况的语法糖。

在下面的代码中,我使用 ShowIf 方法在每行的每 5 个元素之后插入一条竖线。

PdfDocumentBuilder.Create().Generate("positioning-showif.pdf", doc => doc.Pages(page =>
{
    page.Size(200, 100);
    page.Content().Row(r =>
    {
        for (int i = 0; i < 10; ++i)
        {
            r.AutoItem().Text(i.ToString());
            r.AutoItem().ShowIf(i > 0 && (i + 1) % 5 == 0).LineVertical(0.5);
        }
    });
}));

上述代码的结果见 positioning-showif.pdf

ShowOnce

你可以阻止某段内容在后续页面上重复。使用 ShowOnce 方法,指示布局引擎只完整渲染一次内容。

查看下面的代码,了解 ShowOnce 调用如何阻止 "Environment" 出现在第二页上。

PdfDocumentBuilder.Create().Generate("positioning-showonce.pdf", doc => doc.Pages(page =>
{
    page.Size(200, 100);
    page.Content().Row(r =>
    {
        r.RelativeItem()
            .Background(new PdfGrayColor(75))
            .Border(b => b.Thickness(0.5))
            .Padding(5)
            .ShowOnce()
            .Text("Environment");

        r.RelativeItem()
            .Border(b => b.Thickness(0.5))
            .Padding(5)
            .Column(c =>
            {
                c.Item().Text(Environment.OSVersion.VersionString);
                c.Item().Text(string.Empty);
                c.Item().Text(
                    Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER")
                    ?? string.Empty
                );
            });
    });
}));

上述代码的结果见 positioning-showonce.pdf

ShowEntire

默认行为是当内容无法在一页内放下时,将其拆分到多个页面。使用 ShowEntire 方法可将整个容器渲染在同一页上。

请注意,当无法将全部内容放在同一页时,库会抛出 LayoutException

由于下面代码中的 ShowEntire 调用,第二项文本从第二页开始。若没有该调用,它会在第一页、紧接第一项文本之后开始。

PdfDocumentBuilder.Create().Generate("positioning-showentire.pdf", doc => doc.Pages(page =>
{
    page.Size(100, 100);
    page.Content().Column(c =>
    {
        c.Item().Text(t =>
        {
            for (var i = 0; i < 4; i++)
                t.Line($"First item line {i + 1}");
        });

        c.Item()
            .Background(new PdfRgbColor(250, 123, 5))
            .ShowEntire()
            .Text(t =>
            {
                for (var i = 0; i < 4; i++)
                    t.Line($"Second item line {i + 1}");
            });
    });
}));

上述代码的结果见 positioning-showentire.pdf

EnsureSpace

某种意义上,EnsureSpaceShowEntire 方法的一种特殊情况。区别在于,EnsureSpace 不要求整个内容都能放在同一页上。该方法只会尝试让指定高度的内容部分适配页面,其余内容会进入下一页。

如果当前页面未占用区域的高度小于请求值,则整个内容会在新页面上渲染。在这种情况下,该方法的结果与 ShowEntire 方法相同。

StopPaging

使用 StopPaging 方法可使输出最多只出现在一页上。在容器上调用此方法会阻止其分页。Layout 附加组件不会将该容器的内容拆分到多个页面,也不会渲染任何无法放入一页的数据。

下面的示例代码向文档添加两组页面。两组都使用一组工作日名称作为内容。第一组只有一页,因为代码对页面的内容容器调用了 StopPaging 方法。

PdfDocumentBuilder.Create().Generate("positioning-stoppaging.pdf", doc =>
{
    static Action<TextContainer> produceText(string heading)
    {
        var text = string.Join('\n', DateTimeFormatInfo.InvariantInfo.DayNames);
        return t =>
        {
            t.Line(heading).BackgroundColor(new PdfRgbColor(250, 123, 5));
            t.Span(text);
        };
    }

    doc.Pages(page =>
    {
        page.Size(100, 100);
        page.Content()
            .StopPaging()
            .Text(produceText("Without paging:"));
    });

    doc.Pages(page =>
    {
        page.Size(100, 100);
        page.Content()
            .Text(produceText("Default behaviour:"));
    });
});

上述代码的结果见 positioning-stoppaging.pdf

SkipOnce

你可以推迟内容的出现。如果某个容器会出现在多于一页上,可以使用 SkipOnce 跳过第一页,并从第二页开始在所有页面上渲染内容。

此能力适用于各种页眉,但也可用于其他内容。查看下面的代码,了解 SkipOnce 调用如何阻止页眉出现在第一页。

PdfDocumentBuilder.Create().Generate("positioning-skiponce.pdf", doc => doc.Pages(page =>
{
    page.Size(298, 210);

    page.Header()
        .SkipOnce()
        .Text("This header will appear starting from page 2")
        .Style(TextStyle.Parent.Underline());

    page.Content().Column(c =>
    {
        for (int i = 0; i < 5; i++)
        {
            if (i > 0)
                c.Item().PageBreak();

            c.Item().Text($"Page {i + 1}");
        }
    });
}));

上述代码的结果见 positioning-skiponce.pdf

内容方向

默认内容方向是从左到右。容器会将其文本和其他内容左对齐。

但有些语言是从右到左书写的(例如阿拉伯语和希伯来语)。在创建这些语言的内容时,请使用 ContentFromRightToLeft 方法。调用它会将容器的内容方向切换为从右到左,同时也会切换默认对齐方式。

如果页面中的大部分内容使用 RTL 语言,可以将页面的默认内容方向设置为从右到左。为此请使用 PageLayout.ContentFromRightToLeft 方法。然后,如果要为选定容器覆盖默认内容方向,请使用 ContentFromLeftToRight 方法。

请注意,内容方向不会影响显式指定的对齐方式。例如,无论为容器设置什么内容方向,右对齐内容都会贴靠在右侧。子元素的视觉顺序会根据内容方向而不同。