TOC PREV NEXT INDEX

Put your logo here!


Chapter 7: Graphics

7.1: Graphics in a GUI World

In the last chapter we explored how to display text on the GUI display. Although text is unquestionably important in modern GUI applications, the first word in the phrase "graphical user interface" is graphical, not textual. Therefore, most people are probably a lot more interested in learning about how to do graphics under Windows rather than text. As it turns out, however, most GUI applications actually deal more with Windows' controls and text objects rather than pure graphical objects. Nevertheless, knowing how to draw graphical objects in a window is an critical skill to master. In this chapter we'll look at the facilities Windows provides for drawing graphical images on the screen.

7.2: Types of Graphic Objects You Can Draw in Windows

Windows attempts to present a device-independent view of output devices to your application software. This means that, to your programmer, a 24-bit video display card looks just like a printer, which looks just like a plotter, which looks just like a film recorder. Well, sort of. Though there are some very real-world differences between these types of output devices, differences of which your applications must be aware, for the most part you draw on the video display exactly the same way you "draw" on a printer. Indeed, in this chapter we will explore how to display graphic images on both a printer and a video display.

Before describing the types of images you can draw on these various devices, it is probably worthwhile to point out that not all Windows output APIs are so universal. For example, game and multimedia applications that use Microsoft's Direct-X subsystem use different mechanisms for displaying their information; these mechanisms are quite a bit different than the ones we'll explore in this chapter (and higher performance), but the drawback is that you cannot "print" such output to a printer device (though it may be perfectly possible to print such data to a video recorder device). So Windows does provide some special API functions for high-performance I/O devices (e.g., video input and output) that don't follow the standard GDI (Graphical Device Interface) model, but most Win32 applications will use the GDI model for output.

Windows' GDI model supports the ability to draw several primitive graphic objects on the display or a printer (we'll just use the term display from now on, but keep in mind that this discussion can apply to the printer as well). These primitives include lines, polylines, curves (arcs, chords, and bezier curves), rectangles, roundangles, ellipses and circles, filled regions, bitmaps, and text. We've already beaten text to death in the last chapter, we'll take a look at these other primitives in this chapter.

7.3: Facilities That the Windows GDI Provides

