TX Text Control .NET Server

Paragraphs

Topic

How text is laid out on a page is decided paragraph by paragraph: its alignment, indents, spacing, page breaks, style, list numbering and writing direction. Your code reads the paragraphs of a document, formats them, defines paragraph styles that keep documents consistent, turns paragraphs into bulleted, numbered and structured lists and sets right-to-left paragraphs for Arabic and Hebrew text.

Character attributes such as fonts and colors are described in Text, and Create a Document shows a first centered paragraph with a frame. Paste an example into Program.cs of a console application to run it.

How Paragraphs Work

A paragraph ends with a carriage return and a line feed (\r\n). ServerTextControl.Paragraphs contains the paragraphs of the main text; headers, footers and text frames have their own Paragraphs collection. Every document has at least one paragraph, and paragraphs are numbered from 1.

A Paragraph has these properties:

PropertyContains
NumberThe number of the paragraph, starting at 1
Start, LengthThe one-based position of its first character and its number of characters, including the paragraph end
Text, LinesIts text and its number of lines
FormatIts formatting, as a ParagraphFormat object
FormattingStyleThe name of its paragraph style
ListFormat, ListNumberTextIts list formatting, and the number of a numbered list item with the text before and after it

The following program prints the paragraphs of Documents/sample.docx with their style, number of lines and alignment, and saves the first paragraph as HTML:

using TXTextControl;

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

    Console.WriteLine($"{tx.Paragraphs.Count} paragraphs");

    foreach (Paragraph paragraph in tx.Paragraphs)
    {
        Console.WriteLine($"#{paragraph.Number} \"{paragraph.FormattingStyle}\", " +
            $"{paragraph.Lines} line(s), {paragraph.Format.Alignment}: " +
            $"{paragraph.ListNumberText}{paragraph.Text}");
    }

    // The paragraph at text position 0
    Paragraph first = tx.Paragraphs.GetItem(0);

    if (first != null)
    {
        string html;
        first.Save(out html, StringStreamType.HTMLFormat);
        Console.WriteLine(html);
    }
}

tx.Paragraphs[1] returns a paragraph by its number. GetItem returns the paragraph at a text position, which starts at 0, or null. Paragraph.Save saves the paragraph to a string or a byte array. For selected text, Selection.ParagraphFormat gets and sets the formatting of all selected paragraphs.

Format Paragraphs

Assign a ParagraphFormat object to Paragraph.Format to format a paragraph. The following program formats a heading, a body paragraph, a note with a frame and an appendix that starts on a new page:

using TXTextControl;

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

    tx.Text = "Terms and Conditions\r\n" +
              "These terms apply to all orders placed with our company. Please read them carefully before you place an order.\r\n" +
              "Note: Prices are subject to change without notice.\r\n" +
              "Appendix";

    // Heading: centered, 12 pt space after it, on the same page as the next paragraph
    tx.Paragraphs[1].Format = new ParagraphFormat(HorizontalAlignment.Center)
    {
        BottomDistance = 240, // 240 twips = 12 pt
        KeepWithNext = true
    };

    // Body: justified, indented on both sides, first line indented further, 1.5 line spacing
    tx.Paragraphs[2].Format = new ParagraphFormat()
    {
        Alignment = HorizontalAlignment.Justify,
        LeftIndent = 720,     // 0.5 inch
        RightIndent = 720,
        HangingIndent = -360, // negative: the first line is indented by another 0.25 inch
        LineSpacing = 150,    // percent of the font size
        BottomDistance = 120,
        KeepLinesTogether = true
    };

    // Note: background color and a box frame
    tx.Paragraphs[3].Format = new ParagraphFormat()
    {
        BackColor = System.Drawing.Color.LightYellow,
        Frame = Frame.Box,
        FrameDistance = 60,
        FrameLineWidth = 10
    };

    // Appendix: always starts at the top of a page
    tx.Paragraphs[4].Format = new ParagraphFormat() { PageBreakBefore = true };

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

All distances are measured in twips, 1/1440 inch. The properties of ParagraphFormat:

