Visual FoxPro 9.0 Report Writer In Action 

 

Cathy Pountney
www.frontier2000.com

August 2004

Applies to:
   Microsoft Visual FoxPro 9.0

Read the companion article, What's New in the Visual FoxPro 9.0 Report Writer.

Summary: In response to customer feedback, Microsoft has significantly improved the Report Writer in VFP 9.0. One of the main improvements is extensibility, which provides hooks into the Report Designer and control over output mechanisms, such as previewing and printing. This article shows how to tap into these hooks and use them to your advantage. My goal is to show lots of inventive things to do and to get your mind thinking along a creative track so you can come up with even more ideas.

Another main feature added to the product is the ability to run multiple-detail band reports natively. This article explains several examples of using multiple-detail band reports. Some of these are straightforward and some of these solve common situations that were very difficult prior to the VFP 9.0 Report Writer. (73 printed pages)

Contents

Introduction
Using Multiple-Detail Bands
About Extensibility
Using a Report Builder
Using Listeners
Using Preview Containers
Conclusion
Bio

Introduction

A prerequisite to this article is that you have already read the article, What's New in the VFP 9.0 Report Writer, also available on the MSDN website. That article discusses the new features of VFP 9.0 and gives basic instructions on how to use them. This article takes over where the other article left off. This article gets into the nitty-gritty of how to put the new features to good use and walks through creating reports that solve real-world problems.

First, you will learn several things you can do with multiple-detail band reports. Next, you will learn about the new extensibility features in the sections, Using Listeners and Using Preview Containers. These sections show how to create reports that solve specific situations, such as forcing pagination on reports and creating a Table of Contents on the fly.

Using Multiple-Detail Bands

Besides the extensibility enhancements in the VFP 9.0 Report Writer, the new multiple-detail band feature is one of the biggest, and most often requested, improvements. This new feature allows multiple child tables to be processed for each record in a parent table. There are unlimited possibilities on what can be done with this new feature.

Simple Reports

A simple multiple-detail band report consists of one parent table that drives the report and two or more child tables that relate to the parent table. The sample report shown in Figure 1 is an example of a simple multiple-detail report.

Click here for larger image

Figure 1. This sample multiple-detail band report has three separate detail bands for each customer. (Click image for larger view.)

The Customer table is the parent table and contains one record for each customer of the insurance company. The Members, Vehicles, and Homes tables are child tables of the Customer table. The Members table holds one record for each family member of the customer. The Vehicles table holds one record for each vehicle insured by the customer. The Homes table holds one record for each home insured by the customer. Each of the child tables have a relation set to the parent table.

In this example, the Customer table is the driving table. When using the Data Environment to open tables, set the InitialSelectedAlias property to the Customer table. When using code to open the tables, make sure the Customer table is the current work area at the time the report is run.

The target alias is the term used to describe which table is the driving table for a particular detail band. In this example, the Members table is the target alias for the detail 1 band, the Vehicles table is the target alias for the detail 2 band, and the Homes table is the target alias for the detail 3 band. Use the Properties dialog box of the applicable detail band to set the target alias.

Calculations

The previous example showed how to print data from three different child tables on the same report. However, a multiple-detail band report does not necessarily need multiple child tables. The same child table can be used in more than one detail band.

Group Totals

Prior to VFP 9.0, printing subtotals in the data group header band (shown in Figure 2) was very difficult. The data had to be preprocessed to calculate the totals prior to running the report. With VFP 9.0, no preprocessing is required.

Click here for larger image

Figure 2. Use a multiple-detail report to print group subtotals at the beginning of the group. (Click image for larger view.)

To create the report shown in Figure 2, follow these steps:

  1. Define the Data Environment.
    1. Add the Customer table.
    2. Add the Vehicles table.
    3. Set the InitialSelectedAlias property to the Customer table.
  2. Create a data group.
    1. Set the Data Group expression to the Customer PK.
    2. Do not place any objects in the data group header or the data group footer bands.
  3. Create multiple-detail bands.
    1. Select Optional Bands... from the Report menu.
    2. Click the Add button to add one more detail band to the report.
    3. Click OK to exit the dialog box.
  4. Define the detail 1 band.
    1. Double-click the gray bar of the detail 1 band to invoke the Properties dialog box.
    2. Set the target alias to "Vehicles", remembering to use the quotes.
    3. Check the Associated header and footer bands check box.
    4. Do not place any objects in the detail 1 band.
  5. Define the detail 1 footer band.
    1. Add the customer name and address objects to the band.
    2. Add the total vehicles and total premiums label objects to the band.
    3. Add a field object for the total vehicles; set the expression to vehicles.premium, set the Calculation type to Count, and set the Reset based on to Detail1.
    4. Add a field object for the total premiums; set the expression to vehicles.premium, set the Calculation type to Sum, and set the Reset based on to Detail1.
  6. Define the detail 2 band.
    1. Double-click the gray bar of the detail 2 band to invoke the Properties dialog box.
    2. Set the target alias to "Vehicles", remembering to use the quotes.
    3. Check the Associated header and footer bands check box.
    4. Add any other objects needed in the detail 2 band.

The above report definition tells the VFP 9.0 Report Writer to process the Vehicles table twice for each customer in the Customer table. The first time, it calculates the total records and dollar amount for the customer and then prints them. The second pass of the Vehicles table prints the actual details. This process repeats for each customer in the Customer table.

Percentages

Another reporting concept is to show percentages of totals of each detail line, as the detail line prints. This can also be handled with multiple-detail bands, as shown in Figure 3.

Click here for larger image

Figure 3. Use a multiple-detail report to print percentages along with each detail line. (Click image for larger view.)

To create the report shown in Figure 3, follow these steps:

  1. Define the Data Environment.
    1. Add the Customer table.
    2. Add the Vehicles table.
    3. Set the InitialSelectedAlias property to the Customer table.
  2. Create a data group.
    1. Set the Data Group expression to the Customer PK.
    2. Add the customer name and address objects to the data group header band.
  3. Create multiple-detail bands.
    1. Select Optional Bands... from the Report menu.
    2. Click the Add button to add one more detail band to the report.
    3. Click OK to exit the dialog box.
  4. Define detail 1 band.
    1. Double-click the gray bar of the detail 1 band to invoke the Properties dialog box.
    2. Set the target alias to "Vehicles", remembering to use the quotes.
    3. Check the Associated header and footer bands check box.
    4. Do not place any objects in the detail 1 band.
  5. Create some report variables.
    1. Create a variable named rnTotalPremium, set the Value to store to Vehicles.premium, set Calculation type to sum, and set the Reset based on to Detail 1.
    2. Create a variable named rnPercent, set the Value to store to ROUND(100 * vehicles.premium / rnTotalPremium, 2), and set the Calculation type to None.
  6. Define the detail 2 band.
    1. Double-click the gray bar of the detail 2 band to invoke the Properties dialog box.
    2. Set the target alias to "Vehicles", remembering to use the quotes.
    3. Check the Associated header and footer bands check box.
    4. Add any objects needed in the detail 2 band.
    5. Add the percent object to the detail 2 band with an expression of rnPercent.
  7. Define the detail 2 footer band.
    1. Add the total premium object, set the expression to vehicles.premium, set the Calculation type to Sum, and set the Reset based on to Detail 2.
    2. Add the total percent object, set the expression to rnPercent, set the Calculation type to Sum, and set the Reset based on to Detail 2.

The above report definition tells the VFP 9.0 Report Writer to process the Vehicles table twice for each customer in the Customer table. The first time totals the premium so it can be used in the second pass. The second pass of the Vehicles table prints the data for the customer, using the report variable that was calculated during the first detail band. This process repeats for each customer in the Customer table.

About Extensibility

Prior to VFP 9.0, the Report Engine handled everything, including processing the data, object positioning, rendering, printing, and previewing. There was no way to hook into the Report Engine and customize it, as can be done with other areas of VFP. One of the most significant changes to the VFP 9.0 Report Writer is the new extensibility features. The Report Designer, the Report Engine, and the Preview Container are now exposed to the developer.

Report Builder

The VFP 9.0 Report Writer includes a new design-time feature called Builder Hooks. Several events of the Report Designer are exposed and an independent Xbase component, called the Report Builder, can be invoked to handle them. This application can be used to invoke your own dialog boxes, augment the native Report Designer behavior, or override the native behavior.

VFP 9.0 includes an extensive Report Builder application that includes new features and provides a better user interface for designing reports. The Report Builder is controlled by a new system variable, _REPORTBUILDER. If this variable is empty, the native dialog boxes appear. To activate the builder hooks, set this variable to an appropriate application. For example, to use the Report Builder shipped with VFP 9.0, issue the following command:

_REPORTBUILDER = HOME() + "REPORTBUILDER.APP"

**Note   **When using the Report Builder in the run-time version of VFP, be sure to distribute the file with your application. Also, be aware that the default search path is that of the vfp9r.dll file. You may use config.fpw to explicitly set the _ReportBuilder system variable at startup.

Report Engine (Listeners)

In the new output system (Object-Assisted Output), the Report Engine handles data-centric chores, such as moving through the scope and expression evaluation. However, when it comes time to create output, it defers the work to a new base class, called ReportListener. The new class renders the report contents in a more sophisticated way, using GDI+, and it also gives Xbase users a chance to interact with the output process. Figure 4 shows how the pieces fit together.

Click here for larger image

Figure 4. Use the new ReportListener class to create more sophisticated reports. (Click image for larger view.)

To use a ReportListener class, use the new clause on the REPORT FORM command as follows:

oListener = CREATEOBJECT("ReportListener")
oListener.ListenerType = 1 && Preview, or 0 for Print
REPORT FORM <name> <clauses> OBJECT oListener

VFP 9.0 also provides a second technique for using a ReportListener class. You may set the value of a new system variable, _REPORTOUTPUT, to the name of an application that can determine which ReportListener class to use based on the type of output chosen.

When using Object-Assisted Output, a report is processed using one of two main modes, depending on its ListenerType property value. These two modes can be thought of as print-appropriate and preview-appropriate, or page-at-a-time and all-pages-at-once. In the first mode, the Listener triggers an OutputPage event as it prepares each page, just as it sends each page to the printer or print queue.

In the second mode, the Listener prepares all pages for rendering and caches them. When it is finished, the OutputPage method can be invoked to ask for the page output of any and all pages included in output, by page number. The Listener uses the value of another system variable, _REPORTPREVIEW, to determine what application should be used to display the results.

Preview Container

In VFP 9.0, another piece of the extensibility puzzle is the Preview Container. With this hook, you can use the new preview container that ships with VFP 9.0 or write your own preview container. The old native preview container is still available when you are not using the new object-assisted output.