In addition to drawing, the Windows GDI provides several facilities to ease the creation of complex graphic images on the display. These facilities include the ability to change coordinate systems in use by the system (for example, using metric or English units rather than pixel units for string coordinates, the abilty to record a sequence of graphic drawing operations for later playback (as a metafile), the ability to maintain an outline (region or path) around an object, the ability to restrain drawing inside a given region (clipping), and the ability to control the color and fill pattern of objects drawn on the display. You'll see many of these features in action throughout this chapter.

7.4: The Device Context

In the last chapter you saw that Windows requires you to do all drawing through a device context. When responding to a Windows w.WM_PAINT message, you'd open a context with the BeginPaint macro invocation and you'd end the drawing context with an invocation of the EndPaint macro invocation. Between those two points you could draw to your heart's content. In the last chapter, we only drew text on the screen, but you use the same sequence of operations when drawing other graphic images to the display.

The last chapter introduced you to the BeginPaint, EndPaint, GetDC, GetWindowDC, and ReleaseDC macros found in the wpa.hhf header file. You'll use those macros with all of the graphic drawing functions in this chapter as well. However, in addition to those macros/functions, there are a couple of additional routines you can use when drawing data with the graphics primitives (including text). The first of these new functions to consider are w.CreateDC and w.DeleteDC:


 
static
 
	CreateDC: procedure
 
	(
 
				lpszDriver :string;
 
				lpszDevice :string;
 
				lpszOutput :string;
 
		var		lpInitData :DEVMODE
 
);
 
	@stdcall;
 
	@returns( "eax" );
 
	@external( "__imp__CreateDCA@16" );
 

 
	DeleteDC: procedure
 
	(
 
		hdc :dword
 
	);
 
		@stdcall;
 
		@returns( "eax" );
 
		@external( "__imp__DeleteDC@4" );
 

 

For the w.CreateDC API call, the lpszDriver parameter should be the string "DISPLAY" or "WINSPOOL" (or the name of some other Windows print provider on your system, though this is usually "WINSPOOL"), the other three parameters should normally be NULL (see the Win32 API documentation for details, but 99% of the time you're going to use "DISPLAY" as the first parameter and NULL as the remaining three parameter values). This call returns a handle to the entire display device (meaning you can draw anywhere on the display, including over the top of other windows on the display; obviously, you should only do this under special circumstances, well-behave applications don't arbitrarily draw on the display). When you are done using the device context you've created via a w.CreateDC call, you call w.DeleteDC to free up that resource. Mainly, we'll use the w.CreateDC call to obtain the device context of a printer device, not the display device.

When creating complex bitmap objects, it's often convenient to build the bitmap in memory first, and then transfer that bitmap to the display device (this, for example, prevents the object from flashing while you are drawing it). You use the w.CreateCompatibleDC function call to do this:


 
static
 
	CreateCompatibleDC: procedure
 
	(
 
		hdc :dword
 
	);
 
		@stdcall;
 
		@returns( "eax" );
 
		@external( "__imp__CreateCompatibleDC@4" );
 

 

The w.CreateCompatibleDC requires a single parameter - the handle of an existing device context. This function creates an in-memory duplicate of that device context. You can draw to this context (by supplying the device context handle this function returns to the drawing routines) and Windows will store the image as a bitmap in memory. Later, you can transfer this bitmap to some actual output device. When you are done using the in-memory context you've created with w.CreateCompatibleDC, you must call w.DeleteDC (passing the handle that w.CreateCompatibleDC returns in EAX) to free up the resources associated with this device context. We'll return to the use of this function a little later in this book.

As noted earlier, Windows provides the ability to record all of your graphic output requests in a special data structure known as a metafile. The w.CreateMetaFile and w.CloseMetaFile functions provide a set of brackets between which Windows will record all GDI function calls to a file whose name you specify as the parameter to the w.CreateMetaFile function. The w.CloseMetaFile function gets passed the handle that w.CreateMetaFile returns in EAX, and w.CloseMetaFile returns a handle to the metafile that you can use in other API calls that expect such a handle.

7.5: Obtaining and Using Device Context Information

A Device Context is an internal Windows data structure that maintains information about the current state of the device. As the last chapter notes, a device context maintains information likethe current font in use, the current output color, and other attributes such as line width, fill pattern, output size, and other such features. Also in the last chapter, you learned about the w.GetDeviceCaps function that displays several of the values held in the device context structure and, in fact, the previous chapter presented a simple program to display various device capability values. In this section we'll explore the uses of some of this data.

As a reminder, here is the function prototype for the w.GetDeviceCaps API function:


 
static
 
    GetDeviceCaps: procedure
 
    ( 
 
        hdc             :dword; 
 
        nIndex          :dword
 
    );
 
        @stdcall;
 
        @returns( "eax" );
 
        @external( "__imp__GetDeviceCaps@8" );
 

 

The hdc parameter is the handle of the device context that you wish to query and the nIndex parameter is a special integer constant that specifies which value you wish this function to return in the EAX register (see Table 7-1 for the constant that w.GetDeviceCaps accepts).

Table 7-1: Common GetDeviceCaps nIndex Values
Index
Value Returned in EAX
w.HORZSIZE
The horizontal size, in millimeters, of the display (or other device).
w.VERTSIZE
The vertical size, in millimeters, of the display (or other device).
w.HORZRES
The display's width, in pixels.
w.VERTRES
The display's height, in pixels.
w.LOGPIXELSX
The resolution, in pixels per inch, along the displays' X-axis.
w.LOGPIXELSY
The resolution, in pixels per inch, along the displays' Y-axis.
w.BITSPIXEL
The number of bits per pixel (specifying color information).
w.PLANES
The number of color planes.
w.NUMBRUSHES
The number of device-specific brushes available.
w.NUMPENS
The number of device-specific pens available.
w.NUMFONTS
The number of device specific fonts available.
w.NUMCOLORS
Number of entries in the device's color table.
w.ASPECTX
Relative width of a device pixel used for drawing lines.
w.ASPECTY
Relative height of a device pixel used for drawing lines.
w.ASPECTXY
Diagonal width of the device pixel used for line drawing (45 degrees).
w.CLIPCAPS
This is a flag value that indicates whether the device can clip images to a rectangle. The w.GetDeviceCaps function returns one if the device supports clipping, zero otherwise.
w.SIZEPALETTE
Number of entries in the system palette (valid only if the display device uses a palette).
w.NUMRESERVED
Number of system reserved entries in the system palette (valid only if the display device uses a palette).
w.COLORRES
Actual color resolution of the device, in bits per pixel. For device drivers that use palettes.
w.PHYSICALWIDTH
For printing devices, the physical width of the output device in whatever units that device uses.
w.PHYSICALHEIGHT
For printing devices, the physical height of the output device.
w.PHYSICALOFFSETX
For printing devices, the horizontal margin.
w.PHYSICALOFFSETY
For printing devices, the vertical margin.
w.SCALINGFACTORX
For printing devices, the scaling factor along the X-axis.
w.SCALINGFACTORY
For printing devices, the scaling factor along the Y-axis.
w.VREFRESH
For display devices only: the current vertical refresh rate of the device, in Hz.
w.DESKTOPHORZRES
Width, in pixels, of the display device. May be larger than w.HORZRES if the display supports virtual windows or more than one display.
w.DESKTOPVERTRES
Height, in pixels, of the display device. May be larger than w.VERTRES if the display supports virtual windows or more than one display.
w.BITALIGNMENT
Preferred horizontal drawing alignment. Specifies the drawing alignment, in pixels, for best drawing performance. May be zero if the hardware is accelerated or the alignment doesn't matter.

The values that w.GetDeviceCaps returns when you specify the w.HORZRES and w.VERTRES constants are the width and height of the display device in pixels. For a display device, these values specify the maximum size of a window that will fit entirely on the display (includng the non-client areas like the title and scroll bars). For printer devices, these two return values specify the maximum printing area. A Windows application can use these values, for example, to determine some default window size to use when the application is creating its first window.

The w.HORZSIZE and w.VERTSIZE return values specify the the size of the display in millimeters1. These return values are quite useful for creating WYSIWYG (What You See Is What You Get) type applications where the dimensions on the display correspond, roughly, to real life. In fact, few applications present their output on the display in real-world dimension. The problem is that the resolution of the display is a little too low to display common character sizes (e.g., 10 point fonts) with a reasonable amount of fidelity. As a result, common fonts on the screen would be difficult to read. To overcome this problem, Windows defines a couple of values you may query via w.GetDeviceCaps with the w.LOGPIXELSX and w.LOGPIXELSY constants that define the number of pixels per logical inch (25.4 millimeters). A logical inch is about 50% larger than a real world inch. That is, if you draw a line segment on the display and make it w.LOGPIXELSX long, then it will actually be about 1.5 inches long if you measure it with a ruler on the display. Do keep in mind that these values that w.GetDeviceCaps returns are approximate. Different displays will have minor differences due to dot pitch differences and, of course, the values that w.GetDeviceCaps returns are rounded to the nearest integer value.

If you are writing a program that displays graphic objects on the screen, then the values that w.GetDeviceCaps returns for w.ASPECTX, w.ASPECTY and w.ASPECTXY will be very important to you. These values specify the relative width, height, and diagonal length of pixels on the screen. What this means is that if you draw a line segment that is n pixels long along the X-axis, you would need to draw a line segment with (n*w.ASPECTY)/w.ASPECTX pixels along the Y-axis to obtain a line that is physically the same size. You would use these calculations to ensure that the shapes you draw on the screen don't come out looking elongated or squashed. For example, when drawing a square on the display, you would not draw an object with n pixels on each edge of the rectangle; doing so would likely produce a rectangle whose sides have a different length than the top and bottom line segments (that is, you'd have a proper rectangle rather than a square). However, if you use the ratio w.ASPECTX:w.ASPECTY when drawing the sides of the rectangle then the image you create will physically look like a square on the display device. Generally, you'll only adjust one of the two coordinates via this ratio. For example, if you want to create a rectangle with the equivalent of n pixels on each side, you might use calculations like the following:


 
xWidth = n
 
yHeight = (n * logPixelsY) / logPixelsX
 

 

Or you could use


 
xWidth = (n * logPixelsX) / logPixelsY
 
yHeight = n
 

 

where logPixelsX and logPixelsY represent the values that w.GetDeviceCaps returns when you pass it the w.LOGPIXELSX and w.LOGPIXELSY constants, respectively.

