Chapter 7
Extending the Reporting System at Run Time

In addition to the design-time extensibility of VFP 9’s reporting system discussed in Chapter 6, “Extending the Reporting System at Design Time,” VFP 9 also provides the ability to extend the behavior of the reporting system when reports are run. In this chapter, you will learn about VFP 9’s report listener concept, how it receives events as a report is run, and how you can create your own listeners to provide different types of output besides the traditional print and preview.

As discussed in the “Enhanced run-time capabilities” section of Chapter 5, “Enhancements in the Reporting System,” the new reporting engine in VFP 9 splits responsibility for reporting between the report engine, which now just deals with data-handling and object positioning, and a new object known as a report listener, which handles rendering and output. Report listeners are based on a new base class in VFP 9, ReportListener.

During the run of a report, VFP raises events in a report listener as they happen. For example, the LoadReport event of a report listener fires when the report is loaded before being run. When an object is drawn on the report page, the Render method fires. The ReportListener base class has some native behavior, but extensibility really kicks in when you create and use your own subclasses. For example, a subclass of ReportListener could dynamically format a field, so under some conditions it prints with red text and under other conditions it prints in black.

This chapter starts with a discussion of how report listeners work, and then moves on to examining the properties, events, and methods (PEMs) of the ReportListener base class. After that, we discuss some of the subclasses of ReportListener that come with VFP. The rest of the chapter focuses on some cool uses of report listeners to create special effects you can’t do in earlier versions of VFP, including drawing charts without using ActiveX controls and creating your own report previewer.

Report listener basics

Report listeners produce output in two ways. “Page-at-a-time” mode renders a page and outputs it, renders the next page and outputs it, and so forth until the report is done. This mode is typically used when printing a report. In “all-pages-at-once” mode, the report listener renders all the pages and caches them in memory. It then outputs these rendered pages on demand, such as when the user clicks on the next page button in the preview window. This mode is typically used when previewing a report.

Report listeners can be used in a couple of ways. One is by specifying the OBJECT clause of the REPORT command. OBJECT supports two ways of using it: by specifying a report listener and by specifying a report type.

To tell VFP to use a specific listener for a report, instantiate the listener class, and
then specify the object’s name in the OBJECT clause of the REPORT command. Here’s
an example:

loListener = createobject('MyReportListener')

report form MyReport object loListener

If you’d rather not instantiate a listener manually, you can have VFP do it for you automatically by specifying a report type:

report form MyReport object type 1

The defined types are 0 for outputting to a printer, 1 for previewing, 2 for “page-at-a-time” mode without sending the output to a printer, 3 for “all-pages-at-once” mode without invoking the preview window, 4 for XML output, and 5 for HTML output. Other user-defined types can also be used; this is discussed in the “Registering listeners” section of this chapter.

When you run a report this way, the application specified in the new _REPORTOUTPUT system variable (ReportOutput.APP in the VFP home directory by default) is called to figure out which listener class to instantiate for the specified type. ReportOutput.APP looks for the specified listener type in a listener registry table. If it finds the desired class, it instantiates the class and gives the reporting engine a reference to the listener object. ReportOutput.APP is primarily an object factory, but it also includes some listeners that provide XML and HTML output and other utility functions.

Another way to use report listeners is with the new SET REPORTBEHAVIOR 90 command. This command turns on “object-assisted” reporting so the REPORT command behaves as if you specified OBJECT TYPE 0 when you use the TO PRINT clause or OBJECT TYPE 1 when you use the PREVIEW clause.

ReportListener

The next sections in this chapter examine the PEMs of ReportListener to understand its capabilities. One thing to note about ReportListener is that the unit of measure (for example, the values returned by the GetPageWidth method, and the size parameters passed to the Render method) is 960th of an inch.

Properties

Table 1 shows the properties of ReportListener.


Table 1. The properties of the ReportListener base class.

Property

Type

Description

AllowModalMessages

L

If .T, allows modal messages showing the progress of the report (the default is .F.).

CommandClauses

O

An object based on the Empty base class with properties indicating the clauses used in the REPORT or LABEL command. See Table 2 for the properties of this object.

CurrentDataSession

N

The data session ID for the report’s data.

CurrentPass

N

Indicates the current pass through the report. A report with _PageTotal or TwoPassProcess set to .T. requires two passes; others only require one. 0 indicates the first pass of a two-pass report or that only one pass is required, while 1 indicates the second pass.

DynamicLineHeight

L

.T. (the default) to use GDI+ line spacing, which varies according to font characteristics, or .F. to use old-style fixed line spacing.

FRXDataSession

N

The data session ID for the FRX cursor (a read-only copy of the report file the reporting engine is running opened for a ReportListener’s use).

GDIPlusGraphics

N

The handle for the GDI+ graphics object used for rendering. Read-only.

ListenerType

N

The type of report output the listener produces. The default is -1, which specifies no output, so you’ll need to change this to a more reasonable value. See the discussion of the OutputPage method for a list of values.

OutputPageCount

N

The number of pages rendered. Read-only.

OutputType

N

The output type as specified in the OBJECT TYPE clause of the REPORT or LABEL command.

PageNo

N

The current page number being rendered. Read-only.

PageTotal

N

The total number of pages in the report. Read-only.

PreviewContainer

O

A reference to the display surface the report will be output to for previewing.

PrintJobName

C

The name of the print job as it appears in the Windows Print Queue dialog.

QuietMode

L

.T. (the default is .F.) to suppress progress information.

SendGDIPlusImage

N

1 or higher (the default is 0) to send a handle to an image for a General field to the Render method. This is numeric rather than logical to allow the possibility for subclasses to treat images differently if desired.

TwoPassProcess

L

Indicates whether two passes will be used for the report. Set this to .T. to force a prepass even if _PageTotal isn’t used somewhere in the report.

 

The CommandClauses property contains a reference to an Empty object with properties representing the various clauses of the REPORT or LABEL command, plus a few other goodies. Table 2 lists these properties.

Table 2. The properties of the CommandClauses object.

Property

Type

Description

ASCII

L

.T. if the ASCII keyword was specified when outputting to a file.

DE_Name

C

The name of the DataEnvironment object for the report. The name specified with the NAME clause or the name of the report if not specified.

Environment

L

.T. if the ENVIRONMENT keyword was specified.

File

C

The name of the report to run.

Heading

C

The heading specified with the HEADING keyword.

InScreen

L

.T. if the IN SCREEN keyword was specified.

InWindow

C

The name of the window specified with the IN WINDOW keyword.

IsDesignerLoaded

L

.T. if the report is run from within the Report Designer.

IsDesignerProtected

L

.T. if the PROTECTED keyword was specified.

IsReport

L

.T. if this is a report or .F. if it’s a label.

NoConsole

L

.T. if the NOCONSOLE keyword was specified.

NoDialog

L

.T. if the NODIALOG keyword was specified.

NoEject

L

.T. if the NOEJECT keyword was specified.

NoPageEject

L

.T. if the NOPAGEEJECT keyword was specified.

NoReset

L

.T. if the NORESET keyword was specified.

NoWait

L

.T. if the NOWAIT keyword was specified with the PREVIEW keyword.

Off

L

.T. if the OFF keyword was specified.

OutputTo

N

The type of output specified in the TO clause: 0 = no TO clause was specified, 1 = printer, 2 = file

PDSetup

L

.T. if the PDSETUP keyword was specified.

Plain

L

.T. if the PLAIN keyword was specified.

Preview

L

.T. if the PREVIEW keyword was specified.

PrintPageCurrent

N

Defaults to 0. However, a preview window can set this to the page currently displayed when it calls the listener’s OnPreviewClose method. The listener could use this to enable the “Print current page” option in a Print dialog.

PrintRangeFrom

N

Defaults to 1. However, a listener could set it to the starting page number to print from when printing after preview.

PrintRangeTo

N

Defaults to -1. However, a listener could set it to the ending page number to print to when printing after preview.

Prompt

L

.T. if the PROMPT keyword was specified.

RangeFrom

N

The starting page specified in the RANGE clause; 1 if not specified.

RangeTo

N

The ending page specified in the RANGE clause; -1 if not specified.

RecordTotal

N

The total number of records being reported on in the main cursor.

Sample

L

.T. if the SAMPLE keyword was specified with the LABEL command.

StartDataSession

N

The data session that the REPORT or LABEL command was issued from.

Summary

L

.T. if the SUMMARY keyword was specified with the REPORT command.

ToFile

C

The name of the file specified with the TO FILE clause.

ToFileAdditive

L

.T. if the ADDITIVE keyword was specified when outputting to a file.

Window

C

The name of the window specified with the WINDOW keyword.

 

A special comment about data session handling is in order. Four data sessions are actually involved during a report run. The first is the data session the ReportListener is instantiated in; SET(‘DATASESSION’) will give you the appropriate value when issued in a ReportListener method. The second is the data session the REPORT or LABEL command was issued from; check the StartDataSession property of the CommandClauses object to determine the data session ID. The third is the data session the FRX cursor is open in. The FRXDataSession property contains the data session ID for this cursor, so use SET DATASESSION TO This.FRXDataSession if you need access to the FRX. The fourth is the data session the report’s data is in. If the report has a private data session, this will be a unique data session; otherwise, it’ll be the data session the REPORT or LABEL command was issued from. The CurrentDataSession property tells you which data session to use, so if a ReportListener needs to access the report’s data, you need to SET DATASESSION TO This.CurrentDataSession. Remember to save the ReportListener’s data session and switch back to it after selecting either the report data or FRX data session.

