This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.

Using GDI+ in VFP 9.0 with the FFC Library, Part 1

Walter Nicholls

A new set of GDI+ FFC graphics classes will ship with the final version of VFP 9.0, but you can get a first look at them by checking the VFP home page for download instructions (http://msdn.com/vfoxpro). The GDI+ classes should be ready for download in early August or soon thereafter, and can be used with the VFP 9.0 Public Beta, available from the same Web site. Walter Nicholls introduces the classes in Part 1 of this four-part series by drawing images directly onto a VFP form.

One of the most anticipated and heralded new features in Visual FoxPro 9.0 is the enhanced reporting engine. Although this engine has numerous amazing features, the most exciting for me is the ability to write custom "plug-in" objects that modify or extend the appearance of items on the report (see Figure 1).

	There are two keys to doing this. One is the ReportListener class, which provides a way to hook in to the report generation and rendering process. The other is direct access to the GDI+ drawing surface that the report engine uses to render the report.

	As an added bonus, some of the improvements made in VFP 9.0 to support the ReportListener class also make it possible to use GDI+ in other contexts. With a bit of careful planning, you can write a single VFP class that can draw on a report, a form, or even just to a file on disk.

	Visual FoxPro ships with a collection of class libraries called the "Foundation Classes" (FFC). Version 9.0 includes a class library, _GDIPLUS.VCX, which provides you with a very easy way to use GDI+ in a Visual FoxPro application.

	Note: We expect the GDI+ Foundation Classes to be posted on the Microsoft Web site for download some time in early August 2004, or soon thereafter. Check for an announcement on the Microsoft Visual FoxPro home page at http://msdn.com/vfoxpro.

	In Part 1 of this series on the GDI+ FFC classes, I'll give you a whirlwind tour of the _GDIPLUS library and demonstrate some of the key classes by drawing on a Visual FoxPro form. In Part 2, I'll show you how to use the same techniques to extend the reporting system, using a ReportListener class along with the GDI+ classes.

I'm going to make this look really, really easy

Look ahead a few pages and find the "How about a pie chart?" section. The figure shown there is the finished product that we'll build in this article, and I promise to make it look really, really easy. By the end of the article, you're going to see that it was really, really easy!

	Keep in mind that this article was written using the Public Beta version of Visual FoxPro 9.0 (version 09.00.0000.1720). At the time of this writing, the features of VFP 9.0 aren't set in stone, and some behavior may differ in the released product. However, you should be able to successfully use the examples illustrated here (and available in the accompanying Download file) with the downloadable VFP 9.0 Public Beta.

The relationship between the FFC library and the GDI+ API

The piece of functionality we refer to as "GDI+" is actually provided to programmers in several forms. In the .NET world, there are System.Drawing and its related namespaces. C++ programmers use an almost identical API defined in gdiplus.h. However, the interface provided by GDIPLUS.DLL (and already available to VFP developers through API calls) is actually a non-object-oriented interface known as the "GDI+ Flat API."

	To make working with GDI+ much easier, the new VFP 9.0 FFC library, _GDIPLUS.VCX, provides an object-oriented interface that's very similar to the .NET System.Drawing namespace. This means that books, articles, and reference material like MSDN, originally written for .NET and C++ programmers, can easily be applied to programming with the FFC library by making just a few changes.

	The main differences are:

  • All class names are prefixed with the letters "Gp".
  • Some property names are different (for example, PenWidth instead of Width).
  • Constants are declared as #define constants with the prefix "GDIPLUS_" instead of enumerations.
  • Only a subset of the full GDI+ functionality is exposed, but it's easy to extend.

	Most of these differences are due to language constraints. For instance, "Point" and "Width" have other meanings in Visual FoxPro, and we don't have the same distinction between integers and floats. Table 1 shows the classes defined in _GDIPLUS.VCX, compared to the equivalent .NET classes.

****

Table 1. The classes in the _GDIPLUS.VCX FFC library, compared to the .NET classes.

FFC class

Description

.NET class(es)

GpGraphics

Drawing surface (canvas) and drawing operations.

Graphics

GpColor

Color with red, green, blue, and alpha components.

Color

GpPoint

Location in a 2-D plane (x, y).

Point, PointF

GpSize

Dimensions (width, height).

Size, SizeF

GpRectangle

Location and size together.

Rectangle, RectangleF

GpPen

Used for drawing lines and curves.

Pen

GpBrush

Abstract base class for brush classes.

Brush

GpSolidBrush

Used for filling areas with solid color.

SolidBrush

GpHatchBrush

Used for filling areas with a pattern.

HatchBrush

GpFontFamily

Describes a family of related fonts (for example, "Arial").

FontFamily

GpFont

Describes a specific font including size, weight, decoration. Used for drawing text.

Font

GpStringFormat

Text layout and formatting information such as alignment.

StringFormat

GpImage

Base class for images (includes vector and bitmap formats).

Image

GpBitmap

Image consisting of a 2-D array of pixels.

Bitmap

Using GDI+ to draw on a form

When I started working with GDI+ in Visual FoxPro, I expected that, since VFP already uses GDI+ internally, I could just use the GDI+ functions and everything would work fine. It turned out I was wrong–but not too wrong. There are some things you need to take care of before using the GDI+ functions, and you have to work within a few limitations.

	Instead of using a Visual FoxPro control–such as an Image or Container–we're going to use GDI+ functions to draw directly on the form surface. The key to this is the form Paint() event, which VFP calls after redrawing the form background and any controls on it.

Keeping the drawing surface clear

You should ensure that the area on which you want to draw isn't used by VFP for anything else. For starters, don't put any other controls (buttons, textboxes, and so on) there, because they'll just get overpainted. I like to place an invisible shape object on the form to show where the drawing is going to take place. This not only reminds me to keep other controls out of the way, but also conveniently gives my drawing code a position and size to work within.

	If you want to draw on a page of a page frame, you'll need some mechanism to ensure that your drawing code executes only when the relevant page is active.

	Finally, don't forget the output from the "?" statement, or the chatter that results from having SET TALK ON. You can prevent this by setting the form property AllowOutput to .F., which will prevent your form from becoming the active output window. I normally set this property in my base form class, since it's unlikely to be desired behavior.

Off-screen drawing

To improve performance, Visual FoxPro normally uses a technique known as "double buffering." In this mode, VFP doesn't draw controls directly on the window surface, but instead draws to an off-screen bitmap, and then copies this image as required to repaint the window. This is much faster than redrawing each and every control every time the window is moved or revealed.

	Unfortunately, we don't have access to this off-screen drawing surface, and anything we draw to the form surface will be obliterated when the window is refreshed–and the Paint() event is called only when the off-screen bitmap is repainted.

	Working around this is beyond the scope of this article, so for now either run the examples with off-screen bitmaps turned off, or just be aware of this limitation. You can disable off-screen bitmaps with SYS(602,0) and turn them back on with SYS(602,1). This is a global setting, so I don't suggest you put it in the Init() of your form or anything like that.

GDI+ initialization

Before any GDI+ functions can be used, the GDI+ system must be "started up." Visual FoxPro does this automatically–but only if it needs to, and it can't read your mind! It's dead easy to force a startup of GDI+, however, with just one line of code:

createobject( "ReportListener" )

	You don't need to worry about shutting GDI+ down; VFP will do this for you when the application exits.

Setting the stage–a basic form

For the rest of this article, I'm going to work with the same basic form–a modeless, resizable form, with an invisible shape control (shpPlaceholder) centered in it (see Figure 2). I'll use the new Anchor property (added in VFP 9.0) to ensure that when the form is resized, the shape control will also be resized. Table 2 shows some key properties and methods of the form, which is available in the accompanying Download file as gpintro_blank.scx. ******

Table 2. Properties and methods of the base form.

Property/Method

Value/Contents

Width, Height

350, 250

AllowOutput

.F.

BorderStyle

3 (Sizable)

Caption

"Blank form - GDI+ Intro"

WindowType

0 (Modeless)

Init()

* Ensure GDI+ is initialized
createobject("ReportListener")

shpPlaceholder.Left, .Top, .Width, .Height

25, 25, 300, 200

shpPlaceholder.Anchor

15 (keep same distance from all edges of the form)

shpPlaceholder.Visible

.F.

	I'll also assume that you've copied the FFC libraries into an "FFC/" subdirectory under your project. If you want anything different, adjust the paths accordingly. Now we're ready to start using GDI+.

Starting out–the GpGraphics object

To do anything visible, you need a GpGraphics object. This represents a drawing surface–a window, a report page, or any of several other possibilities. The GpGraphics object provides properties and methods for finding (or changing) the size and resolution of the drawing surface, and methods for drawing on that surface. Table 3 shows some of the most important properties and methods of GpGraphics.

****

Table 3. Some of the GpGraphics methods.

Property/Method names and descriptions

CreateFromHWND( hwnd )

CreateFromHDC( hdc)

CreateFromImage( oImage )

SetHandle( nGDIPlusHandle [,lOwned] )

Description: Different ways of acquiring a drawing surface.

Clear( color )

Description:

Fill the entire drawing surface in the given color.

DrawLine( oPen, x1,y1, x2,y2 )

DrawLines( oPen, aPoints[] )

DrawArc( oPen, x,y,width,height, start, sweep )

DrawBezier( oPen, x1,y1, x2,y2, x3,y3, x4,y4 )

DrawCurve( oPen, aPoints[] )

Description:

Draw lines and curves.

DrawRectangle( oPen, x,y, width,height )

DrawRectangle( oPen, oRect )

DrawRectangles( oPen, aRects[] )

DrawEllipse( oPen, x,y, width,height )

DrawEllipse( oPen, oRect)

DrawClosedCurve( oPen, aPoints[] )

DrawPie( oPen, x,y, width,height, start,sweep )

DrawPie( oPen, oRect, start, sweep )

DrawPolygon( oPen, aPoints[] )

Description:

Draw outlined shapes.

FillRectangle( oPen, x,y, width,height )

FillRectangle( oPen, oRect )

FillRectangles( oPen, aRects[] )

FillEllipse( oPen, x,y, width,height )

FillEllipse( oPen, oRect)

FillClosedCurve( oPen, aPoints[] )

FillPie( oPen, x,y, width,height, start,sweep )

FillPie( oPen, oRect, start, sweep )

FillPolygon( oPen, aPoints[] )

Description:

Draw filled shapes.

DrawImageAt( oImage, x,y )

DrawImageAt( oImage, oPoint )

DrawImageScaled( oImage, x,y,width, height )

DrawImageScaled( oImage, oRect )

DrawImagePortionAt( oImage, oDestPoint, oSrcRect, nSrcUnit )

DrawImagePortionScaled( oImage, oDestRect, oSrcRect, nSrcUnit )

Description:

Draw image or partial image.

DrawStringA( cString, oFont, oRect, [oStringFormat], [oBrush] )

DrawStringW( cUnicodeString, oFont, oRect, [oStringFormat], [oBrush] )

Description:

Draw text.

MeasureStringA( cString, oFont, layoutarea, [oStringFormat], [@nChars], [@nLines]

MeasureStringW( cUnicodeString, oFont, layoutarea, [oStringFormat], [@nChars], [@nLines]

Description:

Calculate size (bounding box) of a text string.

ResetTransform()

TranslateTransform( xOffset, yOffset [,matrixorder] ) RotateTransform( nAngle [,matrixorder] )

ScaleTransform( xScale, yScale [,matrixorder] )

Description:

Change the way things are drawn by manipulating the coordinate system.

Save( @graphicsstate )

Description:

Save current state of graphics context.

Restore( graphicsstate )

Description:

Restored previously saved state.

How to get a GpGraphics object

In some cases, the underlying GDI+ object already exists and we simply need a VFP interface to it. For instance, the VFP 9.0 reporting system uses GDI+ internally. Each page in a report is encapsulated by a Graphics object, with a handle to that object stored in a property of ReportListener. In that case, you'd simply instantiate a GpGraphics object and associate it with that handle (more on that in the next article).

	In other cases, you need to create a GDI+ Graphics object yourself. For example, to draw on a form, you'd create a GpGraphics object based on the form's native Window handle (HWnd):

  local oGr as GpGraphics of ffc/_gdiplus.vcx
oGr = newobject('GpGraphics','ffc/_gdiplus.vcx')
oGr.CreateFromHWND( Thisform.HWnd )

	This GpGraphics object now is set up appropriately for the form, with the coordinate space defined in pixels and (0,0) representing the top left corner. If you don't have a pre-existing basis for the GpGraphics object–for example, when creating a bitmap to save to a file–then you'd have to do slightly more work.

Drawing lines and shapes

To show the first examples, I'm going to introduce a number of classes in quick succession. We'll start off by drawing an ellipse on a form, as in Figure 3.

	To draw something on the window surface, we need a tool. In this step, we use a GpPen object, which can draw lines (in this case, the outline of the ellipse). We also need to set the color of that tool, using a GpColor object. See Table 4 and Table 5 for the most important properties and methods of GpPen and GpColor.

****

Table 4. Important properties and methods of the GpPen class.

Property/Method name

Description

PenColor

The color in which this pen will draw (ARGB value).

PenWidth

Width of the pen unit.

PenType

Style of lines (see GDIPLUS_PENTYPE_ constants).

Alignment

The alignment of this pen (see GDIPLUS_PENALIGNMENT_ constants).

DashStyle

The style used for dashed lines.

Create( color [, width] [, unit] )

Create pen in given color.

CreateFromBrush( brush [, width] [, unit] )

Create based on a brush object.

Init( color )

Create in given color.

Init()

Create empty pen object (must call Create() before using).

****

Table 5. The GpColor class.

Property/Method name

Description

Red

Red component (0...255).

Green

Green component (0...255).

Blue

Blue component (0...255).

Alpha

Alpha (transparency). 0 = completely transparent, 255 = completely opaque.

ARGB

GDI+ color value (32-bit integer).

FoxRGB

Visual FoxPro color value, as would be returned by the RGB() or GETCOLOR() function. Note: This value doesn't include alpha information, and assigning to this property will set the .Alpha component to opaque (255).

Set( red,green,blue [,alpha] )

Set color from individual components (if alpha not specified, is 100% opaque).

Init( red,green,blue [,alpha] )

Create specifying individual color components (if alpha not specified, is 100% opaque).

Init( argb )

Create using a single GDI+ color value.

Init()

Create a default color–opaque black.

	Here's code to create a GpColor object in dark blue:

  oLineColor = newobject( ;
  'GpColor','ffc/_gdiplus.vcx','', 0,0,100 )

	Here's code to create a GpPen object using that color, and specified as three pixels wide:

  * first create 'empty' pen, then create the
* underlying GDI+ object
oPen = newobject('GpPen', 'ffc/_gdiplus.vcx' )
oPen.Create( m.oLineColor, 3 )

	To draw an ellipse on the form surface, we can use the DrawEllipse method of the GpGraphics object. This code will use the pen created previously to draw the ellipse, positioned in the space defined by the placeholder shape object:

  * Draw ellipse on the form surface, using the
* placeholder shape for position and dimensions
oGr.DrawEllipse( m.oPen ;
  , Thisform.shpPlaceholder.Left ;
  , Thisform.shpPlaceholder.Top ;
  , Thisform.shpPlaceholder.Width ;
  , Thisform.shpPlaceholder.Height ;
  )

	Put all of the code presented so far into the Paint() event of a form, and when you run it, you'll see the graphics-enhanced form shown in Figure 3.

About rectangles

As we move through this example, we're going to be using the same coordinates over and over again. To save a bit of code, we can create a GpRectangle object and reference this as a single parameter. Here's the ellipse-drawing code fragment again, this time using a GpRectangle object to define the bounds of the ellipse drawing area:

  oBounds = newobject( ;
  'GpRectangle','ffc/_gdiplus.vcx','' ;
  , Thisform.shpPlaceholder.Left ;
  , Thisform.shpPlaceholder.Top ;
  , Thisform.shpPlaceholder.Width ;
  , Thisform.shpPlaceholder.Height ;
  )
oGr.DrawEllipse( m.oPen, m.oBounds )

Filling area–brushes

Now let's fill the ellipse with bright green. To draw filled areas, we need a new tool–a brush. The GDI+ FFC library provides several brushes, but for drawing in a solid color we need GpSolidBrush:

  * Create a bright green brush
local oFillColor as GpColor of ffc/_gdiplus.vcx ;
  , oBrush as GpSolidBrush of ffc/_gdiplus.vcx
oFillColor = newobject( ;
  'GpColor','ffc/_gdiplus.vcx','' ;
  , 0,255,0 ) && green
oBrush = newobject( ;
  'GpSolidBrush', 'ffc/_gdiplus.vcx', '' ;
  , m.oFillColor )

	To fill the ellipse on the screen, we can use the FillEllipse() method:

  oGr.FillEllipse( m.oBrush, m.oBounds )

	To draw the outline of the ellipse on top of the shaded ellipse, we need to call FillEllipse first, followed by DrawEllipse. This will get you the result shown in Figure 4

How about a pie chart?

Now that you've got the basics down, it's time to look at drawing a pie chart. The code I present here takes a two-column array as a property of the form, with each row in the array providing data for a single slice of the pie, as shown in the following pseudo-code:

  aSliceData[ nRow, 1 ] = data value
aSliceData[ nRow, 2 ] = fill color for the slice 
                        (in GDI+ ARGB format)
nSliceTotal = total of all the data values

	The Paint() event then takes this array and calculates the appropriate angles to draw the pie slices. The only new concepts here are the DrawPie() and FillPie() methods. These should be self-explanatory: They're used just like DrawEllipse and FillEllipse, with the addition of a starting angle and a sweep angle (extent of the pie slice), which are both expressed in degrees.

	I've kept this deliberately simple, but you could easily extend this to include labels and flag values (perhaps to "pull out" one of the slices). Try to avoid putting too much code in the Paint() event, though–fetching slice data with a remote SQL query is probably not a good idea!

	The following is the key section of the Paint() code–the full source code is in the accompanying Download file as gpintro_piechart.scx. See Figure 5 for the results.

  * Create the drawing objects
local oLineColor as GpColor of ffc/_gdiplus.vcx ;
  , oPen as GpPen of ffc/_gdiplus.vcx ;
  , oBrush as GpSolidBrush of ffc/_gdiplus.vcx
oLineColor = newobject( ;
  'GpColor','ffc/_gdiplus.vcx','' ;
  , 0,0,0 ) && black
oPen = newobject( ;
  'GpPen', 'ffc/_gdiplus.vcx','';
  , m.oLineColor )  && 1-pixel-wide pen
oBrush = newobject( ;
  'GpSolidBrush', 'ffc/_gdiplus.vcx','' )
oBrush.Create()  && don't specify colour yet

* Work out from the slice data what the starting
* angles should be
local nSlices
nSlices = alen(This.aSliceData,1)
local aAngles[m.nSlices+1], iSlice
aAngles[1] = 0  && start at 0 degrees (+ve x axis)
for iSlice = 2 TO m.nSlices
  aAngles[m.iSlice] = aAngles[m.iSlice-1] ;
  + 360*This.aSliceData[m.iSlice-1,1]/This.nSliceTotal
endfor
aAngles[m.nSlices+1] = 360  && Stop at full circle

* Draw the pie slices
for iSlice = 1 to m.nSlices
  oBrush.BrushColor = This.aSliceData[m.iSlice,2]
  oGr.FillPie(m.oBrush, m.oBounds ;
       , aAngles[m.iSlice] ;
       , aAngles[m.iSlice+1] - aAngles[m.iSlice])
  oGr.DrawPie(m.oPen, m.oBounds ;
       , aAngles[m.iSlice] ;
       , aAngles[m.iSlice+1] - aAngles[m.iSlice])
endfor

Drawing text

The final step toward our finished product is to draw some text over the surface of the pie chart. Because the pie chart includes so many different colors, which could make ordinary text hard to read, let's draw the words "VFP9 is cool!" over the figure, so that they appear to cast a shadow. It's time for a new tool–the GpFont class, described in Table 6. ******

Table 6. The GpFont class.

Property/Method name

Description

FontName

Name of the font–for example, "Arial."

Style

Style–for example, Bold, Italic (a collection of GDIPLUS_FONTSTYLE_ bits).

Size

Size in units.

Unit

Unit–by default, points (1/72 of an inch).

Create( fontname, size [,style [,unit]] )

Create GDI+ Font object using the specified font name and style.

Create( GpFontFamily, size [,style [,unit]] )

Create GDI+ Font object using the specified font family object and style.

Init( fontname/family, size [,style [,unit]] )

Same as instantiating an empty GpFont and then calling Create().

GetHeight( GpGraphics )

Get the line spacing of this font, in the units of the given GpGraphics object.

GetHeightGivenDPI( nDPI )

Get the line spacing of this font, for specified resolution (dots per inch).

	To draw text over the pie chart, we'll create a GpFont object in Arial, and set it to bold and 32 points high:

  oFont = newobject('GpFont','ffc/_gdiplus.vcx')
oFont.Create( "Arial" ;       && font name
  , 32 ;                      && size in units below
  , GDIPLUS_FONTSTYLE_BOLD;   && attributes
  , GDIPLUS_UNIT_POINT ;      && units
  )

	We also need a brush object, which we've seen before, and we must specify where the text is to be drawn–let's put it right in the middle of the picture. We already have a GpRectangle object defining the boundary, so the final step is to tell GDI+ how the text should be formatted within that rectangle. Welcome to the GpStringFormat class (see Table 7).

  * Get a basic string format object, then set properties
oStringFormat = newobject( ;
  'GpStringFormat','ffc/_gdiplus.vcx')
oStringFormat.Create( )
oStringFormat.Alignment ;
  = GDIPLUS_STRINGALIGNMENT_Center
oStringFormat.LineAlignment ;
  = GDIPLUS_STRINGALIGNMENT_Center

****

Table 7. Some of the properties and methods of the GpStringFormat class.

Property/Method name

Description

Alignment

Horizontal alignment of text.

LineAlignment

Vertical alignment.

FormatFlags

Formatting flags–see the GDIPLUS_STRINGFORMATFLAGS_ constants defined in gdiplus.h.

Trimming

How a string is trimmed if it doesn't fit in the bounding region.

HotkeyPrefix

Interpretation of the Windows "hotkey" prefix.

Create( [flags] [, languageID] )

Create new string format object.

GetGenericDefault( [lMakeClone] )

Get handle to or make copy of a default string format.

GetGenericTypographic( [lMakeClone] )

Get handle to or make copy of a typographic string format.

	We need to draw the text string twice: in a solid color on top, and offset by a few pixels in a semi-transparent shadow. This introduces yet another new concept–the "alpha" component of a color value. Here's the code:

  * Now draw the text with a drop-shadow.
* First, shrink the bounding box by 4 pixels
* and move 4 pixels to the right and down
oBounds.W = oBounds.W - 4
oBounds.H = oBounds.H - 4
oBounds.X = oBounds.X + 4
oBounds.Y = oBounds.Y + 4
* and draw the shadow in a 66% black
oBrush.BrushColor = 0xA8000000
oGr.DrawStringA( This.cOverlayText ;
  , oFont, oBounds, oStringFormat, oBrush )

* Now move the bounding box back to its original
* position, and draw the same string in opaque white
oBounds.X = oBounds.X - 4 
oBounds.Y = oBounds.Y - 4
oBrush.BrushColor = 0xFFFFFFFF
oGr.DrawStringA( This.cOverlayText ;
  , oFont, oBounds, oStringFormat, oBrush )

	You should now see the chart shown in Figure 6. Notice that if you resize the form, the text wraps as necessary. You can control this wrapping (and a whole lot of other stuff) with other properties of the GpStringFormat object.

A few closing comments

You may notice that the method to draw text is called "DrawStringA" and not just "DrawString." This is because there are two versions of this function. DrawStringA is for when you have text in a Visual FoxPro string, which is inherently 8-bit. DrawStringW is for when you have a Unicode string, where characters are 16-bit (the W stands for "Wide"). This is particularly useful inside a ReportListener, where you're often passed Unicode rather than VFP string values.

	Creating GDI+ objects like GpGraphics and GpFont can be expensive (in terms of CPU time). To improve performance, you can cache these between Paint() events (perhaps in a form property). Just be aware of situations that might cause the cached object to become invalid. For instance, if a form is resized, the bounding box of your drawing area might change!

	Also, in terms of performance, there are a few other tricks. If you're comfortable with ARGB color values, you can avoid using GpColor objects entirely–any function that takes a color is also quite happy being passed a 32-bit number. Another trick that can improve performance substantially is double-buffering–but that's a subject for another article!

Next month: Charting in reports!

In the next installment of this series, I'll show you how to move the chart into a VFP 9.0 report, and will also touch on some other interesting techniques.

Download 408NICHOLLS.ZIP

To find out more about FoxTalk and Pinnacle Publishing, visit their Web site at http://www.pinpub.com/

Note: This is not a Microsoft Corporation Web site. Microsoft is not responsible for its content.

This article is reproduced from the August 2004 issue of FoxTalk. Copyright 2004, by Pinnacle Publishing, Inc., unless otherwise noted. All rights are reserved. FoxTalk is an independently produced publication of Pinnacle Publishing, Inc. No part of this article may be used or reproduced in any fashion (except in brief quotations used in critical articles and reviews) without prior consent of Pinnacle Publishing, Inc. To contact Pinnacle Publishing, Inc., please call 1-800-788-1900.