The w.GetDeviceCaps API call also returns some interesting information about the color capabilities of the display device. For example, when supplying the w.NUMCOLORS index, w.GetDeviceCaps will return the current number of entries in the systems' color table. This is the number of different colors that Windows will guarantee that it can display simultaneously on the screen. This is not the maximum number of colors the display can handle, it is simply the number of colors that Windows can currently display on the adapter. For those display adapters with palettes, this number corresponds to the current number of initialized palette entries. Note that w.GetDeviceCaps returns -1 when you pass w.NUMCOLORS as the index value if the display supports more than 256 colors. To compute the number of colors the display adapter is capable of displaying, you have to use the w.PLANES and w.BITSPIXEL return values. The w.PLANES value specify the number of "bit planes" (or memory banks) present on the video display adapter. On most modern video display cards, this value is one; older technology video display cards used multiple memory banks in order to map a large amount of memory into the 128K data region allocated for video cards on the original IBM PC. However, this memory banking scheme is fairly slow, so most modern PCs map the video display card into linear memory. Nevertheless, it is possible to still find some PCs using this older technology (though such cards are rapidly fading away from the scene). It probably isn't absolutely safe to assume that w.GetDeviceCaps will always return one when you specify the w.PLANES index value, but it's probably rare for this API to return any other value for this index under any modern version of Windows.

The w.BITSPIXEL index tells w.GetDeviceCaps to return the number of bits per pixel, organized as a linear memory array, on the display. Note that both the w.PLANES and w.BITSPIXEL indexes tell w.GetDeviceCaps to return the number of bits per pixel. One of these return values will always be one and the other may be greater than one. The difference between the two is the underlying hardware technology the display adapter uses. Older video display cards split n bits of color information across n banks of memory (where n is usually eight or less). Newer video cards associate n contiguous bits in a linear memory space with each pixel on the display rather than spreading the bits for each pixel across multiple memory banks. Because bank switching requires a lot of additional work, manipulating pixels stored in contiguous bits in memory is far more efficient so almost all modern (high-performance) video cards use this scheme. Therefore, it's probably safe to assume that the value w.GetDeviceCaps returns for the w.BITSPIXEL index value is the number of bits per pixel on the display on any modern machine. However, if you expect your software to work on older machines, you'll have to grab both values to determine the number of bits per pixel. The total number of colors available on modern video display cards is 2bitsPerPixel. You can compute this value using the following expression:

Number_of_colors = 1 shl (bitsPerPixel * numPlanes);
 

bitsPerPixel is the value w.GetDeviceCaps returns for the w.BITSPIXEL index and numPlanes is the value it returns when you pass it the w.PLANES index. The shift left operation in this expression computes two raised to the power (bitsPerPixel * numPlanes). Because at least one of bitsPerPixel and numPlanes is the value one, we could also compute this as "1 shl (bitsPerPixel + numPlanes - 1)" if multiplication is a slow operation on the CPU you're using.

Note that w.GetDeviceCaps will return the value 16 in response to a w.BITSPIXEL query if the actual number of bits per pixel is 15. This is because many so-called 16-bit video display cards actually provide only a 15-bit color depth, yet their drivers return 16 bits as the color depth. Other cards returns 15 bits. To avoid confusion, Windows calls both 15-bit and 16-bit displays a 16 bit device. This means that you may only be capable of display half the actual number of colors that Windows reports when you're working with a 16-bit display.

Although the underlying display device may only be capable of displaying some limited number of colors, Windows actually uses 32-bit double word values to represent color throughout the system. In particular, Windows uses the w.COLORREF type to represent a color as a combination of the Red, Green, and Blue additive primary colors (an "RGB" value). The layout of an RGB value appears in Figure 7-1. The L.O. three bytes of this double word specify the eight-bit values (0..255) for the red, green, and blue components of the color. The H.O. byte of this double word contains zero.

Figure 7-1: Windows RGB Color Format

With 24 bits (one byte of each of the three additive primary colors), it is possible to represent over 16 million different colors. For example, you may obtain the color magenta by specifying 255 ($FF) for the blue and red components of an RGB color value and zero for the green component. That is, magenta is equal to the RGB value $FF_00FF. Similarly, you may obtain cyan by mixing blue and green (i.e., $FF_FF00) and yellow by mixing green and red (i.e., $00_FFFF). The color white is obtained by mixing all three colors ($FF_FFFF) and you get black by the absence of these three colors (i.e., $00_0000).

The HLA w.hhf header file simply defines w.COLORREF as a dword type. If you want to access the individual color bytes of this type you can easily do so (this is assembly language, after all). The "structured" way to do this is to create a record in your program, similar to the following:


 
type
 
	colorref_t:
 
		record
 
			red		:byte;
 
			green		:byte;
 
			blue		:byte;
 
			alpha		:byte; // "Alpha channel" is the name used for the fourth byte
 
		endrecord;
 

 

If you create an object of type colorref_t, you can access the individual bytes as fields of this record, e.g., colorVar.red, colorVar.green, and colorVar.blue.

The wpa.hhf header file includes a macro named RGB that makes it easy to create double-word (w.COLORREF) constants from the individual color component values. This macro is defined as follows:


 
		 
 
	// RGB macro - Combines three constants to form an RGB constant.
 

 
	#macro RGB( _red_, _green_, _blue_ );
 

 
		( _red_ | ( _green_ << 8 ) | ( _blue_ << 16 ))
 

 
	#endmacro
 

 

One very important thing to note about this macro is that its parameters must all be constants and it returns a constant value as its "result". You use this macro as the operand of some other instruction or assembler directive, you do not use this macro as though it were a function. Here's a typical use that defines the color magenta as a symbol in the const section of an HLA program:


 
const
 
	magenta :w.COLORREF := RGB( $FF, $00, $FF );
 

 

Again, and this is very important, the RGB operands must all be constants. You may not supply register or memory variables as arguments to this macro. If you would like a generic"function" that computes RGB values from eight-bit constants, registers, or memory locations, you could write a macro like the following:


 
// mkRGB - converts three eight-bit color values into an RGB value:
 

 
#macro mkRGB( _red_, _green_, _blue_ );
 
	movzx( _blue_, eax );
 
	shl( 16, eax );
 
	mov( _green_, ah );
 
	mov( _red_, al );
 
#endmacro
 
	.
 
	.
 
	.
 
	mkRGB( redByteVar, $f0, bl ); // RGB value is left in EAX.
 

 

Although Windows works with 24-bit color values throughout the system, not all display adapters are capable of displaying this many colors on the screen simultaneously (or, the user may have selected a smaller number of characters for Windows to use when accessing the display card). As a result, the fact that you've specified a 24-bit color value does not imply that Windows can actually display that color on the screen. To accomodate this descrepency, Windows will often use a technique known as dithering to increase the number of available colors. Dithering increases the number of colors by drawing adjacent pixels using different colors that, when blended, approximate the desired color. For example, if you draw an alternating sequence of red and blue pixels, and then view the result from a distance, the result looks like a solid magenta color. Though dithering increases the maximum number of colors the user perceives on the screen, there are several primary disadvantages of dithering. First, because it takes multiple pixels to do dithering, dithering effectively reduces your screen resolution. Also, dithering only works well when coloring large areas, it doesn't work well for small or thin objects and it doesn't work at all for single pixels. Dithering is fairly obvious when the viewer is close to the video display; the image looks coarse and grainy when you apply dithering. Another big disadvantage to dithering is that it is slow - Windows has to compute, on the fly, dithering values when it attempts to display a color that the video display mode doesn't directly support. This extra computation can slow down the rendering of some object by a fair amount.