A new system variable, _REPORTPREVIEW, contains the name of the application that determines what preview container to use. By default, this variable points to ReportPreview.app. This new application contains a lot of improvements over the old preview, including: more zoom levels, control of the toolbar, control of the caption, multiple page preview, and better quality using GDI+. Figures 5 and 6 show the differences between the old preview surface and the new preview surface.

Click here for larger image

Figure 5. The older preview container is not as sophisticated as we would like it to be. (Click image for larger view.)

Click here for larger image

Figure 6. The new preview container has much better quality and more sophisticated options. (Click image for larger view.)

Notice the difference in quality between the old and new preview containers. The new container has much crisper fonts than the old one. This can be credited to using GDI+ to output information. Also note the differences in the docked toolbar. The toolbar on the newer style preview has more options, including multiple-page layouts.

Old Versus New

A new command, SET REPORTBEHAVIOR, may be used to turn Object-Assisted Output on or off. The new output rendering engine and the new Preview surface both have some significant differences from the old style of output. Alignment, kerning, and spacing are different between GDI and GDI+, which could significantly change how existing reports look. Therefore, REPORTBEHAVIOR is set to 80 by default, which turns the Object-Assisted Output off and reports processes as they did prior to VFP 9.0.

To globally turn Object-Assisted Output on, change the value of this setting in the Options dialog box (shown in Figure 7), or issue the following command:

SET REPORTBEHAVIOR 90

Figure 7. Use the Run-time behavior option on the Options dialog box to globally change the SET REPORTBEHAVIOR setting.

When REPORTBEHAVIOR is set to 90, REPORT FORM commands automatically behave as though the OBJECT clause was used, without any further changes to the code. VFP uses the _REPORTOUTPUT system variable to determine what application to use for nominating the appropriate ReportListener class for each REPORT FORM command.

With the new extensibility features in the VFP 9.0 Report Writer, you can tap into the Report Builder, the Report Engine, and the Preview surface. You can globally turn on the new features or you can turn them on individually for various reports. You can also leave the new features off and run reports as if you were in prior versions of VFP.

Using a Report Builder

The Report Builder is the extensibility portion of the Report Writer that is used at design-time. You can use the Report Builder with VFP 9.0 or you can create your own. This section gets you familiar with the shipped Report Builder, and then briefly discusses creating your own.

ReportBuilder.app

The new Report Builder, called ReportBuilder.app and shipped with VFP 9.0, has a much better user interface than older versions of VFP. New pageframe-style dialog boxes have been implemented, making it easier to change properties.

The Report Properties Dialog Box

The Report Properties dialog box uses a pageframe and pages for all the various aspects of report properties. This makes it much quicker to set properties because you no longer have to close one dialog box, open another from the menu, close that one, open another, and so on. Invoke this dialog box by right-clicking an unused area of the report, and then selecting Properties.... The dialog box can also be invoked by selecting Properties... from the Report menu. Figures 8-14 show all the pages of the new Report Properties dialog box.

Figure 8. Use the Page Layout page of the Report Properties dialog box to set columns, margins, printers, and so on.

Figure 9. Use the Optional Bands page of the Report Properties dialog box to set the Title band, Summary band, and to define multiple-detail bands.

Figure 10. Use the Data Grouping page of the Report Properties dialog box to define data groups.

Figure 11. Use the Variables page of the Report Properties dialog box to define report variables.

Figure 12. Use the Protection page of the Report Properties dialog box to take advantage of the new Protection features of VFP 9.0.

Figure 13. Use the Ruler/Grid page of the Report Properties dialog box to set up the ruler and grid.

Figure 14. Use the Data Environment page of the Report Properties dialog box to take advantage of the new Data Environment reuse features.

Having all these report properties available on one dialog box is a real time saver.

Object Properties

Another UI improvement is the implementation of pageframes and pages for layout object properties. This dialog box can be invoked by double-clicking a layout object. Figures 15 through 21 show all the pages for a field object. Many of the same pages are applicable to other types of objects, such as shapes.

Figure 15. Use the General page of the Field Properties dialog box to set the expression, position, and stretching.

Figure 16. Use the Style page of the Field Properties dialog box to set the font, color, and backstyle of an object.

Figure 17. Use the Format page of the Field Properties dialog box to set the format expression, several format options, and the new trim mode for expressions.

Figure 18. Use the Print when page of the Field properties dialog box to define print when logic.

Figure 19. Use the Calculate page of the Field Properties dialog box to define any calculations on the object.

Figure 20. Use the Protection page of the Field Properties dialog box to take advantage of the new Protection feature in VFP 9.0.

Figure 21. Use the Other page of the Field Properties dialog box to define comments, user data, ToolTips, and run-time extensions.

Just as with the Report Properties dialog box, this new "all-in-one" Object Properties dialog box is a real time saver.

Your Report Builder

VFP 9.0 ships with an Xbase Report Builder, called ReportBuilder.app. You may use this Report Builder or you may create your own and set the value of the _REPORTBUILDER system variable to point to your Report Builder. Writing your own Report Builder is beyond the scope of this article, but you can look at how Microsoft built ReportBuilder.app by looking at the source code included with VFP 9.0. The source code is located in the HOME() + 'tools\xsource' directory.

Using Listeners

VFP 9.0 introduces a new base class, called ReportListener, which is a hook into the report engine. Natively, this class handles rendering, printing, and custom previewing. However, it can be subclassed to add more functionality, such as XML, HTML, and TIFF output, custom GDI+ drawing, and many other features.

This section discusses the base ReportListener class and the subclasses shipped with the VFP 9.0 foundation classes. It also discusses a variety of different things that can be done with ReportListener classes, such as printing negative numbers in red, rotating text, forcing pagination to keep groups of information on the same page, creating Table of Contents on the fly, and much more.

Base Listener

The base ReportListener class has several properties that control the way reports are processed. Here are a few common ones.

  • AllowModalMessages (default = .f.): This property determines whether the ReportListener may provide modal messages as part of its user interface.
  • DynamicLineHeight (default = .f.): This property determines whether the ReportListener should use standard GDI+ line spacing according to the font characteristics, or whether the older fixed-spacing should be used. In addition, this property affects whether the backcolor of opaque field objects is painted as the width of the text being printed (.t.), or the width of the defined field (.f.).
  • ListenerType: This property determines what type of output is generated.
    • -1 = Do not generate output.
    • 0 = Send output, page by page, to a printer driver.
    • 1 = Prepare all pages and send to preview container.
    • 2 = Create output on a page-by-page basis, but do not send to the printer.
    • 3 = Create output for all pages, but do not send to the preview container.
    • 4 = Provide XML output.
    • 5 = Provide HTML output.
  • PrintJobName: This property indicates the name used when the print job is output to the printer queue. It is also used in the title bar of the preview container.
  • QuietMode (default = .f.): This property determines whether the ReportListener may provide any user feedback or user interface.

Here is an example of how to instantiate a ReportListener, change these properties, and then run a report.

LOCAL ox AS ReportListener

ox = CREATEOBJECT('ReportListener')
ox.AllowModalMessages = .t.
ox.DynamicLineHeight = .t.
ox.ListenerType = 0
ox.PrintJobName = 'My Special Report'
ox.QuietMode = .t.

REPORT FORM MyReport OBJECT ox

The above is a simple example of how to change output. The next section discusses some additional ways to change output using the ReportListener subclasses supplied with VFP 9.0.

Supplied Listeners

VFP 9.0 ships a _ReportListener class library within the foundation classes, located in the HOME() + 'FFC' directory. It contains the following ReportListener classes:

  • _ReportListener: This class adds error handling, session handling, and other common report run-time tasks to the ReportListener base class. It provides the ability to chain a series of reports as well as the means to delegate or share output activities to a chain of Listener-successors.
  • UpdateListener: This class provides user feedback while report output is generated.
  • UtilityReportListener: This class adds the ability to handle configuration tables and output target files.
  • DebugListener: This class provides debugging output to help developers understand what happens during an object-assisted report run.
  • XMLListener: This class provides XML output from a report. This is discussed in greater detail in the section titled, XML Output.
  • XMLDisplayListener: This class tunes XML settings suitably for presentation output needs, and adds image-file-publishing capabilities.
  • HTMLListener: This class applies custom specifications, tuned to HTML production, to its parent class' XML generation process. This is discussed in greater detail in the section titled, HTML Output.

These supplied classes can really help you understand how Report Listeners work and can give you a jumpstart on using them.

The next three sections discuss how to use the supplied ReportListener classes to generate XML output, HTML output, and multiple types of output at once.

XML Output

The xmlListener class in the _ReportListener class library of the FFC directory creates XML output from a report. This class has several PEMs that give it a lot of flexibility. Here is a simple example of how to use it.

*-- Make sure the XML file doesn't exist
ERASE MyXMLOutput.XML

*-- Create the Listener class
SET CLASSLIB TO HOME() + 'FFC\_REPORTLISTENER'
ox = CREATEOBJECT('xmlListener')

*-- Set some properties
ox.TargetFileName = 'MyXMLOutput'

*-- Run the report
REPORT FORM accounts OBJECT ox

This example creates an XML file with the RDL information at the top, followed by the data.

HTML Output

Another class included in the _ReportListener class library is one called htmlListener, which outputs a report to HTML. Here is an example of how to use this class.

*-- Make sure the HTML file doesn't exist
ERASE MyHTMLOutput.HTM

*-- Create the Listener class
SET CLASSLIB TO HOME() + 'FFC\_REPORTLISTENER'
ox = CREATEOBJECT('htmlListener')

*-- Set some properties
ox.TargetFileName = 'MyHTMLOutput'

*-- Run the report
REPORT FORM accounts OBJECT ox

Successor Listeners

The _ReportListener class, which resides in the HOME() + '\FFC\_ReportListener' class library, contains code to deal with successor listeners. The concept of successors allows more than one ReportListener object to be chained together at the same time. One of the advantages of this feature is the ability to output two different types of reports simultaneously. For example, a report can be previewed and XML can be generated at the same time. The report does not have to be run twice in order for this to happen. The following code shows how to do this.

*-- Use the FFC ReportListener
SET CLASSLIB TO HOME(1) + 'FFC\_ReportListener'

*-- Define the XML output
loXML = CREATEOBJECT('XMLListener')
loXML.ListenerType = 4
loXML.QuietMode = .t.
loXML.TargetFileName = 'MyXMLOutput'

*-- Define the preview output
loPreview = CREATEOBJECT('_ReportListener')
loPreview.ListenerType = 1 && Preview
loPreview.OutputType = 1 && Preview
loPreview.Successor = loXML

*-- Run the report 
REPORT FORM accounts OBJECT loPreview

As you can see, creating XML output, HTML output, and multiple outputs at once is very easy when you use the supplied ReportListener classes. However, that is not all that can be done. As the next section discusses, you can create your own ReportListener classes to extend the VFP 9.0 Report Writer even further.

