Yet Another Code Site |
In Part 1 of this paper, "About TRichEdit::Print()," we saw that the VCL does not give you very much control in fact, essentially no control over how the contents of a TRichEdit are printed on a page.
This part will cover printing Rich Edit controls in depth and supply you with the details needed to print a Rich Edit control on a specific portion of a page, calculate the number of pages needed to print an entire document, and print selected pages of a document. If you skipped Part 1, no problem. You can start here if you are already convinced that TRichEdit::Print() is not what you need.
Part 3 of this paper, "Previewing Rich Edit Controls," will describe creating a print preview window for Rich Edit controls. If you already understand using FORMATRANGE and EM_FORMATRANGE to print Rich Edit controls, you can skip ahead to Part 3. Otherwise, I suggest you at least scan though this paper.
To start with, Rich Edit controls print themselves or, more accurately, they "render" or draw themselves. This means that you provide information about:
Then you ask the control to render itself.
The basic process for rendering a Rich Edit control is:
The CHARRANGE and FORMATRANGE structures are going to become close friends, so spend the time now to get to know them well.
The CHARRANGE structure looks like this:
typedef struct _charrange { LONG cpMin; LONG cpMax; } CHARRANGE;
Before starting the rendering loop (steps 2 and 3 above), you typically set cpMin to 0 and cpMax to 1. While in the rendering loop, you simply set cpMin to the value returned by the EM_FORMATRANGE message (below).
The FORMATRANGE structure is declared as:
typedef struct _formatrange { HDC hdc; HDC hdcTarget; RECT rc; RECT rcPage; CHARRANGE chrg; } FORMATRANGE;
The structure's values are:
hdc - The handle to the device context that will be rendered (drawn) on. Typically a printer or screen device context. This can also be a user-created device context if you need to draw on a bitmap in memory.
hdcTarget - Typically the handle to the device context for the printer. May be set to zero if rendering and target DCs are identical, i.e., if they are the same DC. May also be set to zero if the formatting buffer is already filled.
rc - A rectangle describing the area on the page to render in. The text will be formatted to fit within the rectangle.
rcPage - A rectangle describing the entire "page" within which "rc" is located.
chrg - A CHARRANGE structure as described above and used below.
As above, and for the remainder of this discussion, I will occasionally refer to the hdc and hdcTarget parameters as the rendering and target DCs, respectively. Make no mistake the drawing will always take place on the rendering DC but will be based on (or look like) the target DC. Stated another way, text will be wrapped as if it was rendered to the target DC but will be drawn on the rendering DC.
The EM_FORMATRANGE message is sent, of course, with the SendMessage WinAPI call. So, the SendMessage() call to render the Rich Edit control call is:
SendMessage(handle, EM_FORMATRANGE, (WPARAM) fRender, (LPARAM) lpFmt);
The help text says:
fRender - Value specifying whether to render the text. If this parameter is non-zero, the text is rendered. Otherwise, the text is just measured.
lpFmt - Pointer to a FORMATRANGE structure containing information about the output device, or NULL to free information cached by the control.
fRender is a simple flag if zero, nothing is drawn on the output device; if non-zero, the output is actually rendered. Passing zero is useful to calculate the pages that would be printed. When you pass zero, the Rich Edit control measures the size of the rendering rectangle that would be used and returns this value in the rc member of the FORMATRANGE structure.
lpFmt is the address of a FORMATRANGE structure as described above. If this parameter is zero (or NULL), then the formatting buffer will be cleared. Any time this parameter is not zero, information about the page rendered is stored in the formatting buffer.
The return value for the EM_FORMATRANGE message is the beginning offset for the next page to be rendered.
Hopefully, an example will make this clear. The code is based on a project that I have been working on for more than a year now. For this paper, I have extracted the pertinent code from the class, simplified it where possible, and put it into event functions that respond to simple button clicks. To follow along with the example:
Close();
The example demonstrates the following general steps:
The second step is not truly required if you are printing the entire contents of the TRichEdit control. However, you will probably want to advise the user of the total number of pages that will be printed before actually printing or allow the user to select a range of pages to be printed. To do that, you will need to pre-calculate the number of pages. Step 2 is included to demonstrate this.
One purpose of this paper is to demonstrate how to print a TRichEdit with arbitrary margins. However, for the purpose of the exercise, we will assume that the printer is loaded with paper that is the (US) standard 8.5" by 11" size. Further, we will hard-code two-inch margins.
The example also assumes that we are printing to the default printer. This printer is, in VCL-speak, Printer() and is an automatically instantiated object of the TPrinter class.
Obviously, a bit of work will be required to change these assumptions. Other limitations or shortcomings are noted in the following discussion.
Here we go . First, add the following to the main form's *.cpp file after the TForm* Form1 declaration (created by default) to include printer declarations so that we can use TPrinter.
#include <vcl\printers.hpp>
After that, add the following so that we can use the standard template library (STL) vector class.
#include <vector>
Immediately after that, add the following code.
typedef struct tagTPageOffset { long int Start; long int End; RECT rendRect; } TPageOffsets;
The above structure that will be used later to store the information that describes the starting and ending offsets and rendering rectangle for each page that could be rendered by the TRichEdit control. For now, just take my word for it.
All of the above said and done, add an OnClick handler to PrintBtn. All of the following code will be in this OnClick handler.
First, we get the size of a printed page (assuming 8.5" by 11") in printer device units.
int wPage = ::GetDeviceCaps(Printer()->Handle, PHYSICALWIDTH); int hPage = ::GetDeviceCaps(Printer()->Handle, PHYSICALHEIGHT);
Next, get the device units per inch for the printer.
int xPPI = ::GetDeviceCaps(Printer()->Handle, LOGPIXELSX); int yPPI = ::GetDeviceCaps(Printer()->Handle, LOGPIXELSY);
Convert the page size from device units to twips.
int wTwips = ::MulDiv(wPage, 1440, xPPI); int hTwips = ::MulDiv(hPage, 1440, yPPI);
Save the page size in twips.
RECT pageRect; pageRect.left = pageRect.top = 0; pageRect.right = wTwips; pageRect.bottom = hTwips;
Next, calculate the size of the rendering rectangle in twips. Remember two-inch margins are hard-coded, so the below code reduces the width of the output by four inches.
RECT rendRect; rendRect.left = rendRect.top = 0; rendRect.right = pageRect.right - 1440 * 4; rendRect.bottom = pageRect.bottom - 1440 * 4;
Now we declare the STL vector class that will be used to save the information about each page. This is used to to save a list of text offsets that correspond to printed pages as well as the values for a single page.
std::vector<TPageOffsets> FPageOffsets;
Define a single page and set the starting offset to zero.
TPageOffsets po; po.Start = 0;
Define and initialize a TFormatRange structure. As described above, this structure is passed to the TRichEdit with a request to format as much text as will fit on a page starting with the chrg.cpMin offset and ending with the chrg.cpMax. Initially, we tell the Rich Edit control to start at the beginning (cpMin = 0) and print as much as possible (cpMax = -1) . We also tell it to render to the printer.
TFormatRange fr; fr.hdc = Printer()->Handle; fr.hdcTarget = Printer()->Handle; fr.chrg.cpMin = po.Start; fr.chrg.cpMax = -1;
In order to recognize when the last page is rendered, we need to know how much text is in the control. (For this example, we will use the WM_GETTEXTLENGTH message. TRichEdit uses Rich Edit version 1.0 so this is fine. However, if you are implementing the Rich Edit 2.0 control, you will need to use the EM_GETTEXTLENEX message.)
int lastOffset = ::SendMessage(RichEdit1->Handle, WM_GETTEXTLENGTH, 0, 0);
As a precaution, clear the formatting buffer:
::SendMessage(RichEdit1->Handle, EM_FORMATRANGE, (WPARAM) 0, (LPARAM) 0);
Printers frequently cannot print at the absolute top-left position on the page. In other words, there is usually a minimum margin on each edge of the page. When rendering to the printer, Rich Edit controls adjust the top-left corner of the rendering rectangle for the amount of the page that is unprintable. Since we are printing with two-inch margins, we are presumably already within the printable portion of the physical page. We use the SetViewportOrgEx() Windows API call to move the origin of the printable page up and left by the amount of the unprintable margins to undo the control's adjustment. Notice that we first save the device context settings with SaveDC().
::SaveDC(fr.hdc); ::SetMapMode(fr.hdc, MM_TEXT); int xOffset = -::GetDeviceCaps(Printer()->Handle, PHYSICALOFFSETX); int yOffset = -::GetDeviceCaps(Printer()->Handle, PHYSICALOFFSETY); xOffset += ::MulDiv(1440 + 1440, xPPI, 1440); yOffset += ::MulDiv(1440 + 1440, yPPI, 1440); ::SetViewportOrgEx(fr.hdc, xOffset, yOffset, NULL);
Now we build a table of page entries, one entry for each page that would be printed. Before explaining, here is the code:
do { fr.rc = rendRect; fr.rcPage = pageRect; po.Start = fr.chrg.cpMin; fr.chrg.cpMin = ::SendMessage(RichEdit1->Handle, EM_FORMATRANGE,(WPARAM) 0, (LPARAM) &fr); po.End = fr.chrg.cpMin - 1; po.rendRect = fr.rc; FPageOffsets.push_back(po); } while (fr.chrg.cpMin != -1 && fr.chrg.cpMin < lastOffset);
There are a number of things to notice in the above code.
First, we are using the EM_FORMATRANGE message to tell the control to render itself. However, the WPARAM value is set to zero to tell the control to simply measure the rectangle which would be rendered, but not to actually draw anything on the device.
Next, note that we are saving the value returned in the rc member of the FORMATRANGE structure. There are only glancing references to this fr.rc is modified by the call. The actual size of the rendering rectangle is returned here. The really odd thing is that the returned rectangle may actually be larger than the rendering rectangle passed into the call. I believe that this is only an issue when the rendering and target device contexts are different. They are the same here, but in Part 3 of this paper they will be different.
Finally, the value returned by the SendMessage() call is one character greater than the size of the text rendered. It is the beginning offset for the next page.
At this point, FPageOffsets contains one entry for each page that would have been rendered. You could use the following to determine the actual number of pages to be printed:
int pageCount = FPageOffsets.size();
For this exercise, we are printing only the first page so:
pageCount = 1;
Ok. We are ready to print. We are going to print to the default printer, Printer(). Printer() returns a pointer to a global TPrinter that is automatically instantiated by the VCL.
Now there is a small problem with TPrinter. When we used Printer()->Handle above, we did not get a handle to a device context. What Printer()->Handle really returned was a handle to an "information context," or an HIC. Before drawing on the real printer device context (HDC), we must call Printer()->BeginDoc(). However, after that call, Printer()->Handle will actually return a true device context handle. That is, the prior value returned by Printer()->Handle is no longer valid. After we call Printer()->EndDoc(), Printer()->Handle once again returns an HIC, but it may not be the same HIC returned before the Printer()->BeginDoc() call.
The moral of this story? Do not save TPrinter::Handle across TPrinter::BeginDoc()/EndDoc() calls.
That said (and hopefully re-read until understood), we have to clear the Rich Edit's formatting buffer because the HDC/HIC to which we will be rendering has changed. Let's go ahead and restore the HDCs settings, too.
::SendMessage(RichEdit1->Handle, EM_FORMATRANGE, (WPARAM) 0, (LPARAM) 0); ::RestoreDC(fr.hdc, -1);
Now, we are almost ready to actually print. First, we tell the printer that we are beginning to print, next we set the rendering and target devices to the printer, then we save the printer's HDC settings, and then we adjust the page origins to account for the unprintable portion of the page.
Printer()->BeginDoc(); fr.hdc = Printer()->Handle; fr.hdcTarget = Printer()->Handle; ::SaveDC(fr.hdc); ::SetViewportOrgEx(fr.hdc, xOffset, yOffset, NULL);
Ready to print? Ok, here we go .
bool firstPage = true; int currPage = 0; do { if (firstPage) firstPage = false; else Printer()->NewPage();
[ Bug Report ]
As presented, the second and subsequent pages are aligned to the top, left corner of the page. Presumably, NewPage() resets the viewport origin. To correct this problem, add the following here (it may be deleted above):
::SetViewportOrgEx(fr.hdc, xOffset, yOffset, NULL);
The correction has been applied to the code sample from Part 4 of this paper. It has not been added to the code samples available at the end of Part 3 of this paper.
Thanks to Jan verhave for chasing this one down and sharing it!
[ End Bug Report ]
fr.rc = FPageOffsets[currPage].rendRect; fr.rcPage = pageRect; fr.chrg.cpMin = FPageOffsets[currPage].Start; fr.chrg.cpMax = FPageOffsets[currPage].End; fr.chrg.cpMin = ::SendMessage(RichEdit1->Handle, EM_FORMATRANGE, (WPARAM) 1, (LPARAM) &fr); } while (++currPage < pageCount);
Here is what happened above: We defined and initialized variables to track the current page number and set a flag for the first page. Next, we entered a loop to print each page (remember that we are printing only one page if you religiously followed the code). Within the loop, we started a new page (if not printing the first page) and set the formatting rectangle and beginning and ending offsets to the saved values for the current page. Then we actually printed the page (SendMessage() with WPARAM not zero).
[ Begin Big Note ]
A diligent netizen, Mark Wigmore, pointed out that I elsewhere implied that this series of papers would explain how to implement page headers and/or footers. In fact, the original paper did not explain that clearly. Here I hope to remedy that oversight.
If you are not already familiar with drawing text on a TCanvas, now is the time to look it up in the "Using C++ Builder" help. Spend some time learning about the Text* methods. The bottom line is that you can draw on Printer()->Canvas using any of the TCanvas methods at any time between the Printer()->BeginDoc() and Printer()->EndDoc() calls.
The logical place to do this is in the do-while rendering loop since it makes one pass for each printed page. The only real "trick" is to make sure that the Rich Edit rendering rectangle is below/above the header/footer respectively. Well, the other trick is to understand that Rich Edit controls always use twips for measurements whereas TCanvas methods use the logical units for the current mapping mode for the device context. Above, we set the printer mapping mode to MM_TEXT, which sets the logical units to device units. So, here you will use printer device units (i.e., pixels; if your printer resolution is 300 vertical pixels per inch, then you have 300 device units per vertical inch).
Use the WinAPI GetDeviceCaps() function to get the printer pixels per inch. Make the calls near the top of the rendering function (in this case, the OnClick handler). For example:
int printerHorzUnitsPerInch = ::GetDeviceCaps(Printer()->Handle, LOGPIXELSX); int printerVertUnitsPerInch = ::GetDeviceCaps(Printer()->Handle, LOGPIXELSY);
gets the horizontal and vertical pixels per inch for the printer.
To add complexity (I always seem to do that), you may want to have a first-page header that is different than the second and remaining page headers. Assume for the moment that you have set the top of the Rich Edit rendering rectangle to one inch (1440 twips) and that you wish to have a header one-half inch from the top and left edges of the page. Using the above, you might re-code the do-while loop to start like this:
do { // set page origin to top left so that TextOut() coordinates are
// from top of page ::SetViewportOrgEx(fr.hdc, 0, 0, NULL);
// if not first page, eject the page and print the second-page header if (firstPage) { firstPage = false; Printer()->Canvas->TextOut(printerHorzUnitsPerInch / 2, printerVertUnitsPerInch / 2, "First page"); } else { Printer()->NewPage(); Printer()->Canvas->TextOut(printerHorzUnitsPerInch / 2, printerVertUnitsPerInch / 2, "Second and following pages"); }
// set page origin to offset where the text should be rendered ::SetViewportOrgEx(fr.hdc, xOffset, yOffset, NULL);
Note that you are not limited to TCanvas::Text* functions. You can use any TCanvas method to draw anything anywhere on the page. Naturally, if you draw something at a position on the page that will subsequently be drawn on by the Rich Edit control, it will be obscured by the Rich Edit control's drawing.
Of course, if you implement a page preview function as described in the third part of this paper, you will need to add functionally similar code to the preview rendering method. When doing this, pay attention to the mapping mode.
My thanks to Robin Huang for helping me chase down bugs in earlier versions of this note.
[ End Big Note ]
At this point, we have finished rendering the contents of the Rich Edit control. Now we restore the printer's HDC settings and tell Windows that we are through printing this document.
::RestoreDC(fr.hdc, -1); Printer()->EndDoc();
Finally, we clear the Rich Edit controls formatting buffer and delete the saved page table information.
::SendMessage(RichEdit1->Handle, EM_FORMATRANGE, (WPARAM) 0, (LPARAM) 0); FPageOffsets.erase(FPageOffsets.begin(), FPageOffsets.end());
That's it.
Back To Part 1: About TRichEdit::Print()
On to Part 3: Previewing Rich Edit Controls
Home | Top Of Page | Code | Papers | FAQs | Links | Search | Feedback |
Page updated |
Copyright © 1998-2001 Thin Air Enterprises and Robert Dunn. All rights reserved.