In many cases, applications don't really require an exact color match. For example, some programmer might want to display some text with a color that is roughly 25% red and 0% blue and green (that is, a dark red color). Now most video modes are probably capable of displaying an object whose color is 25% red. However, is 25% red represented by the eight-bit value 63 or 64? Pick the wrong one and Windows will attempt to dither the color (producing a low-quality image and running slowly). How do you make sure you pick the "pure" color that Windows can efficiently render? Well, Windows provides a nifty API function that will tell you what the closest "pure" color is to an RGB value you've specified: w.GetNearestColor:


 
static
 
    GetNearestColor: procedure
 
    ( 
 
        hdc             :dword; 
 
        crColor         :dword
 
    );
 
        @stdcall;
 
        @returns( "eax" );
 
        @external( "__imp__GetNearestColor@8" );
 

 

As usual, hdc is the handle of the device context whose color values you wish to check. The crColor parameter is the RGB value whose nearest color you'd like to find. This function returns the nearest color in the EAX register. So if you want to set some color to (approximately) 25% red, you could do this with the following code:


 
	w.GetNearestColor( hDC, RGB( 64, 0, 0 ) ); // 64 is approximately 25% of 255.
 
	mov( eax, red25 );
 

 
	// Now use red25 as the color for 25% red...
 

 

Another couple of useful device context values are the Window Origin and Viewport Origin values. The window origin specifies where the logical display's (0,0) point falls in on the display device (the viewport). The viewport origin specifies where the physical screen's (0,0) point falls in the logical window. These two origin value provide different "views" of the same concept - how Windows maps the logical coordinate space to the physical display coordinate space. An application, should it choose to modify the coordinate systems, would normally modify either the window coordinates or the viewport coordinates, but not both (which would tend to get confusing). This discussion will center around the use of the Window Origin values but it generally applies to the Viewport Origin values as well.

By default, when Windows first opens an application's window, it maps logical coordinate (0,0) on the display to the upper-left hand corner of the physical display device. All coordinates within your application are relative to this mapping. By calling the w.SetWindowOrgEx API function, however, you can tell Windows to map all coordinate values relative to some other point on the display. For example, you can tell Windows that point (100,100) in your application's coordinate system should correspond to physical point (0,0) on the display. You might want to do this, for example, if it's more convenient to work with coordinates in your application that are 100 and above. Here are the functions you'll find useful for getting and setting the window origin:


 
type
 
    POINT: 
 
        record
 
            x: dword;
 
            y: dword;
 
        endrecord;
 

 
static
 

 
    GetWindowOrgEx: procedure
 
    ( 
 
            hdc             :dword; 
 
        var lpPoint         :POINT
 
    );
 
        @stdcall;
 
        @returns( "eax" );
 
        @external( "__imp__GetWindowOrgEx@8" );
 

 
    SetWindowOrgEx: procedure
 
    ( 
 
            hdc             :dword; 
 
            X               :dword; 
 
            Y               :dword; 
 
        var lpPoint         :POINT
 
    );
 
        @stdcall;
 
        @returns( "eax" );
 
        @external( "__imp__SetWindowOrgEx@16" );
 

 

The w.GetWindowOrgEx function returns the current window origin for the device context provided by hdc in the variable you specify via the lpPoint parameter. The w.SetWindowOrgEx function sets the origin for the device context you specify via the X and Y parameters and returns the original origin in the lpPoint parameter (this function ignores lpPoint if you pass NULL in this parameter location). The following code fragment, for example, sets the window origin to (100,100) for use in our hypothetical example given earlier:


 
	w.SetWindowOrgEx( hDC, 100, 100, NULL );
 

 

The device context maintains several default drawing objects within the device context. For example, the default pen specifies how Windows will draw lines within that context; the default brush specifies how Windows will fill areas when drawing within the context; the default font specifies how Windows will render text to the device, the default colors to use, and so on. We'll discuss how to use the w.SelectObject functions to set these default values a little later in this chapter.

There is a minor problem you'll encounter when using device contexts - you create them in response to an API call such as BeginPaint and you destroy the context via a call to an API like EndPaint. Specifically, whenever you invoke BeginPaint, Windows creates a new device context complete with a set of default values. So if you change the default color Windows uses to display text, Windows will "forget" your change when you call EndPaint to finish the current output operation. This is convenient for certain values, but sometimes you'll want to set the default device context value that Windows uses. Windows provides an option that lets you tell Windows not to reset the device context every time you invoke something like BeginPaint. To do this, you include the w.CS_OWNDC constant as part of the window class style when creating the window in the first place, e.g., in an application's main program:


 
// Set up the window class (wc) object:
 
    
 
    mov( @size( w.WNDCLASSEX ), wc.cbSize );
 
    mov( w.CS_HREDRAW | w.CS_VREDRAW | w.CS_OWNDC, wc.style ); // Change this line
 
    mov( &WndProc, wc.lpfnWndProc );
 
    mov( NULL, wc.cbClsExtra );
 
		.
 
		.
 
		.
 

 

With this modification, Windows will allocate a small amount of storage (under 1K) to maintain all the device context values between the EndPaint invocation and the next BeginPaint invocation. So now, for example, when you set the drawing color to red, it will remain red, even across calls to BeginPaint/EndPaint, until you explicitly change it to something else.

The drawback to using the w.CS_OWNDC style is the fact that Windows will preserve all default values you set, not just one or two that you'd like to preserve across BeginPaint/EndPaint invocations. If you need to be able to change only a few default values but make other changes temporary (i.e., within the bounds of the BeginPaint/EndPaint sequence), then you'll need some way of preserving the existing device context values and restoring them later. You can accomplish this with the w.SaveDC and w.RestoreDC calls:


 
static
 

 
    SaveDC: procedure
 
    ( 
 
        hdc                 :dword
 
    );
 
        @stdcall;
 
        @returns( "eax" );
 
        @external( "__imp__SaveDC@4" );
 

 
    RestoreDC: procedure
 
    ( 
 
        hdc                 :dword; 
 
        nSavedDC            :dword
 
    );
 
        @stdcall;
 
        @returns( "eax" );
 
        @external( "__imp__RestoreDC@8" );
 

 

