Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

RichTextBoxDocument

4.94/5 (20 votes)
3 Oct 2009CPOL6 min read 95.6K   6.9K  
A PrintDocument class for printing and previewing RichTextBox controls
Image 1

Introduction

This article describes the implementation of a RichTextBoxDocument class that allows printing and previewing RichTextBox controls.

Background

The RichTextBox control is useful and very powerful. It is also easy to use and to extend using the underlying DLL that implements most of its functionality (the DLL is usually riched20.dll, but may vary depending on the system).

There are many articles on CodeProject that discuss enhancements to the standard RichTextBox, most dealing with improved commands for styling the contents, supporting tables, and so on.

One of the few missing features in the RichTextBox is the ability to preview and print its contents. I recently posted an article describing the implementation of an enhanced PrintPreviewDialog, and people asked me about using it to preview RichTextBox documents. That prompted me to write the RichTextBoxDocument class presented here.

The RichTextBoxDocument inherits from the PrintDocument class and implements the methods needed to render RichTextBox controls into the document. It can be used with the standard PrintPreview and PrintPreviewDialog controls, and also with the enhanced PrintPreviewDialog presented in my original article.

Using the Code

To use the RichTextBoxDocument class, start by adding a copy of the RichTextBoxDocument.cs file to your project. Then create an instance of the class passing a reference to the RichTextBox control you want to render. Optionally set the Header and Footer properties, then print or preview the document using a print preview dialog or the document's own Print method.

For example:

C#
// create the document passing the RichTextBox to be rendered
var doc = new RichTextBoxDocument(richTextBox1);

// set document header and footer
doc.Header = string.Format("\tDocument {0}", richTextBox1.Name);
doc.Footer = string.Format("{0}\t{1}\tPage [page] of [pages]",
    DateTime.Today.ToShortDateString(),
    DateTime.Now.ToShortTimeString());

// preview the document
using (var dlg = new PrintPreviewDialog())
{
    dlg.Document = doc;
    dlg.ShowDialog(this);
}

The RichTextBoxDocument Class

The RichTextBoxDocument class inherits from PrintDocument and overrides the document rendering methods to render the contents of a RichTextBox. It also allows the caller to specify headers and footers to be added to each page.

OnPrintPage Implementation

The core of the RichTextBoxDocument class is an OnPrintPage method that renders RTF content, headers, and footers on each page of the document. The code looks like this:

C#
// render a page into the PrintDocument
protected override void OnPrintPage(PrintPageEventArgs e)
{
    // update current page
    _currentPage++;

    // render RTF content
    FORMATRANGE fmt = GetFormatRange(e, _firstChar);
    int nextChar = FormatRange(_rtb, true, ref fmt);
    e.Graphics.ReleaseHdc(fmt.hdc);

    // render header
    if (!string.IsNullOrEmpty(Header))
    {
        var rc = e.MarginBounds;
        rc.Y = 0;
        rc.Height = e.MarginBounds.Top;
        RenderHeaderFooter(e, Header, HeaderFont, rc);
        e.Graphics.DrawLine(Pens.Black, rc.X, rc.Bottom, rc.Right, rc.Bottom);
    }

    // render footer
    if (!string.IsNullOrEmpty(Footer))
    {
        var rc = e.MarginBounds;
        rc.Y = rc.Bottom;
        rc.Height = e.PageBounds.Bottom - rc.Y;
        RenderHeaderFooter(e, Footer, FooterFont, rc);
        e.Graphics.DrawLine(Pens.Black, rc.X, rc.Y, rc.Right, rc.Y);
    }

    // check whether we're done
    e.HasMorePages = nextChar > _firstChar && nextChar < _rtb.TextLength;

    // save start char for next time
    _firstChar = nextChar;

    // fire event as usual
    base.OnPrintPage(e);
}

The code performs the following tasks:

  1. Update the current page index. This value may be used in the headers and footers.
  2. Render the RTF content on the page using the GetFormatRange and FormatRange methods described below.
  3. Render the header and footer for this page. Headers and footers are specified as strings that may contain up to three tab-separated parts. The first part is left-aligned, the second is center-aligned, and the last is right-aligned. Headers and footers may contain tags that are replaced with the current page and page count (e.g. "\t\tPage [page] of [pages]").
  4. Figure out whether there are more pages to print.

Rendering RTF

The RichTextBox control is a wrapper around the riched20.dll (or some other version depending on your system). This underlying DLL can render its content into any device using the EM_FORMATRANGE message.

The EM_FORMATRANGE message allows the caller to specify a document range to be rendered, a target device, and a target rectangle. It returns the index of the first character that did not fit the target rectangle so you the caller can continue printing on the next page.

The RichTextBoxDocument class uses two helper methods to wrap calls to the EM_FORMATRANGE message. The first method is called GetFormatRange. It creates and initializes a FORMATRANGE structure with the document's target device and margin bounds. Here is the implementation:

C#
// build a FORMATRANGE structure with the proper page size and hdc
// (the hdc must be released after the FORMATRANGE is used)
FORMATRANGE GetFormatRange(PrintPageEventArgs e, int firstChar)
{
    // get page rectangle in twips (RichEd20.dll uses twips)
    var rc = e.MarginBounds;
    rc.X = (int)(rc.X * 14.4 + .5);
    rc.Y = (int)(rc.Y * 14.4 + .5);
    rc.Width = (int)(rc.Width * 14.4 + .5);
    rc.Height = (int)(rc.Height * 14.40 + .5);

    // set up FORMATRANGE structure with the target device/rect
    var fmt = new FORMATRANGE();
    fmt.hdc = fmt.hdcTarget = e.Graphics.GetHdc();
    fmt.rc.SetRect(rc);
    fmt.rcPage = fmt.rc;

    // specify the document range to render
    fmt.cpMin = firstChar;
    fmt.cpMax = -1;

    // done
    return fmt;
}

