TX Text Control .NET Server

Tables

Topic

Line items, price lists and schedules go into tables. On the server, ServerTextControl fills tables from your data, formats their cells, rows and columns, changes their structure, calculates totals with formulas and reads the tables of documents you load.

Create a Document inserts a first table with a header row. To repeat a table row for each record of your data in a template, use a merge block, described in Merge Blocks, instead of a loop.

How Tables Are Organized

ServerTextControl.Tables returns the TableCollection of the main text. Headers, footers and text frames have their own collections, such as HeaderFooter.Tables.

  • A table and its parts. A Table has the collections Rows, Columns and Cells. Rows and columns are numbered from 1, and Cells.GetItem(row, column) returns a single cell.
  • IDs. Tables.Add takes the number of rows and columns and an ID between 10 and short.MaxValue. GetItem with the ID returns the table, or null if no table has this ID. Add without parameters opens a dialog box and isn't meant for the server.
  • The input position. New tables are inserted at the current input position. Tables.GetItem() without a parameter returns the table that contains the input position, and methods such as Rows.Add work on the row, column or cell at the input position or on the selected cells.
  • Units. Widths, heights, border widths and distances are measured in twips, 1/1440 inch.

Fill a Table From Data

The following program creates the line item table of an invoice. It adds one row for the column headings and one row for each item:

using System.Globalization;
using TXTextControl;

var lineItems = new[]
{
    new { Description = "TX Text Control .NET Server License", Quantity = 1, Price = 199.99 },
    new { Description = "Priority Support - 12 Months", Quantity = 1, Price = 99.98 }
};

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();

    tx.Selection.Text = "Invoice INV-2025-001\r\n\r\n";

    // A header row and one row for each item
    tx.Tables.Add(lineItems.Length + 1, 3, 10);
    Table table = tx.Tables.GetItem(10);

    // One format for all cells: borders of 1 pt and space left and right of the text
    TableCellFormat cellFormat = new TableCellFormat()
    {
        LeftBorderWidth = 20,
        TopBorderWidth = 20,
        RightBorderWidth = 20,
        BottomBorderWidth = 20,
        LeftTextDistance = 100,
        RightTextDistance = 100
    };

    foreach (TableCell cell in table.Cells)
    {
        cell.CellFormat = cellFormat;
    }

    // Column headings in row 1
    string[] headings = { "Description", "Quantity", "Price" };

    for (int column = 1; column <= headings.Length; column++)
    {
        table.Cells.GetItem(1, column).Text = headings[column - 1];
    }

    table.Rows.GetItem(1).IsHeader = true;

    // One row for each item, starting in row 2
    for (int i = 0; i < lineItems.Length; i++)
    {
        int row = i + 2;
        table.Cells.GetItem(row, 1).Text = lineItems[i].Description;
        table.Cells.GetItem(row, 2).Text = lineItems[i].Quantity.ToString(CultureInfo.InvariantCulture);
        table.Cells.GetItem(row, 3).Text = lineItems[i].Price.ToString(CultureInfo.InvariantCulture);
    }

    // The first column is 4 inches wide
    table.Columns.GetItem(1).Width = 4 * 1440;

    // Alternative text for accessible PDF documents
    table.DescriptiveText = "Line items of invoice INV-2025-001";

    tx.Save("invoice-table.pdf", StreamType.AdobePDF);
}

The code works as follows:

Format Cells, Rows and Columns

A TableCellFormat applies to a cell, a row or a column, depending on whether you assign it to TableCell.CellFormat, TableRow.CellFormat or TableColumn.CellFormat. Its properties:

PropertySets
BackColorThe background color
LeftBorderWidth, TopBorderWidth, RightBorderWidth, BottomBorderWidthThe width of each border in twips
LeftBorderColor, TopBorderColor, RightBorderColor, BottomBorderColorThe color of each border
LeftTextDistance, TopTextDistance, RightTextDistance, BottomTextDistanceThe distance between the border and the text in twips
VerticalAlignmentTop, Center or Bottom
TextType, NumberFormatWhether the text is a number, and how numbers are displayed

