Get a site

VC++ 6.0 ebook chapter index
homepage aranna.altervista.org
Free counters!

Paragraph Formatting

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 justified—that 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.

Simple Text Formatting

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 job—to 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) ;

Working with Paragraphs

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.C -- Justified Type Program #1
                 (c) Charles Petzold, 1998
  -----------------------------------------*/

#include <windows.h>
#include "resource.h"

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
	 
TCHAR szAppName[] = TEXT ("Justify1") ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     HWND     hwnd ;
     MSG      msg ;
     WNDCLASS wndclass ;
     
     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = szAppName ;
     wndclass.lpszClassName = szAppName ;
     
     if (!RegisterClass (&wndclass))
     {
          MessageBox (NULL, TEXT ("This program requires Windows NT!"),
               szAppName, MB_ICONERROR) ;
          return 0 ;
     }
     
     hwnd = CreateWindow (szAppName, TEXT ("Justified Type #1"),
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;
     
     ShowWindow (hwnd, iCmdShow) ;
     UpdateWindow (hwnd) ;
     
     while (GetMessage (&msg, NULL, 0, 0))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return msg.wParam ;
}

void DrawRuler (HDC hdc, RECT * prc)
{
     static int iRuleSize [16] = { 360, 72, 144, 72, 216, 72, 144, 72,
                                   288, 72, 144, 72, 216, 72, 144, 72 } ;
     int        i, j ;
     POINT      ptClient ;
     
     SaveDC (hdc) ;
     
          // Set Logical Twips mapping mode
     
     SetMapMode (hdc, MM_ANISOTROPIC) ;
     SetWindowExtEx (hdc, 1440, 1440, NULL) ;
     SetViewportExtEx (hdc, GetDeviceCaps (hdc, LOGPIXELSX),
                            GetDeviceCaps (hdc, LOGPIXELSY), NULL) ;
     
          // Move the origin to a half inch from upper left
     
     SetWindowOrgEx (hdc, -720, -720, NULL) ;
     
          // Find the right margin (quarter inch from right)
     
     ptClient.x = prc->right ;
     ptClient.y = prc->bottom ;
     DPtoLP (hdc, &ptClient, 1) ;
     ptClient.x -= 360 ;
     
          // Draw the rulers
     
     MoveToEx (hdc, 0,          -360, NULL) ;
     LineTo   (hdc, ptClient.x, -360) ;
     MoveToEx (hdc, -360,          0, NULL) ;
     LineTo   (hdc, -360, ptClient.y) ;
     
     for (i = 0, j = 0 ; i <= ptClient.x ; i += 1440 / 16, j++)
     {
          MoveToEx (hdc, i, -360, NULL) ;
          LineTo   (hdc, i, -360 - iRuleSize [j % 16]) ;
     }
     
     for (i = 0, j = 0 ; i <= ptClient.y ; i += 1440 / 16, j++)
     {
          MoveToEx (hdc, -360, i, NULL) ;
          LineTo   (hdc, -360 - iRuleSize [j % 16], i) ;
     }
     
     RestoreDC (hdc, -1) ;
}
void Justify (HDC hdc, PTSTR pText, RECT * prc, int iAlign)
{
     int   xStart, yStart, cSpaceChars ;
     PTSTR pBegin, pEnd ;
     SIZE  size ;
     
     yStart = prc->top ;
     do                            // for each text line
     {
          cSpaceChars = 0 ;        // initialize number of spaces in line

          while (*pText == ` `)    // skip over leading spaces
               pText++ ;

          pBegin = pText ;         // set pointer to char at beginning of line
          
          do                       // until the line is known
          {
               pEnd = pText ;      // set pointer to char at end of line

                    // skip to next space 
               
               while (*pText != `\0' && *pText++ != ` `) ;

               if (*pText == `\0')
                    break ;

                    // after each space encountered, calculate extents

               cSpaceChars++ ;
               GetTextExtentPoint32(hdc, pBegin, pText - pBegin - 1, &size) ;
          }
          while (size.cx < (prc->right - prc->left)) ;
          
          cSpaceChars-- ;               // discount last space at end of line
          
          while (*(pEnd - 1) == ` `)    // eliminate trailing spaces
          {
               pEnd-- ;
               cSpaceChars-- ;
          }

               // if end of text and no space characters, set pEnd to end
          
          if (*pText == `\0' || cSpaceChars <= 0)
               pEnd = pText ;
          GetTextExtentPoint32 (hdc, pBegin, pEnd - pBegin, &size) ;
          
          switch (iAlign)               // use alignment for xStart
          {
          case IDM_ALIGN_LEFT:
               xStart = prc->left ;
               break ;
               
          case IDM_ALIGN_RIGHT:
               xStart = prc->right - size.cx ;
               break ;
               
          case IDM_ALIGN_CENTER:
               xStart = (prc->right + prc->left - size.cx) / 2 ;
               break ;
               
          case IDM_ALIGN_JUSTIFIED:
               if (*pText != `\0' && cSpaceChars > 0)
                    SetTextJustification (hdc,
                                          prc->right - prc->left - size.cx,
                                          cSpaceChars) ;
               xStart = prc->left ;
               break ;
          }
               // display the text
          
          TextOut (hdc, xStart, yStart, pBegin, pEnd - pBegin) ;

               // prepare for next line

          SetTextJustification (hdc, 0, 0) ;
          yStart += size.cy ;
          pText = pEnd ;
     }
     while (*pText && yStart < prc->bottom - size.cy) ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static CHOOSEFONT cf ;
     static DOCINFO    di = { sizeof (DOCINFO), TEXT ("Justify1: Printing") } ;
     static int        iAlign = IDM_ALIGN_LEFT ;
     static LOGFONT    lf ;
     static PRINTDLG   pd ;
     static TCHAR      szText[] = { 
                              TEXT ("You don't know about me, without you ")
                              TEXT ("have read a book by the name of \"The ")
                              TEXT ("Adventures of Tom Sawyer,\" but that ")
                              TEXT ("ain't no matter. That book was made by ")
                              TEXT ("Mr. Mark Twain, and he told the truth, ")
                              TEXT ("mainly. There was things which he ")
                              TEXT ("stretched, but mainly he told the truth. ")
                              TEXT ("That is nothing. I never seen anybody ")
                              TEXT ("but lied, one time or another, without ")
                              TEXT ("it was Aunt Polly, or the widow, or ")
                              TEXT ("maybe Mary. Aunt Polly -- Tom's Aunt ")
                              TEXT ("Polly, she is -- and Mary, and the Widow ")
                              TEXT ("Douglas, is all told about in that book ")
                              TEXT ("-- which is mostly a true book; with ")
                              TEXT ("some stretchers, as I said before.") } ;
     BOOL              fSuccess ;
     HDC               hdc, hdcPrn ;
     HMENU             hMenu ;
     int               iSavePointSize ;
     PAINTSTRUCT       ps ;
     RECT              rect ;
     
     switch (message)
     {
     case WM_CREATE:
               // Initialize the CHOOSEFONT structure

          GetObject (GetStockObject (SYSTEM_FONT), sizeof (lf), &lf) ;

          cf.lStructSize    = sizeof (CHOOSEFONT) ;
          cf.hwndOwner      = hwnd ;
          cf.hDC            = NULL ;
          cf.lpLogFont      = &lf ;
          cf.iPointSize     = 0 ;
          cf.Flags          = CF_INITTOLOGFONTSTRUCT | CF_SCREENFONTS | 
                              CF_EFFECTS ;
          cf.rgbColors      = 0 ;
          cf.lCustData      = 0 ;
          cf.lpfnHook       = NULL ;
          cf.lpTemplateName = NULL ;
          cf.hInstance      = NULL ;
          cf.lpszStyle      = NULL ;
          cf.nFontType      = 0 ;      
          cf.nSizeMin       = 0 ;
          cf.nSizeMax       = 0 ;
  
          return 0 ;
     case WM_COMMAND:
          hMenu = GetMenu (hwnd) ;
          
          switch (LOWORD (wParam))
          {
          case IDM_FILE_PRINT:
                                   // Get printer DC

               pd.lStructSize = sizeof (PRINTDLG) ;
               pd.hwndOwner   = hwnd ;
               pd.Flags       = PD_RETURNDC | PD_NOPAGENUMS | PD_NOSELECTION ;

               if (!PrintDlg (&pd))
                    return 0 ;

               if (NULL == (hdcPrn = pd.hDC))
               {
                    MessageBox (hwnd, TEXT ("Cannot obtain Printer DC"),
                                szAppName, MB_ICONEXCLAMATION | MB_OK) ;
                    return 0 ;
               }
                    // Set margins of 1 inch

               rect.left   = GetDeviceCaps (hdcPrn, LOGPIXELSX) -
                             GetDeviceCaps (hdcPrn, PHYSICALOFFSETX) ;

               rect.top    = GetDeviceCaps (hdcPrn, LOGPIXELSY) -
                             GetDeviceCaps (hdcPrn, PHYSICALOFFSETY) ;

               rect.right  = GetDeviceCaps (hdcPrn, PHYSICALWIDTH) -
                             GetDeviceCaps (hdcPrn, LOGPIXELSX) -
                             GetDeviceCaps (hdcPrn, PHYSICALOFFSETX) ;

               rect.bottom = GetDeviceCaps (hdcPrn, PHYSICALHEIGHT) - 
                             GetDeviceCaps (hdcPrn, LOGPIXELSY) -
                             GetDeviceCaps (hdcPrn, PHYSICALOFFSETY) ;

                    // Display text on printer

               SetCursor (LoadCursor (NULL, IDC_WAIT)) ;
               ShowCursor (TRUE) ;

               fSuccess = FALSE ;
               if ((StartDoc (hdcPrn, &di) > 0) && (StartPage (hdcPrn) > 0))
               {
                         // Select font using adjusted lfHeight

                    iSavePointSize = lf.lfHeight ;
                    lf.lfHeight = -(GetDeviceCaps (hdcPrn, LOGPIXELSY) *
                                         cf.iPointSize) / 720 ;

                    SelectObject (hdcPrn, CreateFontIndirect (&lf)) ;
                    lf.lfHeight = iSavePointSize ;

                         // Set text color 

                    SetTextColor (hdcPrn, cf.rgbColors) ;
               
                         // Display text

                    Justify (hdcPrn, szText, &rect, iAlign) ;

                    if (EndPage (hdcPrn) > 0)
                    {
                         fSuccess = TRUE ;
                         EndDoc (hdcPrn) ;
                    }
               }
               ShowCursor (FALSE) ;
               SetCursor (LoadCursor (NULL, IDC_ARROW)) ;

               DeleteDC (hdcPrn) ;

               if (!fSuccess)
                    MessageBox (hwnd, TEXT ("Could not print text"),
                                szAppName, MB_ICONEXCLAMATION | MB_OK) ;
               return 0 ;

          case IDM_FONT:
               if (ChooseFont (&cf))
                    InvalidateRect (hwnd, NULL, TRUE) ;
               return 0 ;
               
          case IDM_ALIGN_LEFT:
          case IDM_ALIGN_RIGHT:
          case IDM_ALIGN_CENTER:
          case IDM_ALIGN_JUSTIFIED:
               CheckMenuItem (hMenu, iAlign, MF_UNCHECKED) ;
               iAlign = LOWORD (wParam) ;
               CheckMenuItem (hMenu, iAlign, MF_CHECKED) ;
               InvalidateRect (hwnd, NULL, TRUE) ;
               return 0 ;
          }
          return 0 ;

     case WM_PAINT:
          hdc = BeginPaint (hwnd, &ps) ;
          
          GetClientRect (hwnd, &rect) ;
          DrawRuler (hdc, &rect) ;
          
          rect.left  += GetDeviceCaps (hdc, LOGPIXELSX) / 2 ;
          rect.top   += GetDeviceCaps (hdc, LOGPIXELSY) / 2 ;
          rect.right -= GetDeviceCaps (hdc, LOGPIXELSX) / 4 ;

          SelectObject (hdc, CreateFontIndirect (&lf)) ;
          SetTextColor (hdc, cf.rgbColors) ;
          
          Justify (hdc, szText, &rect, iAlign) ;
          
          DeleteObject (SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ;
          EndPaint (hwnd, &ps) ;
          return 0 ;
          
     case WM_DESTROY:
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

JUSTIFY1.RC

//Microsoft Developer Studio generated resource script.

#include "resource.h"
#include "afxres.h"

/////////////////////////////////////////////////////////////////////////////
// Menu

JUSTIFY1 MENU DISCARDABLE 
BEGIN
    POPUP "&File"
    BEGIN
        MENUITEM "&Print",                      IDM_FILE_PRINT
    END
    POPUP "&Font"
    BEGIN
        MENUITEM "&Font...",                    IDM_FONT
    END
    POPUP "&Align"
    BEGIN
        MENUITEM "&Left",                       IDM_ALIGN_LEFT, CHECKED
        MENUITEM "&Right",                      IDM_ALIGN_RIGHT
        MENUITEM "&Centered",                   IDM_ALIGN_CENTER
        MENUITEM "&Justified",                  IDM_ALIGN_JUSTIFIED
    END
END

RESOURCE.H

// Microsoft Developer Studio generated include file.
// Used by Justify1.rc

#define IDM_FILE_PRINT                  40001
#define IDM_FONT                        40002
#define IDM_ALIGN_LEFT                  40003
#define IDM_ALIGN_RIGHT                 40004
#define IDM_ALIGN_CENTER                40005
#define IDM_ALIGN_JUSTIFIED             40006

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.

JUSTIFY1 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.

click to view full size

Figure 17-10. A typical JUSTIFY1 display.

Previewing Printer Output

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.C -- Justified Type Program #2
                 (c) Charles Petzold, 1998
  -----------------------------------------*/

#include <windows.h>
#include "resource.h"

#define OUTWIDTH 6       // Width of formatted output in inches
#define LASTCHAR 127     // Last character code used in text

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;

TCHAR szAppName[] = TEXT ("Justify2") ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     HWND     hwnd ;
     MSG      msg ;
     WNDCLASS wndclass ;
     
     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = szAppName ;
     wndclass.lpszClassName = szAppName ;
     if (!RegisterClass (&wndclass))
     {
          MessageBox (NULL, TEXT ("This program requires Windows NT!"),
               szAppName, MB_ICONERROR) ;
          return 0 ;
     }
     
     hwnd = CreateWindow (szAppName, TEXT ("Justified Type #2"),
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;
     
     ShowWindow (hwnd, iCmdShow) ;
     UpdateWindow (hwnd) ;
     
     while (GetMessage (&msg, NULL, 0, 0))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return msg.wParam ;
}

void DrawRuler (HDC hdc, RECT * prc)
{
     static int iRuleSize [16] = { 360, 72, 144, 72, 216, 72, 144, 72,
                                   288, 72, 144, 72, 216, 72, 144, 72 } ;
     int        i, j ;
     POINT      ptClient ;
     
     SaveDC (hdc) ;
     
          // Set Logical Twips mapping mode
     
     SetMapMode (hdc, MM_ANISOTROPIC) ;
     SetWindowExtEx (hdc, 1440, 1440, NULL) ;
     SetViewportExtEx (hdc, GetDeviceCaps (hdc, LOGPIXELSX),
                            GetDeviceCaps (hdc, LOGPIXELSY), NULL) ;
     
          // Move the origin to a half inch from upper left
     
     SetWindowOrgEx (hdc, -720, -720, NULL) ;
     
          // Find the right margin (quarter inch from right)
     
     ptClient.x = prc->right ;
     ptClient.y = prc->bottom ;
     DPtoLP (hdc, &ptClient, 1) ;
     ptClient.x -= 360 ;
     
          // Draw the rulers
     
     MoveToEx (hdc, 0,               -360, NULL) ;
     LineTo   (hdc, OUTWIDTH * 1440, -360) ;
     MoveToEx (hdc, -360,               0, NULL) ;
     LineTo   (hdc, -360,      ptClient.y) ;
     
     for (i = 0, j = 0 ; i <= ptClient.x && i <= OUTWIDTH * 1440 ;
                         i += 1440 / 16, j++)
     {
          MoveToEx (hdc, i, -360, NULL) ;
          LineTo   (hdc, i, -360 - iRuleSize [j % 16]) ;
     }
     
     for (i = 0, j = 0 ; i <= ptClient.y ; i += 1440 / 16, j++)
     {
          MoveToEx (hdc, -360, i, NULL) ;
          LineTo   (hdc, -360 - iRuleSize [j % 16], i) ;
     }
     
     RestoreDC (hdc, -1) ;
}

/*----------------------------------------------------------------------
   GetCharDesignWidths:  Gets character widths for font as large as the
                         original design size
  ----------------------------------------------------------------------*/

UINT GetCharDesignWidths (HDC hdc, UINT uFirst, UINT uLast, int * piWidths)
{
     HFONT             hFont, hFontDesign ;
     LOGFONT           lf ;
     OUTLINETEXTMETRIC otm ;

     hFont = GetCurrentObject (hdc, OBJ_FONT) ;
     GetObject (hFont, sizeof (LOGFONT), &lf) ;

          // Get outline text metrics (we'll only be using a field that is
          //   independent of the DC the font is selected into)

     otm.otmSize = sizeof (OUTLINETEXTMETRIC) ;
     GetOutlineTextMetrics (hdc, sizeof (OUTLINETEXTMETRIC), &otm) ;
          // Create a new font based on the design size

     lf.lfHeight = - (int) otm.otmEMSquare ;
     lf.lfWidth  = 0 ;
     hFontDesign = CreateFontIndirect (&lf) ;

          // Select the font into the DC and get the character widths

     SaveDC (hdc) ;
     SetMapMode (hdc, MM_TEXT) ;
     SelectObject (hdc, hFontDesign) ;

     GetCharWidth (hdc, uFirst, uLast, piWidths) ;
     SelectObject (hdc, hFont) ;
     RestoreDC (hdc, -1) ;

          // Clean up

     DeleteObject (hFontDesign) ;

     return otm.otmEMSquare ;
}

/*---------------------------------------------------------------------
   GetScaledWidths:  Gets floating point character widths for selected
                     font size
  ---------------------------------------------------------------------*/

void GetScaledWidths (HDC hdc, double * pdWidths)
{
     double  dScale ;
     HFONT   hFont ;
     int     aiDesignWidths [LASTCHAR + 1] ;
     int     i ;
     LOGFONT lf ;
     UINT    uEMSquare ;

          // Call function above

     uEMSquare = GetCharDesignWidths (hdc, 0, LASTCHAR, aiDesignWidths) ;

          // Get LOGFONT for current font in device context

     hFont = GetCurrentObject (hdc, OBJ_FONT) ;
     GetObject (hFont, sizeof (LOGFONT), &lf) ;
          // Scale the widths and store as floating point values

     dScale = (double) -lf.lfHeight / (double) uEMSquare ;

     for (i = 0 ; i <= LASTCHAR ; i++)
          pdWidths[i] = dScale * aiDesignWidths[i] ;
}

/*--------------------------------------------------------------
   GetTextExtentFloat:  Calculates text width in floating point
  --------------------------------------------------------------*/

double GetTextExtentFloat (double * pdWidths, PTSTR psText, int iCount)
{
     double dWidth = 0 ;
     int    i ;

     for (i = 0 ; i < iCount ; i++)
          dWidth += pdWidths [psText[i]] ;

     return dWidth ;
}

/*------------------------------------------------------------------
   Justify:  Based on design units for screen/printer compatibility
  ------------------------------------------------------------------*/

void Justify (HDC hdc, PTSTR pText, RECT * prc, int iAlign)
{
     double dWidth, adWidths[LASTCHAR + 1] ;
     int    xStart, yStart, cSpaceChars ;
     PTSTR  pBegin, pEnd ;
     SIZE   size ;

          // Fill the adWidths array with floating point character widths

     GetScaledWidths (hdc, adWidths) ;

          // Call this function just once to get size.cy (font height)

     GetTextExtentPoint32(hdc, pText, 1, &size) ;
     
     yStart = prc->top ;
     do                            // for each text line
     {
          cSpaceChars = 0 ;        // initialize number of spaces in line
          while (*pText == ` `)    // skip over leading spaces
               pText++ ;

          pBegin = pText ;         // set pointer to char at beginning of line
          
          do                       // until the line is known
          {
               pEnd = pText ;      // set pointer to char at end of line

                    // skip to next space 
               
               while (*pText != `\0' && *pText++ != ` `) ;

               if (*pText == `\0')
                    break ;

                    // after each space encountered, calculate extents

               cSpaceChars++ ;
               dWidth = GetTextExtentFloat (adWidths, pBegin, 
                                                      pText - pBegin - 1) ;
          }
          while (dWidth < (double) (prc->right - prc->left)) ;
          
          cSpaceChars-- ;               // discount last space at end of line
          
          while (*(pEnd - 1) == ` `)    // eliminate trailing spaces
          {
               pEnd-- ;
               cSpaceChars-- ;
          }

               // if end of text and no space characters, set pEnd to end
          
          if (*pText == `\0' || cSpaceChars <= 0)
               pEnd = pText ;
          
          dWidth = GetTextExtentFloat (adWidths, pBegin, pText - pBegin - 1) ;
          
          switch (iAlign)               // use alignment for xStart
          {
          case IDM_ALIGN_LEFT:
               xStart = prc->left ;
               break ;
          case IDM_ALIGN_RIGHT:
               xStart = prc->right - (int) (dWidth + .5) ;
               break ;
               
          case IDM_ALIGN_CENTER:
               xStart = (prc->right + prc->left - (int) (dWidth + .5)) / 2 ;
               break ;
               
          case IDM_ALIGN_JUSTIFIED:
               if (*pText != `\0' && cSpaceChars > 0)
                    SetTextJustification (hdc,
                                          prc->right - prc->left - 
                                                       (int) (dWidth + .5),
                                          cSpaceChars) ;
               xStart = prc->left ;
               break ;
          }
               // display the text
          
          TextOut (hdc, xStart, yStart, pBegin, pEnd - pBegin) ;

               // prepare for next line

          SetTextJustification (hdc, 0, 0) ;
          yStart += size.cy ;
          pText = pEnd ;
     }
     while (*pText && yStart < prc->bottom - size.cy) ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static CHOOSEFONT cf ;
     static DOCINFO    di = { sizeof (DOCINFO), TEXT ("Justify2: Printing") } ;
     static int        iAlign = IDM_ALIGN_LEFT ;
     static LOGFONT    lf ;
     static PRINTDLG   pd ;
     static TCHAR      szText[] = { 
                              TEXT ("Call me Ishmael. Some years ago -- never ")
                              TEXT ("mind how long precisely -- having little ")
                              TEXT ("or no money in my purse, and nothing ")
                              TEXT ("particular to interest me on shore, I ")
                              TEXT ("thought I would sail about a little and ")
                              TEXT ("see the watery part of the world. It is ")
                              TEXT ("a way I have of driving off the spleen, ")
                              TEXT ("and regulating the circulation. Whenever ")
                              TEXT ("I find myself growing grim about the ")
                              TEXT ("mouth; whenever it is a damp, drizzly ")
                              TEXT ("November in my soul; whenever I find ")
                              TEXT ("myself involuntarily pausing before ")
                              TEXT ("coffin warehouses, and bringing up the ")
                              TEXT ("rear of every funeral I meet; and ")
                              TEXT ("especially whenever my hypos get such an ")
                              TEXT ("upper hand of me, that it requires a ")
                              TEXT ("strong moral principle to prevent me ")
                              TEXT ("from deliberately stepping into the ")
                              TEXT ("street, and methodically knocking ")
                              TEXT ("people's hats off -- then, I account it ")
                              TEXT ("high time to get to sea as soon as I ")
                              TEXT ("can. This is my substitute for pistol ")
                              TEXT ("and ball. With a philosophical flourish ")
                              TEXT ("Cato throws himself upon his sword; I ")
                              TEXT ("quietly take to the ship. There is ")
                              TEXT ("nothing surprising in this. If they but ")
                              TEXT ("knew it, almost all men in their degree, ")
                              TEXT ("some time or other, cherish very nearly ")
                              TEXT ("the same feelings towards the ocean with ")
                              TEXT ("me.") } ;
     BOOL              fSuccess ;
     HDC               hdc, hdcPrn ;
     HMENU             hMenu ;
     int               iSavePointSize ;
     PAINTSTRUCT       ps ;
     RECT              rect ;
     
     switch (message)
     {
     case WM_CREATE:
               // Initialize the CHOOSEFONT structure

          hdc = GetDC (hwnd) ;
          lf.lfHeight = - GetDeviceCaps (hdc, LOGPIXELSY) / 6 ;
          lstrcpy (lf.lfFaceName, TEXT ("Times New Roman")) ;
          ReleaseDC (hwnd, hdc) ;

          cf.lStructSize    = sizeof (CHOOSEFONT) ;
          cf.hwndOwner      = hwnd ;
          cf.hDC            = NULL ;
          cf.lpLogFont      = &lf ;
          cf.iPointSize     = 0 ;
          cf.Flags          = CF_INITTOLOGFONTSTRUCT | CF_SCREENFONTS | 
                              CF_TTONLY | CF_EFFECTS ;
          cf.rgbColors      = 0 ;
          cf.lCustData      = 0 ;
          cf.lpfnHook       = NULL ;
          cf.lpTemplateName = NULL ;
          cf.hInstance      = NULL ;
          cf.lpszStyle      = NULL ;
          cf.nFontType      = 0 ;      
          cf.nSizeMin       = 0 ;
          cf.nSizeMax       = 0 ;
  
          return 0 ;

     case WM_COMMAND:
          hMenu = GetMenu (hwnd) ;
          
          switch (LOWORD (wParam))
          {
          case IDM_FILE_PRINT:
                                   // Get printer DC

               pd.lStructSize = sizeof (PRINTDLG) ;
               pd.hwndOwner   = hwnd ;
               pd.Flags       = PD_RETURNDC | PD_NOPAGENUMS | PD_NOSELECTION ;

               if (!PrintDlg (&pd))
                    return 0 ;

               if (NULL == (hdcPrn = pd.hDC))
               {
                    MessageBox (hwnd, TEXT ("Cannot obtain Printer DC"),
                                szAppName, MB_ICONEXCLAMATION | MB_OK) ;
                    return 0 ;
               }
                    // Set margins for OUTWIDTH inches wide

               rect.left  = (GetDeviceCaps (hdcPrn, PHYSICALWIDTH) -
                             GetDeviceCaps (hdcPrn, LOGPIXELSX) * OUTWIDTH) / 2 
                           - GetDeviceCaps (hdcPrn, PHYSICALOFFSETX) ;
               
               rect.right = rect.left + 
                             GetDeviceCaps (hdcPrn, LOGPIXELSX) * OUTWIDTH ;

                    // Set margins of 1 inch at top and bottom

               rect.top    = GetDeviceCaps (hdcPrn, LOGPIXELSY) -
                             GetDeviceCaps (hdcPrn, PHYSICALOFFSETY) ;
               rect.bottom = GetDeviceCaps (hdcPrn, PHYSICALHEIGHT) - 
                             GetDeviceCaps (hdcPrn, LOGPIXELSY) -
                             GetDeviceCaps (hdcPrn, PHYSICALOFFSETY) ;

                    // Display text on printer

               SetCursor (LoadCursor (NULL, IDC_WAIT)) ;
               ShowCursor (TRUE) ;

               fSuccess = FALSE ;

               if ((StartDoc (hdcPrn, &di) > 0) && (StartPage (hdcPrn) > 0))
               {
                         // Select font using adjusted lfHeight

                    iSavePointSize = lf.lfHeight ;
                    lf.lfHeight = -(GetDeviceCaps (hdcPrn, LOGPIXELSY) *
                                         cf.iPointSize) / 720 ;

                    SelectObject (hdcPrn, CreateFontIndirect (&lf)) ;
                    lf.lfHeight = iSavePointSize ;

                         // Set text color 

                    SetTextColor (hdcPrn, cf.rgbColors) ;
               
                         // Display text

                    Justify (hdcPrn, szText, &rect, iAlign) ;

                    if (EndPage (hdcPrn) > 0)
                    {
                         fSuccess = TRUE ;
                         EndDoc (hdcPrn) ;
                    }
               }
               ShowCursor (FALSE) ;
               SetCursor (LoadCursor (NULL, IDC_ARROW)) ;

               DeleteDC (hdcPrn) ;

               if (!fSuccess)
                    MessageBox (hwnd, TEXT ("Could not print text"),
                                szAppName, MB_ICONEXCLAMATION | MB_OK) ;
               return 0 ;
          case IDM_FONT:
               if (ChooseFont (&cf))
                    InvalidateRect (hwnd, NULL, TRUE) ;
               return 0 ;
               
          case IDM_ALIGN_LEFT:
          case IDM_ALIGN_RIGHT:
          case IDM_ALIGN_CENTER:
          case IDM_ALIGN_JUSTIFIED:
               CheckMenuItem (hMenu, iAlign, MF_UNCHECKED) ;
               iAlign = LOWORD (wParam) ;
               CheckMenuItem (hMenu, iAlign, MF_CHECKED) ;
               InvalidateRect (hwnd, NULL, TRUE) ;
               return 0 ;
          }
          return 0 ;

     case WM_PAINT:
          hdc = BeginPaint (hwnd, &ps) ;
          
          GetClientRect (hwnd, &rect) ;
          DrawRuler (hdc, &rect) ;
          
          rect.left  += GetDeviceCaps (hdc, LOGPIXELSX) / 2 ;
          rect.top   += GetDeviceCaps (hdc, LOGPIXELSY) / 2 ;
          rect.right = rect.left + OUTWIDTH * GetDeviceCaps (hdc, LOGPIXELSX) ;

          SelectObject (hdc, CreateFontIndirect (&lf)) ;
          SetTextColor (hdc, cf.rgbColors) ;
          
          Justify (hdc, szText, &rect, iAlign) ;
          
          DeleteObject (SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ;
          EndPaint (hwnd, &ps) ;
          return 0 ;
          
     case WM_DESTROY:
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

JUSTIFY2.RC


//Microsoft Developer Studio generated resource script.

#include "resource.h"
#include "afxres.h"

/////////////////////////////////////////////////////////////////////////////
// Menu

JUSTIFY2 MENU DISCARDABLE 
BEGIN
    POPUP "&File"
    BEGIN
        MENUITEM "&Print",                      IDM_FILE_PRINT
    END
    POPUP "&Font"
    BEGIN
        MENUITEM "&Font...",                    IDM_FONT
    END
    POPUP "&Align"
    BEGIN
        MENUITEM "&Left",                       IDM_ALIGN_LEFT, CHECKED
        MENUITEM "&Right",                      IDM_ALIGN_RIGHT
        MENUITEM "&Centered",                   IDM_ALIGN_CENTER
        MENUITEM "&Justified",                  IDM_ALIGN_JUSTIFIED
    END
END

RESOURCE.H

// Microsoft Developer Studio generated include file.
// Used by Justify2.rc

#define IDM_FILE_PRINT                  40001
#define IDM_FONT                        40002
#define IDM_ALIGN_LEFT                  40003
#define IDM_ALIGN_RIGHT                 40004
#define IDM_ALIGN_CENTER                40005
#define IDM_ALIGN_JUSTIFIED             40006

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 you want to use 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.