該頁面可以包含自動翻譯的文字。

容器的大小、位置與渲染

內容是文件中最重要的部分,這點毋庸置疑。另一個關鍵部分是能產生清晰、專業且有效溝通的格式。正確排版的文件在視覺上更具吸引力,也更易讀且更容易瀏覽。

你可能已經知道如何使用容器來組織內容,也知道如何為它們套用背景色彩。本文說明如何為容器指定大小與位置,也涵蓋內容的條件式渲染等進階功能。並且說明對由右至左內容方向的支援。

容器定位

LayoutContainer 類別提供你專業排列容器所需的一切。透過套用內距與對齊,你可以建立給使用者留下正面印象的文件。

本文是 PDF 產生用 Layout API 系列的一部分。如果你是第一次使用此 API,請先閱讀 Getting Started with 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 方法。

請注意,內容方向不會影響明確指定的對齊方式。例如,不論為容器設定何種內容方向,靠右對齊的內容都會貼齊右側。子元素的視覺順序則會依內容方向而不同。