Rows and columns have settings of their own. TableRow.MinimumHeight is the height a row has at least, and the row grows when its text needs more room. With TableRow.AllowPageBreak set to false, a row isn't split across two pages. To format the text in a cell, select the cell with TableCell.Select and set the attributes of tx.Selection:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();

    tx.Tables.Add(3, 3, 10);
    Table table = tx.Tables.GetItem(10);

    TableCellFormat cellFormat = new TableCellFormat()
    {
        LeftBorderWidth = 20,
        TopBorderWidth = 20,
        RightBorderWidth = 20,
        BottomBorderWidth = 20,
        LeftBorderColor = System.Drawing.Color.Gray,
        TopBorderColor = System.Drawing.Color.Gray,
        RightBorderColor = System.Drawing.Color.Gray,
        BottomBorderColor = System.Drawing.Color.Gray,
        LeftTextDistance = 144,
        TopTextDistance = 144,
        RightTextDistance = 144,
        BottomTextDistance = 144,
        VerticalAlignment = VerticalAlignment.Center
    };

    foreach (TableCell cell in table.Cells)
    {
        cell.CellFormat = cellFormat;
        cell.Text = $"Row {cell.Row}, Column {cell.Column}";
    }

    // The first row is at least 0.5 inch high and stays on one page
    TableRow firstRow = table.Rows.GetItem(1);
    firstRow.MinimumHeight = 720;
    firstRow.AllowPageBreak = false;

    table.Columns.GetItem(1).Width = 2 * 1440;

    // Bold, right-aligned text in the first cell
    table.Cells.GetItem(1, 1).Select();
    tx.Selection.Bold = true;
    tx.Selection.ParagraphFormat.Alignment = HorizontalAlignment.Right;

    tx.Save("formatted-table.pdf", StreamType.AdobePDF);
}

The other character and paragraph attributes of a selection are described in Text and Paragraphs.

Page Breaks in Table Rows

The paragraph settings KeepWithNext and PageBreakBefore work in tables as well, but row by row. A row holds several paragraphs, one or more in each cell, and only the first paragraph of the row decides how the whole row is treated; the settings of its other paragraphs are ignored:

  • PageBreakBefore in the first paragraph moves the whole row to the top of a new page.
  • KeepWithNext in the first paragraph keeps the whole row on the same page as the row below it.

How the two settings are set for a paragraph is shown in Paragraphs. AllowPageBreak, described above, decides something else: whether a single row may be split across two pages.

Add Rows and Columns and Merge Cells

Rows, columns and merged cells are added at the input position. To put the input position into a cell, set ServerTextControl.InputPosition to the position of the cell. TableCell.Start is one-based, while an InputPosition counts from zero, so subtract 1:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();

    tx.Tables.Add(3, 3, 10);
    Table table = tx.Tables.GetItem(10);

    // Put the input position into row 3, column 1
    tx.InputPosition = new InputPosition(table.Cells.GetItem(3, 1).Start - 1);

    // The table and the cell at the input position
    Table current = tx.Tables.GetItem();
    TableCell currentCell = current.Cells.GetItem();
    Console.WriteLine($"Row {currentCell.Row}, column {currentCell.Column}");

    // Two rows below row 3 and a column right of column 1
    if (table.Rows.CanAdd)
    {
        table.Rows.Add(TableAddPosition.After, 2);
    }

    if (table.Columns.CanAdd)
    {
        table.Columns.Add(TableAddPosition.After);
    }

    // Remove row 2
    table.Rows.GetItem(2).Select();

    if (table.Rows.CanRemove)
    {
        table.Rows.Remove();
    }

    // Merge the cells of row 1 into one cell
    table.Select(1, 1, 1, table.Columns.Count);

    if (table.CanMergeCells)
    {
        table.MergeCells();
    }

    table.Cells.GetItem(1, 1).Text = "Invoice INV-2025-001";

    tx.Save("table-structure.docx", StreamType.WordprocessingML);
}

The code works as follows:

Table.Split is different: it splits the whole table into two tables before or after the row at the input position, if CanSplit is true.

Split a Table at Bookmarks