Examine the code in TestDataSessions.PRG and run it to see how these different data sessions work.

Text Box: "

The Developer Download files for this chapter, available at www.hentzenwerke.com, include TestDataSessions.PRG and two reports it uses: PrivateDS.FRX and DefaultDS.FRX.

Report events

Report events, which fire when something affects the report as a whole, are shown in
Table 3.

Table 3. Report events of the ReportListener base class.

Event

Parameters

Description

LoadReport

None

Analogous to the Load event of a form in that it’s the first event fired and returning .F. prevents the report from running. Because this event fires before the FRX loads and the printer spool opens, this is the one place where you can change the contents of the FRX on disk or change the printer environment before the report runs.

UnloadReport

None

Like the Unload event of a form, UnloadReport fires after the report runs. This is typically used for clean up tasks.

BeforeReport

None

Fires after the FRX loads, but before the report is run.

AfterReport

None

Fires after the report runs.

Band events

Band events fire as a band is processed. These events are shown in Table 4.

Table 4. Band events of the ReportListener base class.

Event

Parameters

Description

BeforeBand

nBandObjCode,
nFRXRecno

Fires before a band is processed. The first parameter represents the value of the OBJCODE field in the FRX for the specified band, and the second is the record number in the FRX cursor for the band’s record.

AfterBand

nBandObjCode,
nFRXRecno

Fires after a band is processed. Same parameters as BeforeBand.

Object events

These events fire as a report object is being processed.

EvaluateContents(nFRXRecno, oObjProperties): this event fires for each field (but not label) object just before it’s rendered, and gives the listener the opportunity to change the appearance of the field. The first parameter is the FRX record number for the field object being processed and the second is an object containing properties with information about the field object. The properties this object contains are shown in Table 5. You can change any of these properties to change the appearance of the field in the report. If you do so, set the Reload property of the object to .T. to notify the report engine that you changed one or more of the other properties. Also, return .T. if other listeners can make more changes to the field.

Table 5. Properties of the object parameter passed to EvaluateContents.

Property

Type

Description

FillAlpha

N

The alpha, or transparency, of the fill color. Allows finer control than simply transparent or opaque. The values range from 0 for transparent to 255 for opaque.

FillBlue

N

The blue portion of an RGB() value for the fill color.

FillGreen

N

The green portion of an RGB() value for the fill color.

FillRed

N

The red portion of an RGB() value for the fill color.

FontName

C

The font name.

FontSize

N

The font size.

FontStyle

N

A value representing the font style. Additive values of 1 (bold), 2 (italics), 4 (underlined), and 128 (strikethrough).

PenAlpha

N

The alpha of the pen color.

PenBlue

N

The blue portion of an RGB() value for the pen color.

PenGreen

N

The green portion of an RGB() value for the pen color.

PenRed

N

The red portion of an RGB() value for the pen color.

Reload

L

Set this to .T. to notify the report engine that you changed one or more of the other properties.

Text

C

The text to be output for the field object.

Value

-

The actual value of the field to output.

 

AdjustObjectSize(nFRXRecno, oObjProperties): this event fires for each shape or image object just before it’s rendered. It gives you the ability to change the object, and is generally used when you want to replace the shape or image with a custom rendered object and need to size the object dynamically. The first parameter is the FRX record number for the object being processed and the second is an object containing properties with information about the shape or image. The properties this object contains are shown in Table 6. If you change Height or Width, set the Reload property of the object to .T. to notify the report engine that you changed these properties. Changing the height of an object that spans pages isn’t supported; if you change the height of an object so it won’t fit on the rest of the current page, the entire object is moved to the next page. The MaxHeightAvailable and Reattempt properties help you determine how much room is left on the current page and whether the object is pushed to the next page.


Table 6. Properties of the object parameter passed to AdjustObjectSize.

Property

Type

Description

Height

N

The height of the object in 960ths of an inch, from 0 to 64000. Increasing this value (decreasing it is ignored) causes other floating objects in the band to be pushed down and the band to stretch.

Left

N

The left position of the object. Read-only.

Top

N

The top position of the object. Read-only.

Width

N

The width of the object in 960ths of an inch, from 0 to 64000.

MaxHeightAvailable

N

The maximum amount of room available on the page for the object. Read-only.

Reattempt

L

.T. if the object has been pushed to the next page because it won’t fit on the current page. Read-only.

Reload

L

Set this to .T. to notify the report engine that you changed one or more of the other properties.

 

Render(nFRXRecno, nLeft, nTop, nWidth, nHeight, nObjectContinuationType, cContentsToBeRendered, GDIPlusImage): this event is the big one. The report engine calls it at least once for each object being rendered (more than once for objects that span bands or pages). As with the other object events, the first parameter is the FRX record number for the object being rendered. The next four parameters represent the position and size of the object. nObjectContinuationType indicates whether a field, shape, or line object spans a band or page; it contains one of four possible values:

·         0: This object is complete; it doesn’t continue onto the next band or page.

·         1: The object has been started, but will not finish on the current page.

·         2: The object is in the middle of rendering; it neither started nor finished on the current page.

·         3: The object has been finished on the current page.

cContentsToBeRendered contains the text of a field or the filename of a picture if appropriate. For fields, the contents are provided in Unicode, appropriately translated to the correct locale using the FontCharSet information associated with the FRX record. Use STRCONV() to convert the string if you want to do something with it, such as storing it in a table. GDIPlusImage is used if a picture comes from a General field and the SendGDIPlusImage property is greater than 0; it contains the graphics handle for the image.

You can supply code in this method if you want to render an object differently than it would otherwise be done. Note, however, that pretty much anything you need to do will require calling GDI+ API functions, so this isn’t for the faint of heart. See the “_GDIPlus.VCX” topic later in this chapter.


Methods

The methods of ReportListener are shown in Table 7.

Table 7. The methods of the ReportListener base class.

Event

Parameters

Description

CancelReport

None

Allows VFP code to terminate a report early. Required so the ReportListener can do necessary cleanup such as closing the print spooler.

OnPreviewClose

lPrint

This method should be called from a preview window when the user closes the preview window or prints a report from preview.

OutputPage

nPageNo,
eDevice,
nDeviceType
[ , nLeft,
nTop,
nWidth,
nHeight
[ , nClipLeft,
nClipTop,
nClipWidth,
nClipHeight ] ]

Outputs the specified rendered page to the specified device. The optional nLeft through nClipHeight parameters allow the listener to specify exactly what area on the target device to use for rendering when the device type is a container. This is discussed in more detail in the text.

IncludePageInOutput

nPageNo

Returns .T. if the specified page is included in the output or not.

SupportsListenerType

nType

Returns .T. if the listener supports the specified type of output.

GetPageHeight

None

Returns the page height during a report run.

GetPageWidth

None

Returns the page width during a report run.

DoStatus

cMessage

Provides modeless feedback during a report run.

UpdateStatus

None

Updates the feedback UI.

ClearStatus

None

Removes the modeless feedback UI.

DoMessage

cMessage
[ , nParams
[ , cTitle ] ]

Provides modal feedback during a report run if AllowModalMessages is .T; otherwise, calls DoStatus. nParams and cTitle are optional parameters; if passed, they are used as the second and third parameters in a call to MESSAGEBOX().

 

The OutputPage method warrants more discussion. The nDeviceType parameter determines the type of output this method should perform; it also determines the type of parameter expected for eDevice. Table 8 lists the types of output supported in the base class ReportListener and the values for nDeviceType and eDevice. Subclasses could support other types of output, such as PDF or other custom formats.


Table 8. The types of output supported by OutputPage.

nDeviceType

Description

eDevice

-1

No device

0

0

Printer

Printer handle

1

Graphics device

GDI+ graphic handle

2

VFP preview window

Reference to VFP control to output to

100

EMF file

File name

101

TIFF file

File name

102

JPEG file

File name

103

GIF file

File name

104

PNG file

File name

105

BMP file

File name

201

Multi-page TIFF

File name (the file must already exist)

 

The ListenerType property affects the value of OutputPage. Table 9 shows the different values for ListenerType and the effect each has on output.

Table 9. How the different values of ListenerType affect OutputPage.

ListenerType

Output Type

How OutputPage is Affected

0

“Page-at-a-time” mode, sent to printer

The report engine calls OutputPage after each page is rendered in order to output to a printer. The report engine passes 0 (printer) to this method as nDeviceType and the GDI+ handle for the printer as eDevice.

1

“All-page-at-once” mode, previewer automatically invoked

After all rendering is complete, the report engine invokes a preview window, either by calling (_ReportPreview) to create one or using the one in Listener.PreviewContainer. The preview window calls OutputPage to display the specified page. In this case, nDeviceType is 2 and eDevice is a reference to a VFP control used as a placeholder for the output.