Your Listeners

The next several sections discuss how to create your ReportListener classes to solve several different real-world reporting needs. The concept of directives and how to use them to alter the color of text, change the font size of text, and rotate text is explained. You will also learn how to force page breaks when you want them, instead of when the Report Writer wants to. Finally, some additional concepts, such as creating multi-page TIFF files, printing x-up forms, and printing bar charts is discussed.

Figure 22 shows a screenshot of the class browser with the ReportListener classes used in the next several sections. Refer to this to understand the hierarchy of all the classes.

Figure 22. MyReportListeners is a class library containing all the classes needed for the examples in this section.

Your Listener: Directives

Creating a generic listener that can parse directives from the USER field in the FRX table is one way to handle several common reporting tasks. The sample report shown in Figure 23 uses directives to dynamically change font colors, rotate text, and reduce the font size of large descriptions.

Click here for larger image

Figure 23. This example uses the ROTATETEXT directive for the Type column heading, the SQUEEZETEXT directive in the Description column, and the REDNEGATIVE directive for the Balance column. (Click image for larger view.)

The USER field has always been in the FRX, but it was never exposed to developers through the user interface. Using the new Report Builder provided with VFP 9.0, changes this. The USER field can be accessed through the Other page of the Properties dialog box.

To make the concept of directives work, a few rules need to be followed. The first rule is how directives are entered into the USER field of the report. A consistent pattern must be established so it can be parsed. The following pattern is used in these examples:

*:LISTENER||<directivename>||<Parameter1>||<Parameter2>>

Start with *:LISTENER to identify the line as a directive. Next, use the delimiter characters of double-bars (||), followed by the name of the directive. Optionally, follow with another delimiter and the first parameter, another delimiter and the second parameter, and so on until there are no more parameters.

Next, a new class based on the custom class needs to be created for each directive. Follow a naming convention for the classes that mimics the name of the directive. In these examples, all directive classes are named, MyDirectives_, followed by the name of the directive. These classes are called by the ReportListener class when appropriate, and they do the brunt of the special processing. Whenever a new directive is needed, simply create a new directive class. The ReportListener class does not need to be modified because it is designed to be generic.

Finally, a ReportListener class needs to be created. It needs to iterate through the objects on a report looking for directives. Each time it finds a directive, it needs to instantiate the appropriate custom directive class and link it to the object. The ReportListener class then needs to have code in various methods to call the appropriate method in the custom directive classes to do the special processing.

Abstract ReportListener class

Create a new class, called MyReportListener, based on the VFP 9.0 ReportListener base class. This class is going to contain some methods that are needed by different ReportListener classes that may be developed in the future. Therefore, it is best to create an abstract class that all the rest of the ReportListener classes are subclassed from. Add code to the Init() method, the BeforeBand() method, and create a new method, called GetFRXRecord().

Init() method

In the Init() method of the abstract ReportListener class, take advantage of two classes in the HOME() + 'FFC' directory. These classes are used later on and save you from having to do a lot of coding.

*-- MyReportListener::Init()
DODEFAULT()

*-- Create a graphics object for later use
This.AddProperty('ogpGraphics', ;
   NEWOBJECT('gpGraphics', HOME() + 'FFC\_GDIPlus'))

*-- Create the FRXCursor object for later use
This.AddProperty('oFRXCursor', ;
   NEWOBJECT('frxCursor', HOME() + 'FFC\_FRXCursor'))

In the BeforeBand() method, add code to set the graphics handle used in GDI+ calls.

*-- MyReportListener::BeforeBand()
LPARAMETERS nBandObjCode, nFRXRecno

DODEFAULT(nBandObjCode, nFRXRecNo)

* Because the GDI+ plus handle changes on every page, we need to set the 
* handle for our GPGraphics object.
This.ogpGraphics.SetHandle(This.GDIPlusGraphics)

Next, create a new method.

GetFRXRecord() method

There are several occasions where the FRX record of the object being printed needs to be referenced. Create a new method, called GetFRXRecord(), to switch to the appropriate datasession, scatter the FRX record to an object, restore the datasession, and then return the object to the caller.

*-- MyReportListener::GetFRXRecord()
LPARAMETERS pnFRXRecNo

LOCAL lnSession, loFRX

*-- Switch to the FRX
lnSession = SET("Datasession")
SET DATASESSION TO This.FRXDataSession

*-- Goto the record
GOTO pnFRXRecNo

*-- Get the data
SCATTER MEMO NAME loFRX 

*-- Restore the datasession
SET DATASESSION TO lnSession

*-- Return the data
RETURN loFRX

ReportListener_Directives class

Create a new class, called MyReportListener_Directives, and base it on the abstract MyReportListener class. Next, add some new properties and add code to several methods.

Properties

Add seven new properties to the MyReportListener_Directives class.

  • aFRXRecords[1]: This one-dimensional array eventually gets re-dimensioned with the same number of rows to match the total number of records in the FRX table. For any record with directives, a collection is created in the array to hold references to each custom directive class needed.

  • cDelimiter (default to ||): This property is used to identify the delimiter characters used in the directive in the USER field of a report object. By adding this as a property, instead of hard-coding, the delimiter can easily be changed.

    **Caution   **Do not use a delimiter character that is common to VFP commands and expressions. For example, do not use a comma.

  • lRender (default to .t.): This property can be used to suppress the actual rendering of an object, if needed.

  • nAdjustHeight (default to 0): This property can be used to manipulate the height of an object during the Render() method.

  • nAdjustLeft (default to 0): This property can be used to manipulate the left position of an object during the Render() method.

  • nAdjustTop (default to 0): This property can be used to manipulate the top position of an object during the Render() method.

  • nAdjustWidth (default to 0): This property can be used to manipulate the width of an object during the Render() method.

Now that the properties are defined, create some new methods and add code to some existing methods.

GetFRXDirectives() method

Create a new method, called GetFRXDirectives(). This method retrieves all the directives for a given FRX record, parses the information, creates a collection object, and then instantiates the applicable custom directive class for each directive. The name of the custom directive class to instantiate is derived from the directive in the FRX. An object reference to the directive class is added to the collection, along with any additional parameters following the directive.

*-- MyReportListener_Directives::GetFRXDirectives()

*-- Get the FRX Directives of the object
LPARAMETERS tnFRXRecNo

LOCAL    loFRX, ;
      lnLines, ;
      laLines, ;
      ln, ;
      lcText, ;
      loObj, ;
      llSuccess, ;
      lnSession, ;
      lnWordCount, ;
      lnWord

*-- Get the FRX record
loFRX = This.GetFRXRecord(tnFRXRecNo)

*-- Set the data session so the directive 
*-- objects get created in the same data 
*-- session as the report data
lnSession = SET("Datasession")
SET DATASESSION TO This.CurrentDataSession

*-- Process the USER field looking for directives
DIMENSION laLines[1]
lnLines = ALINES(laLines, loFRX.USER)
IF lnLines > 0
   FOR ln = 1 TO lnLines

      lnWordCount = GETWORDCOUNT(laLines[ln], This.cDelimiter)
      IF lnWordCount >= 2 AND ;
            GETWORDNUM(laLines[ln], 1, This.cDelimiter) == '*:LISTENER'

         *-- Add a collection to the array
         *-- if it isn't already there
         IF VARTYPE(This.aFRXRecords[tnFRXRecNo]) <> 'O'
            This.aFRXRecords[tnFRXRecNo] = CREATEOBJECT('Collection')
         ENDIF
      
         *-- Parse out the directive and try to add the object
         lcDirective = GETWORDNUM(laLines[ln], 2, This.cDelimiter)
         llSuccess = .t.
         TRY
            This.aFRXRecords[tnFRXRecNo].Add( ;
               CREATEOBJECT('MyDirectives_' + lcDirective, This))
         CATCH
            llSuccess = .f.
         ENDTRY
         IF NOT llSuccess
            LOOP
         ENDIF      
      
         *-- Get the object just created
         loObj = This.aFRXRecords[tnFRXRecNo]

         *-- Parse out the parameters
         IF lnWordCount >= 3

            DIMENSION loObj[loObj.Count].aParameters[lnWordCount-2]
            FOR lnWord = 3 TO lnWordCount
               loObj[loObj.Count].aParameters[lnWord-2] = ;
                  GETWORDNUM(laLines[ln], lnWord, This.cDelimiter)
            ENDFOR
         ENDIF

         *-- Process any additional INIT code for the directive
         loObj[loObj.Count].AdditionalInit()

      ENDIF
   ENDFOR         

   *-- If an empty collection exists, get rid of it
   IF VARTYPE(This.aFRXRecords[tnFRXRecNo]) = 'O' AND ;
         This.aFRXRecords[tnFRXRecNo].Count = 0
      This.aFRXRecords[tnFRXRecNo] = .f.
   ENDIF

ENDIF

*-- Restore the datasession
SET DATASESSION TO lnSession

BeforeReport() method

After defining the GetFRXDirectives() method, add to the BeforeReport() method to figure out how many records are in the FRX, dimension the new array property, and call the GetFRXDirectives() method for each FRX record.

*-- MyReportListener_Directives::BeforeReport()

LOCAL lnSession

*-- Switch to the FRX
lnSession = SET('DATASESSION')
SET DATASESSION TO This.FRXDataSession

*-- Create the array
DIMENSION This.aFRXRecords[RECCOUNT()]
STORE .f. TO This.aFRXRecords

*-- Look for directives
GOTO TOP
SCAN
   This.GetFRXDirectives(RECNO())
ENDSCAN

*-- Restore the datasession
SET DATASESSION TO lnSession

AdjustObjectSize() method

The next method that needs code is the AdjustObjectSize() method. Various directives need to perform special actions in this method. Therefore, this method needs to call a related method in the custom directive class to do the special processing.

*-- MyReportListener_Directives::AdjustObjectSize()
LPARAMETERS tnFRXRecno, toObjProperties

LOCAL loObj, lcExec, loFRX

*-- Process the directives, if applicable
IF VARTYPE(This.aFRXRecords[tnFRXRecNo]) <> 'L'

   *-- Get the FRX data
   loFRX = This.GetFRXRecord(tnFRXRecNo)

   FOR EACH loObj IN This.aFRXRecords[tnFRXRecNo]
      loObj.DoAdjustObjectSize(tnFRXRecNo, toObjProperties, loFRX)
   ENDFOR
   
ENDIF

EvaluateContents() method

The next method that needs code is the EvaluateContents() method. Various directives need to perform special actions in this method. Therefore, this method needs to call a related method in the custom directive class to do the special processing.

*-- MyReportListener_Directives::EvaluateContents()
LPARAMETERS tnFRXRecno, toObjProperties

LOCAL loObj, lcExec, loFRX

