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's long, which is 64-bit. Therefore, to get the correct default marshalling, use Int32 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.