2

“Page-at-a-time” mode, not sent to printer

The report engine calls OutputPage after each page is rendered but no output is sent to the printer. The report engine passes -1 as nDeviceType and 0 as eDevice.

3

“All-page-at-once” mode, no automatic preview window

OutputPage must be called manually to output the specified page after all rendering is complete.

 

By the way, because report listeners use VFP code, it’s now possible to trace code during report execution, something that wasn’t possible before and was the source of a lot of frustration for those using user-defined functions (UDFs) in their reports.

Registering listeners

Now that you know what a ReportListener looks like, you can create different subclasses that have the behavior you need. Before you do that, though, let’s look at how to tell ReportOutput.APP about them.

Like ReportBuilder.APP (see Chapter 6, “Extending the Reporting System at Design Time,” for details on ReportBuilder.APP), ReportOutput.APP uses a registry table to keep track of the listeners it knows about. Although this table is built into ReportOutput.APP, you can create a copy of it called OutputConfig.DBF using DO (_ReportOutput) WITH -100. If ReportOutput.APP finds a table with this name in the current directory or VFP path, it uses that table as the source of listeners it looks at when running a report. Table 10 shows the structure of this table.

Table 10. The structure of the listener registry table used by ReportOutput.APP.

Field Name

Type

Values

Description

OBJTYPE

I

100 for a listener record

Other record types are used as well; see the VFP documentation for details.

OBJCODE

I

Any valid listener type

The listener type (e.g. 1 for preview).

OBJNAME

V(60)

 

The class to instantiate.

OBJVALUE

V(60)

 

The class library the class specified in OBJNAME is found in.

OBJINFO

M

 

The application containing the class library.

 

You aren’t restricted to using the built-in range of listener types (0 through 5); you can assign your own value to the OBJCODE column in a record you add to the registry table, and then specify that value in the OBJECT TYPE clause of a REPORT or LABEL command.

Note that ReportOutput.APP only looks for the first record with OBJTYPE = 100 and OBJCODE set to the desired listener type. So, you need to remove or unregister (set OBJCODE to another value such as by adding 100 to it) other listener records of the same type. Also, note that the registry table contains quite a few records with OBJTYPE set to something other than 100. The listeners built into ReportOutput.APP, especially XMLListener, use these for their own purposes.

You don’t need to register a listener to use it; you can simply instantiate it manually and pass a reference to it to the OBJECT clause of the REPORT command. This mechanism is a little more work, but it gives you better control, doesn’t require an external copy of ReportOutput.APP’s registry table, and allows you to do things such as chain report listeners together. This is the mechanism we recommend and use in the rest of this chapter.

Utilities in the FFC

The FFC (FoxPro Foundation Classes) subdirectory of the VFP home directory includes a few class libraries that assist with reporting issues.

_ReportListener

_ReportListener.VCX contains some subclasses of ReportListener that have more functionality than the base class. The most useful of these is _ReportListener. (_ReportListener.VCX is also contained within ReportOutput.APP.)

One of the most important features of _ReportListener is support for successors. It’s possible you will want more than one report listener used when running a report. For example, if you want to both preview a report and output it to HTML at the same time,
more than one report listener must be involved. As you will see in upcoming sections in
this chapter, listeners can be used for tasks such as dynamically formatting or rotating text, and it’s a better idea to create small listeners that do one thing rather than a monolithic listener that does everything. A report that needs more than one of these behaviors requires multiple listeners.

_ReportListener allows chaining of listeners by providing a Successor property that may contain an object reference to another listener. To support this mechanism, most events call the same method in the successor object if it exists, using code similar to:

if vartype(This.Successor) = 'O'

  This.Successor.ThisMethodName()

endif vartype(This.Successor) = 'O'

For example, suppose ListenerA and ListenerB each perform some task and are both subclasses of _ReportListener, and you want to use both listeners for a certain report. Here’s how to chain these listeners together:

loListener = createobject('ListenerA')

loListener.Successor = createobject('ListenerB')

report form MyReport object loListener

The report engine only communicates with the listener specified in the REPORT or LABEL command; this one is the “lead” listener. However, as the report engine raises report events, the lead listener calls the appropriate methods of its successor, and the successor calls the appropriate methods of its successor, and so on down the chain. This type of architecture is known as a “chain of responsibility,” because any listener in the chain can decide to take some action or pass the message on to the next item in the chain.

Because the report engine automatically sets properties of the lead listener, such as FRXDataSession and CurrentDataSession, _ReportListener sets these properties of successor listeners as necessary. The SetSuccessorDynamicProperties method, which is called from many other methods, is responsible for setting the properties that change frequently: OutputPageCount, PageNo, and PageTotal. Other properties are set as required; for example, BeforeReport sets the FRXDataSession, CurrentDataSession, CurrentPass, TwoPassProcess, and CommandClauses properties of the successor to this class’ values.

Another interesting capability of _ReportListener is chaining reports. The AddReport method adds a report to the custom ReportFileNames collection. Pass this method the name of a report and optionally the report clauses to use (such as the RANGE clause) and a reference to another listener object. The RemoveReports method removes all reports from the collection. RunReports runs the reports; pass it .T. for the first parameter to remove reports from the collection after they run and .T. for the second parameter to ignore any listeners specified in AddReport. The following code, taken from TestChainedReports.PRG, runs the TestDynamicFormatting and TestRotate reports as if they were a single report.

use _samples + 'Northwind\orders'

loListener = newobject('_ReportListener', home() + 'ffc\_ReportListener.vcx')

loListener.OutputType = 1

loListener.AddReport('TestDynamicFormatting.frx', 'next 20 nopageeject')

loListener.AddReport('TestRotate.frx', 'next 20')

loListener.RunReports()

Text Box: "

The Developer Download files for this chapter, available at www.hentzenwerke.com, include TestChainedReports.PRG, TestDynamicFormatting.FRX, and TestRotate.FRX.

A number of utility methods exist that make it easier to work with listeners. For example, SetFRXDataSession switches to the data session for the FRX cursor, SetCurrentDataSession switches to the data session for the report’s data, and ResetDataSession restore the data session to the one the report listener runs in.

_ReportListener has several custom properties. DrivingAlias contains the name of the main cursor for the report. ReportUsesPrivateDataSession is .T. if, as its name implies, the report uses a private data session. IsRunning is .T. if a report is running, and IsRunningReports is .T. if a collection of reports is being run. IsSuccessor is .T. if this isn’t the lead listener. SharedGDIPlusGraphics, SharedPageHeight, and SharedPageWidth contain the value of the GDIPlusGraphics property and the return values of the GetPageHeight and GetPageWidth methods so they can be used in a successor.

UpdateListener

In addition to _ReportListener, _ReportListener.VCX contains UpdateListener, a subclass of _ReportListener that displays feedback information about the report run. It has several properties you can set to customize the appearance of the feedback. InitStatusText contains the message to display before the report is run. PrepassStatusText contains the message to display while the report “prepass” is performed to calculate the value of _PAGETOTAL. RunStatusText contains the message to display while the report is running. ThermFormCaption contains the caption for the feedback form. ThermCaption contains an expression that’s evaluated to display the text inside a thermometer. Because it’s based on _ReportListener, UpdateListener can be chained together with other listeners.

Here’s an example, taken from TestUpdateListener.PRG, that demonstrates how to use this listener:

use _samples + 'Northwind\orders'

loListener = newobject('UpdateListener', home() + 'ffc\_ReportListener.vcx')

with loListener

  .InitStatusText   = 'Preparing report...'

  .RunStatusText    = 'Running...'

  .ThermFormCaption = 'Report Progress'

endwith

loListener.ListenerType = 1

report form TestDynamicFormatting.FRX preview object loListener

lnRun = loListener.ReportStopRunDateTime - loListener.ReportStartRunDateTime

wait window 'The report took ' + transform(lnRun) + ' seconds to run'


Text Box: "

The Developer Download files for this chapter, available at www.hentzenwerke.com, include TestUpdateListener.PRG.

_GDIPlus.VCX

As noted when discussing the Render method (the “Object events” section earlier in this chapter), a listener that performs custom rendering will almost certainly have to use GDI+ functions to do so. GDI+ is a set of hundreds of Windows API functions that perform various graphical manipulations and output. For information about GDI+, see http://msdn.microsoft.com/library/en-us/gdicpp/GDIPlus/GDIPlusReference/FlatGraphics.asp.

To make it easier to work with GDI+ functions, Microsoft thoughtfully included _GDIPlus.VCX, written by Walter Nicholls, in the FFC directory. _GDIPlus consists of wrapper classes for GDI+ functions, making them both easier to use and object oriented. The “GDI Plus API Wrapper Foundation Classes” topic in the VFP Help lists these classes and provides a little background about them. Interestingly, it recommends you read the documentation for similar .NET framework classes, since the _GDIPlus classes were somewhat modeled on their .NET equivalents.