*-- Process the directives, if applicable
IF VARTYPE(This.aFRXRecords[tnFRXRecNo]) <> 'L'

   *-- Get the FRX data
   loFRX = This.GetFRXRecord(tnFRXRecNo)

   FOR EACH loObj IN This.aFRXRecords[tnFRXRecNo]
      loObj.DoEvaluateContents(tnFRXRecNo, toObjProperties, loFRX)
   ENDFOR
   
ENDIF

Render() method

Various directives need to perform special actions in the Render() method. However, it is not quite as easy as the EvaluateContents() method. The actual rendering cannot be done by the custom directive class. It must be done by this method. Therefore, this method calls a DoBeforeRender() method in the custom directive class, then it renders the object if needed, then it calls a DoAfterRender() method in the custom directive class. It is also important to note that NODEFAULT is used in this method so the object does not render twice.

*-- MyReportListener_Directives::Render()
LPARAMETERS tnFRXRecno, tnLeft, tnTop, ;
   tnWidth, tnHeight, ;
   tnObjectContinuationType, ;
   tcContentsToBeRendered, tGDIPlusImage

LOCAL loObj

*-- Process the directives, if applicable
IF VARTYPE(This.aFRXRecords[tnFRXRecNo])<>'L'

   *-- Do the Before Render code
   FOR EACH loObj IN This.aFRXRecords[tnFRXRecNo]
      loObj.DoBeforeRender( ;
            tnFRXRecno, tnLeft, tnTop, ;
            tnWidth, tnHeight, ;
            tnObjectContinuationType, ;
            tcContentsToBeRendered, ;
            tGDIPlusImage)
   ENDFOR
   
   *-- Render the object now
   IF This.lRender
      ReportListener::Render(tnFRXRecno, ;
         tnLeft + This.nAdjustLeft, ;
         tnTop + This.nAdjustTop, ;
         tnWidth + This.nAdjustWidth, ;
         tnHeight + This.nAdjustHeight, ;
         tnObjectContinuationType, ;
         tcContentsToBeRendered, ;
         tGDIPlusImage)
   ENDIF
      
   *-- Do the After Render code
   FOR EACH loObj IN This.aFRXRecords[tnFRXRecNo]
      loObj.DoAfterRender( ;
         tnFRXRecno, tnLeft, tnTop, ;
         tnWidth, tnHeight, ;
         tnObjectContinuationType, ;
         tcContentsToBeRendered, ;
         tGDIPlusImage)
   ENDFOR

   *-- Suppress the normal behavior
   NODEFAULT
      
ENDIF

Abstract Directive Class

After creating the MyReportListener_Directives class, the next step is to create the custom directive classes. This is where all the special processing takes place. Create a new class called MyDirectives, and base it on the VFP Custom base class. Next, add a few properties and add code to a few methods.

Properties

Three properties need to be added to the abstract class.

  • aParameters[1]: This array holds the parameters that apply to a directive. For example, if the USER field of a report contains *:LISTENER||ROTATETEXT||-90, this property contains one row with a value of -90. If additional parameters were indicated, this array would contain additional rows and additional values.
  • nSaveGraphicsHandle (default to 0): This contains a value set by special GDI+ calls. It gets set in the DoBeforeRender() method and is used by the DoAfterRender() method to reset things.
  • oListener: This contains an object reference back to the ReportListener object.

Init() method

The Init() method gets passed a reference to the ReportListener object. This method needs to save this reference to the oListener property for later use in other methods.

*-- MyDirectives::Init()
LPARAMETERS toListener

*-- Remember the Listener object
This.oListener = toListener

ConvertFontStyleToCodes() method

The EvaluateContents() method allows font properties to be changed. However, it references font styles with a numeric value instead of a string of the font style codes. Create a new method, called ConvertFontStyleToCodes(), to handle this conversion.

*-- MyDirectives::ConvertFontStyleToCodes()

*-- Convert FontStyle from numeric value
*-- to character codes
LPARAMETERS tnFontStyle

LOCAL lcStyle
lcStyle = ''
IF BITTEST(tnFontStyle, 0)
   lcStyle = lcStyle + 'B'
ENDIF
IF BITTEST(tnFontStyle, 1)
   lcStyle = lcStyle + 'I'
ENDIF
IF BITTEST(tnFontStyle, 2)
   lcStyle = lcStyle + 'U'
ENDIF
IF BITTEST(tnFontStyle, 7)
   lcStyle = lcStyle + 'S'
ENDIF
IF EMPTY(lcStyle)
   lcStyle = 'N'
ENDIF

RETURN lcStyle

ConvertRGBToGDI() method

When working with GDI+, it references colors differently than VFP does. Therefore, create a method, called ConvertRGBToGDI(), on this abstract class to do the conversion.

*-- MyDirectives::ConvertRGBToGDI()
LPARAMETERS tnAlpha, tnRed, tnGreen, tnBlue

RETURN (0x1000000 * tnAlpha) + ;
   (0x10000 * tnRed) + ;
   (0x100 * tnGreen) + ;
   tnBlue

Empty methods

In the abstract class, create some methods that get called by the ReportListener class. No code is required at the abstract level, other than a parameter statement in four of them. Use the following parameter statements for each method.

  • DoAdjustObjectSize()

    LPARAMETERS tnFRXRecno, toObjProperties, toFRX

  • DoAfterRender()

    LPARAMETERS tnFRXRecNo, tnLeft, tnTop, ;
    tnWidth, tnHeight, ;
    tnObjectContinuationType, ;
    tcContentsToBeRendered, ;
    tGDIPlusImage

  • DoBeforeRender()

    LPARAMETERS tnFRXRecNo, tnLeft, tnTop, ;
    tnWidth, tnHeight, ;
    tnObjectContinuationType, ;
    tcContentsToBeRendered, ;
    tGDIPlusImage

  • DoEvaluateContents()

    LPARAMETERS tnFRXRecNo, toObjProperties, toFRX

  • AdditionalInit()

    (No parameter statement is needed – leave this method empty)

Not all directives need all these methods, but the abstract class needs to have them defined so the ReportListener can make the calls, whether or not they do anything. The abstract directive class is now done and you can create the directive classes that are sub-classed from this abstract class. These are the classes that actually process a given directive.

RedNegative Directive class

To make negative numbers turn red, create a new directive class, called MyDirectives_RedNegative. Base this class on the abstract directive class and add code to the DoEvaluateContents() method.

DoEvaluateContents()

This method starts by evaluating the expression about to be printed. It then changes the pen color to red if the value is negative. It also changes the ReLoad property to tell VFP something has been changed and VFP needs to reload the properties before rendering.

*-- MyDirectives_RedNegative::DoEvaluateContents()

*-- Use red if the value is negative, 
*-- otherwise, use black
LPARAMETERS tnFRXRecNo, toObjProperties, toFRX

LOCAL llNegative

DODEFAULT(tnFRXRecNo, toObjProperties, toFRX)

*-- Is this negative?
TRY
   llNegative = (VAL(toObjProperties.Text) < 0)
CATCH
   llNegative = .f.
ENDTRY

*-- Set the color
IF llNegative
   toObjProperties.PenRed = 255
   toObjProperties.PenGreen = 0
   toObjProperties.PenBlue = 0

   toObjProperties.Reload = .T.
ENDIF

To use this directive, enter *:LISTENER||REDNEGATIVE in the USER field of any applicable layout object.

SqueezeText Directive class

To reduce the font size of text to make it fit in the defined area, create a new directive class, called MyDirectives_SqueezeText. Base this class on the abstract directive class and add code to the DoEvaluateContents() method.

DoEvaluateContents()

This method checks to see if the text being printed fits in the defined area. If not, the font size is reduced by one and it checks again. It keeps checking until it finds a font that allows all the text to be printed in the defined area. A minimum font size of 4 is honored to prevent text from printing too small, but this may be overridden with a parameter in the directive.

*-- MyDirectives_SqueezeText::DoEvaluateContents()

*-- If the text won't fit, squish the font 
*-- to a smaller size
LPARAMETERS tnFRXRecNo, toObjProperties, toFRX

DODEFAULT(tnFRXRecNo, toObjProperties, toFRX)

LOCAL lnSmallest, lnFontSize, lnWidth, ;
   lnMaxWidth, lcText, lnDecimals, ;
   lcStyle

*-- What is the smallest font size to use 
*-- (default to 4)?
lnSmallest = IIF(EMPTY(This.aParameters[1]), ;
   4, VAL(This.aParameters[1]))

*-- Prep for loop
lcStyle = This.ConvertFontStyleToCodes( ;
   toObjProperties.FontStyle)
lnMaxWidth = toFRX.Width && In FRUs
lnFontSize = toObjProperties.FontSize
lnDecimals = SET("Decimals")
SET DECIMALS TO 3

*-- Add an extra character to make sure it 
*-- fits. Otherwise, it can be just a tiny 
*-- bit too close and doesn't squish 
*-- correctly.
lcText = toObjProperties.Text + 'X'

*-- Change the font, if necessary
DO WHILE .t.

   *-- If this is the smallest font, get out
   IF lnFontSize <= lnSmallest
      EXIT
   ENDIF

   *-- Using lnFontSize, how wide would the
   *-- text be (in FRUs)?
   lnWidth = This.oListener.oFRXCursor.GetFRUTextWidth(lcText, ;
         toObjProperties.FontName, lnFontSize, lcStyle )

   *-- If the text fits with this font, 
   *-- get out
   IF lnWidth <= lnMaxWidth
      EXIT
   ENDIF

   *-- Reduce the font size
   lnFontSize = lnFontSize-1 
   
ENDDO   

*-- Change the font, if needed
IF toObjProperties.FontSize <> lnFontSize
   toObjProperties.FontSize = lnFontSize
   toObjProperties.Reload = .T.
ENDIF

*-- Restore decimals
SET DECIMALS TO &lnDecimals

To use this directive, enter *:LISTENER||SQUEEZETEXT in the USER field of any applicable layout object. A minimum font size of 4 is already built into the directive class. To enforce a different minimum font size, enter it as a parameter following the directive. For example, *:LISTENER||SQUEEZETEXT||6, enforces a minimum font size of 6.

RotateText Directive class

To rotate text, create a new directive class, called MyDirectives_RotateText. Base this class on the abstract directive class and add code to the DoBeforeRender() and DoAfterRender() methods.

DoBeforeRender() method

The DoBeforeRender() method gets the rotation value from a parameter of the directive. It then makes several calls to the GDI+ classes to set values needed by the Render() method in the ReportListener class.

*-- MyDirectives_RotateText::DoBeforeRender()

*-- Rotate Text
LPARAMETERS tnFRXRecNo, tnLeft, tnTop, ;
   tnWidth, tnHeight, ;
   tnObjectContinuationType, ;
   tcContentsToBeRendered, ;
   tGDIPlusImage