PropertySetsDefault
AlignmentLeft, Right, Center or JustifyLeft
LeftIndent, RightIndentThe indents from the left and right edge of the text area0
HangingIndentThe hanging indent; a negative value indents the first line further0
TopDistance, BottomDistanceThe space before and after the paragraph0
LineSpacingThe line spacing in percent of the font size, from 1 to 10000100
AbsoluteLineSpacingA fixed line spacing in twips; 0 takes it from the font0
KeepLinesTogetherNo page break inside the paragraphfalse
KeepWithNextThe paragraph stays on the page of the next paragraphfalse
PageBreakBeforeThe paragraph always starts at the top of a pagefalse
WidowOrphanLinesHow many lines of the paragraph must not be separated from the rest at a page break, from 0 (off) to 30
Frame, FrameStyle, FrameLineWidth, FrameLineColor, FrameDistanceLines around the paragraph with their style, width, color and distance from the textNo frame, 15 twips
BackColorThe background colorTransparent
DirectionLeftToRight or RightToLeft textLeftToRight

A heading that must start on a new page is better served by PageBreakBefore than by a page break character: when the text before it grows or shrinks, a forced page break can leave an empty page, as the blog article Document Templates Tip: Say No to Forced Page Breaks shows.

Tab stops are set with three arrays of ParagraphFormat: TabPositions with the positions in twips, TabTypes with the types LeftTab, RightTab, CenterTab, DecimalTab or RightBorderTab and TabLeaders with the values None, Period, Dot, Dash or Underline, which fill the space before a tab stop. A paragraph has up to 14 tab stops.

Use Paragraph Styles

A ParagraphStyle combines character attributes and paragraph formatting under a name. Paragraphs that use a style change when the style changes, which keeps all headings or body paragraphs of a document alike. The following program creates styles for headings and body text, applies them and then changes the color of the headings:

using TXTextControl;

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

    tx.Text = "Delivery Terms\r\n" +
              "Shipping\r\n" +
              "Goods are shipped within five business days.\r\n" +
              "Payment Terms\r\n" +
              "Invoices are payable within 30 days.";

    // Heading style, based on the root style [Normal]
    ParagraphStyle heading = new ParagraphStyle("Contract Heading");
    heading.FontName = "Arial";
    heading.FontSize = 320; // 16 pt
    heading.Bold = true;
    heading.ParagraphFormat.TopDistance = 240;
    heading.ParagraphFormat.BottomDistance = 120;
    heading.ParagraphFormat.KeepWithNext = true;
    tx.ParagraphStyles.Add(heading);

    // Subheading style, based on the heading style
    ParagraphStyle subheading = new ParagraphStyle("Contract Subheading", "Contract Heading");
    subheading.FontSize = 280; // 14 pt
    tx.ParagraphStyles.Add(subheading);

    // Body text style
    ParagraphStyle body = new ParagraphStyle("Contract Body");
    body.ParagraphFormat.Alignment = HorizontalAlignment.Justify;
    body.ParagraphFormat.LineSpacing = 120;
    tx.ParagraphStyles.Add(body);

    // Apply the styles to the paragraphs
    tx.Paragraphs[1].FormattingStyle = "Contract Heading";
    tx.Paragraphs[2].FormattingStyle = "Contract Subheading";
    tx.Paragraphs[3].FormattingStyle = "Contract Body";
    tx.Paragraphs[4].FormattingStyle = "Contract Heading";
    tx.Paragraphs[5].FormattingStyle = "Contract Body";

    // Change a style later: every paragraph that uses it changes
    ParagraphStyle existing = tx.ParagraphStyles.GetItem("Contract Heading");

    if (existing != null)
    {
        existing.ForeColor = System.Drawing.Color.DarkBlue;
        existing.Apply();
    }

    tx.Save("styles.docx", StreamType.WordprocessingML);
}

The code works as follows:

  • The ParagraphStyle constructor with a name creates a style based on the root style [Normal], which uses Arial at 10 pt by default. With the name of a base style as second parameter, the new style inherits the attributes of that style.
  • The character attributes, such as FontName and Bold, and ParagraphStyle.ParagraphFormat define the style.
  • ParagraphStyles.Add adds the style to the document, and Paragraph.FormattingStyle applies it to a paragraph by its name.
  • GetItem returns a style by its name, or null. After changing a style, Apply applies it to the document.

