Expressing font characters in terms of outlines opens up lots of potential in combining fonts with other graphics techniques. Earlier we saw how fonts can be rotated. This final section shows some other tricks. But before we continue, let's look at two important preliminaries: graphics paths and extended pens.
A path is a collection of straight lines and curves stored internally to GDI. Paths were introduced in the 32-bit versions of Windows. The path may initially seem similar to the region, and indeed you can convert a path to a region and use a path for clipping. However, we'll see shortly how they differ.
To begin a path definition, you simply call
BeginPath (hdc) ;
After this call, any line you draw (such as straight lines, arcs, and Bezier splines) will be stored internally to GDI as a path and not rendered on the device context. Often a path consists of connected lines. To make connected lines, you use the LineTo, PolylineTo, and BezierTo functions, all of which draw lines beginning at the current position. If you change the current position by using MoveToEx, or if you call any of the other line-drawing functions, or if you call one of the window/viewport functions that cause a change in the current position, you create a new subpath within the entire path. Thus, a path contains one or more subpaths, where each subpath is a series of connected lines.
Each subpath within the path can be open or closed. A closed subpath is one in which the first point of the first connected line is the same as the last point of the last connected line, and moreover, the subpath is concluded by a call to CloseFigure. CloseFigure will close the subpath with a straight line, if necessary. Any subsequent line-drawing function begins a new subpath. Finally, you end the path definition by calling
EndPath (hdc) ;
At this point you then call one of the following five functions:
StrokePath (hdc) ; FillPath (hdc) ; StrokeAndFillPath (hdc) ; hRgn = PathToRegion (hdc) ; SelectClipPath (hdc, iCombine) ;
Each of these functions destroys the path definition after completion.
StrokePath draws the path using the current pen. You might wonder: What's the point? Why can't I just skip all this path stuff and draw the lines normally? I'll tell you why shortly.
The other four functions close any open paths with straight lines. FillPath fills the path using the current brush according to the current polygon-filling mode. StrokeAndFillPath does both jobs in one shot. You can also convert the path to a region or use the path for a clipping area. The iCombine argument is one of the RGN_ constants used with the CombineRgn function, and it indicates how the path is combined with the current clipping region.
Paths are more flexible than regions for filling and clipping because regions can be defined only by combinations of rectangles, ellipses, and polygons. Paths can be composed of Bezier splines and, at least in Windows NT, arcs. In GDI, paths and regions are stored quite differently. The path is a collection of line and curve definitions, and the region (in the general sense) is a collection of scan lines.
When you call StrokePath, the path is rendered using the current pen. Back in Chapter 4, I discussed the CreatePen function that you use to create a pen object. With the introduction of paths, Windows also supports an extended pen function call named ExtCreatePen. This function reveals why it's sometimes useful to create a path and stroke it rather than to draw lines without using a path. The ExtCreatePen function looks like this:
hPen = ExtCreatePen (iStyle, iWidth, &lBrush, 0, NULL) ;
You can use this function for normal line drawing, but in that case some of the features aren't supported by Windows 98. Even when used for rendering paths, some features are still not supported by Windows 98, which I've indicated above by setting the last two arguments to 0 and NULL.
For the first argument to ExtCreatePen, you can use any of the styles described in Chapter 4 for CreatePen. You can additionally combine these styles with PS_GEOMETRIC, where the iWidth argument denoting the width of the line is in logical units and is subject to transforms, or PS_COSMETIC, where the iWidth argument must be 1. In Windows 98, pens with a dashed or dotted style must be PS_COSMETIC. This restriction is lifted for Windows NT.
One of the arguments to CreatePen is a color; rather than a color, ExtCreatePen uses a brush to color the interiors of PS_GEOMETRIC pens. That brush can even be defined by a bitmap.
When you're drawing wide lines, you might also be concerned about the appearance of the ends of the lines. When lines or curves are connected, you might also be concerned about the appearance of the joins between the lines. With pens created by CreatePen, these ends and joins are always rounded. With ExtCreatePen, you have a choice. (Actually, in Windows 98, you have a choice only when you use the pen to stroke a path; Windows NT is more flexible.) The ends of wide lines can be defined using one of the following pen styles in ExtCreatePen:
PS_ENDCAP_ROUND PS_ENDCAP_SQUARE PS_ENDCAP_FLAT
The "square" style is different from the "flat" style in that it extends the line for one-half the width. Similarly, joins between lines in a path can be specified by
PS_JOIN_ROUND PS_JOIN_BEVEL PS_JOIN_MITER
The "bevel" style cuts off the end of the join and the "miter" style turns it into a spike. This can be better illustrated with a program called ENDJOIN, which is shown in Figure 17-12.
Figure 17-12. The ENDJOIN Program.
ENDJOIN.C/*---------------------------------------- ENDJOIN.C -- Ends and Joins Demo (c) Charles Petzold, 1998 ----------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("EndJoin") ; 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 = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Ends and Joins Demo"), 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 ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) { static int iEnd[] = { PS_ENDCAP_ROUND, PS_ENDCAP_SQUARE, PS_ENDCAP_FLAT } ; static int iJoin[]= { PS_JOIN_ROUND, PS_JOIN_BEVEL, PS_JOIN_MITER } ; static int cxClient, cyClient ; HDC hdc ; int i ; LOGBRUSH lb ; PAINTSTRUCT ps ; switch (iMsg) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SetMapMode (hdc, MM_ANISOTROPIC) ; SetWindowExtEx (hdc, 100, 100, NULL) ; SetViewportExtEx (hdc, cxClient, cyClient, NULL) ; lb.lbStyle = BS_SOLID ; lb.lbColor = RGB (128, 128, 128) ; lb.lbHatch = 0 ; for (i = 0 ; i < 3 ; i++) { SelectObject (hdc, ExtCreatePen (PS_SOLID | PS_GEOMETRIC | iEnd [i] | iJoin [i], 10, &lb, 0, NULL)) ; BeginPath (hdc) ; MoveToEx (hdc, 10 + 30 * i, 25, NULL) ; LineTo (hdc, 20 + 30 * i, 75) ; LineTo (hdc, 30 + 30 * i, 25) ; EndPath (hdc) ; StrokePath (hdc) ; DeleteObject ( SelectObject (hdc, GetStockObject (BLACK_PEN))) ; MoveToEx (hdc, 10 + 30 * i, 25, NULL) ; LineTo (hdc, 20 + 30 * i, 75) ; LineTo (hdc, 30 + 30 * i, 25) ; } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, iMsg, wParam, lParam) ; } |
The program draws three V-shaped wide lines using the end and join styles in the order listed above. The program also draws three identical lines using the stock black pen. This shows how the wide line compares with the normal thin line. The results are shown in Figure 17-13.
Figure 17-13. The ENDJOIN display.
I hope it's now apparent why Windows supports a StrokePath function: If you were to draw the two lines individually, GDI would be forced to use the line ends on each of them. Only if they're in a path definition does GDI know that the lines are connected and then use a line join.
Of what good is this? Well, think about it: The characters in outline fonts are defined by a series of coordinate values. These coordinates define straight lines and splines. Thus, the straight lines and curves can become part of a path definition.
And yes, it works! This is demonstrated in the FONTOUT1 program shown in Figure 17-14.
Figure 17-14. The FONTOUT1 program.
FONTOUT1.C/*------------------------------------------ FONTOUT1.C -- Using Path to Outline Font (c) Charles Petzold, 1998 ------------------------------------------*/ #include <windows.h> #include "..\\eztest\\ezfont.h" TCHAR szAppName [] = TEXT ("FontOut1") ; TCHAR szTitle [] = TEXT ("FontOut1: Using Path to Outline Font") ; void PaintRoutine (HWND hwnd, HDC hdc, int cxArea, int cyArea) { static TCHAR szString [] = TEXT ("Outline") ; HFONT hFont ; SIZE size ; hFont = EzCreateFont (hdc, TEXT ("Times New Roman"), 1440, 0, 0, TRUE) ; SelectObject (hdc, hFont) ; GetTextExtentPoint32 (hdc, szString, lstrlen (szString), &size) ; BeginPath (hdc) ; TextOut (hdc, (cxArea - size.cx) / 2, (cyArea - size.cy) / 2, szString, lstrlen (szString)) ; EndPath (hdc) ; StrokePath (hdc) ; SelectObject (hdc, GetStockObject (SYSTEM_FONT)) ; DeleteObject (hFont) ; } |
This program, and the remainder of the programs in this chapter, also use the EZFONT and FONTDEMO files shown earlier.
The program creates a 144-point TrueType font and calls the GetTextExtentPoint32 function to obtain the dimensions of the text box. It then calls the TextOut function in a path definition so that the text is centered in the client window. Because the TextOut function is called in a path bracket—that is, between calls to BeginPath and EndPath—GDI does not display the text immediately. Instead, the character outlines are stored in the path definition.
After the path bracket is ended, FONTOUT1 calls StrokePath. Because no special pen has been selected into the device context, GDI simply draws the character outlines using the default pen, as shown in Figure 17-15.
Figure 17-15. The FONTOUT1 display.
But what have we here? We've got outlined characters, as we expect, but why is the text string surrounded by a rectangle?
Well, recall that the text background mode is by default OPAQUE rather than TRANSPARENT. That rectangle is the outline of the text box. This clearly demonstrates the two-step approach that GDI uses when drawing text in the default OPAQUE mode. First it draws a filled rectangle, and then it draws the characters. The outline of the text box rectangle thus also becomes part of the path.
Using the ExtCreatePen function, you can outline the characters of a font with something other than the default pen. This is demonstrated in the FONTOUT2 program shown in Figure 17-16.
Figure 17-16. The FONTOUT2 program.
FONTOUT2.C/*------------------------------------------ FONTOUT2.C -- Using Path to Outline Font (c) Charles Petzold, 1998 ------------------------------------------*/ #include <windows.h> #include "..\\eztest\\ezfont.h" TCHAR szAppName [] = TEXT ("FontOut2") ; TCHAR szTitle [] = TEXT ("FontOut2: Using Path to Outline Font") ; void PaintRoutine (HWND hwnd, HDC hdc, int cxArea, int cyArea) { static TCHAR szString [] = TEXT ("Outline") ; HFONT hFont ; LOGBRUSH lb ; SIZE size ; hFont = EzCreateFont (hdc, TEXT ("Times New Roman"), 1440, 0, 0, TRUE) ; SelectObject (hdc, hFont) ; SetBkMode (hdc, TRANSPARENT) ; GetTextExtentPoint32 (hdc, szString, lstrlen (szString), &size) ; BeginPath (hdc) ; TextOut (hdc, (cxArea - size.cx) / 2, (cyArea - size.cy) / 2, szString, lstrlen (szString)) ; EndPath (hdc) ; lb.lbStyle = BS_SOLID ; lb.lbColor = RGB (255, 0, 0) ; lb.lbHatch = 0 ; SelectObject (hdc, ExtCreatePen (PS_GEOMETRIC | PS_DOT, GetDeviceCaps (hdc, LOGPIXELSX) / 24, &lb, 0, NULL)) ; StrokePath (hdc) ; DeleteObject (SelectObject (hdc, GetStockObject (BLACK_PEN))) ; SelectObject (hdc, GetStockObject (SYSTEM_FONT)) ; DeleteObject (hFont) ; } |
This program creates (and selects into the device context) a red dotted pen with a width of 3 points (1/24th inch) before calling StrokePath. The results when the program runs under Windows NT are shown in Figure 17-17.
Figure 17-17. The FONTOUT2 display.
You can also use paths to define areas for filling. You create the path in the same way as shown in the past two programs, select a filling pattern, and call FillPath. Another function you can call is StrokeAndFillPath, which both outlines a path and fills it with one function call.
The StrokeAndFillPath function is demonstrated in the FONTFILL program shown in Figure 17-18.
Figure 17-18. The FONTFILL program.
FONTFILL.C/*----------------------------------------- FONTFILL.C -- Using Path to Fill Font (c) Charles Petzold, 1998 -----------------------------------------*/ #include <windows.h> #include "..\\eztest\\ezfont.h" TCHAR szAppName [] = TEXT ("FontFill") ; TCHAR szTitle [] = TEXT ("FontFill: Using Path to Fill Font") ; void PaintRoutine (HWND hwnd, HDC hdc, int cxArea, int cyArea) { static TCHAR szString [] = TEXT ("Filling") ; HFONT hFont ; SIZE size ; hFont = EzCreateFont (hdc, TEXT ("Times New Roman"), 1440, 0, 0, TRUE) ; SelectObject (hdc, hFont) ; SetBkMode (hdc, TRANSPARENT) ; GetTextExtentPoint32 (hdc, szString, lstrlen (szString), &size) ; BeginPath (hdc) ; TextOut (hdc, (cxArea - size.cx) / 2, (cyArea - size.cy) / 2, szString, lstrlen (szString)) ; EndPath (hdc) ; SelectObject (hdc, CreateHatchBrush (HS_DIAGCROSS, RGB (255, 0, 0))) ; SetBkColor (hdc, RGB (0, 0, 255)) ; SetBkMode (hdc, OPAQUE) ; StrokeAndFillPath (hdc) ; DeleteObject (SelectObject (hdc, GetStockObject (WHITE_BRUSH))) ; SelectObject (hdc, GetStockObject (SYSTEM_FONT)) ; DeleteObject (hFont) ; } |
FONTFILL uses the default pen for outlining the path but creates a red hatched brush using the HS_DIAGCROSS style. Notice that the program sets the background mode to TRANSPARENT when creating the path but then resets it to OPAQUE when filling the path so that it can use a blue background color for the area pattern. The results are shown in Figure 17-19.
You may want to try a few variations on this program to observe the effects. First, if you comment out the first SetBkMode call, you'll get the background of the text box covered with the pattern but not the characters themselves. That's usually not what you want, but you can certainly do it.
Also, when filling characters and using them for clipping, you want to leave the default ALTERNATE polygon-filling mode in effect. My experience indicates that TrueType fonts are constructed so that nothing strange will happen (such as the interiors of Os being filled) if you use the WINDING fill mode, but you'll want to play it safe by sticking with ALTERNATE.
Figure 17-19. The FONTFILL display.
Finally, you can use a path, and hence a TrueType font, to define a clipping region. This is demonstrated in the FONTCLIP program shown in Figure 17-20.
Figure 17-20. The FONTCLIP program.
FONTCLIP.C/*----------------------------------------------- FONTCLIP.C -- Using Path for Clipping on Font (c) Charles Petzold, 1998 -----------------------------------------------*/ #include <windows.h> #include "..\\eztest\\ezfont.h" TCHAR szAppName [] = TEXT ("FontClip") ; TCHAR szTitle [] = TEXT ("FontClip: Using Path for Clipping on Font") ; void PaintRoutine (HWND hwnd, HDC hdc, int cxArea, int cyArea) { static TCHAR szString [] = TEXT ("Clipping") ; HFONT hFont ; int y, iOffset ; POINT pt [4] ; SIZE size ; hFont = EzCreateFont (hdc, TEXT ("Times New Roman"), 1200, 0, 0, TRUE) ; SelectObject (hdc, hFont) ; GetTextExtentPoint32 (hdc, szString, lstrlen (szString), &size) ; BeginPath (hdc) ; TextOut (hdc, (cxArea - size.cx) / 2, (cyArea - size.cy) / 2, szString, lstrlen (szString)) ; EndPath (hdc) ; // Set clipping area SelectClipPath (hdc, RGN_COPY) ; // Draw Bezier splines iOffset = (cxArea + cyArea) / 4 ; for (y = -iOffset ; y < cyArea + iOffset ; y++) { pt[0].x = 0 ; pt[0].y = y ; pt[1].x = cxArea / 3 ; pt[1].y = y + iOffset ; pt[2].x = 2 * cxArea / 3 ; pt[2].y = y - iOffset ; pt[3].x = cxArea ; pt[3].y = y ; SelectObject (hdc, CreatePen (PS_SOLID, 1, RGB (rand () % 256, rand () % 256, rand () % 256))) ; PolyBezier (hdc, pt, 4) ; DeleteObject (SelectObject (hdc, GetStockObject (BLACK_PEN))) ; } DeleteObject (SelectObject (hdc, GetStockObject (WHITE_BRUSH))) ; SelectObject (hdc, GetStockObject (SYSTEM_FONT)) ; DeleteObject (hFont) ; } |
This is a program where I've deliberately excluded the SetBkMode call to achieve a different effect. The program draws some text in a path bracket and then calls SelectClipPath. It then draws a series of Bezier spline curves with random colors.
If the FONTCLIP program had called SetBkMode with the TRANSPARENT option, the Bezier curves would have been restricted to the interiors of the character outlines. With the background mode in the default OPAQUE option, the clipping region is restricted to the interior of the text box but not the characters themselves. This is shown inFigure 1721.
Figure 17-21. The FONTCLIP display.
You'll probably want to insert a SetBkMode call into FONTCLIP to see the difference with the TRANSPARENT option.
The FONTDEMO shell program allows you to print as well as display these effects, and even better, you can try some of your own special effects.