DODEFAULT(tnFRXRecNo, tnLeft, tnTop, ;
   tnWidth, tnHeight, ;
   tnObjectContinuationType, ;
   tcContentsToBeRendered, ;
   tGDIPlusImage)

LOCAL   lnX, ;
      lnY, ;
      lnRotate, ;
      lnHandle
      
*-- What's the rotation
lnRotate = VAL(This.aParameters[1])

*-- Rotate if needed
IF lnRotate <> 0         

   * get appropriate versions of coords
   lnX = tnLeft
   lnY = tnTop

   * save the current state of the graphics handle
   lnHandle = 0
   This.oListener.ogpGraphics.Save(@lnHandle)
   This.nSaveGraphicsHandle = lnHandle
   
   * now move the 0,0 point to where we'd like it to be so that when 
   * we rotate we're rotating around the appropriate point
   This.oListener.ogpGraphics.TranslateTransform(lnX, lnY, 0)
   
   * now change the angle at which the draw will occur
   This.oListener.ogpGraphics.RotateTransform(lnRotate, 0)

   * restore the 0,0 point
   This.oListener.ogpGraphics.TranslateTransform(-lnX, -lnY, 0)

ENDIF

DoAfterRender() method

After the object is rendered, the DoAfterRender() method is called. In this method, the GDI+ settings are reset so the next object is rendered correctly. Otherwise, all the rest of the objects on the report get rotated as well.

*-- MyDirectives_RotateText::DoAfterRender()

*-- Rotate Text
LPARAMETERS tnFRXRecNo, tnLeft, tnTop, ;
   tnWidth, tnHeight, ;
   tnObjectContinuationType, ;
   tcContentsToBeRendered, ;
   tGDIPlusImage

DODEFAULT(tnFRXRecNo, tnLeft, tnTop, ;
   tnWidth, tnHeight, ;
   tnObjectContinuationType, ;
   tcContentsToBeRendered, ;
   tGDIPlusImage)

* Put back the state of the graphics handle 
This.oListener.ogpGraphics.Restore(This.nSaveGraphicsHandle)

To use this directive, enter *:LISTENER||ROTATETEXT||nnn in the USER field of any applicable layout object, where nnn represents the degrees of rotation. A positive number rotates clockwise, and a negative number rotates counter-clockwise. For example, *:LISTENER||ROTATETEXT||-90 rotates the text counter-clockwise 90 degrees.

Creating the Report

To use the three directives just created, create a new report or modify an existing report.

RedNegative directive

Add the following directive to the USER field of one of the numeric fields on the report:

*:LISTENER||REDNEGATIVE

SqueezeText directive

Add the following directive to the USER field of a text field that has a lot of information, such as a description field. Make sure the field is sized narrower than some of the larger descriptions.

*:LISTENER||SQUEEZETEXT

RotateText directive

Add the following directive to the USER field of one of the objects to be rotated.

*:LISTENER||ROTATETEXT||-90

Running the Report

The ReportListener class is defined, all the custom directive classes are defined, so the only thing left to do is run the report. Use the following code to instantiate the ReportListener class and preview the report.

SET CLASSLIB TO MyReportListeners
ox=NEWOBJECT('MyReportListener_Directives', 'MyReportListeners')
ox.ListenerType = 1 && Preview
REPORT FORM accounts OBJECT ox

Your Listener: Pagination

VFP offers the option to start each data group on a new page. However, it does not offer the option to conditionally start a data group on a new page if the entire data group set does not fit on the rest of the page. Along the same lines, VFP does not offer the option to keep an entire detail band on the same page when stretchable fields are involved.

These two problems can both be solved with VFP 9.0 and some ingenious manipulation. The trick to making this work is shape objects. The ReportListener class has a method, called AdjustObjectSize(), which is called for all shapes. In this method, the height of the object may be altered. Knowing this, a shape can strategically be placed on the report just prior to the set of information that needs to stay on the same page. This shape can programmatically be expanded to fill the rest of the page when the set of information does not fit. By having the shape fill the rest of the page, the information that needs to stay together automatically gets pushed to the next page.

Another key to making this concept work is that the report needs to be preprocessed to figure out how much room is needed for each set of information that needs to stay together. Once the preprocess pass is completed, run the report for real, using the information gathered during the preprocess pass.

The Report Definition

Start by designing a report as needed, including all the bands, objects, and report variables. Once the report is complete, add the data groups, shapes, and directives needed for pagination.

Data Groups

The first special item needed for pagination is data groups. Wrap the set of information that needs to stay together with two data groups. Figure 24 shows an example of wrapping the detail band with the two data groups.

Click here for larger image

Figure 24. Shapes and data groupsgroups are used to control pagination. (Click image for larger view.)

The top-level data group is based on an expression that gets evaluated at run time. This expression either evaluates to a value that identifies the set of information that needs to stay together, RECNO() in this situation, or it evaluates to nothing. During the preprocess pass of running this report, this expression evaluates to RECNO() so every detail band prints on a new page. This is needed to figure out the height of the entire detail band. During the real pass of this report, the value of this expression is blank, therefore, it does not affect the pagination of the report.

The second data group added to the report is based on the set of information that needs to stay together. When keeping an entire detail band together on a page, use RECNO() as the expression for this data group. The data group header band and data group footer band created by this second data group is used to hold shape objects used by the ReportListener class.

To keep an entire data group set together on a page, meaning the data group header, the detail bands, and the data group footer, follow the same steps just described. Add two new data groups above the regular data group, which means a total of three data groups exist on the report, as shown in Figure 25.

Click here for larger image

Figure 25. Use three data groups to help keep an entire data group set on one page. (Click image for larger view.)

Shape Objects

Once the data groups are defined, the next step is to create three shape objects used as triggers in the ReportListener class. The first shape object is placed in the second data group header band. The second shape object is placed in the second data group footer band. The third shape object is placed at the top of the page footer band. Create these shapes with a height of .02 inches and a Forecolor of red.

During the preprocess pass of the report, the position of the shape object in the data group header and the shape object in the data group footer is saved in a cursor for each set of data. The value of the object in the page footer only needs to be saved once, so it is saved to a property.

During the second pass of the report, the values saved in the cursor and the property are referenced to determine if there is enough room left on the page to print each new set of data. If there is not enough room, the shape object in the data group header is manipulated so it takes up the rest of the page. Thus, when the data starts to print, it automatically starts on the next page.

Now that the data groups are defined and the shape objects have been added, the final step required on the report is to add directives to various objects so the ReportListener knows what to do.

Directives

The final step in the creation of the report is to add the directives to the USER field of each of the shape objects.

  • Shape object in the data group header:

    *:LISTENER||PAGINATION||START||1
    

  • Shape object in the data group footer:

    *:LISTENER||PAGINATION||END||1
    

  • Shape object in the page footer:

    *:LISTENER||PAGINATION||PAGEFOOTER
    

Notice the first two shapes use two parameters after the name of the directive. The first parameter identifies which type of pagination record this is. The second parameter identifies which set these records belong to by assigning the same integer value to the START and END records. Theoretically, this means more than one set of pagination directives may be used on a report. The PAGEFOOTER directive does not need a second parameter because it applies to the entire report.

ReportListener_Pagination class

Create a new class, called MyReportListener_Pagination, and base it on the MyReportListener_Directives class. Next, add some properties and put code in one method.

Properties

Two new properties need to be added to this class:

  • lPreProcess (default to .f.): This property is used as a flag to determine whether the report is being run as the first preprocess pass, or the second real pass.
  • nPageFooterPos (default to 0): This property is used to hold the position of the page footer, used in the calculations for determining whether there is still enough printable room on the page.

These are the only two properties needed. Now add some method code.

BeforeReport() method

Besides all the code inherited from the MyReportListener_Directive class, this class needs code in one more method, BeforeReport(). It needs to create the cursor used to store positions of the shape objects.

*-- MyReportListener_Pagination::BeforeReport()
DODEFAULT()

IF NOT This.lPreProcess
   RETURN
ENDIF

LOCAL lnSession, lcAlias

*-- Make sure the right datasession is set
lcAlias = ALIAS()
lnSession = SET('DATASESSION')
SET DATASESSION TO This.CurrentDataSession

*-- Build the temp cursor
CREATE CURSOR tmpPagination ;
   (nFRXRecNo I, nCounter I, cType C(1), nSet I, nPage I, nPos I)
INDEX ON BINTOC(nFRXRecNo) + BINTOC(nCounter) TAG FRXCounter
INDEX ON cType + BINTOC(nSet) + BINTOC(nCounter) TAG TypeSetCtr ADDITIVE

*-- Restore the data session
SET DATASESSION TO lnSession
SELECT (lcAlias)

Pagination Directive class

Create a new class, called MyDirectives_Pagination, based on the MyDirectives class. Next, add a few properties and code to a few methods.

Properties

Three new properties need to be added to the abstract class.

  • cPaginationType (default to ""): This is used to identify which type of pagination record this is.
  • lAddHeight (default to .f.): This determines if the height of the shape object is added to the starting position when it is saved in the cursor.
  • nCounter (default to 0): This is a counter used to uniquely identify each set of data being printed. For example, when the detail band has been wrapped with the special Start and End shapes, this counts each detail band. When a data group has been wrapped, this represents the number of data group sets processed.

AdditionalInit() method

The AdditionalInit() method needs code to set some properties, based on the parameters used in the directive statement.

*-- MyDirectives_Pagination::AdditionalInit()

*-- Set some properties
DO CASE
   CASE This.aParameters[1] == 'START'
      This.cPaginationType = 'S'
      THis.lAddHeight = .f.
      
   CASE This.aParameters[1] == 'END'
      This.cPaginationType = 'E'
      This.lAddHeight = .t.

   CASE This.aParameters[1] == 'PAGEFOOTER'
      This.cPaginationType = 'F'
      This.lAddHeight = .t.
ENDCASE

DoAdjustObjectSize() method

The DoAdjustObjectSize() method needs code to conditionally alter the size of the shape object during the real pass. First, the START record is looked for in the cursor. Next, the END record is looked for in the cursor. Once those two are found, the code calculates the amount of room needed for this set and compares it to the amount of room left on the page. If it will not fit, the height of this shape object is adjusted to be equal to the amount of room left on the page.

*-- MyDirectives_Pagination::DoAdjustObjectSize()
LPARAMETERS tnFRXRecno, toObjProperties, toFRX

LOCAL loObj, lnStartPage, lnStartPos, ;
   lnSet, lnEndPage, lnEndPos, lcCounter

*-- If this isn't the START record, get out
*-- If this is the preprocess pass, get out
*-- If no PageFooter position saved, get out
IF NOT This.cPaginationType == 'S' OR ;
      This.oListener.lPreProcess OR ;
      This.oListener.nPageFooterPos = 0
   RETURN 