Documents loaded from MS Word bring their styles along, such as "Heading 1", and Paragraph.FormattingStyle returns their names. ParagraphFormat.StructureLevel gives a style or paragraph a level between 0 and 10, where 0 is body text and the levels 1 to 10 are the ones a table of contents is generated from, as Tables of Contents describes. The blog article Applying and Updating Formatting Styles from Master Templates (DOTX) in DOCX Documents copies the styles of one document to others.

List, Change and Remove Styles

ServerTextControl.ParagraphStyles is a ParagraphStyleCollection, and ServerTextControl.InlineStyles, with the character styles described in Use Inline Styles, an InlineStyleCollection. Both derive from FormattingStyleCollection. The following program lists the paragraph styles of a document with their base styles and removes one of them:

using TXTextControl;

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

    foreach (ParagraphStyle style in tx.ParagraphStyles)
    {
        // null for the root style
        string baseStyle = style.BaseStyle?.Name ?? "(none)";
        Console.WriteLine($"{style.Name}, based on {baseStyle}, followed by {style.FollowingStyle}");
    }

    // The heading paragraphs are followed by body text when a user presses ENTER
    ParagraphStyle heading = tx.ParagraphStyles.GetItem("Contract Heading");

    if (heading != null)
    {
        heading.FollowingStyle = "Contract Body";
    }

    // true if the style was removed
    bool removed = tx.ParagraphStyles.Remove("Contract Subheading");
    Console.WriteLine($"Style removed: {removed}");

    tx.Save("styles-changed.docx", StreamType.WordprocessingML);
}

The code works as follows:

  • BaseStyle returns the style a style is based on, a ParagraphStyle or an InlineStyle, and null for the root style. It's read only; the base style is set with the constructor.
  • FollowingStyle is the name of the style the next paragraph gets when a user presses ENTER at the end of a paragraph with this style. It's saved with the document and matters when the document is edited, for example in the Document Editor.
  • Remove takes the name of a style and returns true if the style was removed.

Inline styles have two more methods. InlineStyle.IsInheritedFromParagraph tells whether attributes of the style come from the surrounding paragraph, and InlineStyle.ResetToParagraph makes attributes follow the paragraph again. Both take a combination of InlineStyle.Attributes values, such as InlineStyle.Attributes.FontName | InlineStyle.Attributes.FontSize. Selection.RemoveInlineStyles removes the inline styles from the selected text.

Create Bulleted and Numbered Lists

A ListFormat turns a paragraph into a list item. The following program creates a bulleted list, a numbered list, a second numbered list that starts again with a) and a structured list whose items show the numbers of their parent levels:

using TXTextControl;

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

    tx.Text = "Required documents:\r\n" + // paragraph 1
              "Passport\r\n" +
              "Birth certificate\r\n" +
              "Steps:\r\n" +              // paragraph 4
              "Fill in the form\r\n" +
              "Sign the form\r\n" +
              "Later:\r\n" +              // paragraph 7
              "Wait for confirmation\r\n" +
              "Pay the fee\r\n" +
              "Contents:\r\n" +           // paragraph 10
              "Scope\r\n" +
              "Definitions\r\n" +
              "Obligations";

    // Bulleted list
    ListFormat bullets = new ListFormat(ListType.Bulleted);
    tx.Paragraphs[2].ListFormat = bullets;
    tx.Paragraphs[3].ListFormat = bullets;

    // Numbered list: 1. 2.
    ListFormat numbers = new ListFormat(ListType.Numbered) { TextAfterNumber = "." };
    tx.Paragraphs[5].ListFormat = numbers;
    tx.Paragraphs[6].ListFormat = numbers;

    // Numbered lists continue by default: restart the numbering, with letters a) b)
    tx.Paragraphs[8].ListFormat = new ListFormat(ListType.Numbered)
    {
        NumberFormat = NumberFormat.Letters,
        TextAfterNumber = ")",
        RestartNumbering = true
    };
    tx.Paragraphs[9].ListFormat = new ListFormat(ListType.Numbered)
    {
        NumberFormat = NumberFormat.Letters,
        TextAfterNumber = ")"
    };

    // Structured list: the numbers of higher levels are shown in front of lower levels, 1. 1.1. 2.
    tx.Paragraphs[11].ListFormat = new ListFormat(ListType.Structured)
    {
        Level = 1,
        NumberFormat = NumberFormat.ArabicNumbers,
        RestartNumbering = true
    };
    tx.Paragraphs[12].ListFormat = new ListFormat(ListType.Structured) { Level = 2, NumberFormat = NumberFormat.ArabicNumbers };
    tx.Paragraphs[13].ListFormat = new ListFormat(ListType.Structured) { Level = 1, NumberFormat = NumberFormat.ArabicNumbers };

    foreach (Paragraph paragraph in tx.Paragraphs)
    {
        Console.WriteLine($"{paragraph.ListNumberText} {paragraph.Text}");
    }

    tx.Save("lists.docx", StreamType.WordprocessingML);
}