The most frequently used class is GPGraphics. It provides methods for drawing on a GDI+ canvas as well as other utility functions. It requires a GDI+ handle to work with, so pass its SetHandle method the value of the GDIPlusGraphics property (or SharedGDIPlusGraphics if you’re using a subclass of _ReportListener) before calling other methods. You can then call methods such as DrawArc, DrawCurve, DrawLine, and DrawPie to draw shapes on the report page or RotateTransform, ScaleTransform, and TranslateTransform to change the way drawing occurs.

Some of these methods require a reference to another type of GDI+ object, such as a pen, brush, font, or color object. Other classes in _GDIPlus, such as GPPen, GPSolidBrush, GPHatchBrush, GPFont, and GPColor represent these objects. Using these classes is fairly easy: instantiate the desired one, call the Create method to initialize it with the desired attributes such as color, and then pass it to a GPGraphics method.

A couple of examples in this chapter use _GDIPlus classes to perform custom rendering tasks. See the discussion of the SFRotateDirective class in the “Directive handlers” section for an example that rotates text and the “Custom rendering” section for an example that renders column charts.

Creating your own listeners

While the built-in ReportListener class (and even the listeners provided in the FFC) has a lot of functionality, it’s almost certain you will eventually want to do more than what comes in the box. Fortunately, you can create your own listeners by subclassing ReportListener or _ReportListener and adding the functionality you need. The rest of this chapter explores some of the possibilities with report listeners.

SFReportListener

While working with _ReportListener, we discovered we needed some additional behavior in our listeners. SFReportListener, defined in SFReportListener.VCX, is a subclass of _ReportListener that handles some things _ReportListener doesn’t. It also provides a few utility methods that most listeners require.

One complication with successors that _ReportListener doesn’t handle (there are several, actually) is if a successor calls its CancelReport method to cancel the report, the report doesn’t actually cancel because only the CancelReport method of the lead listener cancels the report. So, the Assign method of the Successor property uses BINDEVENT() to ensure that when the successor’s CancelReport method is called, so is the current listener’s. This causes calls to CancelReport to ripple back up the chain to the lead listener.

lparameters toSuccessor

dodefault(toSuccessor)

if vartype(toSuccessor) = 'O'

  bindevent(toSuccessor, 'CancelReport', This, 'CancelReport', 1)

endif vartype(toSuccessor) = 'O'

There is a new problem now: the behavior of _ReportListener.CancelReport is to call down the chain so all successor listeners have a chance to do something when a report is canceled. When the current listener calls its successor’s CancelReport method, its own CancelReport fires again because of event binding. This would result in an endless loop unless something is done about it. SFReportListener.CancelReport handles this by overriding _ReportListener.CancelReport to not call down the successor chain if it was called from a successor via event binding.

local laEvents[1], ;

  lnEvents

if not This.IsSuccessor

  ReportListener::CancelReport()

  nodefault

endif not This.IsSuccessor

if not isnull(This.Successor)

  lnEvents = aevents(laEvents, 0)

  if lnEvents = 0 or ;

    not upper(laEvents[1, 1].Name) == upper(This.Successor.Name)

    This.SetSuccessorDynamicProperties()

    This.Successor.CancelReport()

  endif lnEvents = 0 ...

endif not isnull(This.Successor)

Rendering is another complication. Rendering requires a GDI+ handle, similar to the way SQL passthrough commands work with a SQL connection handle or low-level file functions work with a file handle. This handle is contained in the GDIPlusGraphics property, which the reporting engine sets to the appropriate value. However, since the reporting engine doesn’t know anything about successors, it only sets the property of the lead listener. The first issue you run into, then, is you can’t set GDIPlusGraphics of a successor to the proper value because this property is read-only. _ReportListener handles this by setting a custom SharedGDIPlusGraphics property of successors to the proper value. So, a listener subclass can pass the value of SharedGDIPlusGraphics to any GDI+ functions it needs to call. However, a second issue is you can’t expect to use DODEFAULT() in the Render method of a successor to get the usual rendering; because the base behavior is to use GDIPlusGraphics as the GDI+ handle and that property contains the wrong value (it defaults to 0) for all but the lead listener, rendering doesn’t work. The only object that can successfully use base class rendering behavior is the lead listener.

So, you now have a problem: you want a listener to change the way something is rendered in a report, so you use GDI+ functions to change the GDI+ state to make the appropriate changes (such as rotating some text), but you can’t use DODEFAULT() to perform the actual rendering because that doesn’t work anywhere but in the lead listener.

Fortunately, there is a workaround: SFReportListener.Render calls custom BeforeRender and AfterRender methods, which in a subclass can do any GDI+ state change before the usual rendering takes place and do any necessary cleanup afterward. Note that this code uses ReportListener::Render rather than DODEFAULT() to get the base behavior because you want to skip the behavior in _ReportListener.Render.

lparameters tnFRXRecno, ;

  tnLeft, ;

  tnTop, ;

  tnWidth, ;

  tnHeight, ;

  tnObjectContinuationType, ;

  tcContentsToBeRendered, ;

  tiGDIPlusImage