ENDIF

*-- Set the counter 
*-- NOTE: I have to add 1 because the 
*-- render method is what sets it and
*-- that hasn't been called yet.
lcCounter = BINTOC(This.nCounter + 1)

*-- Find the starting position
IF SEEK(BINTOC(tnFRXRecNo) + lcCounter, ;
      'tmpPagination', 'FRXCounter')
   lnStartPage = tmpPagination.nPage
   lnStartPos = tmpPagination.nPos
   lnSet = tmpPagination.nSet
ELSE
   *-- Get out if can't find it
   RETURN
ENDIF

*-- Find the ending position
IF SEEK('E' + BINTOC(lnSet) + lcCounter, ;
      'tmpPagination', 'TypeSetCtr')
   lnEndPage = tmpPagination.nPage
   lnEndPos = tmpPagination.nPos
ELSE
   *-- Get out if can't find it
   RETURN
ENDIF

*-- If the set spans more than one page, 
*-- don't bother going any further
IF NOT lnStartPage = lnEndPage
   RETURN
ENDIF

*-- If it won't fit, increase the height of 
*-- the shape to fill up the rest of the page
IF toObjProperties.Top + lnEndPos ;
      - lnStartPos > ;
      This.oListener.nPageFooterPos
   toObjProperties.Height = ;
      (This.oListener.nPageFooterPos-;
      toObjProperties.Top)
   toObjProperties.Reload = .t.
ENDIF

DoBeforeRender() method

The DoBeforeRender() method needs code to increase the counter, and then save the position of the shape in the temporary cursor or the applicable property. The counter is increased in the preprocess pass and the real pass. The cursor is only updated during the preprocess pass. The final step in this method is to set the lRender property to .f. so the shape object is not actually rendered.

*-- MyDirectives_Pagination::DoBeforeRender()
LPARAMETERS tnFRXRecno, tnLeft, tnTop, ;
         tnWidth, tnHeight, ;
         tnObjectContinuationType, ;
         tcContentsToBeRendered, ;
         tGDIPlusImage

IF This.cPaginationType == 'F'
   *-- PAGEFOOTER

   *-- If this is the preprocess pass and the first page, 
   *-- save the position of this record
   IF This.oListener.lPreProcess AND This.oListener.PageNo = 1
      This.oListener.nPageFooterPos = ;
         tnTop + IIF(This.lAddHeight, tnHeight, 0)
   ENDIF

ELSE
   *-- START and END

   *-- Increase the counter for this record
   *-- or mark the position as saved
   This.nCounter = This.nCounter + 1 

   *-- If this is the preprocess pass, 
   *-- save the position of this record
   IF This.oListener.lPreProcess

      INSERT INTO tmpPagination VALUES ( ;
         tnFRXRecNo, ;
         This.nCounter, ;
         This.cPaginationType, ;
         VAL(This.aParameters[2]), ;
         This.oListener.PageNo, ;
         tnTop + ;
         IIF(This.lAddHeight, tnHeight, 0);
         ) 

   ENDIF

ENDIF

*-- Don't bother rendering the shape object
*-- because it's just a place holder
This.oListener.lRender = .f.

Running the Report

Use the following code to instantiate the ReportListener class and preview the report. First, it instantiates the ReportListener class. Then it sets some properties on the ReportListener object and runs the preprocess pass of the report. Next, it changes some of the properties and runs the real pass of the report.

SET REPORTBEHAVIOR 90

SET CLASSLIB TO MyReportListeners
LOCAL ox AS ReportListener
ox = CREATEOBJECT('MyReportListener_Pagination')

*-- Do the first pass to calculate group heights
ox.lPreProcess = .t.
ox.ListenerType = -1 
ox.QuietMode = .t.
ox.AddProperty('cGroupPageBreak', 'customer_id')
WAIT WINDOW 'Preprocessing report...' NOWAIT
REPORT FORM Orders_KeepGroupTogether OBJECT ox

*-- Do the real pass
ox.lPreProcess = .f.
ox.ListenerType = 1 && Preview
ox.QuietMode = .f.
ox.cGroupPageBreak = '""'
REPORT FORM Orders_KeepGroupTogether OBJECT ox

It took a lot of coding to get to this point, but now that the classes are done, it is quite simple to reuse them in additional reports.

Your Listener: TIFF Output

Creating a TIFF file is quite simple using a ReportListener. This section shows how to create a ReportListener that can output a report to a multi-page TIFF file, or to individual files for each page.

**Note   **The performance of multi-page TIFF files can be poor on large reports consisting of 20 or more pages. You may want to experiment with outputting separate TIFF files and then combining them with GDI+ calls.

ReportListener_TIFF class

Create a new class, called MyReportListener_TIFF, based on the MyReportListener class. Next, add a few properties and some code in a few methods.

Properties

One property needs to be set and two new properties need to be added.

  • ListenerType: Set this existing property to 2.
  • cFileName: Create this property to hold the name of the output file. A default value is optional.
  • lCombinePages (default to .t.): This property determines whether one multi-page TIFF file is created for all the pages, or whether individual TIFF files are created for each page.

The next step is to create code in two methods.

OutputPage() Method

The OutputPage() method needs to be overridden with code to output the page to a TIFF file. The code needs to decide whether to output one TIFF file or individual files for each page. Also, the default behavior needs to be suppressed.

*-- MyReportListener_TIFF::OutputPage()
LPARAMETERS tnPageNo, teDevice, tnDeviceType, ;
   tnLeft, tnTop, tnWidth, tnHeight, ;
   tnClipLeft, tnClipTop, tnClipWidth, tnClipHeight

#DEFINE OutputNothing -1
#DEFINE OutputTIFF 101
#DEFINE OutputTIFFAdditive (OutputTIFF+100)

LOCAL lcFileName

*-- Generate TIFF output, if applicable
IF tnDeviceType == OutputNothing

   DO CASE
      CASE This.lCombinePages AND tnPageNo = 1
         *-- First page, combined file
         tnDeviceType = OutputTIFF
         lcFileName = This.cFileName

      CASE This.lCombinePages
         *-- Subsequent pages, combined file
         tnDeviceType = OutputTIFFAdditive
         lcFileName = This.cFileName
         
      OTHERWISE
         *-- Individual files
         tnDeviceType = OutputTIFF
         lcFileName = This.cFileName + TRANSFORM(tnPageNo, '@L 9999')
   ENDCASE

   *-- Output the page
   THIS.OutputPage(tnPageNo, lcFileName, tnDeviceType)

   *-- Suppress the default behavior
   NODEFAULT
ENDIF

ShowFile() method

A new method, called ShowFile(), may be created to show the generated TIFF file(s). This can be called by the program that generated the report to display it to the user. This step is not required to generate TIFF files and is only included so you can easily view the TIFF files created by this example.

*-- MyReportListener_TIFF::ShowFile()
LPARAMETERS tnPageNo

DECLARE INTEGER ShellExecute ;
   IN SHELL32.DLL ;
   INTEGER nWinHandle,;
   STRING cOperation,;
   STRING cFileName,;
   STRING cParameters,;
   STRING cDirectory,;
   INTEGER nShowWindow

LOCAL lcFileName, lnPage
IF VARTYPE(tnPageNo) = 'N' AND tnPageNo > 0
   *-- Individual pages
   lcFileName = This.cFileName + TRANSFORM(tnPageNo, '@L 9999') + '.TIF'
ELSE
   *-- One combined file
   lcFileName = This.cFileName + '.TIF'
ENDIF

ShellExecute(0,"Open", lcFileName, "", "", 1)

CLEAR DLLS ShellExecute

Running the Report

Once this simple class has been created, run the report with the following code. It creates the ReportListener object, sets the file name, runs the report, and then shows the output to the user. The following code shows an example of creating a multi-page TIFF file and another example of creating individual files for each page.

*-- Create the ReportListener class
SET CLASSLIB TO MyReportListeners
ox = CREATEOBJECT('MyReportListener_TIFF')

*-- Set some properties
ox.cFileName = 'Sample'

*-- Run the report (combined pages)
REPORT FORM accounts OBJECT ox RANGE 1,5 && limit to 5 pages

*-- Run the report (individual pages)
ox.lCombinePages = .f.
REPORT FORM accounts OBJECT ox RANGE 1,5 && limit to 5 pages

*-- Show the multi-page TIFF file
ox.ShowFile()

*-- Show the individual files
FOR ln = 1 TO 5
   ox.ShowFile(ln)
ENDFOR

Your Listener: X-Up Forms

Trying to print 2-up, 3-up, or whatever-up forms using the VFP Report Writer was very difficult prior to VFP 9.0. However, using shapes, directives, data groups, and a ReportListener class, it can be done with VFP 9.0.

The Report Definition

Start by creating a new report. The report definition shown in Figure 26 is an example of an x-up order form. Notice the height of the page header band and the page footer band is zero. This is very important for this example to work. In addition, the report is defined as Whole Page in the Page Setup dialog box.

Click here for larger image

Figure 26. Use shapes, directives, and data groupsgroups to create x-up forms. (Click image for larger view.)

Data Groups

The first step needed for x-up forms is three data groups. For the first data group, use the applicable expression, such as Order_ID. The second data group is used to force the report to move to the next form. The expression for this data group should be a combination of the special fudge property, plus the expression used in the first data group, such as, ox.cFudgeBreak + Order_ID. Finally, the third data group uses the expression of RECNO() to wrap the detail band with a data group.

Shape Objects

Once the data groups are defined, the next step is to create three shape objects that are used as triggers in the ReportListener class. The first shape object is placed at the top of the second data group header band and is defined with a height of .02 inches. The second shape object is placed at the top of the third data group header band and also has a height of .02 inches. The third shape object is placed in the second group footer band, and has a height equal to the height of the band. Change the Forecolor of each object to red.

Directives

The next step in the creation of the report is to add the directives to the USER field of each of the shape objects and to the USER field of a few bands.

  • Shape object in the second data group header:

    *:LISTENER||XUP||TOP
    

  • Shape object in the third data group header:

    *:LISTENER||XUP||BEFOREDETAIL
    

  • Shape object in the second data group footer:

    *:LISTENER||XUP||BOTTOM
    

  • Detail band:

    *:LISTENER||XUP||DETAILBAND
    

  • Second data group footer band:

    *:LISTENER||XUP||GROUPFOOTERBAND
    

Group Footer

The regular items printed in the second group footer band are marked as Fixed relative to bottom of band. This allows them to move down the page as the height of the shape object is adjusted to fudge the height of the form.

