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

容器的大小、位置和渲染

内容是文档最重要的部分。 这个毋庸置疑。 另一个关键部分是创建清晰、专业和有效沟通的格式。 格式正确的 文档具有视觉吸引力、可读性并且易于浏览。

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

集装箱定位

LayoutContainer 类提供了专业地安排容器所需的一 切。 通过应用填充和对齐,您可以创建给用户留下积极印象的文档。

本文是有关用于 PDF 生成的 Layout API 的系列文章的一部分。 如果您是 API 新手,请先阅读 Layout API 入门 部分。

Docotic.Pdf 库 9.5.17548-dev 布局附加组件 9.5.17548-dev
回归测试 14,726 通过 NuGet 总下载量 4,514,921

尺寸

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

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

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 方 法。

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