A long table, for example one that a merge block filled, can be split into several tables at rows that a template marks with a bookmark. The following program builds a table with a header row and six items, marks row 5 with a bookmark named _split and splits the table there. The new table gets a copy of the header row:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();

    // A header row and six items, with a bookmark in row 5
    tx.Tables.Add(7, 2, 10);
    Table items = tx.Tables.GetItem(10);
    items.Cells.GetItem(1, 1).Text = "Item";
    items.Cells.GetItem(1, 2).Text = "Price";
    items.Rows.GetItem(1).IsHeader = true;

    for (int row = 2; row <= 7; row++)
    {
        items.Cells.GetItem(row, 1).Text = $"Item {row - 1}";
        items.Cells.GetItem(row, 2).Text = $"{row * 10}.00";
    }

    tx.Select(items.Cells.GetItem(5, 1).Start - 1, 0);
    tx.DocumentTargets.Add(new DocumentTarget("_split"));

    foreach (DocumentTarget target in tx.DocumentTargets)
    {
        if (target.TargetName != "_split")
        {
            continue;
        }

        // The table that contains the bookmark
        tx.InputPosition = new InputPosition(target.Start - 1);
        Table table = tx.Tables.GetItem();

        if (table == null || !table.CanSplit)
        {
            continue;
        }

        // Copy the header row in the TX Text Control format
        byte[] headerRow = null;

        foreach (TableRow row in table.Rows)
        {
            if (row.IsHeader)
            {
                row.Select();
                tx.Selection.Save(out headerRow, BinaryStreamType.InternalUnicodeFormat);
                break;
            }
        }

        tx.Select(target.Start - 1, 0);
        table.Split(TableAddPosition.Before);

        // Insert the copy at the top of the new table
        tx.Selection.Start += 1;

        if (headerRow != null)
        {
            tx.Selection.Load(headerRow, BinaryStreamType.InternalUnicodeFormat);
        }
    }

    Console.WriteLine($"Tables: {tx.Tables.Count}");
    tx.Save("split-table.pdf", StreamType.AdobePDF);
}

The code works as follows:

  • DocumentTarget.Start is the one-based position of the bookmark, and Tables.GetItem() returns the table at the input position.
  • TableRow.Select selects the header row, and Selection.Save copies it into a byte array.
  • Split with TableAddPosition.Before ends the first table above the row with the bookmark. One position further is the first cell of the new table, where Selection.Load inserts the copied row.

The program prints Tables: 2: the first table has the header row and items 1 to 3, the second a copy of the header row and items 4 to 6. The blog article Splitting Tables at Bookmark Positions and Cloning Table Headers splits the tables of a merged document this way.

Calculate With Formulas

A cell can calculate its text from other cells of the same table with a formula, as in a spreadsheet. The following program adds up the prices of two line items and shows the total with two decimal places:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();

    // Address cells with a column letter and a row number, such as B2
    tx.FormulaReferenceStyle = FormulaReferenceStyle.A1;
    tx.IsFormulaCalculationEnabled = true;

    tx.Tables.Add(3, 2, 10);
    Table table = tx.Tables.GetItem(10);

    TableCellFormat amountFormat = new TableCellFormat()
    {
        TextType = TextType.Number,
        NumberFormat = "#.00"
    };

    string[] descriptions = { "TX Text Control .NET Server License", "Priority Support - 12 Months" };
    string[] prices = { "199.99", "99.98" };

    for (int row = 1; row <= 2; row++)
    {
        table.Cells.GetItem(row, 1).Text = descriptions[row - 1];

        TableCell price = table.Cells.GetItem(row, 2);
        price.CellFormat = amountFormat;
        price.Text = prices[row - 1];
    }

    table.Cells.GetItem(3, 1).Text = "Total";

    TableCell total = table.Cells.GetItem(3, 2);
    total.CellFormat = amountFormat;
    total.Formula = "SUM(B1:B2)";

    tx.Save("total.pdf", StreamType.AdobePDF);
}

The code works as follows:

  • FormulaReferenceStyle sets how formulas address cells: A1 with a column letter and a row number, R1C1 with a row number and a column number.
  • IsFormulaCalculationEnabled turns on the calculation, which updates a formula cell when the text of a cell it refers to changes.
  • TableCellFormat.TextType with Number makes the text of a cell a number, which formulas can calculate with. NumberFormat #.00 shows two decimal places; Number Formats below describes the format strings.
  • TableCell.Formula sets the formula. A formula cell can itself be referred to by another formula, but references to other tables or documents aren't supported.

How a Formula Is Built