You pass a handle to the device context whose values you want to save or restore to both of these routines. The w.SaveDC call will save a copy of the device context's values internal to Windows and return a handle by which you can restore those values in the EAX register. The w.RestoreDC function uses this handle returned by w.SaveDC as the second parameter to tell Windows which internal values to restore. To use these functions you'd typically write code like the following:


 
	BeginPaint( hdc );
 

 
		<< code that changes device context values that you want to keep as the default>>
 

 
		w.SaveDC( hdc );
 
		mov( eax, SavedDCHandle ); // Save for later...
 

 
			<< code that makes temporary changes to the device context >>
 

 
		w.RestoreDC( hdc, SavedDChandle ); // Restore the values we've changed
 

 
	EndPaint;
 

 

Note that w.SaveDC pushes a copy of the current context values onto a stack and w.RestoreDC pops the values off of that stack. The handle that w.SaveDC returns can be thought of as the saved context stack pointer. Therefore, if you call w.SaveDC multiple times without calling w.RestoreDC inbetween, each context is written to a separate section of memory (i.e., a new entry on the stack). You may pop the last entry pushed via the call w.RestoreDC( hdc, -1 ); This spares having to actually save the return value from w.SaveDC if you use w.SaveDC and w.RestoreDC in a stack like fashion. Note, however, that if you save two contexts on the stack and save the two handles that w.SaveDC returns, and then you pass the first (earliest) handle that w.SaveDC returns to w.RestoreDC, the w.RestoreDC function pops both contexts off the stack, you cannot restore a later-pushed-context after restoring an earlier-pushed-context.

As mentioned a little bit earlier, we'll return to the discussion of how to set certain device context values at appropriate moments in this chapter. In the meantime, however, it's time to begin discussing how to actually draw things into a device context (i.e., onto the screen).

7.6: Line Drawing Under Windows

Line drawing under Windows is accomplished using five different routines: w.MoveToEx, w.LineTo, w.PolyLine, w.PolyLineTo, and w.PolyPolyLine. Here are the prototypes for these five functions:


 
type
 
    POINT: 
 
        record
 
            x: dword;
 
            y: dword;
 
        endrecord;
 

 
static
 

 
    LineTo: procedure
 
    ( 
 
        hdc             :dword; 
 
        nXEnd           :dword; 
 
        nYEnd           :dword
 
    );
 
        @stdcall;
 
        @returns( "eax" );
 
        @external( "__imp__LineTo@12" );
 

 
    MoveToEx: procedure
 
    ( 
 
            hdc         :dword; 
 
            X           :dword; 
 
            Y           :dword; 
 
        var lpPoint     :POINT
 
    );
 
        @stdcall;
 
        @returns( "eax" );
 
        @external( "__imp__MoveToEx@16" );
 

 
    Polyline: procedure
 
    ( 
 
            hdc             :dword; 
 
        var lppt            :POINT; 
 
            cPoints         :dword
 
    );
 
        @stdcall;
 
        @returns( "eax" );
 
        @external( "__imp__Polyline@12" );
 

 
    PolylineTo: procedure
 
    ( 
 
            hdc             :dword; 
 
        var lppt            :POINT; 
 
            cCount          :dword
 
    );
 
        @stdcall;
 
        @returns( "eax" );
 
        @external( "__imp__PolylineTo@12" );
 

 
    PolyPolyline: procedure
 
    ( 
 
            hdc             :dword; 
 
        var lppt            :POINT; 
 
        var lpdwPolyPoints  :var; 
 
            cCount          :dword
 
    );
 
        @stdcall;
 
        @returns( "eax" );
 
        @external( "__imp__PolyPolyline@16" );
 

 

All of these functions exist as keyword macros in the BeginPaint/EndPaint multi-part macros. You invoke these macros between the BeginPaint/EndPaint pair; they have the following declarations:


 
      #keyword LineTo( _x_, _y_ );
 
      #keyword MoveToEx( _x_, _y_, _lpPoint_ );
 
      #keyword Polyline( _lppt_, _cPoints_ );
 
      #keyword PolylineTo( _lppt_, _cPoints_ );
 
      #keyword PolyPolylineTo( _lppt_, _lpdwPolyPoints_, _cCount_ );
 

 

The invocation sequence is similar to the Win32 API functions except you don't need the "w." prefix and you don't supply the hdc (first) parameter.

The unusual thing about Windows' line drawing routines is that most of them make use of the "current pen position" maintained in the device context. For example, consider the LineTo function that draws a line in the window specified by the hdc parameter to BeginPaint. You'll notice that this function has only two parameters, an x-coordinate value and a y-coordinate value; lines, however, are defined by two endpoints, not a single point. The LineTo API function will actually draw a line from the "current point" (that the device context maintains) to the point specified by the (x,y) coordinate value you pass to LineTo. After drawing the line, the LineTo function sets the internal "current point" value to the endpoint of the line (that is, the (x,y) coordinate we pass LineTo becomes the new "current point" after the line is drawn). This feature makes it very easy to draw complex connected shapes as a sequence of connected lines by making sequential calls to LineTo.

There are two problems with LineTo's behavior; specifically, "how do we set the initial starting point?" and "what happens when we want to draw two unconnected line segments?" Well, the MoveToEx macro comes to our rescue in this case. The MoveToEx macro (i.e., w.MoveToEx API function) sets the "current point" inside the device context but has no other effect on the display. You can use this function to set the initial endpoint of a line and then call LineTo to draw the line from that point to the line's end point, e.g.,


 
// Draw a line from (10, 10) to (100, 50):
 

 
	MoveToEx( 10, 10, NULL );
 
	LineTo( 100, 50 );
 

 

The third parameter in the MoveToEx invocation specifies a variable of type w.POINT where Windows will store the device context's "current point" value. If this argument is NULL (as in this example), then Windows will not bother storing the value.

Because drawing a complex object as a sequence of line segments is so common, the operation of the MoveToEx and LineTo functions is just what you want a large percentage of the time. However, if you decide to draw a fairly complex object made up of dozens, hundreds, or even thousands of line segments, the calls to LineTo can become a performance bottleneck,. The problem is that many modern display adapters provide hardware acceleration of various graphic primitives (including line drawing). As a result of this acceleration, the system actually winds up spending more time calling the LineTo function than actually drawing the line. To reduce the overhead of the LineTo function invocations when drawing objects made up of complex line segments, Windows provides the Polyline, PolylineTo, and PolyPolylineTo functions. These functions take an array of points (or an array of polylines, that is, an array of array of points) and pass that data on down to the GDI driver that is responsible for drawing lines. The line drawing code can quickly draw the sequence of line segments without the overhead of multiple calls to the line drawing function.