Another point to make here is what happens when an order overflows to another form. Usually, when an order overflows to a second or third form, the totals only need to print on the last page. To deal with this situation, the ReportListener class about to be explained sets a property that indicates when a continuation situation occurs. This property can be checked in the Print When logic of any item in the group footer band that needs to conditionally print. For example, use NOT ox.lContinued as the Print When logic in the total fields. Use the reverse logic, ox.lContinued, for the label object that says, continued on next page.... This way the bottom of the form either prints continued, or it prints the total.

ReportListener_Xup class

Create a new class called MyReportListener_Xup, based on the MyReportListener_Directive class. This class needs a few properties added to it, but it does not need code in any methods because all the work is done by the applicable directive class.

Properties

Six new properties need to be added to this class.

  • cFudgeBreak (default to ="0"): This is used to force a form break, whenever an order overflows a form. Notice the equal sign on the default value for this property. This is required to force the property to a character value instead of a numeric value.
  • lContinued (default to .f.): This indicates whether this order has overflowed to a new form.
  • nCounter (default to 0): This is used to count the number of forms that have been processed.
  • nDetailBandHeight (default to 0): This is programmatically set to the height of the detail band, and is used to determine when an order needs to overflow to a new form.
  • nGroupFooterBandHeight (default to 0): This is programmatically set to the height of the second group footer band, and is used to determine when an order needs to overflow to a new form.
  • nXup (default to 2): Set this to the number of forms on a page.

Xup Directive Class

Create a new directive class, called MyDirectives_XUp, based on the abstract directive class. No properties need to be added or changed, but code needs to be added in three methods.

AdditionalInit() method

The AdditionalInit() method needs to have code that gets and saves the height of the DETAILBAND and GROUPFOOTERBAND records.

*-- MyDirectives_xup::AdditionalInit()
IF INLIST(This.aParameters[1], 'DETAILBAND', 'GROUPFOOTERBAND')

   *-- Switch datasessions
   LOCAL lnSession
   lnSession = SET("Datasession")
   SET DATASESSION TO This.oListener.FRXDataSession

   *-- Save the height
   IF This.aParameters[1] == 'DETAILBAND'
      This.oListener.nDetailBandHeight = HEIGHT * 960 / 10000
   ELSE
      This.oListener.nGroupFooterBandHeight = HEIGHT * 960 / 10000
   ENDIF
   
   *-- Restore the datasession
   SET DATASESSION TO lnSession

ENDIF

DoAdjustObjectSize() method

The code in this class handles the three different shape objects added to the report. When the TOP record is processed, the form counter is incremented and the lContinued property is set to .f..

When the BEFOREDETAIL record is processed, the code checks to see if there is enough room to print another detail band and the group footer band. If there is not enough room, the cFudgeBreak property is changed and the lContinued property is set to .t..

When the BOTTOM record is processed, the code checks to see how much room is left at the end of the form. It then changes the height of the shape object to force the group footer to appear at the bottom of the form.

*-- MyDirectives_xup::DoAdjustObjectSize()
LPARAMETERS tnFRXRecno, toObjProperties, toFRX

LOCAL lnNeed, lnFormHeight

DO CASE
   CASE This.aParameters[1] == 'TOP'
      *-- Increase the counter
      This.oListener.nCounter = This.oListener.nCounter + 1 
      This.oListener.lContinued = .f.
      
   CASE This.aParameters[1] == 'BEFOREDETAIL'
      *-- If there isn't room left for 
      *-- the next detail and the footer,
      *-- force a "form break"
      lnNeed = This.oListener.nDetailBandHeight + ;
         This.oListener.nGroupFooterBandHeight

      lnFormHeight = ;
         (MOD(This.oListener.nCounter-1, ;
         This.oListener.nXup)+1) * ;
         (This.oListener.GetPageHeight() / ;
         This.oListener.nXup)
   
      IF toObjProperties.Top + lnNeed > lnFormHeight

         *-- Force a break so the group header reprints
         This.oListener.cFudgeBreak = ;
            IIF(This.oListener.cFudgeBreak = '0', '1', '0')
         This.oListener.lContinued = .t.
      ENDIF

   CASE This.aParameters[1] == 'BOTTOM'
      *-- Fill up the rest of the "form"
      lnFormHeight = ;
         (MOD(This.oListener.nCounter-1, ;
         This.oListener.nXup)+1) * ;
         (This.oListener.GetPageHeight() / ;
         This.oListener.nXup)
   
      lnShape = toObjProperties.Top + ;
         toObjProperties.Height
         
      IF lnShape < lnFormHeight
         toObjProperties.Height = ;
            lnFormHeight-;
            toObjProperties.Top
         toObjProperties.Reload = .t.
      ENDIF

ENDCASE

DoBeforeRender() method

The code in this class sets the lRender property to .f. so the shape objects do not actually render.

*-- MyDirectives_Xup::DoBeforeRender()
LPARAMETERS tnFRXRecno, tnLeft, tnTop, ;
         tnWidth, tnHeight, ;
         tnObjectContinuationType, ;
         tcContentsToBeRendered, ;
         tGDIPlusImage

*-- Don't render the shape objects because
*-- they are just place holders
IF INLIST(This.aParameters[1], ;
      'TOP', 'BEFOREDETAIL', 'BOTTOM')
   This.oListener.lRender = .f.
ENDIF

Running the Report

Use the following code to run the report. First, it instantiates the ReportListener class. Then it sets some properties on the ReportListener object, including the number of forms per page. Finally, it runs the report.

SET CLASSLIB TO MyReportListeners
ox = CREATEOBJECT('MyReportListener_Xup')
ox.ListenerType = 1
ox.nXUp = 3

REPORT FORM Orders_2Up OBJECT ox RANGE 1, 10

The Xup ReportListener class just created is generic enough to handle any number of forms per page. Now you can create 2-up W2s, 3-up Order Forms, 4-up Receipts, and any thing else you can think of. The next section describes how to create a bar chart.

Your Listener: Bar Charts

Bar charts, like the one shown in Figure 27, can be created using a combination of shapes and directives.

Click here for larger image

Figure 27. Use a combination of shapes and directives to create bar charts in VFP 9.0. (Click image for larger view.)

Unlike many of the examples shown so far, much of the work for this example is done in the report itself, and only a small amount of the work is done by the ReportListener and its directives.

The Report Definition

Start by creating a new report and adding a summary band to it. Because this report is a summary, nothing is required in the detail band. Change the height of the detail band to zero so it does not take up any room on the report, as shown in Figure 28.

Click here for larger image

Figure 28. The final report definition for a bar chart looks like this. (Click image for larger view.)

Report Variables

The next step is to create Report Variables to calculate the values to print in each bar. For this example, create twelve variables defined as follows:

  • rnMonth1: The Value to store is IIF(MONTH(order_date)=1, 1, 0), the Calculation type is set to SUM, and the Reset value based on is End of Report.
  • rnMonth2: The Value to store is IIF(MONTH(order_date)=2, 1, 0), the Calculation type is set to SUM, and the Reset value based on is End of Report.
  • rnMonth3: The Value to store is IIF(MONTH(order_date)=3, 1, 0), the Calculation type is set to SUM, and the Reset value based on is End of Report.
  • And so on, through rnMonth12.

Next, create one more Report Variable that calculates the total maximum value of all the bars.

  • rnMaxValue: The Value to store is as follows. No calculation types need to be set on this variable. However, be sure this variable appears at the bottom of the list of variables. It relies on the other variables so they must be processed before this variable is processed.
    (INT(MAX(rnMonth1, rnMonth2, rnMonth3, rnMonth4, rnMonth5, rnMonth6, rnMonth7, rnMonth8, rnMonth9, rnMonth10, rnMonth11, rnMonth12)+9)/10)*10
    

Shape Objects

Start by creating twelve vertical rectangles for the bars. Make them each the exact same size, and change the color as needed. Add the following directive to each bar, changing the variable name from rnMonth1 to the applicable month.

*:LISTENER||VBAR||BAR||rnMonth1||rnMaxValue

Next, add a large rectangle to outline the entire graph, and select Send to Back to make sure this outline is behind all the bars.

Finally, add three small horizontal lines as tick marks on the left side of the graph. Add one at the very top, one at the very bottom, and one in the middle.

Field and Label Objects

Add the labels for each month across the bottom of the chart. Next, add a field object next to each of the three ticks on the left side of the chart. The expression for each of these is as follows:

  • Top Tick: Set this expression to rnMaxValue.
  • Middle Tick: Set this expression to rnMaxValue / 2.
  • Bottom Tick: Set this expression to 0.

Add a label object with an expression of Total Orders to the left side of the chart. Notice the position of this in the sample shown in Figure 28. It looks odd here, but when the following directive is added to the USER field, it rotates vertically when the report prints.

*:LISTENER||ROTATETEXT||-90

The last set of field objects are for printing the numbers across the top of each bar. In order to make this work correctly, each field object must be the same height as the corresponding bar, although it may be wider if necessary. Place the field object over the top of the bar. The expressions for the field objects are rnMonth1, rnMonth2, rnMonth3, and so on. Formatting may be added as needed. Finally, add the following directive to the USER field of each field object, changing the rnMonth1 variable for each one.

*:LISTENER||VBAR|TEXT||rnMonth1||rnMaxValue

No special ReportListener class is needed for this example, as it just uses the MyReportListener_Directives class already created. However, a directive class is needed.

VBar Directive class

Create a new directive class, called MyDirectives_VBar, based on the abstract directive class. No properties need to be added or changed, but code needs to be added in two methods.

DoBeforeRender() method

The code in this method compares the value of this bar against the maximum value to determine how tall this bar should be. The nAdjustTop and nAdjustHeight properties are then adjusted, depending on whether the item being processed is the bar or the text. For bars, the height and top position is adjusted. For text objects, the top position is adjusted.

*-- MyDirectives_VBarText::DoBeforeRender()
LPARAMETERS tnFRXRecNo, tnLeft, tnTop,    tnWidth, tnHeight,    ;
   tnObjectContinuationType,    tcContentsToBeRendered,    ;
   tGDIPlusImage

LOCAL lnPercent, lnAdjust

*-- Calculate the adjustment
lnPercent = EVALUATE(This.aParameters[2]) / EVALUATE(This.aParameters[3])
lnAdjust = tnHeight * (1-lnPercent)

*-- Apply the adjustment
IF This.aParameters[1] == 'BAR'
   *-- Bar
   This.oListener.nAdjustTop = lnAdjust
   This.oListener.nAdjustHeight = (lnAdjust * -1)
ELSE
   *-- Text
   This.oListener.nAdjustTop = lnAdjust-150
ENDIF

DoAfterRender() method

This method needs code to reset the nAdjustTop and nAdjustHeight properties so everything else still renders properly.

*-- MyDirectives_VBar::DoAfterRender()
LPARAMETERS tnFRXRecNo, tnLeft, tnTop,    tnWidth, tnHeight, ;
   tnObjectContinuationType,    tcContentsToBeRendered,    ;
   tGDIPlusImage

