Эта страница может содержать автоматически переведенный текст.

Размер, позиция и отображение контейнеров

Содержимое — самая важная часть документа. В этом нет сомнений. Не менее важная часть — форматирование, которое обеспечивает понятную, профессиональную и эффективную передачу информации. Правильно отформатированный документ визуально привлекателен, удобочитаем и прост в навигации.

Возможно, вы уже знаете, как организовывать содержимое с помощью контейнеров. И как задавать им цвет фона. В этой статье описано, как задавать размер и позицию контейнеров. Также рассматриваются такие возможности, как условное отображение содержимого. Есть информация о поддержке направлений содержимого справа налево.

Расположение контейнеров

Класс LayoutContainer предоставляет все необходимое для профессионального размещения контейнеров. Применяя отступы и выравнивание, вы можете создать документ, который произведет положительное впечатление на пользователей.

Эта статья входит в серию о Layout API для генерации PDF. Если вы впервые знакомитесь с API, сначала прочитайте часть Начало работы с Layout API.

Размер

По умолчанию контейнер занимает минимальную область, необходимую для его содержимого. Иначе говоря, размер контейнера равен естественному размеру его содержимого.

Естественный размер изображения определяется размерами самого файла изображения. Естественный размер текстового фрагмента — это размер области, которая охватывает все глифы в этом фрагменте.

Размер составного контейнера, например Column или Table, зависит от размеров частей контейнера.

Width & Height

Можно задать точную ширину и высоту контейнера с помощью методов Width и Height. Это очень удобно для контейнеров-заполнителей.

Точный размер хорошо подходит и для изображений. Это связано с тем, что Layout API масштабирует их так, чтобы они вписывались в контейнер или заполняли его в зависимости от значения ImageContentMode.

С точными размерами нужно быть осторожнее для составных контейнеров и контейнеров с текстом. Вы получите LayoutException, если содержимое невозможно уместить в заданный размер.

Есть случаи, когда нужно задать только ограничение по ширине или высоте. Ограничения можно задать с помощью методов MinWidth, MinHeight, MaxWidth и MaxHeight.

Обратите внимание, что библиотека выбросит 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 влияет на содержимое как по горизонтали, так и по вертикали. Используйте метод ScaleHorizontal или ScaleVertical, чтобы изменять содержимое только в одном направлении. Два последних метода не сохраняют соотношение сторон содержимого.

Значения Scale меньше 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 градусов

Методы RotateRight и RotateLeft поворачивают содержимое на 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.

Чтобы изменить точку начала поворота, вызовите один из методов Translate перед вызовом Rotate. Не забудьте вернуть начало координат обратно после вызова.

// смещаем точку начала поворота
.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

В некотором смысле EnsureSpace — это частный случай метода ShowEntire. Разница в том, что 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.

Обратите внимание, что направление содержимого не влияет на явно заданное выравнивание. Например, содержимое, выровненное по правому краю, будет прижато к правой стороне независимо от того, какое направление содержимого задано для его контейнера. Визуальный порядок дочерних элементов будет различаться в зависимости от направления содержимого.