Table container

Use Table containers to layout your most complex data. Tables play a crucial role in PDF documents, enhancing their clarity and effectiveness.

Tables provide a structured format for presenting your data. They allow you to present complex details in a concise and visually appealing manner. Instead of lengthy paragraphs, you can use tables to present facts, figures, and trends succinctly.

A well-designed table enhances readability. By aligning columns and rows, you create a structure that guides the reader's eye. You can use backgrounds, borders, and font styles to emphasize specific cells or headings.

Table container

Get a table container by calling the LayoutContainer.Table method. Then you must define at least one column in the container. In the simplest case, you can fill all columns and rows by calling the Cell method multiple times.

Please continue reading to know how to define columns, add a header and/or footer to a table. The text describes all other features of the Table container, too.

This article is part of a series about Layout API for PDF generation. If you are new to the API, read the Getting Started with Layout API part first.

Docotic.Pdf library 9.5.17548-dev Layout add-on 9.5.17548-dev
Regression tests 14,726 passed Total NuGet downloads 4,514,921

Columns and rows

Every table must define at least one column. Use the Table.Columns method to define columns. The method accepts a delegate of type Action<TableColumnContainer>. In the delegate code, add columns to the table by calling methods of the provided object.

The ConstantColumn method adds a column with width equal to an exact number of points.

The RelativeColumn method adds a column with a relative width. The method accepts the number of parts the column should occupy. The total number of parts is the sum of all numbers in all the RelativeColumn calls in this table. Mostly, this method works the same as the RelativeItem of the Row container. Please read the link for the detailed explanation.

There is no need to define rows explicitly. Number of rows depends on column and cell properties. Number of cells can also affect the number of rows. Table can have height larger than the sum of all its rows heights. Rows can have fewer cells than the number of columns. And it is possible for columns to have fewer cells than the number of rows.

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}");
                }
            });
    });
});

You can see the result of the code in table-basic.pdf.

In the resulting PDF, the table occupies all the parent container space. Even though there are not enough cells to cover the area. Uncomment the MinimalBox call to limit table height to the sum of its rows heights.

The code defines three columns and adds 10 cells to the table. Table puts the cells one after another, adding a new row when needed. It's perfectly fine to have a row with only one cell.

The table will place cells on the next page if the current page has no enough space. You can observe this by increasing the number of added cells.

Cells

Let me explain what Layout API provides for working with table cells.

Position

There are two ways to create a table cell:

  • without explicit position information
  • with at least partially specified position

I used the first way in the previous section's code. You call the Cell method overload that does not accept parameters. The method adds a new cell to the column after the last added cell. If there is no space to the right, the cell goes onto the next row. In RTL mode, the method checks for space to the left of the last added cell.

If you know the exact position for the new cell, use the other overload of the Cell method. The one that accepts a delegate of type Action<TableCell>. In the delegate code, specify cell row and index by calling the RowIndex and the ColumnIndex methods of the provided object.

It is possible to specify only a row or column index for the new cell. Column index is 0 when you only specify a row index. And vice versa.

When creating cells, you can mix calls to the both Cell overloads in any order.

I will use the following extension method in the next code examples in this article. This should make the examples more concise and easier to read.

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);
}

Here is an example that creates a table with both explicitly and implicitly placed cells. I used the extension method to apply style and text to cells. Numbers in cells reflect the creation order of the cells.

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");
            });
    });
});

You can see the result of the code in table-cells.pdf.

Row and column spans

Cells can span multiple rows and/or multiple columns. Use the Cell overload that accepts a delegate to specify the number of rows and columns spanned by the 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");
            });
    });
});

You can see the result of the code in table-cellspans.pdf.

Overlapping

It is possible for cells to cover each other. When it happens, the order of creation defines which cell will get on top of another.

You can get some overlapping cells when your cells span more than one column or row. Or when you position cells both explicitly and implicitly.

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);
            });
    });
});

You can see the result of the code in table-overlapping.pdf.

Extend to the bottom

If the layout of your table is complex, the paging mechanism can cause a blank space at the bottom of some of your columns. Use the ExtendCellsToBottom method if you want to get rid of the blank space. The method extends the last cell in each column so that cells end at the bottom of the table.

In the following example, the first page shows the default behaviour. The second page shows how the ExtendCellsToBottom call affects the columns.

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");
                });
        });
    }
});

You can see the result of the code in table-extendcells.pdf.

Tables can have a header and a footer. Use the Header and the Footer methods to access and set up the corresponding TableCellContainer. The container provides the Cell methods to create cells. Those methods work exactly the same as the methods for the main table content.

Header, footer, and main table cells use the same column definition. But all three cell collections are independent and do not affect each other.

When the table does not fit onto one page, the header and footer get repeated on each page occupied by the table.

I used the BoxWithText extension method in the previous examples. I also use the WhiteBoxWithText extension method for the next code.

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);
    }
}

The following example shows how to create a table with a header. I use a table with five essential chemical elements. There is the name and the melting point in both Celsius and Kelvin degrees for each element.

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));
                }
            });
    });
});

You can see the result of the code in table-header.pdf.

Sample code

Please spend some time to check Add tables to PDF documents example. It also shows how to create tables with headers, but does not use any extension methods. And there is a VB.NET version, too.