Getting WYSIWYG Print Results from a .NET RichTextBox
Martin Müller
4voice AG
January 2003
Applies to:
Microsoft® Visual Studio .NET
Windows Forms: RichTextBox Control
Summary: How to use .NET interop mechanisms to add features common to the Win32 rich edit control (for example, WYSIWYG printing or convenient selection formatting) to a .NET RichTextBox. (17 printed pages)
Download the source code for this article: richtextboxprinting.exe
Contents
Introduction
Interop to the Rescue
Building the New RichTextBoxEx Class
Defining Required Win32 Structures
Implementing FormatRange()
Using the New RichTextBoxEx Class
Adding Formatting to the New Class
Conclusion
Introduction
Numerous developers have been able to print a rich edit control so that all of the formatting and embedded images appear on the printout, just the way that they appear on the screen. Using MFC, with C++, the task was simple—just create an MFC application using a Document/View architecture and select CRichEditView as the view class. Visual Studio's wizards built everything else needed.
However, what if a developer wanted to print a .NET RichTextBox class? All examples so far have showed how to use one of .NET's printing classes, PrintDocument, how to add event handlers for the PrintPage event, and how to draw the text using the DrawString member function of the Graphics object received in the event handler.
The previous method is fine when using just plain text or only adjusting the formatting for printing. However, usually the reason for using a RichTextBox is the need to make formatting there, and then get an adequate representation on paper as well. This article explains how to print the contents of a RichTextBox control to get the results you want.
Interop to the Rescue
Virtually all Windows Forms controls are built upon their Win32 counterparts. Looking at the underlying rich edit control, there is a message to have the rich edit control format a range of text for a specific device—exactly what is needed for printing a .NET RichTextBox class. Now we just need to know how to send this message (EM_FORMATRANGE) to the RichTextBox's rich edit control.
This is where the namespace feature, System.Runtime.InteropServices, comes to mind, offering something known as P/invoke, or Platform invoke. The .NET Framework Developer's Guide states that:
"Platform invoke is a service that enables managed code to call unmanaged functions implemented in dynamic-link libraries (DLLs), such as those in the Win32 API…"
The developers who know Visual Basic know how to simply declare an external function to be located in a DLL, and then call this function, just like on of his or her own. With P/invoke very similar results are received, with all .NET languages integrating unmanaged function calls into managed code almost seamlessly.
The system is quite easy to understand. Use the DllImportAttribute to tell .NET which functions to use, specify their signature, and then the CLR usually does the rest: locating and loading the DLL into memory, locating the function entry point, converting all the parameters to unmanaged types and pushing them onto the stack, transferring control to the unmanaged code and supplying the application with managed exceptions, in case something goes wrong.
Building the New RichTextBoxEx Class
First, add all of the necessary name spaces and create a class derived from RichTextBox:
// C#
using System;
using System.Windows.Forms;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Drawing.Printing;
/// <summary>
/// An extension for RichTextBox suitable for printing.
/// </summary>
public class RichTextBoxEx : RichTextBox
{
}
' VB.NET
Imports System
Imports System.Windows.Forms
Imports System.Drawing
Imports System.Runtime.InteropServices
Imports System.Drawing.Printing
' An extension to RichTextBox suitable for printing
Public Class RichTextBoxEx
Inherits RichTextBox
End Class
Next, let's look at the parameters needed for the EM_FORMATRANGE message. These parameters consist of a window handle to send the message to (luckily all Windows Forms controls expose a Handle property to allow access to the underlying control), an integer value specifying whether to actually render the text or just measure it, and a pointer to a FORMATRANGE structure that contains the device contexts, printing areas and the text range to render.
Defining Required Win32 Structures
Within the FORMATRANGE, there are two additional types of structures to be taken care of: RECT and CHARRANGE.
Win32 API defines RECT as…
typedef struct tagRECT {
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT;
...which translates quite easily to a structure in C# or VB.NET.
Note MFC's
LONG
type is 32-bit, unlike .NET'slong
, which is 64-bit. Therefore, to get the correct default marshalling, useInt32
as the corresponding data type.
// C#
[ StructLayout( LayoutKind.Sequential )]
private struct STRUCT_RECT
{
public Int32 left;
public Int32 top;
public Int32 right;
public Int32 bottom;
}
' VB.NET
<StructLayout(LayoutKind.Sequential)> _
private Structure STRUCT_RECT
Public left As Int32
Public top As Int32
Public right As Int32
Public bottom As Int32
End Structure
Note Notice the StructLayoutAttribute before each structure definition. It tells .NET to pack all structure elements in sequential order, in case this structure is used as a parameter for an unmanaged function.
Defining CHARRANGE and FORMATRANGE goes very straightforward once it is explained:
typedef struct _charrange {
LONG cpMin;
LONG cpMax;
} CHARRANGE;
typedef struct _formatrange {
HDC hdc;
HDC hdcTarget;
RECT rc;
RECT rcPage;
CHARRANGE chrg;
} FORMATRANGE;
becomes
// C#
[ StructLayout( LayoutKind.Sequential )]
private struct STRUCT_CHARRANGE
{
public Int32 cpMin;
public Int32 cpMax;
}
[ StructLayout( LayoutKind.Sequential )]
private struct STRUCT_FORMATRANGE
{
public IntPtr hdc;
public IntPtr hdcTarget;
public STRUCT_RECT rc;
public STRUCT_RECT rcPage;
public STRUCT_CHARRANGE chrg;
}
' VB.NET
<StructLayout(LayoutKind.Sequential)> _
Private Structure STRUCT_CHARRANGE
Public cpMin As Int32
Public cpMax As Int32
End Structure
<StructLayout(LayoutKind.Sequential)> _
Private Structure STRUCT_FORMATRANGE
Public hdc As IntPtr
Public hdcTarget As IntPtr
Public rc As STRUCT_RECT
Public rcPage As STRUCT_RECT
Public chrg As STRUCT_CHARRANGE
End Structure
Note Notice that all structure names begin with STRUCT_. In the first version, all structures were named exactly like their Win32 counterparts. This worked fine with C#, but when trying to use the class from VB .NET, the compiler complains that FormatRange is not accessible due to its visibility level.
The problem was that the member function about to be implemented is called FormatRange() (analogous to MFC's CRichEditCtrl's member function) and one of the structures was called FORMATRANGE. This is not a problem for C#, but VB .NET's compiler chokes on structures and functions with the same name (apart from upper or lowercase). Therefore, to keep the samples identical and the VB compiler from getting confused, the structs were renamed.
These are in fact all structures needed for sending the EM_FORMATRANGE message. The following shows how to actually send the message.
Implementing FormatRange()
Similar to declaring how to layout structures for interoperation with unmanaged code, it is also necessary to tell .NET if there is a want to use an unmanaged function—in this case, Win32's function SendMessage().
For that, it is necessary to use the DllImportAttribute and specify which unmanaged DLL to access (in this case user32.dll). Then, declare a static extern (shared in VB .NET) function and how its parameters are to be marshalled:
// C#
[DllImport("user32.dll")]
private static extern Int32 SendMessage(IntPtr hWnd, Int32 msg,
Int32 wParam, IntPtr lParam);
private const Int32 WM_USER = 0x400;
private const Int32 EM_FORMATRANGE = WM_USER+57;
' VB.NET
<DllImport("user32.dll")> _
Private Shared Function SendMessage(ByVal hWnd As IntPtr, _
ByVal msg As Int32, _
ByVal wParam As Int32, _
ByVal lParam As IntPtr) As Int32
End Function
Private Const WM_USER As Int32 = &H400&
Private Const EM_FORMATRANGE As Int32 = WM_USER + 57
Since the value for EM_FORMATRANGE is also needed, the RichEdit.h in the platform SDK states it to be (WM_USER)+57 with WM_USER being 400 hex (from WinUser.h).
There are other examples for P/invoke of SendMessage() on MSDN showing two integer parameters as wParam and lParam, but here an IntPtr is needed because a pointer to a structure has to be marshalled for lParam.
Prepare and fill the structs and call SendMessage(). The only point here is that, since .NET does not know native pointers, to send a pointer to the FORMATRANGE structure it is necessary to use interop's functions to allocate memory and to copy the contents of the struct to this memory:
// C#
/// <summary>
/// Calculate or render the contents of our RichTextBox for printing
/// </summary>
/// <param name="measureOnly">If true, only the calculation is performed,
/// otherwise the text is rendered as well</param>
/// <param name="e">The PrintPageEventArgs object from the
/// PrintPage event</param>
/// <param name="charFrom">Index of first character to be printed</param>
/// <param name="charTo">Index of last character to be printed</param>
/// <returns>(Index of last character that fitted on the
/// page) + 1</returns>
public int FormatRange(bool measureOnly, PrintPageEventArgs e,
int charFrom, int charTo)
{
// Specify which characters to print
STRUCT_CHARRANGE cr;
cr.cpMin = charFrom;
cr.cpMax = charTo;
// Specify the area inside page margins
STRUCT_RECT rc;
rc.top = HundredthInchToTwips(e.MarginBounds.Top);
rc.bottom = HundredthInchToTwips(e.MarginBounds.Bottom);
rc.left = HundredthInchToTwips(e.MarginBounds.Left);
rc.right = HundredthInchToTwips(e.MarginBounds.Right);
// Specify the page area
STRUCT_RECT rcPage;
rcPage.top = HundredthInchToTwips(e.PageBounds.Top);
rcPage.bottom = HundredthInchToTwips(e.PageBounds.Bottom);
rcPage.left = HundredthInchToTwips(e.PageBounds.Left);
rcPage.right = HundredthInchToTwips(e.PageBounds.Right);
// Get device context of output device
IntPtr hdc = e.Graphics.GetHdc();
// Fill in the FORMATRANGE struct
STRUCT_FORMATRANGE fr;
fr.chrg = cr;
fr.hdc = hdc;
fr.hdcTarget = hdc;
fr.rc = rc;
fr.rcPage = rcPage;
// Non-Zero wParam means render, Zero means measure
Int32 wParam = (measureOnly ? 0 : 1);
// Allocate memory for the FORMATRANGE struct and
// copy the contents of our struct to this memory
IntPtr lParam = Marshal.AllocCoTaskMem( Marshal.SizeOf( fr ) );
Marshal.StructureToPtr(fr, lParam, false);
// Send the actual Win32 message
int res = SendMessage(Handle, EM_FORMATRANGE, wParam, lParam);
// Free allocated memory
Marshal.FreeCoTaskMem(lParam);
// and release the device context
e.Graphics.ReleaseHdc(hdc);
return res;
}
' VB.NET
' Calculate or render the contents of our RichTextBox for printing
'
' Parameter "measureOnly": If true, only the calculation is performed,
' otherwise the text is rendered as well
' Parameter "e": The PrintPageEventArgs object from the PrintPage event
' Parameter "charFrom": Index of first character to be printed
' Parameter "charTo": Index of last character to be printed
' Return value: (Index of last character that fitted on the page) + 1
Public Function FormatRange(ByVal measureOnly As Boolean, _
ByVal e As PrintPageEventArgs, _
ByVal charFrom As Integer, _
ByVal charTo As Integer) As Integer
' Specify which characters to print
Dim cr As STRUCT_CHARRANGE
cr.cpMin = charFrom
cr.cpMax = charTo
' Specify the area inside page margins
Dim rc As STRUCT_RECT
rc.top = HundredthInchToTwips(e.MarginBounds.Top)
rc.bottom = HundredthInchToTwips(e.MarginBounds.Bottom)
rc.left = HundredthInchToTwips(e.MarginBounds.Left)
rc.right = HundredthInchToTwips(e.MarginBounds.Right)
' Specify the page area
Dim rcPage As STRUCT_RECT
rcPage.top = HundredthInchToTwips(e.PageBounds.Top)
rcPage.bottom = HundredthInchToTwips(e.PageBounds.Bottom)
rcPage.left = HundredthInchToTwips(e.PageBounds.Left)
rcPage.right = HundredthInchToTwips(e.PageBounds.Right)
' Get device context of output device
Dim hdc As IntPtr
hdc = e.Graphics.GetHdc()
' Fill in the FORMATRANGE structure
Dim fr As STRUCT_FORMATRANGE
fr.chrg = cr
fr.hdc = hdc
fr.hdcTarget = hdc
fr.rc = rc
fr.rcPage = rcPage
' Non-Zero wParam means render, Zero means measure
Dim wParam As Int32
If measureOnly Then
wParam = 0
Else
wParam = 1
End If
' Allocate memory for the FORMATRANGE struct and
' copy the contents of our struct to this memory
Dim lParam As IntPtr
lParam = Marshal.AllocCoTaskMem(Marshal.SizeOf(fr))
Marshal.StructureToPtr(fr, lParam, False)
' Send the actual Win32 message
Dim res As Integer
res = SendMessage(Handle, EM_FORMATRANGE, wParam, lParam)
' Free allocated memory
Marshal.FreeCoTaskMem(lParam)
' and release the device context
e.Graphics.ReleaseHdc(hdc)
Return res
End Function
Note Win32 and .NET use different units when specifying page margins and the like. Within the FORMATRANGE struct, all sizes are expected to be in twips (1/1440th of an inch) whereas page margins and page sizes in .NET are handled in units of .01 inches (hundredths of an inch). Therefore, a utility function was added to convert between twips and hundredths of an inch:
// C#
/// <summary>
/// Convert between 1/100 inch (unit used by the .NET framework)
/// and twips (1/1440 inch, used by Win32 API calls)
/// </summary>
/// <param name="n">Value in 1/100 inch</param>
/// <returns>Value in twips</returns>
private Int32 HundredthInchToTwips(int n)
{
return (Int32)(n*14.4);
}
' VB.NET
' Convert between 1/100 inch (unit used by the .NET framework)
' and twips (1/1440 inch, used by Win32 API calls)
'
' Parameter "n": Value in 1/100 inch
' Return value: Value in twips
Private Function HundredthInchToTwips(ByVal n As Integer) As Int32
Return Convert.ToInt32(n * 14.4)
End Function
When reading the documentation on EM_FORMATRANGE carefully, notice that the value for lParam can also be NULL to "free information cached by the control", so it is a good idea to add another function to be called when printing has finished (add an event handler for a PrintDocument's EndPrint event in the program):
// C#
/// <summary>
/// Free cached data from rich edit control after printing
/// </summary>
public void FormatRangeDone()
{
IntPtr lParam = new IntPtr(0);
SendMessage(Handle, EM_FORMATRANGE, 0, lParam);
}
' VB.NET
' Free cached data from rich edit control after printing
Public Sub FormatRangeDone()
Dim lParam As New IntPtr(0)
SendMessage(Handle, EM_FORMATRANGE, 0, lParam)
End Sub
Just P/invoke SendMessage() with NULL as second parameter.
Using the New RichTextBoxEx Class
Now that the required functions have been implemented, let's take a look at how to use this new class.
The common way for printing under .NET incorporates the use of an instance of the PrintDocument class. This class mainly offers events for the beginning of a printing process, for the beginning of a new page, for the end of the printing process and a method to actually start printing. The flow of events can be depicted as follows:
Figure 1 Sequence of Print Events
By calling the new RichTextBoxEx methods in the appropriate event handler functions, printing the contents of the rich edit control becomes easy.
Start by instantiating a new PrintDocument object, adding the required event handlers and calling the Print() method:
// C#
public void PrintRichTextContents()
{
PrintDocument printDoc = new PrintDocument();
printDoc.BeginPrint += new PrintEventHandler(printDoc_BeginPrint);
printDoc.PrintPage += new PrintPageEventHandler(printDoc_PrintPage);
printDoc.EndPrint += new PrintEventHandler(printDoc_EndPrint);
// Start printing process
printDoc.Print();
}
' VB.NET
Public Sub PrintRichTextContents()
Dim printDoc As New PrintDocument()
AddHandler printDoc.BeginPrint, AddressOf printDoc_BeginPrint
AddHandler printDoc.PrintPage, AddressOf printDoc_PrintPage
AddHandler printDoc.EndPrint, AddressOf printDoc_EndPrint
' Start printing process
printDoc.Print()
End Sub
Now for the implementation of the three event handler functions:
// C#
// variable to trace text to print for pagination
private int m_nFirstCharOnPage;
private void printDoc_BeginPrint(object sender,
System.Drawing.Printing.PrintEventArgs e)
{
// Start at the beginning of the text
m_nFirstCharOnPage = 0;
}
private void printDoc_PrintPage(object sender,
System.Drawing.Printing.PrintPageEventArgs e)
{
// To print the boundaries of the current page margins
// uncomment the next line:
// e.Graphics.DrawRectangle(System.Drawing.Pens.Blue, e.MarginBounds);
// make the RichTextBoxEx calculate and render as much text as will
// fit on the page and remember the last character printed for the
// beginning of the next page
m_nFirstCharOnPage = myRichTextBoxEx.FormatRange(false,
e,
m_nFirstCharOnPage,
myRichTextBoxEx.TextLength);
// check if there are more pages to print
if (m_nFirstCharOnPage < myRichTextBoxEx.TextLength)
e.HasMorePages = true;
else
e.HasMorePages = false;
}
private void printDoc_EndPrint(object sender,
System.Drawing.Printing.PrintEventArgs e)
{
// Clean up cached information
myRichTextBoxEx.FormatRangeDone();
}
' VB.NET
' variable to trace text to print for pagination
Private m_nFirstCharOnPage As Integer
Private Sub printDoc_BeginPrint(ByVal sender As Object, _
ByVal e As System.Drawing.Printing.PrintEventArgs)
' Start at the beginning of the text
m_nFirstCharOnPage = 0
End Sub
Private Sub printDoc_PrintPage(ByVal sender As Object, _
ByVal e As System.Drawing.Printing.PrintPageEventArgs)
' To print the boundaries of the current page margins
' uncomment the next line:
' e.Graphics.DrawRectangle(System.Drawing.Pens.Blue, e.MarginBounds)
' make the RichTextBoxEx calculate and render as much text as will
' fit on the page and remember the last character printed for the
' beginning of the next page
m_nFirstCharOnPage = myRichTextBoxEx.FormatRange(False, _
e, _
m_nFirstCharOnPage, _
myRichTextBoxEx.TextLength)
' check if there are more pages to print
If (m_nFirstCharOnPage < myRichTextBoxEx.TextLength) Then
e.HasMorePages = True
Else
e.HasMorePages = False
End If
End Sub
Private Sub printDoc_EndPrint(ByVal sender As Object, _
ByVal e As System.Drawing.Printing.PrintEventArgs)
' Clean up cached information
myRichTextBoxEx.FormatRangeDone()
End Sub
The reason the member variable m_nFirstCharOnPage is needed is for pagination: it is necessary to tell FormatRange() the range of characters to render and get back the index of the last character that fitted on the page plus one. Therefore, to get correct page breaks, keep the index of the last character printed and start the next page at this index+1. This has to be repeated until m_nFirstCharOnPage has reached the last character in the rich edit control, at which point the printing process is stopped by setting e.HasMorePages to false.
In the end, just call the new FormatRangeDone() function to release cached data from the RichTextBoxEx's Win32 control.
Adding Formatting to the New Class
Now that WYSIWYG print output for a RichTextBox class is available, it enables the addition of more features that RichTextBox lacks, but the Win32 rich edit control has: modifying only parts of a selection's formatting.
Standard RichTextBox offers a SelectionFont property to get and set the font for part of the text. The problem is that if there exists a selection with different fonts, then getting the SelectionFont returns null (Nothing in VB) and it is not possible to just format selected text (bold, for example), without specifying the other font properties as well.
But here again, Win32 messages for a rich edit control can be used to achieve this behavior: EM_GETCHARFORMAT and EM_SETCHARFORMAT.
The process of adding the required definitions and structures to the class is very similar to what was previously demonstrated. The only tricky part arises when looking at a structure named CHARFORMAT:
typedef struct _charformat {
UINT cbSize;
DWORD dwMask;
DWORD dwEffects;
LONG yHeight;
LONG yOffset;
COLORREF crTextColor;
BYTE bCharSet;
BYTE bPitchAndFamily;
TCHAR szFaceName[LF_FACESIZE];
} CHARFORMAT;
The element szFaceName is defined as a fixed array of LF_FACESIZE (which is 32, by the way) TCHARs. When declaring a structure for .NET, it is not possible to specify a fixed size for an array, so it is necessary to tell interop services how to marshal such an array for use with unmanaged code. Once again, there's an attribute to do just this: MarshalAsAttribute.
Using this attribute, it's possible to specify very precisely which types and sizes to use for marshalling. In this case, the array is to be treated as an unmanaged ByValArray with a fixed size of 32 elements (LF_FACESIZE). The element type (TCHAR) is one of char's default marshalling types, so no additional work is needed.
The following is the complete definition:
// C#
[ StructLayout( LayoutKind.Sequential )]
private struct STRUCT_CHARFORMAT
{
public int cbSize;
public UInt32 dwMask;
public UInt32 dwEffects;
public Int32 yHeight;
public Int32 yOffset;
public Int32 crTextColor;
public byte bCharSet;
public byte bPitchAndFamily;
[MarshalAs(UnmanagedType.ByValArray, SizeConst=32)]
public char[] szFaceName;
}
' VB.NET
<StructLayout(LayoutKind.Sequential)> _
Private Structure STRUCT_CHARFORMAT
Public cbSize As Integer
Public dwMask As UInt32
Public dwEffects As UInt32
Public yHeight As Int32
Public yOffset As Int32
Public crTextColor As Int32
Public bCharSet As Byte
Public bPitchAndFamily As Byte
<MarshalAs(UnmanagedType.ByValArray, SizeConst:=32)> _
Public szFaceName As Char()
End Structure
The rest is—once again—similar to implementing FormatRange(): look up and add the required constants for all formatting options and flags, create and fill the required structures and call SendMessage() with EM_GETCHARFORMAT or EM_SETCHARFORMAT.
Conclusion
With .NET's Windows Forms namespace there are many classes to build a rich user interface. Most of these classes are quite similar to the corresponding MFC classes, but sometimes lack features that were available with MFC. Using the techniques shown in this discussion, all the features the base Win32 control offers can be made accessible by resorting to interop services and calling SendMessage() from the managed code.
About the Author
Martin Müller is leader of development at 4voice AG. At the moment, Martin is researching the use of speech recognition and developing speech-enabled solutions for health services and doctors. Martin can be reached by mail here.