RichTextBoxDocument






4.94/5 (19 votes)
A PrintDocument class for printing and previewing RichTextBox controls

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:
// 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:
// 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:
- Update the current page index. This value may be used in the headers and footers.
- Render the RTF content on the page using the
GetFormatRange
andFormatRange
methods described below. - 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]").
- 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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