Special Thanks to developer Charles Overbeck for extreme
patience in answering tons of questions with incredible detail ...
Example files available in reports.zip
However, just like anything of the complexity of the report designer, there are a lot of techniques that are not necessarily obvious (sometimes called tricks) that can be used to make your reports that much more powerful, and look better. That's what this paper is about.
Like a form, a report is really a container for a bunch of objects, with some code of its own, since the report is an object with properties and methods (also just like a form). The report object hierarchy works something like the following:
How a Report is Rendered
The Report Designer
First, there are up to four palettes (which may be floating, they may be turned off,
and so on). These are:
In addition, there is the report designer itself -- this actually has two parts, although
the default is to show just the report. These are called panes (it's a window) -- the
pane on the left is called the Group pane, and the pane on the right is the Report pane.
When you first open the report designer, there is a vertical bar on the left of the
Report pane -- if you drag this bar to the right, you will see the Group pane.
The Group Pane shows the hierarchy of objects in the report down to the
bands (it does not show controls). The Report Pane shows the report
appearance with the corresponding structures shown in the Group
Pane -- this uses live data.
Metrics
Some folks have discovered that it's easier to turn these off, and then to place
your own labels where you need them. This can be done by right clicking on the
field palette surface, and then select the "Customize Tool Windows ..." item
from the popup menu. In the dialog that appears, in the "Add field label" section,
select "None".
Next time you drag a field from the field palette, no label will appear. You then
can either place your own on the pageTemplate above the streamFrame, or anywhere in the detailBand, if that is your desire, and so on.
Alignment Tools
When you are in the report designer, in the toolbar (and in the "Align" menu),
are a set of toolbuttons designed to help you line up controls. To use this,
select two or more controls that you wish to line up (use <Ctrl> and the mouse to select them), and then you can line them up on the left, the right, top or
bottom. (You can also make your controls the same size -- height, width ...)
In addition, you can center controls horizontally in the band your
are working in (or the pageTemplate) by clicking on them and going to
the Layout menu, and selecting "Align" and then "Center Horizontally in
Window". (In most cases you probably don't want to "Center Vertically in
Window" ...)
Lines
Metrics
AutoSort
Autosort is used by the report designer to modify your query's SQL
statement, so that the data "automatically sorts" the way you need it
to. This can be useful in some cases, and in others it can be confusing.
What it does is add (or modify) the ORDER BY clause of the SQL SELECT
statement of the query.
There are cases where you really need the report to use your index tag(s),
and you would want to then turn autoSort off in your report.
Large Tables
To make this a bit quicker, you may find that you need to use a
little-known feature -- the ability to only display a single row.
This can be done by right-clicking on the surface of the report,
and in the popup menu, selecting "Report Designer Properties". In the
dialog that appears, you can then select the "One Row" radio button
in the middle of the dialog, and your report design can be sped up.
Caveat: this affects all reports -- so once you have completed your
changes, you may wish to go back and reset this property to "All Rows".
Navigating In Report Designer
Blank Fields
There are two simple solutions -- one is to add a "dummy" record that
will always sort at the top of the table -- if your key field (the one
you are indexing on) is numeric, set the value in this "dummy" record to 0,
if it is character, set it to something that starts with a space, etc. The
other fields in the "dummy record" should all contain data. Once you are
done, you can then delete this record ...
The second solution is to use a "dummy" table -- one with the exact same
layout as the real table, but in the designer you are working off this other
table which contains data in all the fields ... once your report is designed,
you can simply change the query's SQL statement to the real table.
Null Fields
This isn't a real problem unless you are creating calculated fields in your
reports that assume that there are values contained in the fields. If a
null value is added to a character string, rather than getting the character
plus the null value, you get simply a null value. This works for logical,
numeric, date, and character fields ...
The solution to this problem is to check for the null value. The following
is assuming your calculation is contained in the text property of the text control:
Now, the above is a bit complex to store in the text property, and you may
want to use the canRender event to do this instead (this is covered in the
section of this paper that discusses calculated fields).
A more simple method of doing this is to pass the value of the field
through the String class constructor (this converts nulls to empty strings),
which has the added advantage of typing the long object reference only once:
Displaying Numeric Values
For example, if you have data that must display two digits on the
right of the decimal, but in one calculation or field you have a value that
has zeros for the digits to the right, you may end up seeing an integer value
appear.
If you set a picture clause, you can get the value to always display two
digits on the right of the decimal (999.99, for example).
In addition, in some cases, you may want to display percent symbols and such --
you may want to use transform() (see online help for details) to get everything
to line up.
There are a variety of things you can do to enhance your reports with very little
effort:
Some of the formatting options (Tags) that are useful to know about:
When you design these directly into your report, you can
modify the text property along these lines:
"Some <B>text</B> to be <I>displayed</I>"
or, if you wanted to modify the appearance of a field from
your table, you could add to the codeblock:
{|| "<FONT COLOR='red'>"+this.form.queryname1.rowset.fields["fieldname"].value+"</FONT>"}
There is a lot more, but you should examine the online help, as well as
books on HTML ...
See sample file: "HTMLSAMP.REP"
When you are using the report designer, you will see a very busy screen, and this
can be a bit confusing at first -- although if you've used the form designer
you may be used to it, to an extent.
The default metrics for the report designer are twips, there are
1440 twips to the inch (a twip is a "twentieth of a point";
there are 72 points per inch). This, for people used to thinking
in inches, (or centimeters, or ...) can be a bit mind-boggling,
and trying to determine how wide a page is, or how far from the
left margin you wish to set an object, can be difficult at best.
As it turns out, and some of you reading this have probably
already found this, but sometimes what seems like it should be
obvious isn't ... you can change the report metrics for your
reports! If you think in inches, then by all means, change the
metrics for your reports to inches. The metric property is found
in the inspector for the 'form' -- if you are designing a report,
the form is the report -- and if you click on it, you will see
a combobox with several options: Chars; Twips (default); Points;
Inches; Centimeters; Millimeters; Pixels. Most people don't think
too clearly in anything but Inches, Centimeters or perhaps
Millimeters -- so you may want to get used to setting the metric
property as soon as you start designing a report (you could use
a custom report that set this and any other properties you
commonly change from the defaults for you ...).
Throughout this paper, I am using "inches" as my metric setting.
Some Hints ...
The more you work with the report designer, the more things you find that can
make it easier to work with. The following are hints from various sources
which may make designing your own reports easier:
Field Labels
Top/Menu
When designing your report, and dragging fields from the field palette, the
report designer places labels for each field onto the report. The default is
to place them above the field, and you can change this setting to the 'left' or
to simply not display them at all. Note that you get field labels in
reports, but not when designing labels.
When you are working in the design surface, getting your controls to line
up can be bothersome. You can do it the hard way -- going to the inspector
for one control, looking at the values, and then setting the same properties
for other controls; or you can use the alignment tools.
If you want to place a line between detailBands, there are some options, but
experimentation has shown that your best bet is to place the line at the top
of the detailBand, not at the bottom, or in the group headerBand or footerBand (if
you are using groups). This way, you always have the line at the top of the detailBand
for each row that prints ...
As noted elsewhere in this paper, setting your report metrics to a value that makes
sense to you is a good idea -- twips are hard to work with unless you're a typesetter.
Also as noted in the section of this paper on groups, you may wish to set the value
of this property (of the report itself) to false -- otherwise you may find your
report sorting in ways you didn't expect.
If you work with large tables in your reports, sometimes it takes a long
time to make even the most simple changes -- this is because the designer
is making these changes for each instance of the detailBand that is
generated for the report (remember, the report designer works with
live data).
If you are dealing with a large report, it is helpful to view data on
various pages. As it turns out, in the designer, you can see the next
page by pressing the <PgDn> key on the keyboard, and to
see the previous page, you can press the <PgUp> key.
If you've worked with the report designer for any length of time, you
will have noticed that if you have blank fields in the first few rows, but
want to use those fields in the report, they don't appear -- which makes
it difficult to manipulate them.
This is related to the "Blank Fields" topic -- if you are using DBF7 tables,
the BDE uses "true null" support. This means that a field that has not had a value
entered into it contains a null value. This can be quite confusing when you try to
generate a report, because the isblank() function is not going to return
"true", but the field doesn't appear to contain a value. Note that empty() does
return "true" in this case.
{|| trim( ;
iif( this.form.address1.rowset.fields["last name"].value ) == null, ;
"", this.form.address1.rowset.fields["last name"].value ) ) +", "+;
trim( ;
iif( this.form.address1.rowset.fields["first name"].value ) == null, ;
"", this.form.address1.rowset.fields["first name"].value ) ) }
{|| trim( new String( ;
this.form.address1.rowset.fields["last name"].value ) )+;
", "+;
trim( new String( ;
this.form.address1.rowset.fields["first name"].value ) ) }
Sometimes getting numeric values to display just the way you want them can
be a bit tricky. You should consider using the picture clause or the transform() function to make sure that your numeric values line up just right in your report.
Enhancing the Appearance of Your Reports
Reports can be pretty bland, so sometimes it is desirable to enhance
the appearance a bit to make parts of the report stand out, and/or
just make it look better or easier to read.
These are just a few ideas to get you started. Of course, adding images or shapes
to your report can also enhance them, as well as getting more fancy and using
ActiveX controls ...
<B>Text</B> = Boldface the text between these tags
<I>Text</I> = Italicize the text between these tags
<U>Text</U> = Underline the text between these tags
<BR> = Break a line -- e.g., start a new line where this tag appears
(There appears is a very minor bug here, that causes the
output to be a bit odd, but it's fixed by adding a space
after the closing angle bracket of the tag.)
<P> = Paragraph break -- as above, but if you output your report to
HTML, it includes a blank line
<Hn>Text</Hn> = Header size -- headers can be sizes 1 to 6,
1 is largest
<FONT SIZE=n>Text</FONT> = Font sizes from 1 to 7 -- default is 3,
1 is smallest, 7 is largest (close to H1)
<FONT COLOR='colorname'>Text</FONT> = Color of text can be changed.
In addition the font size and color tags can be combined into one, by simply
issuing: <FONT SIZE=n COLOR='somecolor'> ...</FONT>
A custom report is effectively a template for a report. The advantages to working with these include setting standards for all reports for a project, and setting properties that you commonly change in all your reports.
An example would be for a specific project, you need all your reports to have a specific logo and title text, and the customer wants all the margins to be a specific size.
If you set a custom report up with these properties and objects, then anytime the customer decides to change something (they might decide, for example at a later date, that the margins need to be changed again), you only have to change the margins for the custom report, and all of your reports that are derived from that report will be changed.
Custom controls allow you to do the same sort of thing for the fonts and other attributes of text controls -- one change in your custom control and all reports that use it will reflect that change. This is much easier than modifying every report you have created.
While the use of custom forms and custom controls may mean some initial work to get them exactly as you would like, you can save yourself many hours in the long run ....
The Custom Report GSPROD.CRP is used for many of the sample reports used for this paper. In addition, you should find the file CUSTREP.CC, which contains a set of custom text controls that can be used in reports ...
Custom Mailing Label Layouts
You cannot design your own custom labels in the same fashion as you
can design custom reports as discussed above.
It is however, possible to create a new label definition that can be used by both the Label Wizard and the Label Designer.
The label definitions are stored in a table that is installed with Visual dBASE, in the directory ..\Designer\Label, the table for US customers is LABE0009.DB -- the number in the table name is what will be different if you're using a different language. You can add as many label definitions as you wish here -- however, there is a "catch" -- the table uses Double fields for dimensions, and by default these are shown without any decimal places.
It has been suggested that you create a form using the form wizard, and then set the picture property for most of the numeric fields (not the metric field, which is an integer value) to:
9999.9999
The metric is the same as the metric property: 3 for inches, 4 for centimeters, etc.
A working version of this is in the file CUSTLABL.WFM.
You should read the header of the form carefully -- you may need to modify the path defined at the beginning of the form based on where you installed Visual dBASE 7. This form will let you view the labels currently in the table, and you can add or edit them (you may want to be careful editing label definitions that ship with the product ... or avoid it completely ... and just add your own ...).
Reports evaluate in what is called 'z-order' (just like forms do); which means that the sequence that objects are defined in the source code file (.REP or .LAB for labels) can be important for certain aspects of a report.
What often happens when creating a report is that you get everything lined up and laid out exactly as you want, and then you add a field or text control somewhere to the report. This last object does not appear necessarily in the correct z-order, and this can cause your report to render differently than you might expect.
There is no layout option to change the z-order for reports as there is in the form designer, so any changes must be done by hand in your source code. (Cut/paste the necessary blocks of constructor code ...)
When creating a report that needs to have blank lines suppressed, follow these rules and you should be ok:
This (suppressIfBlank property) must be set for each control on each "line" of the report that you want to suppress.
Example: For mailing labels you might have two address lines -- but in many cases the second one may be blank. Make sure all of your controls are in the proper z-order, and then check that the second address line (and any subsequent lines such as the city/state/zipcode line) has suppressIfBlank set to true and variableHeight set to false.
A working version of this is in the file BLANKS.REP.
VariableHeight text controls -- according to one of the developers:
The height that will be suppresed if a text control is blank is determined entirely by its height property. So if you have a variableHeight item with a height of 0, and it is suppressed when blank, nothing will change. However, if your variableHeight item had a height of 200 twips, then other controls would be moved up 200 twips.Problem: Memos and large character fields, to print properly, must have their 'variableHeight' set to true. At this point in time, the only solution seems to be to print the memo before (z-order) any text that may need to be variable/suppressed ...Because it's more likely that a variableHeight item will have a very small (or even 0) height property, it may seem that suppressIfBlank works differently on variableHeight items, but it is really a function of the value of the height property.
Using a DataModule
The simplest method is to use a DataModule for both a form and the
report (the same DataModule). When you place a DataModule onto a form
or a report, there is a "shared" property, which if set to "All" will
allow you to use the exact same settings from the form in the report
(this also works between forms, but that's another topic altogether).
In your form, if you set a filter or a setRange() for a rowset and then call the report, the data displayed in the report should match that shown in the form.
A working version of this is in the file SINGLE1.WFM and SINGLE1.REP.
Overriding Events in the Report
The second (more complicated) is to override certain methods of the rowset, re-assign
the query and rowsets of the report (you must assign the references
in the report to your query and rowset to the one being used in the
form), and then generate the report.
The following code is called from a pushbutton on a form:
// save current setting for these events: fpOldFirst = form.rowset.first fpOldNext = form.rowset.next // override these two methods: form.rowset.first := {; return true } form.rowset.next := {; return false } // create instance of report (use your report name): set procedure to repone.rep additive // use name of report in constructor (CLASS ... OF REPORT) r = new reponeReport() // replace query in report (use your query names) // (these two just happen to be the same): r.customer1 := form.customer1 // assign rowset: r.streamsource1.rowset := r.customer1.rowset // render the report: r.render() // reset overridden methods: form.rowset.first := fpOldFirst form.rowset.next := fpOldNextA working version of this is in the file SINGLE2.WFM and SINGLE2.REP.
Modifying the Report's Code
The last option, while a bit of work in the report itself, may be
more powerful in the long-run -- this is to store key field values into
custom properties of the application, and in the report look for
them -- if they exist, when the report is generated, you can set
a filter or use setRange() (equivalent to the old Xbase DML's
"set key") for your report's query ... For example, if you are
working with a customer table, you might want to store the customer
number, which would be unique for that row (if you have no unique
fields, you might need to store more than one ...):
// a button's onClick method: _app.CustNum = form.rowset.fields["customer id"].value do repone2.rep // execute report _app.CustNum = null // get rid of custom property
In the report, you would need to set, probably for the query's onOpen event, code along these lines:
// check to see if custom property exists: if type( '_app.CustNum' ) # "U" // it's defined ... ("U" is 'undefined') this.rowset.beginFilter() this.rowset.fields["customer id"].value = _app.CustNum this.rowset.applyFilter() endifA working version of this is in the file SINGLE3.WFM and SINGLE3.REP.
When you set the printer object's orientation, the report designer does not automatically re-set the pageTemplate and streamFrame's height and width properties. This means you need to set them yourself.
Once you have your settings, you might want to save this report layout as a template (just call it "my landscape template.rep" and then copy it if you need another landscape report) before moving on or you might want to create it as a custom report ...
What is meant by column here? Normally when talking about data, columns refer to the fields in a table. What we're really talking about is having multiple streamFrames that are side-by-side on a report, and each streamFrame prints the exact same thing as the other (like a phone book listing, or a dictionary) ... each streamFrame could have multiple fields (data columns) in it ....
First, you need to have some idea how big your columns need to be. There should be at least a bit of room between columns -- the amount of spacing you leave between them is up to you, but without any spacing your report may get hard to read. The columns will need to fit on your pageTemplate (between the margins). Set your metrics to inches ... Once that is done, look at your margins (default margins are .75" on all four sides -- I often reduce that to .5", which gives a bit more printing area).
Determine how much space you want between your columns. Subtract the pageTemplate's left and right margins from the width of the report -- an example would be a standard piece of paper (in America) of 8.5" wide, if my left and right margins are .5" each, this means that my pageTemplate will allow me to print on 7.5" of the page. Next, subtract the amount of space between columns. You might want only a quarter inch -- so for our example, we will use this value. 7.5" - .25" gives us 7.25". Finally, how close to the margins do you want to be? You could put your columns right up against the edge of the pageTemplate (this is acceptable), or you might want some more white space. If you want to use all of the available pageTemplate (recommended), you don't have to do anything else, otherwise determine how much space you need and subtract that value twice from what you currently have. Divide that value by 2 to get the width you need for your columns. This will give us 3.63" per column.
The calculation would look something like the following:
(Width of paper) 8.5" - pageTemplate.marginLeft .5" - pageTemplate.marginRight .5" - (# of columns - 1) * (space between columns) ( 2 - 1 ) * .25 = .25 ---------------------------------------------- --------------------- SubTotal (space left) 7.25 Divide by # of columns 7.25/2 ---------------------- ------- Column width 3.63" per column(Note: The above calculation does not take into account whether you are leaving space on the pageTemplate on the left and/or right sides of the columns ... instead, it assumes you want your streamFrames to be right up to the edges of the pageTemplate.)
Columns on a report are really just multiple streamFrames. Your report defaults to having a single streamFrame, so you should start by setting the width of the first one to 3.63" (based on the calculation above). For this example we are putting the columns up against the edge of the pageTemplate, so set the LEFT property to 0.
NOTE: Due to the nature of custom reports (the fact that only some of the controls are streamed out to derived reports) trying to use a custom report for this is difficult at best. For the example, I am using a copy of a template report based on the custom report ...
Now you have two options -- the first is to copy the current streamFrame, or you can simply drop a new streamFrame onto the report from the control palette. Since you have already set the width for the current streamFrame, I recommend using the windows copy and paste (Ctrl+C and Ctrl+V) to add your second streamFrame to the report surface. Set the top property of streamFrame2 to the same value as that of streamFrame1; set the left property to 3.63 plus the amount of space you wish to have between columns (.25" is what we determined above) -- or 3.88. You now have two columns!
Place a field onto the detailBand in the first streamFrame where you want it to be printed. If you used my suggestion above to copy/paste a copy of the first streamFrame, however the field isn't in the second column automatically. Click on the second streamFrame, and in the inspector, notice that there is a streamSource property, which shows "null". Click on this, and select STREAMSOURCE1. NOW you should see your data in both columns!
There is another option for placing multiple streamFrames on a report, and this is to select the streamFrame object from the component palette, and drag/drop a new streamFrame to the report. The one advantage to doing this is that the streamSource property defaults to the main streamSource for the report. The disadvantage is that you then have to set all the position properties ...
A working version of this is in the file MULTCOL.REP.
Using a DataModule or Query
First, there is the data module -- if you are not using these, you should consider
them. Datamodules are designed to replace the Visual dBASE 5.x query (.QBE) file, and
can be used to open multiple tables, set relationships, and more. In a dataModule you
can design calculated fields for use in reports (or in forms) fairly easily. This is
done with the Field class, and usually in the onOpen for a specific query object placed on a dataModule.
Using the CUSTOMER table in the ..\VISUAL DBASE\SAMPLES\MUGS\TABLES directory, an example would be to create a calculated field that combines the first and last names of a customer into a single name field -- follow these steps:
In the navigator, click on the dataModule tab. Double-click the first "Untitled" icon (the second is for custom datamodules, which is another subject completely ...). This will allow you to design a datamodule (if the dialog appears asking about using the wizard, click on 'Designer').
The designer (if you are not familiar with datamodules) looks like the form designer, except that the component palette is limited to just database objects. Click on the navigator again, and drag the customer table to the surface of the datamodule. This will give a small image that has the letters 'SQL' on it (the standard 'query object' in Visual dBASE 7).
In the inspector, click on Rowset, and then the 'I' button. Select indexName, and in the drop-down select 'Last'. This is just so that we know the report will be indexed properly. Click on 'Parent' in the inspector, and then the 'I' button. This will take us back to the query object. Note that you can also click on the "Back" button on the inspector to go back to the query object.
The next thing we're going to do is create a calculated field. This requires a small amount of coding, which will be written into the onOpen event of the query object (meaning that when the query is opened, we will execute the code). In the inspector, click on the "Events" tab, and then on the onOpen event. Click the 'tool' button, and the editor opens. In the editor, enter the following:
f = new Field() f.fieldName = "FullName" f.beforeGetValue = {|| trim( this.parent["first name"].value ) + " " + ; this.parent["last name"].value } this.rowset.fields.add( f )
Save your datamodule and exit the designer.
Once you have created your data module, you can then use it in a report (or a form) easily. To see this in action, create a new report, and drag this new dataModule onto the report. (The report designer automatically will set the references necessary to use the table(s) on your report.)
If the field palette is not on screen, open it up (right click on the report surface ...). Notice in the field palette that in addition to the other fields in the table, your new field is at the bottom. If you drag that onto the streamFrame, you should see this repeated for each row in the table.
If you do not wish to use a dataModule, you can do the exact same steps as above with the difference being that you drag your table onto the surface of the report, and then create the calculated field in the query's onOpen as shown above.
A working version of this is in the file CALCFLD.DMD and CALCFLD1.REP.
Using the Text Property (Codeblocks)
{||this.form.customer1.rowset.fields["First Name"].value}
If you wanted to, you could modify this codeblock. For example, rather than printing just the first name, you could print the full name, in a similar fashion to what we did previously:
{|| trim( this.form.customer1.rowset.fields["First Name"].value ) + " " + ; this.form.customer1.rowset.fields["Last Name"].value }
This can get a bit tricky, especially with a long calculation, but it is do-able.
A working version of this is in the file CALCFLD2.REP.
Using the CanRender Event
rMyRow = this.form.customer1.rowset // shorten the object reference cCity = trim( rMyRow.fields["CITY"].value ) cState = trim( rMyRow.fields["STATE"].value ) cZip = trim( rMyRow.fields["ZIP"].value ) cCountry = trim( rMyRow.fields["COUNTRY"].value ) // assumes that there is information in the first three fields: this.text = cCity + ", " + cState + " " + cZip if not empty( cCountry ) this.text += " <B>"+cCountry+"</B>" // bold endif RETURN TRUE // this is usually added by the report designer
A working version of this is in the file CALCFLD3.REP.
Notice that all of the above can be used with numeric, date and logical values (including combining types) as well as character (this is due to the automatic type conversion feature of Visual dBASE 7).
Referencing A Codeblock/Calculated Field
One of the trickier things involved in working with calculated fields that
are codeblocks is that if you wish to reference the values for these in
other calculations, you may find yourself referring to the text property
(which is not the value you are looking for), rather than the value. The
solution is to force a re-calculation, by adding parentheses to the text
property in the reference, i.e.,;
this.parent.textcontrolName.text() // in the same band
The parentheses act as the call operator, which executes the codeblock.
One example would be to change the value of a text control elsewhere in a report to the value of the current text control:
function MyText_canRender // the name of the function will vary form.pageTemplate1.text3.text := this.text() return true
In order to get checkboxes, we must use a Windows font that has the characters we want to use, and we need to know where those characters are in the character set, and finally check to see if we can get those characters with the language drivers we have set for the BDE.
If you are using an ANSI language driver in the BDE for your native DBASE table driver (if you are not sure, bring up the BDE Administrator; make sure you are on the "Configuration" page; select "Drivers"; select "Native"; select "DBASE"; look at "LANGDRIVER" -- the default is "WEurope ANSI") -- this is what Windows normally uses (ANSI, anyway), and the chances are that the following will work. If not, try setting your DBASE driver to "WEurope ANSI".
If you want to display a logical field on your report with a checkbox that changes to checked if the value is true, and empty if the value is false, you need to place a text control onto the report, and then try the following:
{|| iif( this.form.characters1.rowset.fields["mylogical"].value, ; chr( 254 ), chr( 111 ) }(chr(254) in the Wingdings font is a box with a checkmark, chr(211) in this font is an empty box) Make sure you change the name of the field to the one you need to check ...
If you want to use a different font, make sure that it is one that is installed with the operating system, or that you have the rights to distribute it -- many fonts are shareware and require fees from the developers.
A handy report for viewing what characters are assigned to what in any font is in the file CHARACTR.REP (using table CHARACTR.DBF).
If you want other characters, you need to know what the characters are mapped to. You can change the font by changing it for the one text control. The report defaults to using Wingdings, so you can display (or even print) these characters, and make a decision from there.
Groups are added to the streamSource. Groups have a groupBy property, a value used to determine where groups should break. At the beginning of each group, a headerBand is rendered. At the end of each group, a footerBand is rendered. Groups have various methods that are used as aggregate functions, such as Count, Sum, Max, etc.Sorting The DataBy default, when you assign a value to the groupBy property, the query's SQL statement is changed to contain an ORDER BY clause with the groupBy field. This will ensure that the groups break correctly when the report is rendered. Setting the report's autoSort property to false can turn off this behavior. Do this if you know the rowset is already in the right order, and/or you want to do groups on calculated fields (see the groupBy example on page 17-25 of the Language Reference).
The report itself has a built-in group object, called the reportGroup object. The headerBand of this group is rendered at the very beginning of the report, and the footerBand is rendered at the very end. Its aggregate methods are used for performing aggregates on the entire rowset.
If a report object's autosort property is set to true (the default), then the SQL property of any query that is accessed by a streamSource object that has groups will be modified automatically to include an ORDER BY clause that sorts the rowset in the correct order.Below are some ideas that may be useful ...For example, if you have two Group objects, the first grouping by the field STATE and the second by ZIP, then even if the query's SQL property is set as:
select * from SALESThe rowset will be generated internally with the SQL statement:
select * from SALES order by STATE,ZIP(NOTE: This is streamed out to the query object's SQL statement this way ...)
If autosort is false, the rowset is not altered by the report engine. It assumes that the query is correct and contains the necessary fields in the right order. Therefore, if you use the indexName property to set the rowset order, you should set the report's autoSort to false; otherwise it defeats the purpose of using indexName.
Page Totals
If you are already grouping your report, you may find that there are totals you
would like to place at the bottom of the page. Neat idea. However, there is no way
to hook directly into the internal mechanism that fires for groups in reports. You
can, however, get around this by using the preRender or onRender events that are attached to the groupHeader (or groupFooter) band. By placing specific calculations there, you can then print the results somewhere else.
For example, using the customer table, if you have the table grouped by state you might want to print the number of states on that page; and perhaps keep a running total for the whole report while you were at it.
This means setting up two variables, incrementing them when the group changes, and setting one to zero (the page total) when the page changes.
The first part is pretty easy -- using a control that will always be evaluated before anything else -- the query (or datamodule), you can work with the onOpen event (the report object does not have an 'onOpen' or a 'preRender' event):
form.nPageTotal=0 // Yes, you can use a form reference in form.nRepTotal=0 // the query's onOpen event
There needs to be a way to increment your totals when a group changes. You can cause this to fire in the group headerBand or footerBand's preRender or onRender event:
// because the 'form' reference isn't available here: f = this.parent.parent.parent f.nPageTotal++ f.nRepTotal++
When the page changes you need to reset the value in form.nPageTotal. This can be done with the report's onPage event -- this fires after a page is done rendering, so it is the perfect place to reset form.nPageTotal. To get to this event, in the inspector go to 'form'. Click the onPage event, click the tool button, and enter:
form.nPageTotal=0
Finally, you will want to actually print these values. Place a text control on the pageTemplate under the streamFrame for the page total, set the text to:
{|| "Total States on this page: "+form.nPageTotal }
NOTE: If you get an error here that states "nPageTotal" does not exist, everything is ok -- save the report and exit the designer -- when you return to the designer, this value will have been initialized, and you will be fine.
And the report total would be placed in a similar place, only refer to the appropriate variable.
One question that might come up is, "what if you only want the report total to print on the last page?"
The solution is to use the canRender event for that particular text control, and in the canRender event, check to see if you are currently on the last page. The report itself has a method to let you know: isLastPage() -- which returns a value of 'true' if this is the last page, otherwise it returns 'false'. In the canRender event of this text control, set the following codeblock (in the inspector):
{|| form.isLastPage() }
With this codeblock set, when the report is run this text control will only appear on the last page of the report.
Note: It is possible to place a report total on the last page of the report by using the report's footerBand -- a lot depends on what you want your report to look like.
A working version of this is in the file PAGETOT.REP (using table CUSTOMER.DBF).
Aggregate Functions
The report designer has a set of aggregate functions available for use in reports
that can be used to handle some calculations for you. These include totals, averages,
minimum, maximum, and so on. More details can be found in the online help using
"aggregate calculations (reports)" for your keyword. You can insert these into your
report with the group dialog called from the menu ... (or you can add them yourself --
you may want to try using the Report Wizard once to see how the code gets defined
before trying this on your own ...).
Running Totals
Running Totals can be done in a variety of methods, depending on your needs.
The important thing to remember about these is determining where you need to increment values that you wish to print, and where you wish to print them.
If you wish to simply add values as you print them in the detailBand, you may wish to consider using the aggregate function SUM() noted above. If, however, you wish to work with groups, you would need to do something along the following lines:
In the group footerBand's or the detailBand's preRender event you would want to first check to see if the total you wish to use exists:
if type( "this.myTotal" ) = "U" // does not exist this.myTotal = 0 // initialize it endif
Once you have done this, all you would need to do is add the code to add to the total:
this.myTotal += form.streamSource1.rowset.fields["myField"].value
In your report's detailBand or the group's footerBand, you could then print the value of "myTotal" by referring to it appropriately -- you would need to create a text control, place it on where you want to print it, and then either set its text property to a codeblock or use the canRender event to refer to:
// In a canrender: this.text = form.streamSource1.footerBand1.myTotal RETURN TRUEA working version of this is in the file RUNTOT.REP (using table INVENTORY.DBF).
Note -- if you need to use a complex index for a group or for the detailBand, you should make sure that the report's autoSort property is set to false (see section of this paper on groups above), and use the indexName property for the appropriate rowset to use that index tag ...
To create a report using linked tables, you should follow these steps:
If you need to, you can do nested groups and so on. It is a good idea for a grouped report to have a layout already in mind, as these can get confusing fast. If you place a second group onto a report, it will be the "outer" group, which may not be what you need or want.
In some cases, it may be easier to let the Wizard do the initial layout, and then you can come in and modify the report to your heart's desire ...
This can be accomplished by using a combination of a property of the band (the groupHeaderBand, groupFooterBand, or detailBand) called renderOffset and the streamSource method beginNewFrame() (this is not to be confused with the beginNewFrame property of the band object).
In order for it to work, you need to have some idea how much room you need to print a minimum of the group headerBand and a row in the detailBand, as well as the height property of the streamFrame. The height of the streamFrame is easy, and the renderOffset property (which is read-only) returns the distance from the bottom of the band to the top of the streamFrame. So, if you know that your report requires at least a half-inch in order to print the group headerBand and at least one detailBand, you could force a new page or streamFrame with something like the code shown below (after the discussion of groupFooterBands).
If you do not have a groupFooterBand, use the detailBand, otherwise, use the groupFooterBand, and check the following (in the onRender event):
// in a detailBand: if ( this.streamFrame.height - this.renderOffset ) < .5 // 1/2" this.parent.beginNewFrame() endifIn a group footerBand you would need to check the level of "parent" -- there is one more layer there ... so "this.parent.beginNewFrame()" would become "this.parent.parent.beginNewFrame()".
Forcing a New Page with Multiple Columns (Phone Book Example)
Another example of forcing your report to a new streamFrame or new page would be an
example based on a question that was posted on the Compuserve VDBASE forum by a user -- he was creating a phone book listing for a series of apartment buildings, which was done in three columns. He needed the ability to change to a new page when the apartment building changes.
For this example, we will use an "Apartment" table (which is not one of the sample tables -- it is a quick table thrown together for this example, and has data copied from other sources, and repeats a lot). When setting up the report, set the indexName to the "BLDNAME" ... Place the name and phone number fields onto the streamFrame's detailBand.
Place a group on the report, and set the groupBy property to the "Apartment Building" field. While you are in the inspector, set the headerEveryFrame property to true (otherwise it will not print on new pages ...). Go to the headerBand object in the inspector, and set the beginNewFrame property to true. You should change the text value of the text property that is automatically placed on the group's headerBand to something like:
"Apartment Building "+this.form.apartmnt1.rowset.fields["Apartment Building"].value
We will use the groupFooterBand to print a simple text control to show the end of the current apartment building entries. This should have text like "** End of Apartment Building".
Next, set up the streamFrames -- we need three for this example (suggest width of 2.45 inches).
Now that the report itself is laid out, we get to the more involved code part -- adding code that will make sure that each apartment building starts on its own page.
We will use the group's footerBand to force the printing of the next group to the next page by checking to see which streamFrame we are printing in. If we are not in streamFrame3 (the last streamFrame on the page), we need to force the report to the next page. To do this, set the onRender event for the group's footerBand to:
// do something with streamFrame property if this.streamFrame.name # "STREAMFRAME3" // last streamFrame on page // set a flag (check it in streamFrame's canRender): this.parent.parent.parent.NextPage = true endif
The reason we check for streamFrame3 is that if we are on this streamFrame, we will automatically jump to the next page. We need to see if we're NOT on that streamFrame.
Next, we must set a canRender event for streamFrames (2 and 3). For streamFrame2, set the following:
// is the flag true? If so, if this.parent.parent.NextPage // go to the next frame ... (well, since we're doing // this twice, we really want to go to the next page) this.parent.parent.streamSource1.beginNewFrame() endif
To be sure that this event fires also for streamFrame3, all you have to do is click the "Method" menu, and Link, and link the streamFrame3 canRender event to this same event.
Finally, you want to be sure that this flag does not remain 'true' for the duration of the report -- otherwise you would only ever print in the first streamFrame (or column) -- and never get to the other two, after the flag was set to true the first time. So, in the onRender event of the group's headerBand, set this simple bit of code:
// once it's been rendered, we don't want to // force this again: this.parent.parent.parent.nextPage = false
As with any report, there are probably a variety of things you would want to do to clean this up and make it look better. This should give you some idea of how to force a new page, however ...
A working version of this is in the file APARTMNT.REP (using table APARTMNT.DBF).
Skipping n Labels
Now that you've been warned ... basically, using techniques already discussed in this section of the paper, it is possible to set the canRender event for individual streamFrames (each label is a streamFrame on a pageTemplate) so that if it is one you wish to skip printing on (because the label at that position has been used) you can ...
Before going on, a brief warning: Most printers and/or labels warn that it is a bad idea to re-use a sheet of labels, once they've been run through the printer. If you have never had to deal with a label stuck on the drum of a laser printer, you don't know how difficult it is to clean ...
You would need to create a program that looped through the streamFrames on your report (labels), and set the canRender event to force a call to the streamSource's beginNewFrame() method.
However -- if you are not paying attention -- you will skip those streamFrames for every sheet of your labels, which is probably not what you want. You need to check to see which reportPage you are on.
The pertinent code from the program on the conference CD "SKIPLABL.PRG" is:
p = rMyLab.pageTemplate1 // shorten references // Loop through the streamFrames ... for i = 1 to nSkipLabels cEvent = "p.streamFrame"+i+".canRender" // the event must be set ONLY for the first page -- otherwise // we will skip this streamFrame for all pages ...: &cEvent. = {; iif( this.parent.parent.reportPage = 1, ; this.parent.parent.streamSource1.beginNewFrame(), null ) } next rMyLab.render()
A working version of this is in the file SKIPLABL.PRG. (This needs to be used with your own label (.LAB) file)
Cover Pages Using the Report's headerBand
The simple method of creating a report with a cover page is to use the report's
headerBand, combined with the streamSource's beginNewFrame() event.
The report's reportGroup headerBand only prints once -- at the beginning of a report.
To use this method, start creating a report as you normally would. Once you have a basic layout, in the inspector, select the report's reportGroup headerBand. Set the height property to some large value, say, 5 inches. Place what you need to appear on the headerBand -- text, images, even data from a secondary table can be placed here -- as long as you are not intending to process multiple rows of the secondary table.
Next, in the onRender event for the reportGroup's headerBand, set the following code:
// Set StreamSource's beginNewFrame() event: this.parent.parent.streamSource1.beginNewFrame()
This is a nice and easy way to do the coverPage -- the only drawback is that any controls that you have on the pageTemplate will also display on the same page as the reportGroup's headerBand.
One way to avoid this is to simply use the canRender event for any controls that you do not wish to print on subsequent pages and check to see if the current page number is 1, and return "true", otherwise return "false":
function MyText1_canRender return iif( this.parent.parent.reportPage == 1, true, false )
Another way to avoid this (if you want to) is to use the next method shown in this paper -- multiple pageTemplates.
A working version of this is in the file COVPAGE1.REP (using the CUSTOMER table).
Cover Pages Using Multiple pageTemplates
The second method of creating a cover page requires that you design a report with multiple pageTemplates, which cannot be done directly via the designer (but once you have done some setup, the designer does recognize the two pageTemplates).
To understand what needs to be done -- you must set up a report that has two pageTemplates -- the first will be your cover page, the second will be what is used for the rest of the report. In order to do this, you will need to go into the source code for your report, and copy some of the code, and make some other changes to the code so that the report engine will know how to process it. You can then set some text controls and/or images (useful for logos) on your cover page. Finally, you need to set for one of the text controls some code that will tell the report engine to start the next BAND to print (groupHeaderBand or detailBand ...) on the next streamFrame (which is on the next pageTemplate).
NOTE: Due to the nature of custom reports, some properties are not streamed out, such as the pageTemplate -- hence you will have to do this without using a custom report. The sample report (on the Conference CD) uses a copy of a report that was copied from the custom report normally used for reports for this paper, called GSPTEMP.REP. This template has the same properties as the custom report, but has all the properties streamed out.
Step-by-Step:
this.PAGETEMPLATE1 = new PAGETEMPLATE(this) with (this.PAGETEMPLATE1) height = 11 width = 8.5 marginTop = 0.75 marginLeft = 0.75 marginBottom = 0.75 marginRight = 0.5 gridLineWidth = 0 endwith this.PAGETEMPLATE1.STREAMFRAME1 = new STREAMFRAME(this.PAGETEMPLATE1) with (this.PAGETEMPLATE1.STREAMFRAME1) height = 8.0521 left = 0.25 top = 0.9479 width = 6.5 form.STREAMFRAME1 = form.pagetemplate1.streamframe1 endwithWith these two blocks of code copied, change all references to pageTemplate1 to pageTemplate2. NOTE also that if you have any controls on the report that were on the pageTemplate, such as titles, page numbers, etc., you should set the pageTemplate reference to 2 on these as well.
this.firstPageTemplate = this.form.pagetemplate1 this.form.pagetemplate1.nextPageTemplate = this.form.pagetemplate1 this.form.pagetemplate1.streamframe1.streamSource = this.form.STREAMSOURCE1And make it look like (by coping appropriate lines, and making appropriate changes):
this.firstPageTemplate = this.form.pagetemplate1 this.form.pagetemplate1.nextPageTemplate = this.form.pagetemplate2 this.form.pagetemplate2.nextPageTemplate = this.form.pagetemplate2 this.form.pagetemplate1.streamframe1.streamSource = this.form.STREAMSOURCE1 this.form.pagetemplate2.streamframe1.streamSource = this.form.STREAMSOURCE1(This means copying some statements, and changing references to pageTemplate1 to pageTemplate2 -- be careful though and don't change all references to pageTemplate1!)
What you just did was to ensure that the nextPageTemplate reference for the first pageTemplate points to the second, and the second now refers to itself, so that it loops back on itself (in the first code, pageTemplate1 refers to itself in this property); and we also make sure that both pageTemplates are referring to the same streamSource.
this.parent.parent.streamSource1.beginNewFrame()
this.PAGETEMPLATE1.TEXT1 = new TEXT(this.PAGETEMPLATE1)And move it and the block after it (the with/endwith block of code) so that it appears before the streamFrame for pageTemplate1.
{||form.queryname.rowset.fields["FIELDNAME"].value}And set any font and other properties as you might need ...
A working version of this is in the file COVPAGE2 (using data from the CUSTOMER table).
Page Numbering
If you use the custom control that ships with Visual dBASE to display the current
page number on your report, you will find that the first page of the report with
actual data on it starts at page 2. If you want your report to start with page 1 on
the first page of data (i.e., not the cover page), then the solution is to remove
the custom control, and simply place a text control on the report, with the text
property being set to:
{|| form.reportPage - 1 }
And of course, set the fonts and fontsizes and such appropriately.
Changing the Paper Tray
One last item that might be useful -- I got a lot of ideas from users for this
paper, and one user asked about changing the paperTray as the report is printing.
You might want to use a company letterhead for the cover page, and plain paper for the rest of the report.
In the onPage event for the report, check the reportPage value:
if form.reportPage == 1 // page that just printed form.printer.paperSource = 15 // you will need to check this // for each printer you are using // on my Laserjet 4 at work, 1 = Upper tray // and 2 = lower tray endif
In addition, you would need to set the paperSource for the coverPage in the printer object as the default paperSource for the report, since the first page to print is the cover page.
First, create a new report. Place your table(s)/dataModule(s) on the report. Set your pageTemplate's left margin to a larger value than the right margin (shifting the page over to the right tad) -- example, use .75" for the left, and .5" for the right (or you could get more dramatic and use .25" for the right!). Place a couple of fields onto the streamFrame, so that you can see some text in the report, and then save it.
Bring up the source code in the editor. You will need to copy the pageTemplate definitions, and make some other minor changes to your report:
Copy these two blocks of code, and change the name to PAGETEMPLATE2 wherever it appears as PAGETEMPLATE1 below (this is similar to what was discussed in the Cover Page part of this paper above, but is not exactly the same):
this.PAGETEMPLATE1 = new PAGETEMPLATE(this) with (this.PAGETEMPLATE1) height = 11 width = 8.5 marginTop = 0.75 marginLeft = 0.75 marginBottom = 0.75 marginRight = 0.5 gridLineWidth = 0 endwith this.PAGETEMPLATE1.STREAMFRAME1 = new STREAMFRAME(this.PAGETEMPLATE1) with (this.PAGETEMPLATE1.STREAMFRAME1) height = 8.0521 left = 0.25 top = 0.9479 width = 6.5 form.STREAMFRAME1 = form.pagetemplate1.streamframe1 endwith
Next, change the marginLeft and marginRight values (shown above -- reverse them).
Copy all controls that are on the first pageTemplate (directly) as well, and change the pageTemplate1 references in the copies to pageTemplate2 (or you won't have any headings at the top (or bottom) of your report on one pageTemplate).
Finally, go to the bottom of the code, and find these lines:
this.firstPageTemplate = this.form.pagetemplate1 this.form.pagetemplate1.nextPageTemplate = this.form.pagetemplate1 this.form.pagetemplate1.streamframe1.streamSource = this.form.STREAMSOURCE1
Change the second statement to read:
this.form.pagetemplate1.nextPageTemplate = this.form.pagetemplate2
Copy this statement and make the copy read:
this.form.pagetemplate2.nextPageTemplate = this.form.pagetemplate1
The two pageTemplates are now referring to each other -- pageTemplate1 will execute for odd number pages, and pageTemplate2 will execute for even number pages, giving you the offset you need for binding. However, you still need to assign a streamSource for the second pageTemplate, so copy the third statement in the original code above, and make it read:
this.form.pagetemplate2.streamframe1.streamSource = this.form.STREAMSOURCE1
So when you're done, that last section of code should look like:
this.firstPageTemplate = this.form.pagetemplate1 this.form.pagetemplate1.nextPageTemplate = this.form.pagetemplate2 this.form.pagetemplate2.nextPageTemplate = this.form.pagetemplate1 this.form.pagetemplate1.streamframe1.streamSource = this.form.STREAMSOURCE1 this.form.pagetemplate2.streamframe1.streamSource = this.form.STREAMSOURCE1 this.form.STREAMSOURCE1.rowset = this.form.customer1.rowset
When you go back into the designer, if you got all the changes correct, it should now reflect this, and if you generate a report based on this, you will see the page offset take effect. (Actually, you should also see it in the designer ...)
A working version of this is in the file BINDOFF1 (using data from the CUSTOMER table).
Method 2 (Custom Text Control)
This method may actually be more simple than above. The main reason I would use
the version above is so that you could set headers and footers for each pageTemplate
that were more complicated than simple text changes (for example, alternating page numbers and/or report titles on the left/right like you might find in a book) ... You can do the same with this method, by changing the text of the text objects when you change the pageTemplate's margins. So, you might want to do the binding offsets shown
in the first method only if your pageTemplates are very different ...
This method uses a text control's text property (which is evaluated each time it is rendered) to change properties of the pageTemplate, and in addition, you could change a text control's text itself at the same time ...
You can do this by creating a custom control, or by simply placing a text control onto the report, and in the source code, making the TEXT property point to a method of the report:
text = class::updatethistext
This function would then look like:
function updatethistext nPageNo = this.form.reportPage if mod( nPageNo, 2 ) == 0 // even number form.pageTemplate1.marginLeft := .25 form.pageTemplate1.marginRight := .75 cText = "Even number pages" else form.pageTemplate1.marginLeft := .75 form.pageTemplate1.marginRight := .25 cText = "Odd number pages" endif return ( cText )
One thing I noticed is that the designer sometimes evaluates a text control's "text" event and saves it, so you might want to add the following to be sure that this does not occur (otherwise the event will not execute):
onOpen = {; this.text = class::updatethistext }
A working report showing this is in the file BINDOFF2 (using data from the CUSTOMER table). The custom control BINDING.CC is also provided. Read the header carefully to make sure you understand what it's doing.
What all this boils down to is that when you run a report, you can actually change properties of the report, the database objects used by the report, and/or the visual (and non-visual) controls of the report itself, before actually rendering the report.
This gives you a lot of power. I have found in several of my reports and labels that I didn't have to have multiple copies of the same report with minor changes to them like I used to in Crystal Reports -- I can have a single report, and before I render it, I can change just about anything -- as long as I know how to get to the specific control or property I need to change.
The important part of this is that you have to create an instance of your report, but do not render it ... this means that you cannot simply issue the command:
do myrep.rep
At least, not if you wish to retain some control ... what you can do is to create an instance of the report, and make changes to your properties, and then render it:
set procedure to myrep.rep additive rMyRep = new MyRepReport() // class name in report file // change properties as needed here // we'll look at this in more detail ... rMyRep.render() // this is how we actually generate the report
What kinds of things can we do here? Let's examine the possibilities:
// set the name of the table here: cMyTable = "week1" // create an instance of the report: set procedure to myrep.rep additive rMyRep = new MyRepReport() // change the table used by the query: rMyRep.myquery.active := false rMyRep.myquery.sql := [select * from "]+cMyTable+["] rMyRep.myquery.active := true // now we can render it: rMyRep.render()
// from here on we'll assume that the instance of the report is // being done, and concentrate on the actual changes: rMyRep.myQuery.active := false rMyRep.myquery.sql := [select * from "week1" WHERE somefield = "]+variable+["] rMyRep.myQuery.active := true
This code sample uses the WHERE of the SQL select, but you could also use the filter property of the rowset, or the beginFilter()/applyFilter() or setRange()/clearRange events.
rMyRep.pageTemplate1.mytextcontrol.text := "New Text Value"
rMyRep.pageTemplate1.streamFrame1.mytextcontrol.visible := false
This code can be used to turn the visible property on and off easily; you can also set code for canRender and onRender for various visible controls, and much more.
Many folk have asked for the ability to generate a text value that shows the current page number, and the total number of pages in the report. The question is, how does the report generator know how many pages there are? If you're working with a table, this value is variable -- more records get added, some may get deleted, and so on. It makes it rather difficult to store that information somewhere.
The difficulty only lies in any report where the height of the detailBand is variable. If your detailBand is fixed, so that, say, you always display 50 rows per page, you could actually calculate the number of pages and bypass the following code.
Otherwise, if your detailBand is variable in height, the only method of getting the total number of pages is to do what is called a "two-pass" report. The technique shown might be used for other situations, but was specifically designed for this one.
Basically, you need a custom text control that will generate the report to a temporary file, so that you can obtain the number of pages in the report. In order to do that, you must use the onOpen event of a text control. Here's the basic definition, and the code will be examined in detail:
class PageXofY( parentObj ) of Text( parentObj ) custom with ( this ) height := 1 metric := 0 text := class::GetPageNum OnOpen := class::GetLastPage endwith
After the definition of the events used we will see the endClass statement (necessary for any custom control). Notice that the onOpen event is set to execute an event called "GetLastPage" -- when the report is opened (run, displayed in a reportViewer, displayed in the project manager, etc.) the code in this event will fire. This code looks like:
PROCEDURE GetLastPage // check to see if this has been set: if type( "this.dontLoopOnMe" ) # "U" return endif
We'll come back to the 'dontLoopOnMe' bits. It's a touch complicated.
// have to create an instance of the report: cReportName = this.form.className rRep = new &cReportName.() // new instance of report
Here we have just created a new instance of the report -- this way we can change some of the properties and events, without affecting the actual report.
// Have to assume "pageTemplate1" as well as "PageXofY1" if type( 'rRep.pageTemplate1.PageXofY1' ) == "O" rRep.pageTemplate1.PageXofY1.dontLoopOnMe = true endif
Again, we'll come back to a discussion of this code ...
// Make some changes: rRep.startPage := 1 // we want all rRep.endPage := -1 // pages cOutPut = fUnique( "temp????.prn" ) // unique filename rRep.outPutFileName := cOutPut // to temp file rRep.outPut := 2 // printer file nLastPage = 0 // set counter rRep.onPage := {|| nLastPage++ } // increment for each page
The changes made in this instance of the report are important. The startPage and endPage are just to ensure we get the whole report (although if a filter is set, it will take effect here as well). Notice that we set an "outPutFileName" property, so that we're outputting to a printer file, and then set the "outPut" property to that. Next we create a counter, and then override the "onPage" event with a codeblock (the last statement in the code above) -- in this case, we are incrementing the counter for each page printed.
// Render the report rRep.render()
Render the report to the temporary file.
// Now we're done with this copy of the // report, release it: rRep.release()
Release this instance of the report, as it is no longer necessary.
// done -- cleanup erase &cOutPut
Erase the temporary file.
// This is what it's all about! this.LastPage = nLastPage
Save the value in the counter to a property of the text object ("this" refers to the text control at this point in time).
So, we have the last page number. How do we actually display this information on the report? In the constructor code of this control is the statement:
text := class::GetPageNum
This tells dBASE that every time we want to display this control, we must execute the code in the event GetPageNum, which is shown below (this code is based on the code in REPORT.CC which ships with Visual dBASE for the pageNumber text control):
PROCEDURE GetPageNum local nPage nPage = 0 if TYPE("this.form.reportPage") == "N" nPage := this.form.reportPage endif if TYPE( "this.LastPage" ) == "U" or this.LastPage = 0 this.LastPage = 9999 endif
This small bit of code (above) is here to make sure that we have a last page number to display when designing the report. I purposefully gave a large value (9999) just to be sure there's enough room when laying out the report ...
return ( "Page "+nPage+" of "+this.LastPage )
The value displayed is what is shown above -- notice we are combining numeric values and text ...
Now, back to that "dontLoopOnMe" bit -- why is that there? This code is designed to create an instance of the report that we want to print, and render it. When we Render the report, dBase executes the onOpen event for this control. If we aren't careful, we could end up in an awful loop, because the onOpen event renders the report, which executes that onOpen event, which renders the report ... What this code must do is create a small flag that says "don't do that!" -- or more explicitly, "only do that one time!". So, in the primary instance of the report (the first time the report is rendered), we look to see if a special flag is set (dontLoopOnMe). If it is not set, then we want to execute the onOpen event. The onOpen event then generates a clone of the report (a new instance of it), but ... in that instance of the report, we do NOT want to re-execute the code in the onOpen event. So, we find the control, and create the flag (dontLoopOnMe). That way, when we render the report to the temporary file, the report will come across the check at the beginning of the event that says that if the flag exists, RETURN, rather than executing the rest of the event.
A few warnings: If your report is going to be used on a network with multiple users, you may want to be sure that the table(s) is/are locked if you are using this control. The reason is simple: between the time you generate the last page number, and the time you actually run your report, users could be adding rows (or deleting them) in the table(s) used for the report. This might throw your last page number off completely. Another potential network problem is that if you use a hard-coded filename in the code, one user may over-write another user's output file, and you will run into some serious problems. Using fUnique() to get a unique filename is a good idea (as shown in the above code). The second warning is: if you are creating a potentially very large report -- you may not wish to use this because it will take a very long time to generate the report -- why? Because we must render the report twice! Once to find the last page number, and again to actually render (print) it.
There is a complete working example of this custom class on the CD, named PAGEXOFY.CC. To use it (type the following in the command window):
set procedure to pagexofy.cc additive
This will place the control into the component palette, and when you design your next report, it will be available for your use from the 'Custom' tab of the component palette.
A working report showing this is in the file PAGEXOFY.REP (using data from the CUSTOMER table). The custom control PAGEXOFY.CC is also available. Read the header carefully to make sure you understand what it's doing.
The solution is pretty simple -- store a value to a custom property of the application framework as below:
_app.nPagePlus = 4 // 5 - 1 (starting page number - 1)
Then use the custom page number class that is on the conference CD: PAGEN.CC which checks for the existance of "_app.nPagePlus", and if it is there, it adds that value to the report's pageNo property, and displays the new page number on the report. This class otherwise works exactly like the page number custom control that ships with Visual dBASE (in \CUSTOM\REPORT.CC), and is actually a copy of that control with this minor modification (added at the end of the pageValue method):
if TYPE("_app.nPagePlus" ) == "N" nPage += _app.nPagePlus endif
You may want to make sure that you stub out the value in _app.nPagePlus when you are done rendering your report. The following code would render your report and set the page number to the value you wish:
_app.nPagePlus = 4 do myrep.rep _app.nPagePlus = null
The custom control PAGEN.CC is also available. Read the header carefully to make sure you understand what it's doing.
It is often desireable to allow your user the ability to preview a report, and possibly to select specific pages to print, and/or allow other options. To that end, some effort went into creating the form shown here, which will preview a report, allow the user to see various pages of the report, select pages to print, and more. It uses some of the same code as shown in the "PageXofY" custom control above if the user wishes to view the last page of a report.
The form shown uses a reportViewer control, and some buttons that have various code attached for manipulating the page displayed, and more. The code is pretty well documented, but if there is time during this session, we may get into some of the details of the code.
The form PREVIEW.WFM is available. Read the header carefully to make sure you understand what it's doing. Note that some of the sample reports shown here do not work with this form for some reason ...
At its most basic, you can generate a whole report as one HTML file by following these steps:
If you store these properties in the designer, then every time you render your report it will be rendered as HTML. This may not be what you want to do. So instead, you might want to create a small program that creates an instance of the report, sets these properties in that instance, and then renders the report. This would look like:
/* Send MYREPORT.REP to an HTML file: */ set procedure to myreport.rep additive rMyRep = new MYREPORTREPORT() // check the source code to get the right name rMyRep.output = 4 // HTMLFile rMyRep.outputFileName = "MYREPORT.HTM" // you can include a path ... rMyRep.render() close procedure myreport.rep
There is one thing that can be a bit confusing -- a Visual dBASE report that has been output as HTML uses a file called "TRANS.GIF" -- but when you view the report, there's no image on the report. This is because "TRANS" is short for "TRANSPARENT" -- the GIF exists (check the directory you have the report generated to), but it's very small and effectively empty -- it's transparent. This .GIF file is used to line up the table columns used for your report, to be sure everything is laid out as close as possible to what you see in the designer. In addition, if you erase this file, because it appears to be useless, your report will look very strange -- you will see a "broken image" icon in your browser (actually, depending on the complexity of your report, you may see a lot of them); and your output will not line up the way you expected it to.
One problem with outputting a report is if it is very large -- a large HTML file can take quite a bit of time to download from the internet. You might want to, instead, output your report one page to a file (one "page" being the same as if you printed the report). This can be done fairly easily, but it does take a bit of coding to make it work.
Here are the details of what you would want to do:
Note that there are many files on the Conference CD as files you might want to examine, including sample reports, custom controls and programs. These are all marked by the spinning CD throughout the paper.