Saved as PDF, the document shows the four lists: bullets, the numbers 1. and 2., the letters a) and b) of the restarted list, and the structured list 1., 1.1. and 2., whose second-level item carries the number of its parent in front of its own. The structured list sets NumberFormat explicitly: without it, the Core engine continues the letters of the list before it and numbers the items a., a.a. and b.

The result of the program: a bulleted list, a numbered list with 1. and 2., a restarted list with a) and b) and a structured list numbered 1., 1.1. and 2.

The properties of ListFormat:

PropertySetsDefault
TypeNone, Bulleted, Numbered or StructuredNone
NumberFormatArabicNumbers (1, 2, 3), Letters (a, b, c), CapitalLetters, RomanNumbers or SmallRomanNumbersArabicNumbers
FirstNumber, RestartNumberingThe first number, and whether a new numbered list begins1, false
LevelThe list level, from 1 to 101
TextBeforeNumber, TextAfterNumberText before and after the number, up to 20 characters—
BulletCharacter, FontNameThe bullet character and its font—
HangingIndentThe distance between the number or bullet and the text, in twips360

A paragraph style can carry a list format of its own through ParagraphStyle.ListFormat. The blog article Convert Plain Text to Bulleted Lists in C# with .NET turns lines that start with a hyphen into list items.

Write Right-to-Left Paragraphs

Arabic and Hebrew are written from right to left. The writing direction is a paragraph attribute, ParagraphFormat.Direction, with the values LeftToRight, the default, and RightToLeft of the Direction enumeration. In a right-to-left paragraph, blocks of left-to-right and right-to-left characters are ordered from right to left. The following program adds text from a file to a heading and turns every paragraph after the heading to right to left:

using TXTextControl;

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

    // Arabic or Hebrew text from a UTF-8 file
    string rightToLeftText = File.ReadAllText("Documents/rtl.txt");
    tx.Text = "Invoice INV-2025-001\r\n" + rightToLeftText;

    // Select everything from the second paragraph to the end of the text
    int start = tx.Paragraphs[2].Start - 1;
    tx.Select(start, tx.Text.Length - start);

    // Right to left, with left and right alignment and tab stops mirrored
    if (tx.Selection.ChangeDirection(Direction.RightToLeft))
    {
        Console.WriteLine("The direction of the selected paragraphs was changed.");
    }

    tx.Save("right-to-left.pdf", StreamType.AdobePDF);
}

The code works as follows:

  • Paragraph.Start is one-based, so 1 is subtracted before the position is passed to Select.
  • Selection.ChangeDirection sets the direction of all selected paragraphs and returns true if at least one paragraph was changed. It also mirrors their alignment, so a left-aligned paragraph becomes right-aligned, and it swaps left and right tab stops. Centered and justified paragraphs keep their alignment.
  • Setting Direction in a ParagraphFormat changes only the direction: a left-aligned paragraph stays left-aligned. Use it when you set the alignment yourself, for example new ParagraphFormat() { Direction = Direction.RightToLeft, Alignment = HorizontalAlignment.Right }.

Justified lines with Arabic or Hebrew characters can be filled in another way. ParagraphFormat.Justification decides how the space of a justified line is filled, with a value of Justification:

ValueA justified line is filled with
Spaces (default)Additional space between the words
KashidaInserted Kashida lines
SpacesAndKashidaBoth
tx.SelectAll();
tx.Selection.ParagraphFormat.Alignment = HorizontalAlignment.Justify;
tx.Selection.ParagraphFormat.Justification = Justification.SpacesAndKashida;

For a selection whose paragraphs have different directions, Direction returns its default, LeftToRight. Spell checking and hyphenation use the language of the text, which you set with Selection.Culture, as Text shows.

Next Steps