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:
var doc = new RichTextBoxDocument(richTextBox1);
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());
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:
protected override void OnPrintPage(PrintPageEventArgs e)
{
_currentPage++;
FORMATRANGE fmt = GetFormatRange(e, _firstChar);
int nextChar = FormatRange(_rtb, true, ref fmt);
e.Graphics.ReleaseHdc(fmt.hdc);
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);
}
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);
}
e.HasMorePages = nextChar > _firstChar && nextChar < _rtb.TextLength;
_firstChar = nextChar;
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
and FormatRange
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:
FORMATRANGE GetFormatRange(PrintPageEventArgs e, int firstChar)
{
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);
var fmt = new FORMATRANGE();
fmt.hdc = fmt.hdcTarget = e.Graphics.GetHdc();
fmt.rc.SetRect(rc);
fmt.rcPage = fmt.rc;
fmt.cpMin = firstChar;
fmt.cpMax = -1;
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:
int FormatRange(RichTextBox rtb, bool render, ref FORMATRANGE fmt)
{
int nextChar = SendMessageFormatRange(
rtb.Handle,
EM_FORMATRANGE,
render ? 1 : 0,
ref fmt);
SendMessage(rtb.Handle, EM_FORMATRANGE, 0, 0);
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:
void RenderHeaderFooter(PrintPageEventArgs e, string text, Font font, Rectangle rc)
{
var parts = text.Split('\t');
if (parts.Length > 0)
RenderPart(e, parts[0], font, rc, StringAlignment.Near);
if (parts.Length > 1)
RenderPart(e, parts[1], font, rc, StringAlignment.Center);
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:
const string PAGE = "[page]";
const string PAGES = "[pages]";
void RenderPart(PrintPageEventArgs e, string text, Font font,
Rectangle rc, StringAlignment align)
{
text = text.Replace(PAGE, _currentPage.ToString());
text = text.Replace(PAGES, _pageCount.ToString());
StringFormat fmt = new StringFormat();
fmt.Alignment = align;
fmt.LineAlignment = StringAlignment.Center;
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:
int GetPageCount(PrintPageEventArgs e)
{
int pageCount = 0;
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);
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:
protected override void OnBeginPrint(PrintEventArgs e)
{
_firstChar = 0;
_currentPage = 0;
_pageCount = 0;
if (Header.IndexOf(PAGES) > -1 ||
Footer.IndexOf(PAGES) > -1)
{
_pageCount = -1;
}
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