with This

  if .BeforeRender(tnFRXRecno, tnLeft, tnTop, tnWidth, tnHeight, ;

    tnObjectContinuationType, tcContentsToBeRendered, tiGDIPlusImage)

    ReportListener::Render(tnFRXRecno, tnLeft, tnTop, tnWidth, tnHeight, ;

      tnObjectContinuationType, tcContentsToBeRendered, tiGDIPlusImage)

    .AfterRender()

  endif .BeforeRender(tnFRXRecno ...

  nodefault

endwith

BeforeRender and AfterRender support successor listeners. Here’s the code for BeforeRender:

lparameters tnFRXRecno, ;

  tnLeft, ;

  tnTop, ;

  tnWidth, ;

  tnHeight, ;

  tnObjectContinuationType, ;

  tcContentsToBeRendered, ;

  tiGDIPlusImage

with This

  if vartype(.Successor) = 'O' and pemstatus(.Successor, 'BeforeRender', 5)

    .Successor.BeforeRender(tnFRXRecno, tnLeft, tnTop, tnWidth, ;

      tnHeight, tnObjectContinuationType, tcContentsToBeRendered, ;

      tiGDIPlusImage)

  endif vartype(.Successor) = 'O' ...

endwith

To make it clear how this mechanism works, suppose the lead listener for a report is an SFReportListener object and a successor is a subclass called SFRotateDirective that does text rotation. Here’s what happens when rendering an object. The report engine calls the Render method of the lead listener that in turn calls the BeforeRender method. That method calls down the successor chain, so BeforeRender of each successor has a chance to do whatever is necessary. SFRotateDirective.BeforeRender calls some GDI+ functions to rotate the object about to be rendered. Once the BeforeRender chain finishes, SFReportListener.Render performs its base behavior and causes the object to be rendered in a rotated manner because of what SFRotateDirective did. Render then calls AfterRender that calls down the successor chain so AfterRender of each successor has a chance to do its job. SFRotateDirective.AfterRender resets the GDI+ state back to normal so subsequent objects aren’t rotated.

One thing a subclassed listener might do is some custom GDI+ rendering. Because GPGraphics in _GDIPlus.VCX does a lot of the hard work for us, the Init method of SFReportListener instantiates a GPGraphics object into its custom oGDIGraphics property. GPGraphics needs a GDI+ handle to do its work, so the BeforeBand method calls GPGraphic’s SetHandle method to set the handle to the value of the SharedGDIPlusGraphics property when the band being processed is the page header or title bands. However, there’s another issue: the GDI+ handle changes on every page. So, BeforeBand makes sure that SharedGDIPlusGraphics is updated properly first.

lparameters tnBandObjCode, ;

  tnFRXRecNo

with This

  if inlist(tnBandObjCode, FRX_OBJCOD_PAGEHEADER, FRX_OBJCOD_TITLE)

    if not .IsSuccessor

      .SharedGDIPlusGraphics = .GDIPlusGraphics

    endif not .IsSuccessor

    .oGDIGraphics.SetHandle(.SharedGDIPlusGraphics)

  endif inlist(tnBandObjCode ...

  dodefault(tnBandObjCode, tnFRXRecNo)

endwith

A few other _ReportListener methods have successor issues as well. The DoStatus, EvaluateContents, and AdjustObjectSize methods of _ReportListener don’t handle successors at all, so those methods in SFReportListener do.

GetReportObject returns an object for the specified record in the FRX. This makes it easier to examine information about any FRX object.

lparameters tnFRXRecno

local lnRecno, ;

  loObject

This.SetFRXDataSession()

lnRecno = recno()

go tnFRXRecno

scatter memo name loObject

go lnRecno

This.ResetDataSession()

return loObject

Because events like Render and EvaluateContents fire once for every record in the FRX and for every object that gets rendered (meaning they fire close to the number of objects in the FRX times the number of records in the data set being reported on), you should minimize the amount of work done in these methods. For example, if you store a directive in the USER memo that tells a listener how to process a report object, any code that parses this memo will be called many times, even though it’s really only needed once. (You can access the USER memo for an object in a report from the Other page of the properties dialog for that object in the Report Designer.) So, SFReportListener has a custom array property called aRecords that can contain any information you need about the records in the FRX.

To support this concept, the BeforeReport event dimensions aRecords to the number of records in the FRX and calls the ProcessFRXRecord method (abstract in this class) for each record in the FRX. In a subclass, you can override ProcessFRXRecord to update aRecords with any information you deem necessary.

with This

 

* Switch to the FRX datasession, dimension aRecords to as many records as

* there are in the FRX, then go through each record in the FRX in case we need

* to gather information about it. Switch back to our datasession.

 

  .SetFRXDataSession()

  if alen(.aRecords, 2) > 0

    dimension .aRecords[reccount(), alen(.aRecords, 2)]

  else

    dimension .aRecords[reccount()]

  endif alen(.aRecords, 2) > 0

  scan

    .ProcessFRXRecord()

  endscan

  .ResetDataSession()

 

* Do the usual behavior.

 

  dodefault()

endwith

SFReportListener also has code in the OutputPage method. Because this method is called with a particular page number, and a successor won’t necessarily know what page that is, the code in this method stores the passed page number to a custom nOutputPageNo property that other listeners can use because their property is updated by SetSuccessorDynamicProperties.


Text Box: "

The Developer Download files for this chapter, available at www.hentzenwerke.com, include SFReportListener.VCX. Because SFReportListener is a subclass of _ReportListener, and it’s likely the path to _ReportListener on your system is different than the one built into the class, be sure to open SFReportListener in the Class Designer, locate _ReportListener.VCX in the FFC directory when prompted, and save the class to update the path to the parent class on your system. Also, note that one of the include files, SFReporting.H, includes \Program Files\Microsoft Visual FoxPro 9\FFC\FoxPro_Reporting.h. If you install these samples on a different drive than where VFP is installed, or if VFP is installed in a different directory than \Program Files\Microsoft Visual FoxPro 9, be sure to change the #INCLUDE statement in SFReporting.H to point to the correct path, and then rebuild Samples.PJX with the “recompile all files” option checked.

Report directives

SFReportListenerDirective is a subclass of SFReportListener. Its purpose is to support directives in the USER memo that tell the listener how to process a report object. An example of a directive is *:LISTENER ROTATE = -45, which tells the listener to rotate this object 45 degrees counter-clockwise. Because USER might be used for a variety of purposes, directives supported by SFReportListenerDirective must start with *:LISTENER (those of you who used GENSCRNX in the FoxPro 2.x days will recognize this type of directive).

Different directives handle different objects. They don’t necessarily have to be subclasses of ReportListener (some of the examples you will see later are based on Custom) if they simply change properties of the object being rendered. Because you may use multiple directives for the same object, SFReportListenerDirective maintains a collection of directive handlers and calls the appropriate one as necessary.

The Init method creates the collection of directive handlers and fills it with several commonly used handlers. You can add additional handlers in a subclass or after the class is instantiated by adding to the collection (note that the keyword used for the collection must be upper-case). (In this code and other code in this class, ccDIRECTIVE_* are constants defined in SFReportListener.H.)

with This

  .oDirectiveHandlers = createobject('Collection')

  loHandler = newobject('SFDynamicForeColorDirective', 'SFReportListener.vcx')

  .oDirectiveHandlers.Add(loHandler, ccDIRECTIVE_FORECOLOR)

  loHandler = newobject('SFDynamicBackColorDirective', 'SFReportListener.vcx')

  .oDirectiveHandlers.Add(loHandler, ccDIRECTIVE_BACKCOLOR)

  loHandler = newobject('SFDynamicStyleDirective', 'SFReportListener.vcx')

  .oDirectiveHandlers.Add(loHandler, ccDIRECTIVE_STYLE)

  loHandler = newobject('SFDynamicAlphaDirective', 'SFReportListener.vcx')

  .oDirectiveHandlers.Add(loHandler, ccDIRECTIVE_ALPHA)

endwith

ProcessFRXRecord, called from the Init method of SFReportListener, parses the USER memo of the current FRX record, looking for *:LISTENER directives. Any it finds are checked for validity by seeing whether a handler for it exists in the oDirectiveHandlers collection; if so, the directive is added to a collection object stored in the aRecords element for the report object.

local laLines[1], ;

  lnLines, ;

  lnI, ;

  lcLine, ;

  lnPos, ;

  lcClause, ;

  lcExpr

  loHandler, ;

  loDirective

with This

 

* Process any lines in the User memo.

 

  lnLines = alines(laLines, USER)

  for lnI = 1 to lnLines

    lcLine = alltrim(laLines[lnI])

 

* If we found a listener directive and it's one we support, add it and the

* specified expression to our collection (create the collection the first time

* it's needed).

 

    if upper(left(lcLine, 10)) = ccDIRECTIVE_LISTENER

      lcLine   = substr(lcLine, 12)

      lnPos    = at('=', lcLine)

      lcClause = alltrim(left(lcLine, lnPos - 1))

      lcExpr   = alltrim(substr(lcLine, lnPos + 1))

      try

        loHandler   = .oDirectiveHandlers.Item(upper(lcClause))

        lcExpr      = loHandler.ProcessExpression(lcExpr)

        loDirective = createobject('Empty')

        addproperty(loDirective, 'DirectiveHandler', lcClause)

        addproperty(loDirective, 'Expression',       lcExpr)

 

* Create a collection of all directives this record has.

 

        if vartype(.aRecords[recno()]) <> 'O'

          .aRecords[recno()] = createobject('Collection')

        endif vartype(.aRecords[recno()]) <> 'O'

        .aRecords[recno()].Add(loDirective)

      catch

      endtry

    endif upper(left(lcLine, 10)) = ccDIRECTIVE_LISTENER

  next lnI

endwith

The EvaluateContents method checks to see if the element for the current FRX record in aRecords contains a collection of directives for this report object (since there may be more than one directive for a given object). If so, each item in the collection contains the name of a directive handler object in the oDirectiveHandlers collection and the directive argument (for example, if the directive is *:LISTENER ROTATE = -45, this argument would be “-45”). EvaluateContents calls the HandleDirective method of each handler in the collection, passing it the properties object passed in to EvaluateContents and the directive argument. Here’s the code for EvaluateContents:

lparameters tnFRXRecno, ;

  toObjProperties

local loDirective, ;

  loHandler

with This

  if vartype(.aRecords[tnFRXRecno]) = 'O'

    for each loDirective in .aRecords[tnFRXRecno]

      loHandler = .oDirectiveHandlers.Item(loDirective.DirectiveHandler)

      loHandler.HandleDirective(This, loDirective.Expression, ;

        toObjProperties)

    next loDirective

  endif vartype(.aRecords[tnFRXRecno]) = 'O'

endwith

dodefault(tnFRXRecno, toObjProperties)

Directive handlers

SFReportDirective is an abstract class for subclassing directive handlers from. It’s a subclass of Custom with just two abstract methods: HandleDirective, called from the EvaluateContents method of SFReportListenerDirective to handle the directive, and ProcessExpression, called from the ProcessFRXRecord method of SFReportListenerDirective to convert the directive argument from text into a format the handler can use.

SFDynamicStyleDirective is a directive handler that changes the font style (that is, whether it’s normal, bold, italics, or underlined) for a report object based on a dynamically evaluated expression for every record in the report’s data set. Specify the directive in the USER memo of a report object using the following syntax:

*:LISTENER STYLE = StyleExpression

where StyleExpression is an expression that evaluates to the desired style.

One complication: styles are stored in an FRX as numeric values. So to make it easier to specify styles, SFDynamicStyleDirective allows you to use #NORMAL#, #BOLD#, #ITALIC#, #STRIKETHRU#, and #UNDERLINE# to specify the style. These values are additive, so #BOLD# + #ITALIC# would give bold italicized text. The ProcessExpression method takes care of converting the style text into the appropriate numeric values (the FRX_FONTSTYLE_* constants represent the numeric values for the different styles).

lparameters tcExpression

local lcExpression

lcExpression = strtran(tcExpression, '#NORMAL#', ;

  transform(FRX_FONTSTYLE_NORMAL))

lcExpression = strtran(lcExpression, '#BOLD#', ;

  transform(FRX_FONTSTYLE_BOLD))

lcExpression = strtran(lcExpression, '#ITALIC#', ;

  transform(FRX_FONTSTYLE_ITALIC))

lcExpression = strtran(lcExpression, '#UNDERLINE#', ;

  transform(FRX_FONTSTYLE_UNDERLINED))

lcExpression = strtran(lcExpression, '#STRIKETHRU#', ;

  transform(FRX_FONTSTYLE_STRIKETHROUGH))

return lcExpression

Here’s an example of a directive (taken from the SHIPVIA field in TestDynamicFormatting.FRX) that displays a report object in bold under some conditions and normal under others:

*:LISTENER STYLE = iif(SHIPVIA = 3, #BOLD#, #NORMAL#)

The HandleDirective method evaluates the expression. If the expression is valid, it sets the FontStyle property of the properties object to the desired style and sets Reload to .T. so the report engine knows the report object has changed.

lparameters toListener, ;

  tcExpression, ;

  toObjProperties

local lnStyle

lnStyle = evaluate(tcExpression)

if vartype(lnStyle) = 'N'

  toObjProperties.FontStyle = lnStyle

  toObjProperties.Reload    = .T.

endif vartype(lnStyle) = 'N'

SFDynamicAlphaDirective is very similar to SFDynamicStyleDirective, but it sets the PenAlpha property of the report object to the specified value. Specify the directive using the following syntax:

*:LISTENER ALPHA = AlphaExpression

SFDynamicColorDirective is also very similar to SFDynamicStyleDirective, but it deals with the color of the report object instead of its font style. As with styles, colors must be specified as RGB values, so SFDynamicColorDirective supports colors to be specified as text, such as #RED#, #BLUE#, and #YELLOW#. Specify the directive using the following syntax:

*:LISTENER FORECOLOR = ColorExpression

*:LISTENER BACKCOLOR = ColorExpression

where ColorExpression is an expression that evaluates to the desired color.

The code in the HandleDirective method is similar to the SFDynamicStyleDirective, but it calls SetColor rather than setting the FontStyle property. SetColor is abstract in this class and it’s implemented in two subclasses of SFDynamicColorDirective: SFDynamicBackColorDirective and SFDynamicForeColorDirective. Here’s the code from SFDynamicBackColorDirective to show how the color is set:


lparameters toObjProperties, ;

  tnColor

with toObjProperties

  .FillRed   = bitand(tnColor, 0x0000FF)

  .FillGreen = bitrshift(bitand(tnColor, 0x00FF00), 8)

  .FillBlue  = bitrshift(bitand(tnColor, 0xFF0000), 16)

endwith

The code in the ProcessExpression method of SFDynamicColorDirective is also very similar to the SFDynamicStyleDirective; it converts the color text into the appropriate
RGB values.

TestDynamicFormatting.FRX shows how these two directive handlers (and SFReportListenerDirective) work. It prints records from the sample Northwind Orders table that comes with VFP. The SHIPPEDDATE field has the following in USER:

*:LISTENER FORECOLOR = iif(SHIPPEDDATE > ORDERDATE + 10, #RED#, #BLACK#)

This tells the listener to display this field in red if the date the item was shipped is more than 10 days after it was ordered or black if not. The SHIPVIA field displays in bold if the shipping method is 3 or normal if not, as discussed earlier. This field uses the following expression to display the desired value:

icase(SHIPVIA = 1, 'Fedex', SHIPVIA = 2, 'UPS', SHIPVIA = 3, 'Mail')

The following code (taken from TestDynamicFormatting.PRG) shows how to run this report with SFReportListenerDirective as its listener. Figure 1 shows the results.

use _samples + 'Northwind\orders'

loListener = newobject('SFReportListenerDirective', ;

  'SFReportListener.vcx')

report form TestDynamicFormatting.FRX preview object loListener next 20

SFTranslateDirective allows you to create multi-lingual reports by specifying that certain fields be translated. Its Init method opens a STRINGS table that contains a record for each string with a column for each language. HandleDirective looks up each word in the text in the field being rendered in STRINGS and finds the appropriate translation from the column for the desired language. (It assumes a global variable called gcLanguage contains the language to use for the report; you could, of course, substitute any other mechanism you wish.) If the text is different, it’s written to the Text property of the properties object and Reload is set to .T. so the report engine will use the new string.

 

Figure 1. Dynamically formatting text is easy to do using custom ReportListener classes such as SFReportListenerDirective.

lparameters toListener, ;

  tcExpression, ;

  toObjProperties

local lcText, ;

  lcNewText, ;

  lnI, ;

  lcWord

store toObjProperties.Text to lcText, lcNewText

for lnI = 1 to getwordcount(lcText)

  lcWord = getwordnum(lcText, lnI)

  if seek(upper(lcWord), 'STRINGS', 'ENGLISH')

    lcNewText = strtran(lcNewText, lcWord, trim(evaluate('STRINGS.' + ;

      gcLanguage)))

  endif seek(upper(lcWord) ...

next lnI

if not lcNewText == toObjProperties.Text

  toObjProperties.Text   = lcNewText

  toObjProperties.Reload = .T.

endif not lcNewText == toObjProperties.Text

To use this listener, simply place *:LISTENER TRANSLATE in the USER memo of any field objects you want translated and set gcLanguage to the desired language. Note that because EvaluateContents is only called for field objects, you have to use them instead of label objects. TestTranslate.PRG shows how to add SFTranslateDirective to the collection of directive handlers recognized by SFReportListenerDirective. This sample uses Pig Latin for fun. Figure 2 shows what the report looks like when it’s run.

use _samples + 'Northwind\customers'

loListener = newobject('SFReportListenerDirective', 'SFReportListener.vcx')

loHandler  = newobject('SFTranslateDirective',      'SFReportListener.vcx')

loListener.oDirectiveHandlers.Add(loHandler, 'TRANSLATE')

gcLanguage = 'PigLatin'

report form TestTranslate.FRX preview object loListener

Figure 2. You can dynamically change the text of field objects, such as creating multi-lingual reports.

SFRotateDirective is another directive handler, but it’s based on SFReportListener rather than SFReportDirective because it doesn’t just change the properties of the report object via the properties object. Instead, it overrides the Render method to rotate the report object.

To specify a report object be rotated, put a directive in the USER memo using the following syntax:

*:LISTENER ROTATE = AngleExpression

where AngleExpression is an expression that evaluates to the angle to rotate to (clockwise angles are specified as positive values, counter-clockwise angles as negative).

The BeforeRender method, called just before an object is rendered, starts by checking whether a rotation angle was specified for the report object. (The code in ProcessFRXRecord does that. We won’t look at the code in that method; it’s similar to, albeit simpler, than the SFReportListenerDirective.) If a rotation angle was specified, BeforeRender uses methods of the oGDIGraphics object to save the current GDI+ state and change the drawing angle for the object, and uses DODEFAULT() to do the normal behavior, which is to call the BeforeRender method of any successors.

lparameters tnFRXRecno, ;

  tnLeft, ;

  tnTop, ;

  tnWidth, ;

  tnHeight, ;

  tnObjectContinuationType, ;

  tcContentsToBeRendered, ;

  tnGDIPlusImage

local lnAngle, ;

  lnState

with This

 

* If we're supposed to rotate this object, do so.

 

  lnAngle = evaluate(evl(.aRecords[tnFRXRecno], '0'))

  if lnAngle <> 0

 

* Save the current state of the graphics handle.

 

    .oGDIGraphics.Save(@lnState)

    .nState = lnState

 

* Move the 0,0 point to where we'd like it to be so when we rotate,

* we're rotating around the appropriate point.

 

    .oGDIGraphics.TranslateTransform(tnLeft, tnTop)

 

* Change the angle at which the draw will occur.

 

    .oGDIGraphics.RotateTransform(lnAngle)

 

* Restore the 0,0 point.

 

    .oGDIGraphics.TranslateTransform(-tnLeft, -tnTop)

  endif lnAngle <> 0

 

* Do the usual behavior.

 

  dodefault(tnFRXRecno, tnLeft, tnTop, tnWidth, tnHeight, ;

    tnObjectContinuationType, tcContentsToBeRendered, tnGDIPlusImage)

endwith

AfterRender restores the GDI+ state so subsequent objects render properly. Just for fun, try commenting out the code in this method and run a report. The results are cool but completely impractical.

with This

  if .nState <> 0

    .oGDIGraphics.Restore(.nState)

    .nState = 0

  endif .nState <> 0

 

* Do the usual behavior.

 

  dodefault()

endwith

TestRotate.FRX is a sample report that show how this works. The column headings for the date fields have rotate directives so the date fields can be placed closer together. The following code (taken from TestRotate.PRG) shows how to run this report with SFRotateDirective as its listener. The results are shown in Figure 3.


use _samples + 'Northwind\orders'

loListener = newobject('SFRotateDirective', 'SFReportListener.vcx')

report form TestRotate.FRX preview object loListener next 20

Figure 3. Text can be rotated dynamically by changing the way it’s rendered.

Text Box: "

The Developer Download files for this chapter, available at www.hentzenwerke.com, include SFReportListener.VCX, TestDynamicFormatting.PRG, TestTranslate.PRG, Strings.DBF, TestRotate.PRG, TestDynamicFormatting.FRX, TestRotate.FRX, and TestTranslate.FRX. Run the PRG files to see how the report directive classes discussed in this section work.

SFReportListenerGraphic

The OutputPage method of ReportListener supports outputting pages to graphics files. SFReportListenerGraphic, a subclass of SFReportListener, makes it easier to do this. It has two custom properties: cFileName that is set to the name of the file to create, and nFileType that is either set to the number representing the file type or left at 0, in which case SFReportListenerGraphic will set it to the proper value based on the extension of the filename in cFileName.

If ListenerType is 2 (“page-at-a-time” mode with no output, the default for this class), OutputPage is automatically called after each page is rendered. In that case, OutputPage will handle outputting to the specified file. If ListenerType is 3 (“all-pages-at-once” mode with no output), pages are only output when OutputPage is specifically called, so AfterReport goes through the rendered pages and calls OutputPage for each one. Notice that if a multi-page TIFF file is specified, the first page must be output as a single-page TIFF file, and then subsequent pages are appended to it by outputting them as a multi-page TIFF file. Here’s the code from AfterReport; OutputPage is similar but slightly simpler. (In this code, LISTENER_* are constants defined in FoxPro_Reporting.H, which is referenced by SFReporting.H, which itself is referenced by SFReportListener.H, the include file for this class.)

local lcBaseName, ;

  lcExt, ;

  lnI, ;

  lcFileName

with This

  dodefault()

  if .ListenerType = LISTENER_TYPE_ALLPGS

    if .nFileType = 0

      .GetGraphicsType()

    endif .nFileType = 0

    lcBaseName = addbs(justpath(.cFileName)) + juststem(.cFileName)

    lcExt      = justext(.cFileName)

    for lnI = 1 to .SharedOutputPageCount

      do case

        case .nFileType <> LISTENER_DEVICE_TYPE_MTIF

          lcFileName = forceext(lcBaseName + padl(lnI, 3, '0'), lcExt)

          .OutputPage(lnI, lcFileName, .nFileType)

        case not file(.cFileName)

          .OutputPage(lnI, .cFileName, LISTENER_DEVICE_TYPE_TIF)

        otherwise

          .OutputPage(lnI, .cFileName, .nFileType)

      endcase

      .DoStatus(strtran(strtran(ccSTR_PAGE_X_OF_Y, ccMSG_INSERT1, ;

        transform(lnI)), ccMSG_INSERT2, ;

        transform(.SharedOutputPageCount)))

    next lnI

    .ClearStatus()

  endif .ListenerType = LISTENER_TYPE_ALLPGS

endwith

SFReportListenerGraphic also has a ShowFile method to display the file using the Windows API ShellExecute function, which uses the registered application for the file type.

TestGraphicOutput.PRG shows how SFReportListenerGraphic works. It combines the effects of multiple listeners to render the report properly (this uses the same TestDynamicFormatting.FRX you saw earlier) and output to graphics files.


use _samples + 'Northwind\orders'

loListener = newobject('SFReportListenerGraphic', 'SFReportListener.vcx')

loListener.cFileName = fullpath('TestReport.gif')

loListener.Successor = newobject('SFReportListenerDirective', ;

  'SFReportListener.vcx')

report form TestDynamicFormatting.FRX object loListener range 1, 6

loListener.ShowFile(1)

Text Box: "

The Developer Download files for this chapter, available at www.hentzenwerke.com, include SFReportListener.VCX, TestGraphicOutput.PRG, and TestDynamicFormatting.FRX.

Custom rendering

The combination of the Render method and GDI+ functions provides the ability to render just about anything you wish in place of an object. For example, a common request is to output charts on a report without relying on General fields and ActiveX controls. The report shown in Figure 4 is an example of such a report. The chart shows sales by product category as a column graph. When viewed in the Report Designer, however, all you see is a rectangle where the chart should go.

Figure 4. Using GDI+ functions, you can render shapes as anything you wish.

TestCustomRendering.PRG, which runs the TestCustomRendering.FRX report, uses the SFColumnChartListener class to replace the rectangle with a chart. The code for this class isn’t shown here for space reasons. However, as an example, the following code is taken from the DrawColumnChart method, which is called from the BeforeRender method when the rectangle is about to be rendered. As you can see, this code makes extensive use of the classes in the FFC’s _GDIPlus.VCX, discussed in the “_GDIPlus.VCX” section of this chapter. This code uses several properties of the class:

·         aValues is a two-dimensional array of values to graph. Column 1 contains the names of the product categories and column 2 contains the total sales for each category.

·         aColumnColors is an array containing the color to use for each column.

·         nSpacing is the space between the columns.

·         cLegendFontName and nLegendFontSize are the font name and size to use for the legend.

·         nLegendSpacing is the space between the chart and its legend.

·         nLegendBoxSize is the size of a box in the legend, nLegendBoxSpacing is the spacing between the boxes, and nLegendTextSpacing is the spacing between a box and its associated text.

lparameters tnLeft, tnTop, tnWidth, tnHeight

local lnMax, ;

  lnColumns, ;

  lnI, ;

  lnColumnWidth, ;

  loColumnBrush, ;

  loPen, ;

  loFont, ;

  loStringFormat, ;

  loPoint, ;

  loTextBrush, ;

  lnColors, ;

  lnColor, ;

  lnLeft, ;

  lnHeight, ;

  lnTop

with This

 

* Figure out the highest value and the width of each column.

 

  lnMax     = 0

  lnColumns = alen(.aValues, 1)

  for lnI = 1 to lnColumns

    lnMax = max(lnMax, .aValues[lnI, 2])

  next lnI

  lnColumnWidth = (tnWidth - (lnColumns * .nSpacing))/lnColumns

 

* Create _GDIPlus objects we'll need for drawing.

 

  loColumnBrush  = newobject('GPSolidBrush',   home() + 'ffc\_GDIPlus.vcx')

  loPen          = newobject('GPPen',          home() + 'ffc\_GDIPlus.vcx')

  loFont         = newobject('GPFont',         home() + 'ffc\_GDIPlus.vcx')

  loStringFormat = newobject('GPStringFormat', home() + 'ffc\_GDIPlus.vcx')

  loPoint        = newobject('GPPoint',        home() + 'ffc\_GDIPlus.vcx')

  loTextBrush    = newobject('GPSolidBrush',   home() + 'ffc\_GDIPlus.vcx')

  loPen.Create(.CreateColor(0))  && Black

  loFont.Create(.cLegendFontName, .nLegendFontSize, ;

    GDIPLUS_FontStyle_Regular, GDIPLUS_Unit_Point)

 

* Draw the border for the column chart.

 

  .oGDIGraphics.DrawLine(loPen, tnLeft, tnTop, tnLeft, ;

    tnTop + tnHeight)

  .oGDIGraphics.DrawLine(loPen, tnLeft, tnTop + tnHeight, ;

    tnLeft + tnWidth, tnTop + tnHeight)

 

* Draw the column.

 

  lnColors = alen(.aColumnColors)

  for lnI = 1 to lnColumns

    lnColor = .aColumnColors[(lnI - 1) % lnColors + 1]

    loColumnBrush.Create(lnColor)

    lnLeft   = tnLeft + lnI * .nSpacing + (lnI - 1) * lnColumnWidth

    lnHeight = cast(tnHeight/lnMax * .aValues[lnI, 2] as Numeric(7, 2))

    lnTop    = tnTop + tnHeight - lnHeight

    .oGDIGraphics.DrawRectangle(loPen, lnLeft, lnTop, ;

      lnColumnWidth, lnHeight)

    .oGDIGraphics.FillRectangle(loColumnBrush, lnLeft, lnTop, ;

      lnColumnWidth, lnHeight)

 

* Draw the legend for the column.

 

    lnLeft = tnLeft + tnWidth + .nLegendSpacing

    lnTop  = tnTop + (lnI - 1) * (.nLegendBoxSize + .nLegendBoxSpacing)

    .oGDIGraphics.DrawRectangle(loPen, lnLeft, lnTop, ;

      .nLegendBoxSize, .nLegendBoxSize)

    .oGDIGraphics.FillRectangle(loColumnBrush, lnLeft, lnTop, ;

      .nLegendBoxSize, .nLegendBoxSize)

    lnLeft = lnLeft + .nLegendBoxSize + .nLegendTextSpacing

    loPoint.Create(lnLeft, lnTop)

    loTextBrush.Create(.CreateColor(0)) && Black

    .oGDIGraphics.DrawStringA(.aValues[lnI, 1], loFont, loPoint, ;

      loStringFormat, loTextBrush)

  next lnI

endwith

Text Box: "

The Developer Download files for this chapter, available at www.hentzenwerke.com, include TestCustomRendering.PRG, TestCustomRendering.FRX, and SFReportListener.VCX.

Previewing reports

As discussed in Chapter 5, “Enhancements in the Reporting System,” VFP 9 sports a new preview window for reports. A new system variable, _REPORTPREVIEW, specifies the name of a VFP application used as a “factory” to create the preview window. (A factory is an object that doesn’t provide the functionality required by a client object, but instead creates another object that provides this functionality.) By default, the variable points to ReportPreview.APP in the VFP home directory, but you could substitute your own application if you wish to. The ability to use a VFP application as the preview window provides a lot more control over the appearance and behavior of previewing than in earlier versions.

When you preview a report, by default the PreviewContainer property of the listener used for the report is null. In that case, the reporting engine calls the application pointed to by _REPORTPREVIEW, which instantiates a VFP form to use as the preview window. A reference to the form is stored in PreviewContainer. If PreviewContainer isn’t null, the reporting engine doesn’t bother calling the preview factory application.

Because the preview window is simply a VFP form, you can customize its appearance by setting the appropriate properties. To create an instance of the preview window prior to running a report, pass ReportPreview.APP a variable; it will instantiate the preview window class into that variable. You can then set properties of the form as necessary and store the variable to the PreviewContainer property of the listener for the report.

For example, the following code (taken from CustomizePreview.PRG) displays a preview window with a custom caption and without a toolbar, using 2-up pages displayed at 75% (zoom level 4) starting at page 4:

local loPreview, ;

  loListener

do (_ReportPreview) with loPreview

with loPreview

  .CurrentPage      = 4

  .ToolbarIsVisible = .F.

  .CanvasCount      = 2

  .ZoomLevel        = 4

  .Width            = _screen.Width - 20

  .Caption          = 'Chapter 7 Preview Window'

endwith

loListener = newobject('SFReportListenerDirective', 'SFReportListener.vcx')

loListener.PreviewContainer = loPreview

use _samples + 'Northwind\orders'

report form TestDynamicFormatting object loListener preview

Text Box: "

The Developer Download files for this chapter, available at www.hentzenwerke.com, include CustomizePreview.PRG, SFReportListener.VCX, and TestDynamicFormatting.FRX.

A common question on VFP forums such as the Universal Thread is “how do I remove the print button from the VFP preview toolbar?” In earlier versions, you had to create a custom resource file, customize the preview toolbar to remove the print button, and use the custom resource file in your application. In VFP 9, you simply set the Visible property of the print button in the toolbar to .F. However, there are a couple of minor complications:

·         The PreviewContainer property of the listener doesn’t point to the preview form but to a proxy object for the form; that is, it references an object that acts as an intermediary between the listener and the preview form. The proxy object has an oForm property that references the actual preview form. The preview form has a Toolbar property that contains a reference to the Toolbar, so set the Visible property of cmdPrint in loListener.PreviewContainer.oForm.Toolbar to .F. to hide the print button.

·         The preview window also has a shortcut menu with a print function. The shortcut menu is populated in the InvokeContextMenu method of the preview window, so you might think that removing the print function from the menu requires subclassing the preview form class and overriding this method. Fortunately, the VFP team thought of this, and provided a hook mechanism to allow you to change the menu. This hook is implemented via an object stored in the ExtensionHandler property. If that property contains an object, InvokeContextMenu calls the object’s AddBarsToMenu method after populating the shortcut menu. So, you can create a custom object with an AddBarsToMenu method that removes the print bar and store a reference to that object in the ExtensionHandler property (call SetExtensionHandler to do that). Such a custom object must also have a few other methods because if ExtensionHandler references an object, other methods will also use this object. See the code below for an example of such a class.

The following code, taken from NoPrintButton.PRG, shows how to handle this:

use _samples + 'Northwind\orders'

loListener = newobject('SFReportListenerDirective', 'SFReportListener.vcx')

report form TestDynamicFormatting.FRX preview object loListener next 20 nowait

loExtension = createobject('ExtensionHandler')

loListener.PreviewContainer.SetExtensionHandler(loExtension)

loListener.PreviewContainer.oForm.Toolbar.cmdPrint.Visible = .F.

 

define class ExtensionHandler as Custom

  function AddBarsToMenu(tcMenu, tnNextBar)

    release bar 12 of &tcMenu

  endfunc

 

  function Release

    if type('This.PreviewForm') = 'O'

      This.PreviewForm.ExtensionHandler = .NULL.

      This.PreviewForm = .NULL.

    endif type('This.PreviewForm') = 'O'

  endfunc

 

  function Show(tnStyle)

  endfunc

 

  function Paint

  endfunc

 

  function HandleKeyPress(tnKeyCode, tnShiftAltCtrl)

  endfunc

enddefine

Text Box: "

The Developer Download files for this chapter, available at www.hentzenwerke.com, include NoPrintButton.PRG, SFReportListener.VCX, and TestDynamicFormatting.FRX.

You don’t have to use the preview form class defined in ReportPreview.APP to preview a report. SFPreviewForm.SCX (shown in Figure 5) acts as both a report manager and preview window at the same time. Select a report from the list and click the Preview button to preview the report in the form. The Next and Previous buttons display the next and previous pages in the report.

Figure 5. ReportListener can output to a VFP form so you can create custom
preview windows.

Here’s the key code from the form’s PreviewReport method, called from the Click method of the Preview button:

with Thisform

  .oListener = createobject('ReportListener')

  .oListener.ListenerType = LISTENER_TYPE_ALLPGS

  report form (lcReport) object .oListener

  .oListener.OutputPage(1, .oPreviewContainer, LISTENER_DEVICE_TYPE_CTL)

endwith

Because its ListenerType property is set to 3, the ReportListener renders the pages in “all-pages-at-once” mode, but doesn’t perform any output. Once the rendering is done, the form calls the listener’s OutputPage method, instructing it to output page 1 to the oPreviewContainer shape. (LISTENER_DEVICE_TYPE_CTL is a constant that evaluates to 2, the value used by OutputPage to specify a VFP control.) OutputPage doesn’t actually
output a page to the shape; instead, it uses the size and position of the shape as the location for the output.

Another important method is Paint. The code in this method redisplays the current page whenever the form is redrawn. Without this code, things that cause the form to be redrawn, such as resizing the form, result in the preview disappearing because the shape is redrawn. This code is wrapped in a TRY structure because the form may be painted before the listener has finished rendering the first page.

with This

  if vartype(.oListener) = 'O'

    try

      .oListener.OutputPage(.nCurrentPage, .oPreviewContainer, ;

        LISTENER_DEVICE_TYPE_CTL)

    catch

    endtry

  endif vartype(.oListener) = 'O'

endwith

Note that SFPreviewForm is just a simple demo. It doesn’t handle many of the issues the new VFP 9 preview window does, such as printing from preview or multiple pages at once. Also, since PreviewReport just uses a base class listener, there’s no dynamic formatting, text rotation, or other effects. You could, of course, add these features yourself if you wish.

Text Box: "

The Developer Download files for this chapter, available at www.hentzenwerke.com, include SFPreviewForm.SCX, TestDynamicFormatting.FRX, TestRotate.FRX, and TestTranslate.FRX.

If you want to create your own class to use as a preview window, your class must have a couple of methods (because the report listener will call them):

·         SetReport: this method should accept an object reference to the report listener, and store it somewhere. In order to preview the report, the preview form must call methods of the listener, especially OutputPage, so it needs a reference to the listener. When the report is done, the listener will call SetReport a second time, passing it .NULL. so the reference can be removed. Otherwise, with the listener and the preview form having references to each other, the objects can’t be destroyed. (Note that SFPreviewForm.SCX doesn’t have this method because it’s not being called from the report engine as a preview window but drives the previewing process.)

·         Show: this method should accept the same parameter as the Show method of a form, which indicates whether the form is modal or not.

When the preview form is closed, it should call the OnPreviewClose method of the listener to ensure things are properly cleaned up.

The NewPreview class (in NewPreview.VCX) is a very simple example. It’s just a base class form with a shape named oPreviewContainer and a custom property named oListener. The SetReport method has the following code:

lparameters toListener

This.oListener = toListener

The Paint method displays the first page of the report:

if vartype(This.oListener) = 'O'

  This.oListener.OutputPage(1, This.oPreviewContainer, 2)

endif vartype(This.oListener) = 'O'

The QueryUnload method tells the report listener to clean up:

if vartype(This.oListener) = 'O'

  This.oListener.OnPreviewClose()

endif vartype(This.oListener) = 'O'

That’s it! Here’s some code (NewPreview.PRG) that uses this class as the preview form for a report:

local loPreview, ;

  loListener

loPreview  = newobject('NewPreview', 'NewPreview.vcx')

loListener = createobject('ReportListener')

loListener.ListenerType     = 1

loListener.PreviewContainer = loPreview

use _samples + 'Northwind\orders'

report form TestDynamicFormatting object loListener

Of course, this preview window has almost no functionality; it only displays the first page of the report. To create your own customized preview window with complete functionality, you may want to subclass the FRXPreviewProxy and FRXPreviewForm classes in ReportPreview.APP (the source code is in the Tools\XSource\VFPSource\ReportPreview subdirectory of the VFP home directory after you unzip Tools\XSource\XSource.ZIP) and add the additional behavior you require.

Text Box: "

The Developer Download files for this chapter, available at www.hentzenwerke.com, include NewPreview.VCX,
NewPreview.PRG, and TestDynamicFormatting.FRX.

New SYS() functions

There are a couple of new SYS() functions in VFP 9 related to report listeners.

SYS(2024) returns “Y” if the current report was canceled before completion or “N” if there is no current report or the report finished normally. SYS(2024) is reset to “N” after the UnloadReport event fires, so you can’t use this value from code that executes a REPORT or LABEL command. It’s typically used in methods of a report listener to take different action based on whether the report completed or not.

SYS(2040) indicates the status of a report. It returns “0” if there is no current report, “1” if the report is being previewed, and “2” if it’s being output to a file or printer. This can be used, for example, in the Print When expression of a report object to conditionally output the object based on whether the report is being printed or previewed.

Summary

Microsoft has done an incredible job of opening up the VFP reporting engine, both at design-time and run-time. By passing report events to ReportListener objects, they allow you to react to these events to do just about anything you wish, from providing custom feedback to users to providing different types of output, to dynamically changing the way objects are rendered. We look forward to seeing the type of things the VFP community does with these new features.

 

 

Updates and corrections for this chapter can be found on Hentzenwerke’s website, www.hentzenwerke.com. Click “Catalog” and navigate to the page for this book.