*-- Restore the adjustments
This.oListener.nAdjustTop = 0
This.oListener.nAdjustHeight = 0

Running the Report

Use the following code to run the report. It simply instantiates the ReportListener_BarGraph class, sets the mode to preview, and then runs the report.

*-- Create the ReportListener class
SET CLASSLIB TO MyReportListeners
ox = CREATEOBJECT('MyReportListener_Directives')
ox.ListenerType = 1

*-- Run the report
REPORT FORM TestBarChart OBJECT ox && TO PRINTER PROMPT 

Using Preview Containers

The Preview Container is the surface used to preview reports on the screen. Prior to VFP 9.0, you had no control over this. However, in VFP 9.0, this has been exposed to developers as an object that can be manipulated.

ReportPreview.app

The new Preview Container that ships with VFP 9.0, called ReportPreview.app, is much improved over the older Preview Container. Besides being able to programmatically change the way it works, another big improvement is the multi-page layout option. You can view 1, 2, or 4 pages at once, which is a great help when scanning through long reports. To see how this works, issue the following command against an existing report.

REPORT FORM MyReport OBJECT TYPE 1

Another improvement to the Preview Container is the fact that it now honors the RANGE clause. In previous versions of VFP, the RANGE clause was only honored in printed output, not in the preview. Now that you have seen the new Preview Container, the next sections discuss how to change the output and how to use the better chaining feature to create some reports.

Changing defaults

When printing with the new object-assisted output, VFP looks at the new _REPORTPREVIEW variable to determine which preview container to automatically load. You can, however, manually load your own as follows.

DO (_REPORTPREVIEW) WITH loPreviewContainer
loReportListener = CREATEOBJECT('ReportListener')
loReportListener.ListenerType = 1 && Preview
loReportListener.PreviewContainer = loPreviewContainer

This approach provides the opportunity to change some of the default properties. The following example shows how to change the caption, change the zoom level, dock the preview toolbar at the top, and to force the preview to start up maximized.

LOCAL loPreviewContainer, loReportListener
      
*-- Create the preview container
DO (_REPORTPREVIEW) WITH loPreviewContainer

*-- Change some of the defaults
loPreviewContainer.Caption = 'My Special Report'
loPreviewContainer.ZoomLevel = 5 && 100%
loPreviewContainer.ToolbarIsVisible = .t.

*-- Create the Report Listener
loReportListener = CREATEOBJECT('ReportListener')
loReportListener.ListenerType = 1 && Preview

*-- Assign the preview container to the listener
loReportListener.PreviewContainer = loPreviewContainer

*-- Run the report (with NOWAIT)
REPORT FORM MySpecialReport OBJECT loReportListener NOWAIT

*-- Change some more preview container properties 
loPreviewContainer.oForm.Toolbar.Dock(0) && Dock toolbar at the top
loPreviewContainer.oForm.WindowState = 2 && Maximize preview

Chaining reports

VFP 8.0 introduced the concept of chaining reports with the new NOPAGEEJECT and NORESET clauses. Unfortunately, this only worked with printing and not with previewing. In VFP 9.0, reports may be chained in preview mode as shown with the following code.

#define ListenerPreview   1
REPORT FORM Report1 OBJECT TYPE ListenerPreview NOPAGEEJECT 
REPORT FORM Report2 OBJECT TYPE ListenerPreview NORESET

Now that chaining reports works much better, you can get very creative with putting this feature to good use. For example, you can create a Table of Contents or an Index to help the reader sort through a long report. Chaining helps you help your reader.

Table of Contents—End

Create a Table of Contents on the fly by writing information to a cursor as the main report is being processed. When the main report is done, chain together another report that reads the new cursor and prints the Table of Contents. Figure 29 shows an example of a Table of Contents. Of course, this means the Table of Contents prints at the end of the report and you have to physically move that set of pages to the beginning of the report.

Click here for larger image

Figure 29. Use chained reports to create a Table of Contents. (Click image for larger view.)

To create a Table of Contents at the end of a report, follow these steps:

  1. Create the main report.

    1. Design the layout of the report as needed.

    2. Create a data group based on the information to appear in the Table of Contents.

    3. Create a cursor to hold the page numbers for the Table of Contents. When using the Data Environment to open the tables, add the following code to the Init() method of the Data Environment. When opening the tables in code, create the cursor in the code prior to running the report, as shown below.

      CREATE CURSOR tmpTOC (category_id C(6), cat_name C(25), pagenum I)
      INDEX ON CATEGORY_ID TAG CATEGORY

    4. In the On Entry expression of the data group header, add some code to call a UDF or method to insert a record into the cursor.

      Insert_TOC()
      

  2. Create the Table of Contents report.

    1. Add the code, description, and page number to the detail band
    2. Add the dot leaders by creating an object with an expression of REPLICATE('. ', 50). The 50 should be adjusted to fit the width and font of the report. Make sure the dot leaders object is Sent to Back, so it prints behind the code and description.
  3. Create the program to run the two reports.

    *-- Make sure the new report engine is in place
    SET REPORTBEHAVIOR 90

*-- Prep USE IN SELECT('tmpTOC')

*-- Create a ReportListener ox = CREATEOBJECT('ReportListener') ox.DynamicLineHeight = .f. ox.ListenerType = 1 && Preview

*-- Print the main report REPORT FORM Category OBJECT ox NOPAGEEJECT

*-- Prep and Print the TOC IF USED('tmpTOC') SELECT tmpTOC SET ORDER TO GOTO TOP REPORT FORM Category_TOC OBJECT ox ENDIF

*-- Cleanup USE IN SELECT('tmpTOC')

****************** PROCEDURE INSERT_TOC ****************** *-- Insert TOC record IF NOT SEEK(products.category_id, 'tmpTOC', 'CATEGORY') INSERT INTO tmpTOC ; values (products.category_id, category.category_name, _PAGENO) ENDIF RETURN

There are a few things to note in the above code. First, the code makes sure to close the cursor prior to running the report. This makes sure the cursor is not hanging around from some other report or program previously run. It then checks to make sure the cursor exists before running the table of contents report. This makes sure the Table of Contents is not run if the main report did not run properly.

Secondly, the procedure used to insert the table of contents record checks to see if the particular code already exists. The reason for this is to prevent duplicate records in the situation when the data group header is repeated on additional pages.

Thirdly, notice that a ReportListener class is used to run these reports. This is not required to chain the two reports together. However, because of a change in the way VFP 9.0 renders objects, it is needed to make the dot leaders appear correctly on the Table of Contents page. Set the DynamicLineHeight property to .f., otherwise the dot leaders do not appear until after the end of the defined width of the description, regardless of the fact that the description is being trimmed when printed.

Table of Contents—Beginning

If running the Table of Contents at the end of the main report is not acceptable, it is possible to use a double-pass method to force the Table of Contents to print at the beginning. This gets a little trickier, but it can be done. First, preprocess the main report, then print the Table of Contents, and finally print the main report.

First, prompt the user for the printer. The reason for this is that preprocessing the report uses the current default printer as the printer driver. If the user ends up selecting a different printer when the real report prints, it is possible the unprintable margins of the two printers are not the same. This means the page breaks of the preprocessed report could be different than the page breaks of the real report, thus, the page numbers could be off on the Table of Contents.

To create a Table of Contents at the beginning of a report, follow these steps:

  1. Create the main report and the Table of Contents report as previously shown.
  2. Create the program to run the two reports as follows.
    *-- Make sure the new report engine is in place
    SET REPORTBEHAVIOR 90

*-- Prep USE IN SELECT('tmpTOC') PRIVATE plAddToTOC

*-- Prompt for the printer IF SYS(1037) = '1'

*-- Create a ReportListener ox = CREATEOBJECT('ReportListener') ox.DynamicLineHeight = .f. ox.ListenerType = 1 && Preview

*-- Print the main report nowhere ox.ListenerType = -1 plAddToTOC = .t. REPORT FORM Category OBJECT ox

IF USED('tmpTOC') ox.ListenerType = 1

  *-- Prep and Print the TOC
  SELECT tmpTOC
  SET ORDER TO
  GOTO TOP
  REPORT FORM Category_TOC OBJECT ox NOPAGEEJECT

  *-- Print the main report
  plAddToTOC = .f.
  REPORT FORM Category OBJECT ox

ENDIF

*-- Cleanup USE IN SELECT('tmpTOC')

ENDIF

****************** PROCEDURE INSERT_TOC ****************** *-- Insert TOC record IF plAddToTOC AND NOT SEEK(products.category_id, 'tmpTOC', 'CATEGORY') INSERT INTO tmpTOC ; values (products.category_id, category.category_name, _PAGENO) ENDIF RETURN

Index

  • Creating an Index at the end of a report is very similar to creating a Table of Contents at the end of a report. One difference is that the call to Insert_TOC() needs to be done more often than just in the data group header. For example, call it once for each detail record, as well as once for each data group header.
  • Another difference is that the cursor should be indexed based on the description. Use this index when printing the index report so the items appear in alphabetical order, instead of page number order.

Your Preview Container

VFP 9.0 ships with a Preview Container, called ReportPreview.app. However, you do not have to use the supplied preview container if you do not want to. You can create your own preview container and set the _REPORTPREVIEW system variable to the name of your preview application.

Writing your own Preview Container is beyond the scope of this article; however, the source code for ReportPreview.app is included in the xsource.zip file, which is located in the HOME() + 'tools\xsource' directory and can used as an example for you to use while writing your own Preview Container.

Conclusion

This article certainly has covered a lot of material. The section on multiple-detail band reports showed some practical examples, such as printing percentages with the data. The Report Builder section showed all the new dialog boxes and gave you a look into the new feel of the Report Designer. The section on Listeners was full of code and examples, such as how to generate XML, HTML, and TIFF output. This section dove further into manipulating reports by changing font properties, forcing pagination, and lots of other ideas. The section on Preview Containers introduced you to the new preview surface and discussed ways to take advantage of it.

Bio

Cathy Pountney is a Microsoft Visual FoxPro MVP and has been developing software for 22 years, thirteen of which were as an independent consultant specializing in FoxPro. In 2001 she had the privilege of spending six months as a contractor onsite in Redmond with the Microsoft Fox Team. Currently she works for Optimal Solutions developing VFP applications for schools. Cathy has spoken at many FoxPro conferences and user groups across the U.S., written articles for various magazines, and her book, The Visual FoxPro Report Writer: Pushing it to the Limit and Beyond, is available from Hentzenwerke Publishing. You can contact Cathy at cathy@frontier2000.com, view her website at www.frontier2000.com, and view Optimal's website at www.optimalinternet.com.

© Microsoft Corporation. All rights reserved.