Formulas use the syntax of spreadsheet formulas in Office Open XML (ISO/IEC 29500-1, section 18.17.2), which spreadsheet applications such as Microsoft Excel use as well. A formula is made of four kinds of elements: constants, cell references and functions, which stand for values, and operators, which combine them.

Take the formula PI()*R[-2]C[-1]^2, set in R1C1 style:

  • R[-2]C[-1] is a cell reference: the value of the cell two rows above and one column left of the formula cell.
  • 2 is a numerical constant, and ^ raises the value of the cell to the power of 2.
  • PI() is a function that returns π, 3.14159265..., and * multiplies the squared value by it.

The formula PI()*R[-2]C[-1]^2 with its parts numbered: 1 the constant 2, 2 the operators ^ and *, 3 the function PI() and 4 the cell reference R[-2]C[-1].

Constants

A constant is a value written into the formula. There are five kinds: numerical, logical, string, error and array constants.

ConstantWritten asFormulaResult
Integer49+716
Decimal number, with a period3.9374*5.522
Engineering notation5e44*3e51200000
Negative number, with a leading minus-7.6-9+7-2
LogicalTRUE, FALSE15>7=TRUETRUE
String, in double quotes"Text""TX"&"Text"&"Control"TXTextControl
Error#N/ANA() returns the same value#N/A
Array, in braces{1,2,3;4,5}SUM(10,{1,2,3;4,5})25

In an array constant, commas separate the values of a row and semicolons separate the rows, so {1;2;3} is a column of three values and SUM({1;2;3}) returns 6. An array can hold numbers, logical values and strings.

Comparing strings works as well: "Two"="One" returns FALSE, and "FALSE"="FALSE" returns TRUE.

Operators

Arithmetic operators return a number, comparison operators return TRUE or FALSE, and the concatenation operator joins strings:

OperatorMeaningFormulaResult
+Addition8+614
-Subtraction14-68
*Multiplication4*312
/Division17/28.5
%Percent, after a number290%2.9
^Exponentiation3^29
=Equal to2=2TRUE
>Greater than4>6FALSE
<Less than4<6TRUE
>=Greater than or equal to6>=4TRUE
<=Less than or equal to26<=4FALSE
<>Not equal to2<>2FALSE
&Concatenation"TX"&"Spell"TXSpell

Functions

A function has an uppercase name, followed by its arguments in parentheses and separated by commas. The parentheses are required even when a function takes no argument, as in PI(). An argument can be a number, a logical value, a string or an array, given as a constant, as a cell reference or range, or as the result of another function. A function returns a number, a logical value, a string or an array.

The function POWER(1.234,5.1) with its function name, its two arguments and the parenthesis area labeled.

FormulaResult
ABS(-7.765)7.765
PI()3.141592654
POWER(3,2.5)15.5884573
SUM(1,2,3)6
SUM({1;2;3})6
SUM(A1:A3)6, when cells A1, A2 and A3 hold 1, 2 and 3

Formula Functions lists all 85 functions with their arguments and defaults, and the limits of some of them, such as COUNT, which doesn't count dates. TableCollection.SupportedFormulaFunctions returns their names at runtime. A cell whose formula calls a function that isn't supported, such as VLOOKUP or SUMIF, shows the text UNSUPPORTED.

Cell References

FormulaReferenceStyle decides how a formula refers to other cells of its table: in A1 style or in R1C1 style.

A1 style. A column letter and a row number address a cell. Columns are named A to Z, then AA to AZ, BA to BZ and so on, so B3 is the cell in the second column of the third row. A plain A1 reference is relative: it describes where the cell is as seen from the formula cell, the same as a relative R1C1 reference. A $ in front of the column, the row or both makes that part absolute, so that it doesn't depend on where the formula is: $B$3, $B3 or B$3.

R1C1 style. R with a row number and C with a column number address a cell. The numbers are either relative or absolute:

  • Relative, in square brackets: the number of rows and columns from the formula cell to the referenced cell. Negative numbers go up and to the left, and [0] stays in the same row or column. Text Control recommends relative references.
  • Absolute, without brackets: R5C4 is always the cell in row 5 and column 4, wherever the formula is.

For a formula in row 4 and column 3 (R4C3), the relative references point at these cells:

A table of five rows and four columns, labeled R1 to R5 and C1 to C4, with a formula with a cell reference in the cell at R4 and C3 and numbers in the other cells.

Relative referenceRefers to
R[-3]C[-1]Row 1, column 2 (R1C2)
R[1]C[-2]Row 5, column 1 (R5C1)
R[0]C[1]Row 4, column 4 (R4C4)
R[-2]C[1]Row 2, column 4 (R2C4)

Absolute R1C1 references work, but when the table is edited or its cells are pasted as a new table, they return wrong or invalid values. Use relative references in tables that are edited.

Ranges. Two references separated by a colon form a range: the first is the upper left cell, the second the lower right cell. For a formula in R3C3, SUM(R[-1]C[-2]:R[0]C[-1]) adds up the cells from R2C1 to R3C2, and in A1 style, SUM(A2:B3) adds up the same cells.

A table of five rows and five columns with a formula with a cell reference at R3 and C3 and four highlighted, numbered ranges: 1 from R2C1 to R3C2, 2 from R1C3 to R2C3, 3 from R1C5 to R4C5 and 4 from R5C2 to R5C4.

LEFT, ABOVE, RIGHT and BELOW stand for all cells next to the formula cell in one direction: SUM(ABOVE) adds up the cells above the formula cell, and SUM(LEFT) the cells to its left.

A table with SUM(...) in the cell at R3 and C3 and the four highlighted, numbered ranges that LEFT, ABOVE, RIGHT and BELOW refer to.

Handle Invalid Formulas and Number Formats

A formula with invalid syntax or missing arguments throws a FormulaException, and a number format with invalid syntax throws a NumberFormatException. Both have a CharacterIndex property with the one-based position of the first invalid character. Catch them when formulas or formats come from users or from data:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();

    tx.Tables.Add(3, 2, 10);
    TableCell total = tx.Tables.GetItem(10).Cells.GetItem(3, 2);

    // A formula and a number format as a user might enter them
    string formula = "SUM(B1:B2";
    string numberFormat = "#.00";

    try
    {
        total.CellFormat = new TableCellFormat()
        {
            TextType = TextType.Number,
            NumberFormat = numberFormat
        };

        total.Formula = formula;
    }
    catch (FormulaException exc)
    {
        Console.WriteLine($"Invalid formula at character {exc.CharacterIndex}: {formula}");
    }
    catch (NumberFormatException exc)
    {
        Console.WriteLine($"Invalid number format at character {exc.CharacterIndex}: {numberFormat}");
    }
}

The closing parenthesis of the formula is missing, so the program prints the position where the formula becomes invalid. FormulaException.CharacterIndex and NumberFormatException.CharacterIndex count from 1, so the first character of the string is 1.

Number Formats

A number format decides how the number in a cell is displayed, whether it was typed in, set by your code or calculated by a formula. It applies when TableCellFormat.TextType is Number, and TableCellFormat.NumberFormat holds the format string. Without a format, NumberFormat is an empty string, and a result is shown as it is: a whole number without decimal places, a decimal number with all of its decimal places.

A format string is made of the placeholders # and 0, the separators . and ,, the percent sign % and other characters or text. Only formats for positive numbers are supported. TableCollection.SupportedNumberFormats returns a list of often used formats.

Decimal and Thousands Separators

In a format string, and in the text of a cell whose type is Number, the period is the decimal separator and the comma the thousands separator, whatever the language. The displayed number uses the separators of the system language instead: on a German system, the decimal separator is shown as a comma and the thousands separator as a period. So you always write . and , in code, and the reader sees the separators they are used to.

Placeholders and Decimal Places

# and 0 stand for the digits of the number. # shows only the digits the number has, and 0 fills a position with a zero when the number has no digit there:

NumberFormatDisplayed
7#7
1234#1234
7000007
789000789

A period after the placeholders shows decimal places. The number of placeholders after the period is the maximum number of decimal places, and 0 shows a decimal place even when it is zero. Without a period, the number is rounded to an integer:

NumberFormatDisplayed
12.6#.##12.6
5#.005.00
12.6#.0012.60
0.543#.000.54
17.548#18

Thousands Separator and Scaling

A comma followed by placeholders inserts a thousands separator after every third digit. A comma with no placeholder after it divides the number by 1,000 instead, once for each such comma, which shows large amounts in thousands or millions:

NumberFormatDisplayed
9876543210.1234,###.##9,876,543,210.12
9876543210,###9,876,543,210
12200000#,,12
12200000#.0,,12.2

Percentages

A % together with at least one placeholder multiplies the number by 100 and shows the percent sign where it stands in the format:

NumberFormatDisplayed
0.52#%52%
1#%100%
0.003210.##%0.32%
0.52%#%52

Symbols and Text

Some characters can be written into a format as they are: $, -, +, /, (, ), :, !, ^, &, ', ~, {, }, <, >, = and the space. Any other single character, such as a currency symbol or a letter, needs a backslash in front of it. A digit that isn't a placeholder needs one too, as in \3. Words and longer text go in double quotes:

NumberFormatDisplayed
500#$500$
1-#.00-1.00
12.345+.##+12.35
4\1/#1/4
7-(#+\3)-(7+3)
50#:"50"50:50
7.654~#.0~7.7
67{-#}{-67}
5.6#>=\66>=6
12#.00 $12.00 $
12.99#.00\€12.99€
42\"#\""42"
18#:\X18:X
4.50#.00"€"4.50€
76# "Units"76 Units

The blog article Formatting Numbers in Table Cells shows number formats applied to typed text, formula results and fields in table cells.

Nest Tables

A table can be inserted into a cell of another table. Place the input position in the cell, without selecting its text, and call Tables.Add:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();

    tx.Tables.Add(3, 3, 10);
    Table outerTable = tx.Tables.GetItem(10);

    // Put the input position into cell (1, 1) without selecting text
    outerTable.Cells.GetItem(1, 1).Select();
    tx.Selection.Length = 0;

    tx.Tables.Add(2, 2, 11);
    Table nestedTable = outerTable.NestedTables.GetItem(11);

    foreach (TableCell cell in nestedTable.Cells)
    {
        cell.Text = "Nested";
    }

    Console.WriteLine($"Nesting level {nestedTable.NestedLevel}, outer table {nestedTable.OuterTable.ID}");

    tx.Save("nested.pdf", StreamType.AdobePDF);
}

Table.NestedTables returns the tables inside a table. NestedLevel is 1 for a table that isn't nested, and OuterTable returns the table that contains a nested table, or null for a table at the top level.

Read the Tables of a Loaded Document

The tables of a loaded document are in the same collection. The following program prints the size of every table of Documents/sample.docx and the text of its cells:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();
    tx.Load("Documents/sample.docx", StreamType.WordprocessingML);

    Console.WriteLine($"Tables: {tx.Tables.Count}");

    foreach (Table table in tx.Tables)
    {
        Console.WriteLine($"Table {table.ID}: {table.Rows.Count} rows, {table.Columns.Count} columns, level {table.NestedLevel}");

        foreach (TableCell cell in table.Cells)
        {
            Console.WriteLine($"  ({cell.Row}, {cell.Column}): {cell.Text}");
        }
    }
}

Tables in a loaded document may have no ID. For each of them, ServerTextControl raises the TableCreated event while the document is loaded. The blog article Export Document Tables to CSV in .NET C# writes the tables of a document to CSV files in the same way.

Remove Tables

TableBaseCollection.Remove with an ID removes that table and returns true if it was removed. TableCollection.Remove without a parameter removes the table at the input position, and Clear removes all tables of the collection:

// Remove the table with the ID 10
bool removed = tx.Tables.Remove(10);

// Remove all tables of the main text
tx.Tables.Clear();

Convert Between Tables and Text

Plain text formats and some older documents lay out columns with tab stops instead of a table. A table becomes such text when the text of its cells is joined with tab characters, one paragraph per row, and the left edges of its columns become the tab stops. The following program converts a price table this way and saves the result as price-list.tx:

using System.Text;
using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();

    tx.Tables.Add(3, 3, 10);
    Table table = tx.Tables.GetItem(10);

    string[,] data =
    {
        { "Product", "Quantity", "Price" },
        { "TX Text Control .NET Server License", "1", "199.99" },
        { "Priority Support - 12 Months", "1", "99.98" }
    };

    for (int row = 1; row <= 3; row++)
    {
        for (int column = 1; column <= 3; column++)
        {
            table.Cells.GetItem(row, column).Text = data[row - 1, column - 1];
        }
    }

    // Columns of 3, 1 and 1 inches
    table.Columns.GetItem(1).Width = 3 * 1440;
    table.Columns.GetItem(2).Width = 1440;
    table.Columns.GetItem(3).Width = 1440;

    // One line of tab-separated cell texts per row
    StringBuilder text = new StringBuilder();

    for (int row = 1; row <= table.Rows.Count; row++)
    {
        List<string> cells = new List<string>();

        for (int column = 1; column <= table.Columns.Count; column++)
        {
            cells.Add(table.Cells.GetItem(row, column).Text);
        }

        text.Append(string.Join("\t", cells)).Append("\r\n");
    }

    // A tab stop at the left edge of each column except the first
    int[] tabPositions = tx.Selection.ParagraphFormat.TabPositions;
    Array.Clear(tabPositions);

    for (int column = 2; column <= table.Columns.Count; column++)
    {
        tabPositions[column - 2] = table.Columns.GetItem(column).Position;
    }

    // Replace the table with the text
    int start = table.Cells.GetItem(1, 1).Start - 1;
    tx.Tables.Remove(10);

    tx.Select(start, 0);
    tx.Selection.Text = text.ToString();

    tx.Select(start, text.Length);
    tx.Selection.ParagraphFormat.TabPositions = tabPositions;

    tx.Save("price-list.tx", StreamType.InternalUnicodeFormat);
}

The code works as follows:

  • TableColumn.Position is the horizontal position of a column in twips. The first column starts at 0, so only the other columns need a tab stop.
  • ParagraphFormat.TabPositions is an array of 14 absolute positions in twips, by default multiples of 1134 twips (2 cm). The program reads the array to get its length, clears the default positions and sets its own. Positions of 0 are no tab stops.
  • Tables.Remove with the ID removes the table, and the text is inserted where its first cell was.

The reverse direction reads tab-separated paragraphs, removes them and inserts a table with one row per paragraph and one column per tab-separated part. The tab stops of the first paragraph become the positions of the columns. The following program converts the three paragraphs of price-list.tx back into a table:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();
    tx.Load("price-list.tx", StreamType.InternalUnicodeFormat);

    // Paragraphs 1 to 3 contain tab-separated text
    Paragraph first = tx.Paragraphs[1];
    Paragraph last = tx.Paragraphs[3];

    List<string[]> rows = new List<string[]>();

    for (int i = 1; i <= 3; i++)
    {
        rows.Add(tx.Paragraphs[i].Text.TrimEnd('\r', '\n').Split('\t'));
    }

    int columns = rows.Max(cells => cells.Length);
    int[] tabPositions = first.Format.TabPositions;

    // Remove the paragraphs and insert a table in their place
    int start = first.Start - 1;
    tx.Select(start, last.Start - 1 + last.Length - start);
    tx.Selection.Text = "";

    tx.Tables.Add(rows.Count, columns, 11);
    Table table = tx.Tables.GetItem(11);

    for (int row = 1; row <= rows.Count; row++)
    {
        for (int column = 1; column <= rows[row - 1].Length; column++)
        {
            table.Cells.GetItem(row, column).Text = rows[row - 1][column - 1];
        }
    }

    // The tab stops become the positions of columns 2, 3, ...
    for (int column = 2; column <= columns && tabPositions[column - 2] > 0; column++)
    {
        table.Columns.GetItem(column).Position = tabPositions[column - 2];
    }

    tx.Save("price-table.pdf", StreamType.AdobePDF);
}

Paragraph.Text returns the text of a paragraph with its paragraph break, which TrimEnd removes before the text is split at the tab characters. Paragraph.Format returns the paragraph's format with its tab stops. Both programs handle simple tables only: merged cells and nested tables have no equivalent in tab-separated text. The blog article Text to Table and Table to Text in TX Text Control and C# shows the same conversion for a selection in an editor.

Next Steps

  • Merge Blocks: repeat table rows for each record of a data source.
  • Text: format the text in table cells.
  • Sections: give a wide table a page in landscape orientation.
  • Formula Functions: look up the arguments of every function a formula can call.
  • Tables in the Document Editor: work with tables from JavaScript.