The method starts by converting the document's MarginBounds rectangle into twips (the measurement unit used by riched20.dll). Then it creates a FORMATRANGE structure and initializes it with the target device and target rectangle. Finally, it specifies the range of the document to be rendered.

Document ranges are specified as character offsets, and -1 indicates tells the DLL to print as much as will fit into the target rectangle.

Note that GetFormatRange invokes the GetHdc method to specify the target device. After the FORMATRANGE structure has been used, this hdc must be released with a call to ReleaseHdc.

Once the FORMATRANGE structure is ready, we can use it with the FormatRange method to render or measure the document. The FormatRange implementation is very simple because it delegates all the work to the riched20.dll:

C#
// send the EM_FORMATRANGE message to the RichTextBox to render or measure
// a range of the document into a target specified by a FORMATRANGE structure.
int FormatRange(RichTextBox rtb, bool render, ref FORMATRANGE fmt)
{
    // render or measure part of the document
    int nextChar = SendMessageFormatRange(
        rtb.Handle,
        EM_FORMATRANGE,
        render ? 1 : 0,
        ref fmt);

    // reset after rendering/measuring
    SendMessage(rtb.Handle, EM_FORMATRANGE, 0, 0);

    // return next character to print
    return nextChar;
}

Note that each call to this method sends the EM_FORMATRANG message twice. The first one does the work (render or measure the RTF content). The second call is required by the riched20.dll to reset itself internally.

Rendering Headers and Footers

One of the requirements for this project was the ability to render headers and footers in addition to RTF content. This is pretty easy to do. After rendering the RTF, we take a string, split it to get the left, center, and right aligned parts, then draw it on the page using the DrawString method. The code below shows the code:

C#
// render a header or a footer on the current page
void RenderHeaderFooter(PrintPageEventArgs e, string text, Font font, Rectangle rc)
{
    var parts = text.Split('\t');

    // render left-aligned part
    if (parts.Length > 0)
        RenderPart(e, parts[0], font, rc, StringAlignment.Near);

    // render center-aligned part
    if (parts.Length > 1)
        RenderPart(e, parts[1], font, rc, StringAlignment.Center);

    // render right-aligned part
    if (parts.Length > 2)
        RenderPart(e, parts[2], font, rc, StringAlignment.Far);
}

This part of the code splits a string into three pieces, then renders each one separately with a different alignment. This allows the user to specify headers and footers with left, center, and right-aligned parts. Here is the implementation of the RenderPart method which does most of the work:

C#
// special tags for headers/footers
const string PAGE = "[page]";
const string PAGES = "[pages]";

// render a part of a header or footer on the page
void RenderPart(PrintPageEventArgs e, string text, Font font,
                Rectangle rc, StringAlignment align)
{
    // replace wildcards
    text = text.Replace(PAGE, _currentPage.ToString());
    text = text.Replace(PAGES, _pageCount.ToString());

    // prepare string format
    StringFormat fmt = new StringFormat();
    fmt.Alignment = align;
    fmt.LineAlignment = StringAlignment.Center;

    // render footer
    e.Graphics.DrawString(text, font, Brushes.Black, rc, fmt);
}

The code is straight GDI+. The interesting part is the beginning, where it replaces wildcards with the current page and page count. This allows the caller to create headers and footers with "Page n of m" content.

The tricky part here is getting the page count. This value is not known in advance, but fortunately we can calculate it easily using the FormatRange method described above. Here's how to do it:

C#
// get a page count by using FormatRange to measure the content
int GetPageCount(PrintPageEventArgs e)
{
    int pageCount = 0;

    // count the pages using FormatRange
    FORMATRANGE fmt = GetFormatRange(e, 0);
    for (int firstChar = 0; firstChar < _rtb.TextLength; )
    {
        fmt.cpMin = firstChar;
        firstChar = FormatRange(_rtb, false, ref fmt);
        pageCount++;
    }
    e.Graphics.ReleaseHdc(fmt.hdc);

    // done
    return pageCount;
}

Note that the second parameter in the call to FormatRange is set to false. This causes the method to measure the content but not render it. Counting the pages this way is easy and very fast (measuring is a lot faster than rendering).

Even though counting the pages is relatively fast, we only want to do that when the value will actually be used. To this end, the OnBeginPrint method scans the Header and Footer strings to detect whether either one uses the PAGES tag that needs to be replaced with the page count:

C#
// start printing the document
protected override void OnBeginPrint(PrintEventArgs e)
{
    // we haven't printed anything yet
    _firstChar = 0;
    _currentPage = 0;

    // check whether we need a page count
    _pageCount = 0;
    if (Header.IndexOf(PAGES) > -1 ||
        Footer.IndexOf(PAGES) > -1)
    {
        _pageCount = -1; // need to calculate this
    }

    // fire event as usual
    base.OnBeginPrint(e);
}

This covers all the interesting parts of the RichTextBoxDocument implementation. The class is quite simple and should be easily customizable for those who require additional functionality. For example, it might be interesting to allow users to specify the number of columns to print.

Previewing with the CoolPrintPreview

Although the RichTextBoxDocument class can be used with the regular PrintPreviewDialog class, the sample included with this article uses a CoolPrintPreviewDialog instead.

The main advantage of using a CoolPrintPreviewDialog here is that it shows the pages as they are generated, while the standard PrintPreviewDialog needs to generate the entire document before anything is shown. This is a huge advantage if you are dealing with documents with more than 20 or so pages.

The CoolPrintPreviewDialog source code is included in the sample for convenience. The class is described in detail in a separate CodeProject article which you can find here: An Enhanced PrintPreviewDialog.

History

  • 2nd October, 2009: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)