Equipped with the ability to select and create logical fonts, it's time to try our hand at text formatting. The process involves placing each line of text within margins in one of four ways: aligned on the left margin, aligned on the right margin, centered between the margins, or justifiedthat is, running from one margin to the other, with equal spaces between the words. For the first three jobs, you can use the DrawText function with the DT_WORDBREAK argument, but this approach has limitations. For instance, you can't determine what part of the text DrawText was able to fit within the rectangle. DrawText is convenient for some simple jobs, but for more complex formatting tasks, you'll probably want to employ TextOut.
One of the most useful functions for working with text is GetTextExtentPoint32. (This is a function whose name reveals some changes since the early versions of Windows.) The function tells you the width and height of a character string based on the current font selected in the device context:
GetTextExtentPoint32 (hdc, pString, iCount, &size) ;
The width and height of the text in logical units are returned in the cx and cy fields of the SIZE structure. I'll begin with an example using one line of text. Let's say that you have selected a font into your device context and now want to write the text:
TCHAR * szText [] = TEXT ("Hello, how are you?") ;
You want the text to start at the vertical coordinate yStart, within margins set by the coordinates xLeft and xRight. Your job is to calculate the xStart value for the horizontal coordinate where the text begins.
This job would be considerably easier if the text were displayed using a fixed-pitch font, but that's not the general case. First you get the text extents of the string:
GetTextExtentPoint32 (hdc, szText, lstrlen (szText), &size) ;
If size.cx is larger than (xRight - xLeft), the line is too long to fit within the margins. Let's assume it can fit.
To align the text on the left margin, you simply set xStart equal to xLeft and then write the text:
TextOut (hdc, xStart, yStart, szText, lstrlen (szText)) ;
This is easy. You can now add the size.cy to yStart, and you're ready to write the next line of text.
To align the text on the right margin, you use this formula for xStart:
xStart = xRight - size.cx ;
To center the text between the left and right margins, use this formula:
xStart = (xLeft + xRight - size.cx) / 2 ;
Now here's the tough jobto justify the text within the left and right margins. The distance between the margins is (xRight - xLeft). Without justification, the text is size.cx wide. The difference between these two values, which is
xRight - xLeft - size.cx
must be equally distributed among the three space characters in the character string. It sounds like a terrible job, but it's not too bad. To do it, you call
SetTextJustification (hdc, xRight - xLeft - size.cx, 3)
The second argument is the amount of space that must be distributed among the space characters in the character string. The third argument is the number of space characters, in this case 3. Now set xStart equal to xLeft, and write the text with TextOut:
TextOut (hdc, xStart, yStart, szText, lstrlen (szText)) ;
The text will be justified between the xLeft and xRight margins.
Whenever you call SetTextJustification, it accumulates an error term if the amount of space doesn't distribute evenly among the space characters. This error term will affect subsequent GetTextExtentPoint32 calls. Each time you start a new line, you should clear out the error term by calling
SetTextJustification (hdc, 0, 0) ;
If you're working with a whole paragraph, you have to start at the beginning and scan through the string looking for space characters. Every time you encounter a space character (or another character that can be used to break the line), you call GetTextExtentPoint32 to determine whether the text still fits between the left and right margins. When the text exceeds the space allowed for it, you backtrack to the previous blank. Now you have determined the character string for the line. If you want to justify the line, call SetTextJustification and TextOut, clear out the error term, and proceed to the next line.
The JUSTIFY1 program, shown in Figure 17-9, does this job for the first paragraph of Mark Twain's The Adventures of Huckleberry Finn. You can pick the font you want from a dialog box, and you can also use a menu selection to change the alignment (left, right, centered, or justified). Figure 17-10 shows a typical JUSTIFY1 display.
Figure 17-9.
The JUSTIFY1 program.
JUSTIFY1.C
|
JUSTIFY1.RC
|
RESOURCE.H
|
JUSTIFY1 displays a ruler (in logical inches, of course) across the top and down the left side of the client area. The DrawRuler function draws the ruler. A rectangle structure defines the area in which the text must be justified.
The bulk of the work involved with formatting this text is in the Justify function. The function starts searching for blanks at the beginning of the text and uses GetTextExtentPoint32 to measure each line. When the length of the line exceeds the width of the display area, JUSTIFY1 returns to the previous space and uses the line up to that point. Depending on the value of the iAlign constant, the line is left-aligned, right-aligned, centered, or justified.
JUSTIFY isn't perfect. It doesn't have any logic for hyphens, for example. Also, the justification logic falls apart when there are fewer than two words in each line. Even if we solve this problem, which isn't a particularly difficult one, the program still won't work properly when a single word is too long to fit within the left and right margins. Of course, matters can become even more complex when you start working with programs that can use multiple fonts on the same line (as Windows word processors do with apparent ease). But nobody ever claimed this stuff was easy. It's just easier than if you were doing all the work yourself.
Figure 17-10.
A typical JUSTIFY1 display.
Some text is not strictly for viewing on the screen. Some text is for printing. And often in that case, the screen preview of the text must match the formatting of the printer output precisely. It's not enough to show the same fonts and sizes and character formatting. With TrueType, that's a snap. What's also needed is for each line in a paragraph to break at the same place. This is the hard part of WYSIWYG.
JUSTIFY1 includes a Print option, but what it does is simply set one-inch margins at the top, left, and right sides of the page. Thus, the formatting is completely independent of the screen display. Here's an interesting exercise: change a few lines in JUSTIFY1 so that both the screen and the printer logic are based on a six-inch formatting rectangle. To do this, change the definitions of rect.right in both the WM_PAINT and Print command logic. In the WM_PAINT logic, the statement is
rect.right = rect.left + 6 * GetDeviceCaps (hdc, LOGPIXELSX) ;
In the Print command logic, the statement is
rect.right = rect.left + 6 * GetDeviceCaps (hdcPrn, LOGPIXELSX) ;
If you select a TrueType font, the line breaks on the screen should be the same as on the printer output.
But they aren't. Even though the two devices are using the same font in the same point size and displaying text in the same formatting rectangle, the different display resolutions and rounding errors cause the line breaks to occur at different places. Obviously, a more sophisticated approach is needed for the screen previewing of printer output.
A stab at such an approach is demonstrated by the JUSTIFY2 program shown in Figure 17-11. The code in JUSTIFY2 is based on a program called TTJUST ("TrueType Justify") written by Microsoft's David Weise, which was in turn based on a version of the JUSTIFY1 program in an earlier edition of this book. To symbolize the increased complexity of this program, the Mark Twain excerpt has been replaced with the first paragraph from Herman Melville's Moby-Dick.
Figure 17-11.
The JUSTIFY2 program.
JUSTIFY2.C
|
JUSTIFY2.RC
|
RESOURCE.H
|
JUSTIFY2 works with TrueType fonts only. In its GetCharDesignWidths function, the program uses the GetOutlineTextMetrics function to get a seemingly unimportant piece of information. This is the OUTLINETEXTMETRIC field otmEMSquare.
A TrueType font is designed on an em-square grid. (As I've said, the word "em" refers to the width of a square piece of type, an M equal in width to the point size of the font.) All the characters of any particular TrueType font are designed on the same grid, although they generally have different widths. The otmEMSquare field of the OUTLINETEXTMETRIC structure gives the dimension of this em-square for any particular font. For most TrueType fonts, you'll find that the otmEMSquare field is equal to 2048, which means that the font was designed on a 2048-by-2048 grid.
Here's the key: You can set up a LOGFONT structure for the particular TrueType typeface name but with an lfHeight field equal to the negative of the otmEMSquare value. After creating that font and selecting it into a device context, you can call GetCharWidth. This function gives you the width of individual characters in the font in logical units. Normally, these character widths are not exact because they've been scaled to a different font size. But with a font based on the otmEMSquare size, these widths are always exact integers independent of any device context.
The GetCharDesignWidths function obtains the original character design widths in this manner and stores them in an integer array. The JUSTIFY2 program knows that its text uses ASCII characters only, so this array needn't be very large. The GetScaledWidths function converts these integer widths to floating point widths based on the actual point size of the font in the device's logical coordinates. The GetTextExtentFloat function uses those floating point widths to calculate the width of a whole string. That's the function the new Justify function uses to calculate the widths of lines of text.
Visit Microsoft Press for more information on Programming Windows, Fifth Ed.