The execution of the PolylineTo function is roughly equivalent to a sequence of LineTo calls. It begins drawing line segments from the current position to the first point in an array of points you pass to this function. PolylineTo sets the "current position" to the coordinate of the last point appearing in the array of points. The lppt parameter is a pointer to an array of w.POINT records and the cCount parameter specifies the number of points in that array.

The Polyline and PolyPolyline functions neither use nor modify the "current pen position" value in the device context. The Polyline function (to which you pass an array of points and a count) draws a sequence of lines starting with the first point in the list through each of the remaining points in the list. We'll take a look at an example of a Polyline call a little later in this section. The PolyPolyline API function accepts an array of polylines and draws the object specified by all these points.

When drawing lines, Windows uses several default values in the device context to control how the line is drawn. This includes the color of the line, the width of the line, and the line's style (e.g., solid, dotted, dashed, etc.). The default pen setting is a black, solid pen whose width is one pixel. While this is probably the most common line style you will draw, there is often the need to draw lines of a different color, a different width, or with some other style besides solid. In order to do this, you must create a new pen and select that pen into the current device context. To create a new pen, you use the Windows w.CreatePen API call. Here's the prototype for that function:


 
static
 

 
    CreatePen: procedure
 
    ( 
 
        fnPenStyle  :dword; 
 
        nWidth      :dword; 
 
        crColor     :COLORREF
 
    );
 
        @stdcall;
 
        @returns( "eax" );
 
        @external( "__imp__CreatePen@12" );
 

 

The fnPenStyle parameter specifies a style for the pen. This can be one of the values that Table 7-2 describes. The use of most of these constants is fairly self-explanatory. We'll discuss the purpose of the w.PS_INSIDEFRAME pen style in a later section of this chapter.

Table 7-2: Pen Styles
Pen Style
Description
w.PS_SOLID
Draw a solid line with the pen.
w.PS_DASH
The pen is dashed. This style is valid only when the pen width is one or less in device units.
w.PS_DOT
The pen is dotted. This style is valid only when the pen width is one or less in device units.
w.PS_DASHDOT
The pen has alternating dashes and dots. This style is valid only when the pen width is one or less in device units.
w.PS_DASHDOTDOT
The pen has alternating dashes and double dots. This style is valid only when the pen width is one or less in device units.
w.PS_NULL
Windows does not draw with the pen.
w.PS_INSIDEFRAME
The pen is solid. When this pen is used in any GDI drawing
function that takes a bounding rectangle, the dimensions of the
figure are shrunk so that it fits entirely within the bounding rectangle,
taking into account the width of the pen.

