Concurrent Programming on Windows
In the first chapter, I discussed how the Windows Graphics Device Interface makes use of device drivers for the graphics output devices attached to your computer. In theory, all that a graphics device driver needs for drawing is a SetPixel function and a GetPixel function. Everything else could be handled with higher-level routines implemented in the GDI module. Drawing a line, for instance, simply requires that GDI call the SetPixel routine numerous times, adjusting the x- and y-coordinates appropriately.
In reality, you can indeed do almost any drawing you need with only SetPixel and GetPixel functions. You can also design a neat and well-structured graphics programming system on top of these functions. The only problem is performance. A function that is several calls away from each SetPixel function will be painfully slow. It is much more efficient for a graphics system to do line drawing and other complex graphics operations at the level of the device driver, which can have its own optimized code to perform the operations. Moreover, some video adapter boards contain graphics coprocessors that allow the video hardware itself to draw the figures.
Setting Pixels
Even though the Windows GPI includes SetPixel and GetPixel functions, they are not commonly used. In this book, the only use of the SetPixel function is in the CONNECT program in Chapter 7, and the only use of GetPixel is in the WHATCLR program in Chapter 8. Still, they provide a convenient place to begin examining graphics.
The SetPixel function sets the pixel at a specified x- and y-coordinate to a particular color:
SetPixel (hdc, x, y, crColor) ;
As in any drawing function, the first argument is a handle to a device context. The second and third arguments indicate the coordinate position. Mostly you'll obtain a device context for the client area of your window, and x and y will be relative to the upper left corner of that client area. The final argument is of type COLORREF to specify the color. If the color you specify in the function cannot be realized on the video display, the function sets the pixel to the nearest pure nondithered color and returns that value from the function.
The GetPixel function returns the color of the pixel at the specified coordinate position:
crColor = GetPixel (hdc, x, y) ;
Straight Lines
Windows can draw straight lines, elliptical lines (curved lines on the circumference of an ellipse), and Bezier splines. Windows 98 supports seven functions that draw lines:
- LineTo Draws a straight line.
- Polyline and PolylineTo Draw a series of connected straight lines.
- PolyPolyline Draws multiple polylines.
- Arc Draws elliptical lines.
- PolyBezier and PolyBezierTo Draw Bezier splines.
In addition, Windows NT supports three more line-drawing functions:
- ArcTo and AngleArc Draw elliptical lines.
- PolyDraw Draws a series of connected straight lines and Bezier splines.
These three functions are not supported under Windows 98.
Later in this chapter I'll also be discussing some functions that draw lines but that also fill the enclosed area within the figure they draw. These functions are
- Rectangle Draws a rectangle.
- Ellipse Draws an ellipse.
- RoundRect Draws a rectangle with rounded corners.
- Pie Draws a part of an ellipse that looks like a pie slice.
- Chord Draws part of an ellipse formed by a chord.
Five attributes of the device context affect the appearance of lines that you draw using these functions: current pen position (for LineTo, PolylineTo, PolyBezierTo, and ArcTo only), pen, background mode, background color, and drawing mode.
To draw a straight line, you must call two functions. The first function specifies the point at which the line begins, and the second function specifies the end point of the line:
MoveToEx (hdc, xBeg, yBeg, NULL) ; LineTo (hdc, xEnd, yEnd) ;
MoveToEx doesn't actually draw anything; instead, it sets the attribute of the device context known as the "current position." The LineTo function then draws a straight line from the current position to the point specified in the LineTo function. The current position is simply a starting point for several other GDI functions. In the default device context, the current position is initially set to the point (0, 0). If you call LineTo without first setting the current position, it draws a line starting at the upper left corner of the client area.
A brief historical note: In the 16-bit versions of Windows, the function to set the current position was MoveTo. This function had just three arguments—the device context handle and x- and y-coordinates. The function returned the previous current position packed as two 16-bit values in a 32-bit unsigned long. However, in the 32-bit versions of Windows, coordinates are 32-bit values. Because the 32-bit versions of C do not define a 64-bit integral data type, this change meant that MoveTo could no longer indicate the previous current position in its return value. Although the return value from MoveTo was almost never used in real-life programming, a new function was required, and this was MoveToEx.
The last argument to MoveToEx is a pointer to a POINT structure. On return from the function, the x and y fields of the POINT structure will indicate the previous current position. If you don't need this information (which is almost always the case), you can simply set the last argument to NULL as in the example shown above.
And now the caveat: Although coordinate values in Windows 98 appear to be 32-bit values, only the lower 16 bits are used. Coordinate values are effectively restricted to -32,768 to 32,767. In Windows NT, the full 32-bit values are used.
If you ever need the current position, you can obtain it by calling
GetCurrentPositionEx (hdc, &pt) ;
where pt is a POINT structure.
The following code draws a grid in the client area of a window, spacing the lines 100 pixels apart starting from the upper left corner. The variable hwnd is assumed to be a handle to the window, hdc is a handle to the device context, and x and y are integers:
GetClientRect (hwnd, &rect) ; for (x = 0 ; x < rect.right ; x+= 100) { MoveToEx (hdc, x, 0, NULL) ; LineTo (hdc, x, rect.bottom) ; } for (y = 0 ; y < rect.bottom ; y += 100) { MoveToEx (hdc, 0, y, NULL) ; LineTo (hdc, rect.right, y) ; }
Although it seems like a nuisance to be forced to use two functions to draw a single line, the current position comes in handy when you want to draw a series of connected lines. For instance, you might want to define an array of 5 points (10 values) that define the outline of a rectangle:
POINT apt[5] = { 100, 100, 200, 100, 200, 200, 100, 200, 100, 100 } ;
Notice that the last point is the same as the first. Now you need only use MoveToEx for the first point and LineTo for the successive points:
MoveToEx (hdc, apt[0].x, apt[0].y, NULL) ; for (i = 1 ; i < 5 ; i++) LineTo (hdc, apt[i].x, apt[i].y) ;
Because LineTo draws from the current position up to (but not including) the point in the LineTo function, no coordinate gets written twice by this code. While overwriting points is not a problem with a video display, it might not look good on a plotter or with some drawing modes that I'll discuss later in this chapter.
When you have an array of points that you want connected with lines, you can draw the lines more easily using the Polyline function. This statement draws the same rectangle as in the code shown above:
Polyline (hdc, apt, 5) ;
The last argument is the number of points. We could also have represented this value by sizeof (apt) / sizeof (POINT). Polyline has the same effect on drawing as an initial MoveToEx followed by multiple LineTo functions. However, Polyline doesn't use or change the current position. PolylineTo is a little different. This function uses the current position for the starting point and sets the current position to the end of the last line drawn. The code below draws the same rectangle as that last shown above:
MoveToEx (hdc, apt[0].x, apt[0].y, NULL) ; PolylineTo (hdc, apt + 1, 4) ;
Although you can use Polyline and PolylineTo to draw just a few lines, the functions are most useful when you need to draw a complex curve. You do this by using hundreds or even thousands of very short lines. If they're short enough and there are enough of them, together they'll look like a curve. For example, suppose you need to draw a sine wave. The SINEWAVE program in Figure 5-6 shows how to do it.
Figure 5-6. The SINEWAVE program.
SINEWAVE.C
/*----------------------------------------- SINEWAVE.C -- Sine Wave Using Polyline (c) Charles Petzold, 1998 -----------------------------------------*/ #include <windows.h> #include <math.h> #define NUM 1000 #define TWOPI (2 * 3.14159) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("SineWave") ; 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 ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Sine Wave Using Polyline"), 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 message, WPARAM wParam, LPARAM lParam) { static int cxClient, cyClient ; HDC hdc ; int i ; PAINTSTRUCT ps ; POINT apt [NUM] ; switch (message) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; MoveToEx (hdc, 0, cyClient / 2, NULL) ; LineTo (hdc, cxClient, cyClient / 2) ; for (i = 0 ; i < NUM ; i++) { apt[i].x = i * cxClient / NUM ; apt[i].y = (int) (cyClient / 2 * (1 - sin (TWOPI * i / NUM))) ; } Polyline (hdc, apt, NUM) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } |
The program has an array of 1000 POINT structures. As the for loop is incremented from 0 through 999, the x fields of the POINT structure are set to incrementally increasing values from 0 to cxClient. The program sets the y fields of the POINT structure to sine curve values for one cycle and enlarged to fill the client area. The whole curve is drawn using a single Polyline call. Because the Polyline function is implemented at the device driver level, it is faster than calling LineTo 1000 times. The results are shown in Figure 5-7.
Figure 5-7. The SINEWAVE display.
The Bounding Box Functions
I next want to discuss the Arc function, which draws an elliptical curve. However, the Arc function does not make much sense without first discussing the Ellipse function, and the Ellipse function doesn't make much sense without first discussing the Rectangle function, and if I discuss Ellipse and Rectangle, I might as well discuss RoundRect, Chord, and Pie.
The problem is that the Rectangle, Ellipse, RoundRect, Chord, and Pie functions are not strictly line-drawing functions. Yes, the functions draw lines, but they also fill an enclosed area with the current area-filling brush. This brush is solid white by default, so it may not be obvious that these functions do more than draw lines when you first begin experimenting with them. The functions really belong in the later section "Drawing Filled Areas", but I'll discuss them here regardless.
The functions I've listed above are all similar in that they are built up from a rectangular "bounding box." You define the coordinates of a box that encloses the object—the bounding box—and Windows draws the object within this box.
The simplest of these functions draws a rectangle:
Rectangle (hdc, xLeft, yTop, xRight, yBottom) ;
The point (xLeft, yTop) is the upper left corner of the rectangle, and (xRight, yBottom) is the lower right corner. A figure drawn using the Rectangle function is shown in Figure 5-8. The sides of the rectangle are always parallel to the horizontal and vertical sides of the display.
Figure 5-8. A figure drawn using the Rectangle function.
Programmers who have experience with graphics programming are often familiar with "off-by-one" errors. Some graphics programming systems draw a figure to encompass the right and bottom coordinates, and some draw figures up to (but not including) the right and bottom coordinates. Windows uses the latter approach, but there's an easier way to think about it.
Consider the function call
Rectangle (hdc, 1, 1, 5, 4) ;
I mentioned above that Windows draws the figure within a "bounding box." You can think of the display as a grid where each pixel is within a grid cell. The imaginary bounding box is drawn on the grid, and the rectangle is then drawn within this bounding box. Here's how the figure would be drawn:
The area separating the rectangle from the top and left of the client area is 1 pixel wide.
As I mentioned earlier, Rectangle is not strictly just a line-drawing function. GDI also fills the enclosed area. However, because by default the area is filled with white, it might not be immediately obvious that GDI is filling the area.
Once you know how to draw a rectangle, you also know how to draw an ellipse, because it uses the same arguments:
Ellipse (hdc, xLeft, yTop, xRight, yBottom) ;
A figure drawn using the Ellipse function is shown (with the imaginary bounding box) in Figure 5-9.
Figure 5-9. A figure drawn using the Ellipse function.
The function to draw rectangles with rounded corners uses the same bounding box as the Rectangle and Ellipse functions but includes two more arguments:
RoundRect (hdc, xLeft, yTop, xRight, yBottom, xCornerEllipse, yCornerEllipse) ;
A figure drawn using this function is shown in Figure 5-10.
Figure 5-10. A figure drawn using the RoundRect function.
Windows uses a small ellipse to draw the rounded corners. The width of this ellipse is xCornerEllipse, and the height is yCornerEllipse. Imagine Windows splitting this small ellipse into four quadrants and using one quadrant for each of the four corners. The rounding of the corners is more pronounced for larger values of xCornerEllipse and yCornerEllipse. If xCornerEllipse is equal to the difference between xLeft and xRight, and yCornerEllipse is equal to the difference between yTop and yBottom, then the RoundRect function will draw an ellipse.
The rounded rectangle in Figure 5-10 was drawn using corner ellipse dimensions calculated with the formulas below.
xCornerEllipse = (xRight - xLeft) / 4 ; yCornerEllipse = (yBottom- yTop) / 4 ;
This is an easy approach, but the results admittedly don't look quite right because the rounding of the corners is more pronounced along the larger rectangle dimension. To correct this problem, you'll probably want to make xCornerEllipse equal to yCornerEllipse in real dimensions.
The Arc, Chord, and Pie functions all take identical arguments:
Arc (hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd) ; Chord (hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd) ; Pie (hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd) ;
A line drawn using the Arc function is shown in Figure 5-11; figures drawn using the Chord and Pie functions are shown in Figures 5-12 and 5-13. Windows uses an imaginary line to connect (xStart, yStart) with the center of the ellipse. At the point at which that line intersects the ellipse, Windows begins drawing an arc in a counterclockwise direction around the circumference of the ellipse. Windows also uses an imaginary line to connect (xEnd, yEnd) with the center of the ellipse. At the point at which that line intersects the ellipse, Windows stops drawing the arc.
Figure 5-11. A line drawn using the Arc function.
Figure 5-12. A figure drawn using the Chord function.
Figure 5-13. A figure drawn using the Pie function.
For the Arc function, Windows is now finished, because the arc is an elliptical line rather than a filled area. For the Chord function, Windows connects the endpoints of the arc. For the Pie function, Windows connects each endpoint of the arc with the center of the ellipse. The interiors of the chord and pie-wedge figures are filled with the current brush.
You may wonder about this use of starting and ending positions in the Arc, Chord, and Pie functions. Why not simply specify starting and ending points on the circumference of the ellipse? Well, you can, but you would have to figure out what those points are. Windows' method gets the job done without requiring such precision.
The LINEDEMO program shown in Figure 5-14 draws a rectangle, an ellipse, a rectangle with rounded corners, and two lines, but not in that order. The program demonstrates that these functions that define closed areas do indeed fill them, because the lines are hidden behind the ellipse. The results are shown in Figure 5-15.
Figure 5-14. The LINEDEMO program.
LINEDEMO.C
/*-------------------------------------------------- LINEDEMO.C -- Line-Drawing Demonstration Program (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 ("LineDemo") ; 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 ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Line Demonstration"), 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 message, WPARAM wParam, LPARAM lParam) { static int cxClient, cyClient ; HDC hdc ; PAINTSTRUCT ps ; switch (message) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; Rectangle (hdc, cxClient / 8, cyClient / 8, 7 * cxClient / 8, 7 * cyClient / 8) ; MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, cxClient, cyClient) ; MoveToEx (hdc, 0, cyClient, NULL) ; LineTo (hdc, cxClient, 0) ; Ellipse (hdc, cxClient / 8, cyClient / 8, 7 * cxClient / 8, 7 * cyClient / 8) ; RoundRect (hdc, cxClient / 4, cyClient / 4, 3 * cxClient / 4, 3 * cyClient / 4, cxClient / 4, cyClient / 4) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } |
Figure 5-15. The LINEDEMO display.
Bezier Splines
The word "spline" once referred to a piece of flexible wood, rubber, or metal used to draw curves on a piece of paper. For example, if you had some disparate graph points, and you wanted to draw a curve between them (either for interpolation or extrapolation), you'd first mark the points on a piece of graph paper. You'd then anchor a spline to the points and use a pencil to draw the curve along the spline as it bent around the points.
Nowadays, of course, splines are mathematical formulas. They come in many different flavors, but the Bezier spline has become the most popular for computer graphics programming. It is a fairly recent addition to the arsenal of graphics tools available on the operating system level, and it comes from an unlikely source: In the 1960s, the Renault automobile company was switching over from a manual design of car bodies (which involved clay) to a computer-based design. Mathematical tools were required, and Pierre Bezier came up with a set of formulas that proved to be useful for this job.
Since then, the two-dimensional form of the Bezier spline has shown itself to be the most useful curve (after the straight line and ellipse) for computer graphics. In PostScript, the Bezier spline is used for all curves—even elliptical lines are approximated from Beziers. Bezier curves are also used to define the character outlines of PostScript fonts. (TrueType uses a simpler and faster form of spline.)
A single two-dimensional Bezier spline is defined by four points—two end points and two control points. The ends of the curve are anchored at the two end points. The control points act as "magnets" to pull the curve away from the straight line between the two end points. This is best illustrated by an interactive program, called BEZIER, which is shown in Figure 5-16.
Figure 5-16. The BEZIER program.
BEZIER.C
/*--------------------------------------- BEZIER.C -- Bezier Splines 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 ("Bezier") ; 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 ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Bezier Splines"), 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 DrawBezier (HDC hdc, POINT apt[]) { PolyBezier (hdc, apt, 4) ; MoveToEx (hdc, apt[0].x, apt[0].y, NULL) ; LineTo (hdc, apt[1].x, apt[1].y) ; MoveToEx (hdc, apt[2].x, apt[2].y, NULL) ; LineTo (hdc, apt[3].x, apt[3].y) ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static POINT apt[4] ; HDC hdc ; int cxClient, cyClient ; PAINTSTRUCT ps ; switch (message) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; apt[0].x = cxClient / 4 ; apt[0].y = cyClient / 2 ; apt[1].x = cxClient / 2 ; apt[1].y = cyClient / 4 ; apt[2].x = cxClient / 2 ; apt[2].y = 3 * cyClient / 4 ; apt[3].x = 3 * cxClient / 4 ; apt[3].y = cyClient / 2 ; return 0 ; case WM_LBUTTONDOWN: case WM_RBUTTONDOWN: case WM_MOUSEMOVE: if (wParam & MK_LBUTTON || wParam & MK_RBUTTON) { hdc = GetDC (hwnd) ; SelectObject (hdc, GetStockObject (WHITE_PEN)) ; DrawBezier (hdc, apt) ; if (wParam & MK_LBUTTON) { apt[1].x = LOWORD (lParam) ; apt[1].y = HIWORD (lParam) ; } if (wParam & MK_RBUTTON) { apt[2].x = LOWORD (lParam) ; apt[2].y = HIWORD (lParam) ; } SelectObject (hdc, GetStockObject (BLACK_PEN)) ; DrawBezier (hdc, apt) ; ReleaseDC (hwnd, hdc) ; } return 0 ; case WM_PAINT: InvalidateRect (hwnd, NULL, TRUE) ; hdc = BeginPaint (hwnd, &ps) ; DrawBezier (hdc, apt) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } |
Because this program uses some mouse processing logic that we won't learn about until Chapter 7, I won't discuss its inner workings (which might be obvious nonetheless). Instead, you can use the program to experiment with manipulating Bezier splines. In this program, the two end points are set to be halfway down the client area, and ¼ and ¾ of the way across the client area. The two control points are manipulable, the first by pressing the left mouse button and moving the mouse, the second by pressing the right mouse button and moving the mouse. Figure 5-17 shows a typical display.
Aside from the Bezier spline itself, the program also draws a straight line from the first control point to the first end point (also called the begin point) at the left, and from the second control point to the end point at the right.
Bezier splines are considered to be useful for computer-assisted design work because of several characteristics. First, with a little practice, you can usually manipulate the curve into something close to a desired shape.
Figure 5-17. The BEZIER display.
Second, the Bezier spline is very well controlled. In some splines, the curve does not pass through any of the points that define the curve. The Bezier spline is always anchored at the two end points. (This is one of the assumptions that is used to derive the Bezier formulas.) Also, some forms of splines have singularities where the curve veers off into infinity. In computer-based design work, this is rarely desired. The Bezier curve never does this; indeed, it is always bounded by a four-sided polygon (called a "convex hull") that is formed by connecting the end points and control points.
Third, another characteristic of the Bezier spline involves the relationship between the end points and the control points. The curve is always tangential to and in the same direction as a straight line draw from the begin point to the first control point. (This is visually illustrated by the Bezier program.) Also, the curve is always tangential to and in the same direction as a straight line drawn from the second control point to the end point. These are two other assumptions used to derive the Bezier formulas.
Fourth, the Bezier spline is often aesthetically pleasing. I know this is a subjective criterion, but I'm not the only person who thinks so.
Prior to the 32-bit versions of Windows, you'd have to create your own Bezier splines using the Polyline function. You would also need knowledge of the following parametric equations for the Bezier spline. The begin point is (x0, y0), and the end point is (x3, y3). The two control points are (x1, y1) and (x2, y2). The curve is drawn for values of t ranging from 0 to 1:
x(t) = (1 - t)3 x0 + 3t (1 - t)2 x1 + 3t2 (1 - t) x2 + t3 x3
y(t) = (1 - t)3 y0 + 3t (1 - t)2 y1 + 3t2 (1 - t) y2 + t3 y3
You don't need to know these formulas in Windows 98. To draw one or more connected Bezier splines, you simply call
PolyBezier (hdc, apt, iCount) ;
or
PolyBezierTo (hdc, apt, iCount) ;
In both cases, apt is an array of POINT structures. With PolyBezier, the first four points indicate (in this order) the begin point, first control point, second control point, and end point of the first Bezier curve. Each subsequent Bezier requires only three more points because the begin point of the second Bezier curve is the same as the end point of the first Bezier curve, and so on. The iCount argument is always one plus three times the number of connected curves you're drawing.
The PolyBezierTo function uses the current position for the first begin point. The first and each subsequent Bezier spline requires only three points. When the function returns, the current position is set to the last end point.
One note: when you draw a series of connected Bezier splines, the point of connection will be smooth only if the second control point of the first Bezier, the end point of the first Bezier (which is also the begin point of the second Bezier), and the first control point of the second Bezier are colinear; that is, they lie on the same straight line.
Using Stock Pens
When you call any of the line-drawing functions that I've discussed in this section, Windows uses the "pen" currently selected in the device context to draw the line. The pen determines the line's color, its width, and its style, which can be solid, dotted, or dashed. The pen in the default device context is called BLACK_PEN. This pen draws a solid black line with a width of one pixel. BLACK_PEN is one of three "stock pens" that Windows provides. The other two are WHITE_PEN and NULL_PEN. NULL_PEN is a pen that doesn't draw. You can also create your own customized pens.
In your Windows programs, you refer to pens by using a handle. The Windows header file WINDEF.H defines the type HPEN, a handle to a pen. You can define a variable (for instance, hPen) using this type definition:
HPEN hPen ;
You obtain the handle to one of the stock pens by a call to GetStockObject. For instance, suppose you want to use the stock pen called WHITE_PEN. You get the pen handle like this:
hPen = GetStockObject (WHITE_PEN) ;
Now you must "select" that pen into the device context:
SelectObject (hdc, hPen) ;
Now the white pen is the current pen. After this call, any lines you draw will use WHITE_PEN until you select another pen into the device context or release the device context handle.
Rather than explicitly defining an hPen variable, you can instead combine the GetStockObject and SelectObject calls in one statement:
SelectObject (hdc, GetStockObject (WHITE_PEN)) ;
If you then want to return to using BLACK_PEN, you can get the handle to that stock object and select it into the device context in one statement:
SelectObject (hdc, GetStockObject (BLACK_PEN)) ;
SelectObject returns the handle to the pen that had been previously selected into the device context. If you start off with a fresh device context and call
hPen = SelectObject (hdc, GetStockobject (WHITE_PEN)) ;
the current pen in the device context will be WHITE_PEN and the variable hPen will be the handle to BLACK_PEN. You can then select BLACK_PEN into the device context by calling
SelectObject (hdc, hPen) ;
Creating, Selecting, and Deleting Pens
Although the pens defined as stock objects are certainly convenient, you are limited to only a solid black pen, a solid white pen, or no pen at all. If you want to get fancier than that, you must create your own pens.
Here's the general procedure: You create a "logical pen," which is merely a description of a pen, using the function CreatePen or CreatePenIndirect. These functions return a handle to the logical pen. You select the pen into the device context by calling SelectObject. You can then draw lines with this new pen. Only one pen can be selected into the device context at any time. After you release the device context (or after you select another pen into the device context) you can delete the logical pen you've created by calling DeleteObject. When you do so, the handle to the pen is no longer valid.
A logical pen is a "GDI object," one of six GDI objects a program can create. The other five are brushes, bitmaps, regions, fonts, and palettes. Except for palettes, all of these objects are selected into the device context using SelectObject.
Three rules govern the use of GDI objects such as pens:
- You should eventually delete all GDI objects that you create.
- Don't delete GDI objects while they are selected in a valid device context.
- Don't delete stock objects.
These are not unreasonable rules, but they can be a little tricky sometimes. We'll run through some examples to get the hang of how the rules work.
The general syntax for the CreatePen function looks like this:
hPen = CreatePen (iPenStyle, iWidth, crColor) ;
The iPenStyle argument determines whether the pen draws a solid line or a line made up of dots or dashes. The argument can be one of the following identifiers defined in WINGDI.H. Figure 5-18 shows the kind of line that each style produces.
Figure 5-18. The seven pen styles.
For the PS_SOLID, PS_NULL, and PS_INSIDEFRAME styles, the iWidth argument is the width of the pen. An iWidth value of 0 directs Windows to use one pixel for the pen width. The stock pens are 1 pixel wide. If you specify a dotted or dashed pen style with a physical width greater than 1, Windows will use a solid pen instead.
The crColor argument to CreatePen is a COLORREF value specifying the color of the pen. For all the pen styles except PS_INSIDEFRAME, when you select the pen into the device context, Windows converts the color to the nearest pure color that the device can render. The PS_INSIDEFRAME is the only pen style that can use a dithered color, and then only when the width is greater than 1.
The PS_INSIDEFRAME style has another peculiarity when used with functions that define a filled area. For all pen styles except PS_INSIDEFRAME, if the pen used to draw the outline is greater than 1 pixel wide, then the pen is centered on the border so that part of the line can be outside the bounding box. For the PS_INSIDEFRAME pen style, the entire line is drawn inside the bounding box.
You can also create a pen by setting up a structure of type LOGPEN ("logical pen") and calling CreatePenIndirect. If your program uses a lot of different pens that you initialize in your source code, this method is probably more efficient.
To use CreatePenIndirect, first you define a structure of type LOGPEN:
LOGPEN logpen ;
This structure has three members: lopnStyle (an unsigned integer or UINT) is the pen style, lopnWidth (a POINT structure) is the pen width in logical units, and lopnColor (COLORREF) is the pen color. Windows uses only the x field of the lopnWidth structure to set the pen width; it ignores the y field.
You create the pen by passing the address of the structure to CreatePenIndirect:
hPen = CreatePenIndirect (&logpen) ;
Note that the CreatePen and CreatePenIndirect functions do not require a handle to a device context. These functions create logical pens that have no connection with a device context until you call SelectObject. You can use the same logical pen for several different devices, such as the screen and a printer.
Here's one method for creating, selecting, and deleting pens. Suppose your program uses three pens—a black pen of width 1, a red pen of width 3, and a black dotted pen. You can first define static variables for storing the handles to these pens:
static HPEN hPen1, hPen2, hPen3 ;
During processing of WM_CREATE, you can create the three pens:
hPen1 = CreatePen (PS_SOLID, 1, 0) ; hPen2 = CreatePen (PS_SOLID, 3, RGB (255, 0, 0)) ; hPen3 = CreatePen (PS_DOT, 0, 0) ;
During processing of WM_PAINT (or any other time you have a valid handle to a device context), you can select one of these pens into the device context and draw with it:
SelectObject (hdc, hPen2) ; [ line-drawing functions ] SelectObject (hdc, hPen1) ; [ line-drawing functions ]
During processing of WM_DESTROY, you can delete the three pens you created:
DeleteObject (hPen1) ; DeleteObject (hPen2) ; DeleteObject (hPen3) ;
This is the most straightforward method of creating selecting, and deleting pens, but obviously your program must know what pens will be needed. You might instead want to create the pens during each WM_PAINT message and delete them after you call EndPaint. (You can delete them before calling EndPaint, but you have to be careful not to delete the pen currently selected in the device context.)
You might want to create pens on the fly and combine the CreatePen and SelectObject calls in the same statement:
SelectObject (hdc, CreatePen (PS_DASH, 0, RGB (255, 0, 0))) ;
Now when you draw lines, you'll be using a red dashed pen. When you're finished drawing the red dashed lines, you can delete the pen. Whoops! How can you delete the pen when you haven't saved the pen handle? Recall that SelectObject returns the handle to the pen previously selected in the device context. This means that you can delete the pen by selecting the stock BLACK_PEN into the device context and deleting the value returned from SelectObject:
DeleteObject (SelectObject (hdc, GetStockObject (BLACK_PEN))) ;
Here's another method. When you select a pen into a newly created device context, save the handle to the pen that SelectObject returns:
hPen = SelectObject (hdc, CreatePen (PS_DASH, 0, RGB (255, 0, 0))) ;
What is hPen? If this is the first SelectObject call you've made since obtaining the device context, hPen is a handle to the BLACK_PEN stock object. You can now select that pen into the device context and delete the pen you create (the handle returned from this second SelectObject call) in one statement:
DeleteObject (SelectObject (hdc, hPen)) ;
If you have a handle to a pen, you can obtain the values of the LOGPEN structure fields by calling GetObject:
GetObject (hPen, sizeof (LOGPEN), (LPVOID) &logpen) ;
If you need the pen handle currently selected in the device context, call
hPen = GetCurrentObject (hdc, OBJ_PEN) ;
I'll discuss another pen creation function, ExtCreatePen, in Chapter 17.
Filling in the Gaps
The use of dotted and dashed pens raises the question: what happens to the gaps between the dots and dashes? Well, what do you want to happen?
The coloring of the gaps depends on two attributes of the device context—the background mode and the background color. The default background mode is OPAQUE, which means that Windows fills in the gaps with the background color, which by default is white. This is consistent with the WHITE_BRUSH that many programs use in the window class for erasing the background of the window.
You can change the background color that Windows uses to fill in the gaps by calling
SetBkColor (hdc, crColor) ;
As with the crColor argument used for the pen color, Windows converts this background color to a pure color. You can obtain the current background color defined in the device context by calling GetBkColor.
You can also prevent Windows from filling in the gaps by changing the background mode to TRANSPARENT:
SetBkMode (hdc, TRANSPARENT) ;
Windows will then ignore the background color and not fill in the gaps. You can obtain the current background mode (either TRANSPARENT or OPAQUE) by calling GetBkMode.
Drawing Modes
The appearance of lines drawn on the display is also affected by the drawing mode defined in the device context. Imagine drawing a line that has a color based not only on the color of the pen but also on the color of the display area where the line is drawn. Imagine a way in which you could use the same pen to draw a black line on a white surface and a white line on a black surface without knowing what color the surface is. Could such a facility be useful to you? It's made possible by the drawing mode.
When Windows uses a pen to draw a line, it actually performs a bitwise Boolean operation between the pixels of the pen and the pixels of the destination display surface, where the pixels determine the color of the pen and display surface. Performing a bitwise Boolean operation with pixels is called a "raster operation," or "ROP." Because drawing a line involves only two pixel patterns (the pen and the destination), the Boolean operation is called a "binary raster operation," or "ROP2." Windows defines 16 ROP2 codes that indicate how Windows combines the pen pixels and the destination pixels. In the default device context, the drawing mode is defined as R2_COPYPEN, meaning that Windows simply copies the pixels of the pen to the destination, which is how we normally think about pens. There are 15 other ROP2 codes.
Where do these 16 different ROP2 codes come from? For illustrative purposes, let's assume a monochrome system that uses 1 bit per pixel. The destination color (the color of the window's client area) can be either black (which we'll represent by a 0 pixel) or white (represented by a 1 pixel). The pen also can be either black or white. There are four combinations of using a black or white pen to draw on a black or white destination: a white pen on a white destination, a white pen on a black destination, a black pen on a white destination, and a black pen on a black destination.
What is the color of the destination after you draw with the pen? One possibility is that the line is always drawn as black regardless of the pen color or the destination color. This drawing mode is indicated by the ROP2 code R2_BLACK. Another possibility is that the line is drawn as black except when both the pen and destination are black, in which case the line is drawn as white. Although this might be a little strange, Windows has a name for it. The drawing mode is called R2_NOTMERGEPEN. Windows performs a bitwise OR operation on the destination pixels and the pen pixels and then inverts the result.
The table below shows all 16 ROP2 drawing modes. The table indicates how the pen (P) and destination (D) colors are combined for the result. The column labeled "Boolean Operation" uses C notation to show how the destination pixels and pen pixels are combined.
Pen (P): Destination (D): | 1 1 0 0 1 0 1 0 | Boolean Operation | Drawing Mode |
Results: | 0 0 0 0 | 0 | R2_BLACK |
0 0 0 1 | ~(P ¦ D) | R2_NOTMERGEPEN | |
0 0 1 0 | ~P & D | R2_MASKNOTPEN | |
0 0 1 1 | ~P | R2_NOTCOPYPEN | |
0 1 0 0 | P & ~D | R2_MASKPENNOT | |
0 1 0 1 | ~D | R2_NOT | |
0 1 1 0 | P ^ D | R2_XORPEN | |
0 1 1 1 | ~(P & D) | R2_NOTMASKPEN | |
1 0 0 0 | P & D | R2_MASKPEN | |
1 0 0 1 | ~(P ^ D) | R2_NOTXORPEN | |
1 0 1 0 | D | R2_NOP | |
1 0 1 1 | ~P ¦ D | R2_MERGENOTPEN | |
1 1 0 0 | P | R2_COPYPEN (default) | |
1 1 0 1 | P ¦ ~D | R2_MERGEPENNOT | |
1 1 1 0 | P ¦ D | R2_MERGEPEN | |
1 1 1 1 | 1 | R2_WHITE |
You can set a new drawing mode for the device context by calling
SetROP2 (hdc, iDrawMode) ;
The iDrawMode argument is one of the values listed in the "Drawing Mode" column of the table. You can obtain the current drawing mode by using the function:
iDrawMode = GetROP2 (hdc) ;
The device context default is R2_COPYPEN, which simply transfers the pen color to the destination. The R2_NOTCOPYPEN mode draws white if the pen color is black and black if the pen color is white. The R2_BLACK mode always draws black, regardless of the color of the pen or the background. Likewise, the R2_WHITE mode always draws white. The R2_NOP mode is a "no operation." It leaves the destination unchanged.
We've been examining the drawing mode in the context of a monochrome system. Most systems are color, however. On color systems Windows performs the bitwise operation of the drawing mode for each color bit of the pen and destination pixels and again uses the 16 ROP2 codes described in the previous table. The R2_NOT drawing mode always inverts the destination color to determine the color of the line, regardless of the color of the pen. For example, a line drawn on a cyan destination will appear as magenta. The R2_NOT mode always results in a visible pen except if the pen is drawn on a medium gray background. I'll demonstrate the use of the R2_NOT drawing mode in the BLOKOUT programs in Chapter 7.
Категории