The w.CreatePen nWidth parameter is only valid for the w.PS_SOLID, w.PS_NULL, and w.PS_INSIDEFRAME pen styles. This specifies the width of the line in logical device units. If this field contains zero, the width of the line is always one pixel (which isn't necessarily true if the nWidth value is one).

The crColor parameter is an RGB value that specifies the color that the pen will draw on the screen. Note that you can use the wpa.hhf RGB macro to create an RGB value to pass as this parameter.

When you call w.CreatePen, Windows will create a pen GDI object internally and return a handle to that pen in the EAX register. Note that Windows does not automatically start using this pen. You must select this pen into the device context if you want to use it while drawing lines. This is done with the w.SelectObject API function (or SelectObject #keyword macro found in the wpa.hhf header file). When you select a pen into the current device context via SelectObject, Windows returns the handle to the previous pen that was selected. You can save this former pen value in order to restore the original pen when you are done using the current pen, e.g.,


 
	BeginPaint( hwnd, ps, hdc );
 
			.
 
			.
 
			.
 
		w.CreatePen( w.PS_SOLID, 0, RGB( $FF, $00, $FF )); // Solid, magenta, pen.
 
		mov( eax, magentaPen );									// Save, so we can delete later.
 
		SelectPen( eax );									// Select the magenta pen into the context.
 
		mov( eax, oldPen );									// Save, so we can restore.
 
			.
 
			.
 
			.
 
		SelectPen( oldPen );									// Restore original pen.
 
		w.DeleteObject( magentaPen );
 

 
	EndPaint;
 

 

Although this example might suggest this to be the case, understand that pens you create via w.CreatePen are not part of the device context. That is, you can create a pen outside the BeginPaint/EndPaint sequence and such pen values are persistent outside of the BeginPaint/EndPaint sequence. The calls to w.CreatePen and w.DeletePen appear inside the BeginPaint/EndPaint sequence here mainly for typographical convenience. Their presence in this sequence does not imply that these calls have to take place inside this sequence.

Because the w.CreatePen API function creates a resource inside Windows, you must be sure to delete that pen when you are done using it via a call to w.DeletePen. Because Windows has limited GDI resources available, failure to delete any GDI resources your program uses may lead to "resource leak" which will impact the overall system performance. Though you don't have to create and destroy a GDI object within your Paint procedure, you do need to make sure you delete all resources your program creates before your program quits. Many applications create the pens they need in their Create procedure and then delete those pens in the QuitApplication procedure. This sequence spares the application from having to constantly create and destroy often-used pens.

Windows provides three built-in, or stock, pens that you can use without calling w.CreatePen to create them: a black pen (the default), a white pen, and a null pen (which doesn't affect the display when you draw with it). You may obtain a handle to any of these stock pens by calling the w.GetStockObject API function:


 
static
 
    GetStockObject: procedure( fnObject:dword );
 
        @stdcall;
 
        @returns( "eax" );
 
        @external( "__imp__GetStockObject@4" );
 

 

This function requires a special value to tell Windows exactly what kind of object you want to create. The windows.hhf header file defines three constants that you can use to tell w.GetStockObject to return a handle to one of the stock pens: w.BLACK_PEN, w.WHITE_PEN, and w.NULL_PEN. For example, to quickly obtain the handle for a white pen, you can use the following call:


 
	w.GetStockObject( w.WHITE_PEN );
 
	mov( eax, whitePenHandle );
 

 

Note that you must not delete a stock object.

7.6.1: Background Mode and Color for Lines

When using a pen style that involves dashed or dotted lines, Windows uses the current background mode and color to determine how to draw the space between the dots and dashes that comprise the line. You can set the background color and mode by using the w.SetBkColor and w.SetBkMode API functions, respectively:

static
 

 

 
    SetBkColor: procedure( hdc:dword; crColor:dword );
 
        @stdcall;
 
        @returns( "eax" );
 
        @external( "__imp__SetBkColor@8" );
 

 
    SetBkMode: procedure( hdc:dword; iBkMode:dword );
 
        @stdcall;
 
        @returns( "eax" );
 
        @external( "__imp__SetBkMode@8" );
 

 

The hdc parameter to these functions is, obviously, the handle of the device context whose background color or mode you want to change. The crColor parameter to the w.SetBkColor function is an RGB color value that windows will use when drawing the gaps (if any) between the lines of an image it is rendering. The iBkMode parameter you supply to w.SetBkMode is either w.OPAQUE or w.TRANSPARENT. If you specify the w.OPAQUE mode, then Windows will fill the gaps in a dotted or dashed line with a solid color, the color you specify with the w.SetBkColor function call. Note that in the opaque mode, Windows overwrites whatever was previously on the screen using the solid color you've specified. On the other hand, if you specify w.TRANSPARENT as the background drawing mode, then Windows will ignore the background color and leave whatever image originallyl appeared on the screen in the gaps between the dashes and dots of a stylistic line.

7.6.2: Using the LineTo and MoveToEx API Functions

As a demonstration of the line drawing capabilities of Windows, the Lines.hla program repeatedly draws a set of pseudo-random lines on the display. This application is a typical Windows app with two wndproc procedures - Paint and Size. The Size procedure tracks window size changes so the Paint procedure can keep all lines within the boundaries of the window. The Paint procedure computes the endpoints of a "somewhat random" line to draw and then draws that line to the window.

Because the Size procedure is especially trivial, we'll take a look at it first. This procedure simply copies the values for the new width and height into a couple of global variables (ClientSizeX and ClientSizeY). Here's its code:


 
// Size-
 
//
 
//	This procedure handles the w.WM_SIZE message
 
//	Basically, it just saves the window's size so
 
//	the Paint procedure knows when a line goes out of
 
//	bounds.
 
//
 
//	L.O. word of lParam contains the new X Size
 
//	H.O. word of lParam contains the new Y Size
 

 
procedure Size( hwnd: dword; wParam:dword; lParam:dword );
 
begin Size;
 

 
	// Convert new X size to 32 bits and save:
 

 
	movzx( (type word lParam), eax );
 
	mov( eax, ClientSizeX );
 

 
	// Convert new Y size to 32 bits and save:
 

 
	movzx( (type word lParam[2]), eax );
 
	mov( eax, ClientSizeY );
 
	
 
	xor( eax, eax ); // return success.
 
	
 

 
end Size;
 

 

The Paint procedure is a tiny bit more complicated. The first thing to consider are the variables that the Paint procedure uses to maintain the state of the pseudo-random lines it draws. Here are the declarations:


 
static
 
    hPen        :dword;                     // Pen handle.
 
    OldPen      :dword;                     // Old Pen Handle.
 
    lastRGB     :w.COLORREF;                // Last color we used
 
    x1          :int32 := 0;
 
    y1          :int32 := 0;
 
    x2          :int32 := 25;
 
    y2          :int32 := 25;
 
    lastDeltaX1 :int32 := 1;
 
    lastDeltaY1 :int32 := 1;
 
    lastDeltaX2 :int32 := 2;
 
    lastDeltaY2 :int32 := 2;
 
    changeCntr  :uns32 := 10;
 

 

The hPen variable holds the handle of the new pen this program creates (to control the color of the output); the OldPen variable maintains the handle of the old pen so that Paint can restore the original pen once Paint is finished drawing with the new pen. The lastRGB variable holds the color of the last pen. Paint increments this value to create a new color for each line it draws to the window. The x1, x2, y1, and y2 variables hold the coordinates of the end points of the line that Paint draws. The Paint procedure uses the lastDelta** and changeCntr variables to compute new endpoints for each line it produces. Here's how Paint uses these "delta" variables: on each pass through the Paint procedure, the code adds the value of lastDeltaX1 to the x1 variable, it adds lastDeltaY1 to y1, it adds lastDeltaX2 to x2, and it adds lastDeltaY2 to y2. This generates a new set of end points for the line. Should any one of those end points fall outside the boundaries of the screen, the Paint procedure clips the particular coordinate value so that it remains within the window's boundaries (and Paint also negates the corresponding lastDelta** value so that future adjustments continue to stay within the window's boudaries). The changeCntr variable determines how many line segments that Paint will draw with the current set of lastDelta** values before it chooses some new, semi-random, values for these variables. Here's how the Paint procedure uses changeCntr to determine when to set the lastDelta** values to semi-random values:


 
        // If the changeCntr variable counts down to zero, compute new
 
        // delta values:
 

 
        dec( changeCntr );
 
        if( @z ) then
 

 
            rand.range( 1, 100 ); // Compute a new, random, value for changeCntr
 
            mov( eax, changeCntr );
 

 
            // Compute new (random) values for the lastDelta** variables:
 

 
            rand.range( 0, 10 ); // We want a value in the range -5..+5
 
            sub( 5, eax );
 
            mov( eax, lastDeltaX1 );
 

 
            rand.range( 0, 10 );
 
            sub( 5, eax );
 
            mov( eax, lastDeltaY1 );
 

 
            rand.range( 0, 10 );
 
            sub( 5, eax );
 
            mov( eax, lastDeltaX2 );
 

 
            rand.range( 0, 10 );
 
            sub( 5, eax );
 
            mov( eax, lastDeltaY2 );
 

 
        endif;
 

 

Here's the code sequence that computes the new x1 value (computations for the x2, y1, and y2 values are nearly identical):


 
        // Compute a new starting X-coordinate for the
 
        // line we're about to draw (do this by adding
 
        // the appropriate delta to our current x-coordinate
 
        // value):
 

 
        mov( lastDeltaX1, eax );
 
        add( x1, eax );
 
        if( @s ) then
 

 
            // If we went off the left edge of the window,
 
            // then change the direction of travel for the
 
            // deltaX value:
 

 
            neg( lastDeltaX1 );
 
            add( lastDeltaX1, eax );
 

 
        endif;
 

 
        // Check to see if we went off the right edge of the window.
 
        // Reset the direction of deltaX if this happens:
 

 
        if( eax > ClientSizeX ) then
 

 
            neg( lastDeltaX1 );
 
            mov( ClientSizeX, eax );
 
            dec( eax );
 

 
        endif;
 
        mov( eax, x1 );
 

 

Note that on one pass through the Paint procedure this code draws only a single line segment.

Because the Paint procedure draws only a single line segment in response to a w.WM_PAINT message, it takes a continuous stream of w.WM_PAINT messages in order to generate the free-running display that Lines.hla produces in its window. The real trick to Lines.hla is how it continuously draws these line segments in its window. We can't stick an HLA forever loop inside our Paint procedure to continuously draw a sequence of lines over and over again; if we did that, the program wouldn't respond to messages and there would be no way to quite this program except by running the Windows task manager and killing the process. Because this is somewhat ill-behaved for an application, we've got to dream up a better way of telling the application to continuously draw lines. One sneaky way to do this is to tell Windows to send our application a w.WM_PAINT message just before leaving the Paint procedure. This action, of course, will cause Windows to call our Paint procedure again in short order, but through the standard message handling subsystem so our application can still respond to w.WM_DESTROY messages. The correct way to tell Windows to send our application a w.WM_PAINT message is to call the w.InvalidateRect API function, passing it a NULL rectangle to invalidate (which tells it to invalidate the whole window). Here's the call that does this (which appears at the end of the Paint procedure):

w.InvalidateRect( hwnd, NULL, false ); 
 

The last argument in the w.InvalidateRect parameter list tells this function to whether it should erase the screen before having Paint redraw it. Because we want to keep all the previous lines we've drawn in the window, this call passes false as the value of this parameter.

Here's the complete listing of the Lines.hla program:

// Lines.hla-
 
//
 
//  Simple Application the demonstrates line drawing.
 

 
program Lines;
 
#include( "rand.hhf" )
 
#include( "hll.hhf" )
 
#include( "w.hhf" )
 
#include( "wpa.hhf" )
 

 
?@NoDisplay := true;
 
?@NoStackAlign := true;
 

 

 
type
 
    // Message and dispatch table related definitions:
 
          
 
    MsgProc_t:  procedure( hwnd:dword; wParam:dword; lParam:dword );
 
    
 
    MsgProcPtr_t:
 
        record
 
            
 
            MessageValue    :dword;
 
            MessageHndlr    :MsgProc_t;
 
            
 
        endrecord;
 
    
 

 

 

 
static
 
    hInstance           :dword;         // "Instance Handle" Windows supplies.
 

 
    wc                  :w.WNDCLASSEX;  // Our "window class" data.
 
    msg                 :w.MSG;         // Windows messages go here.
 
    hwnd                :dword;         // Handle to our window.
 
    
 
    
 
    ClientSizeX         :int32 := 0;    // Size of the client area
 
    ClientSizeY         :int32 := 0;    //  where we can paint.
 
    
 
    
 
readonly
 

 
    ClassName   :string := "LinesWinClass";         // Window Class Name
 
    AppCaption  :string := "Lines Program";         // Caption for Window
 

 
    
 
    // The dispatch table:
 
    //
 
    //  This table is where you add new messages and message handlers
 
    //  to the program.  Each entry in the table must be a MsgProcPtr_t
 
    //  record containing two entries: the message value (a constant,
 
    //  typically one of the w.WM_***** constants found in windows.hhf)
 
    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the
 
    //  message.
 
 
 
    
 
    Dispatch    :MsgProcPtr_t; @nostorage;
 

 
        MsgProcPtr_t    
 
            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],
 
            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],
 
            MsgProcPtr_t:[ w.WM_SIZE,    &Size              ],
 
            
 
            // Insert new message handler records here.
 
            
 
            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.
 
            
 
    
 
            
 
    
 
/**************************************************************************/
 
/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */
 
/**************************************************************************/
 

 
// QuitApplication:
 
//
 
//  This procedure handles the w.WM_DESTROY message.
 
//  It tells the application to terminate.  This code sends
 
//  the appropriate message to the main program's message loop
 
//  that will cause the application to terminate.
 
    
 
procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );
 
begin QuitApplication;
 

 
    w.PostQuitMessage( 0 );
 

 
end QuitApplication;
 

 

 

 

 
// Paint:
 
//
 
//  This procedure handles the w.WM_PAINT message.
 

 
procedure Paint( hwnd: dword; wParam:dword; lParam:dword );
 
var
 
    hdc         :dword;         // Handle to video display device context
 
    ps          :w.PAINTSTRUCT; // Used while painting text.
 

 
static
 
    hPen        :dword;                     // Pen handle.
 
    OldPen      :dword;                     // Old Pen Handle.
 
    lastRGB     :w.COLORREF;                // Last color we used
 
    x1          :int32 := 0;
 
    y1          :int32 := 0;
 
    x2          :int32 := 25;
 
    y2          :int32 := 25;
 
    lastDeltaX1 :int32 := 1;
 
    lastDeltaY1 :int32 := 1;
 
    lastDeltaX2 :int32 := 2;
 
    lastDeltaY2 :int32 := 2;
 
    changeCntr  :uns32 := 10;
 

 
begin Paint;
 

 
    // Message handlers must preserve EBX, ESI, and EDI.
 
    // (They've also got to preserve EBP, but HLA's procedure
 
    // entry code already does that.)
 
    
 
    push( ebx );
 
    push( esi );
 
    push( edi );
 
    
 
    // Note that all GDI calls must appear within a 
 
    // BeginPaint..EndPaint pair.
 
    
 
    BeginPaint( hwnd, ps, hdc );
 

 
        inc( lastRGB ); // Increment the color we're using.
 

 
        // If the changeCntr variable counts down to zero, compute new
 
        // delta values:
 

 
        dec( changeCntr );
 
        if( @z ) then
 

 
            rand.range( 1, 100 );
 
            mov( eax, changeCntr );
 

 
            // Compute new (random) values for the lastDelta* variables:
 

 
            rand.range( 1, 10 );
 
            sub( 5, eax );
 
            mov( eax, lastDeltaX1 );
 

 
            rand.range( 1, 10 );
 
            sub( 5, eax );
 
            mov( eax, lastDeltaY1 );
 

 
            rand.range( 1, 10 );
 
            sub( 5, eax );
 
            mov( eax, lastDeltaX2 );
 

 
            rand.range( 1, 10 );
 
            sub( 5, eax );
 
            mov( eax, lastDeltaY2 );
 

 
        endif;
 
        
 
        // Compute a new starting X-coordinate for the
 
        // line we're about to draw (do this by adding
 
        // the appropriate delta to our current x-coordinate
 
        // value):
 

 
        mov( lastDeltaX1, eax );
 
        add( x1, eax );
 
        if( @s ) then
 

 
            // If we went off the left edge of the window,
 
            // then change the direction of travel for the
 
            // deltaX value:
 

 
            neg( lastDeltaX1 );
 
            add( lastDeltaX1, eax );
 

 
        endif;
 

 
        // Check to see if we went off the right edge of the window.
 
        // Reset the direction of deltaX