A print job to call your own
By Lisa Slater Nicholls
This article was first published in FoxTalk in October 2005. As far as I know, it is not publicly available anywhere now.
It has some information of real value, not just to Fox people wrestling with print jobs, but for
people in any environment seeking to do something similar -- so it is with pleasure that I reprint it here.
Please note that, in VFP 9.0 SP2, you'll have a better way to "swap out" a copy of the report at LoadReport time than is described in this article and the helpfile. In fact, the FFC library's _ReportListener class will expose methods to leverage the new feature -- making it much more fun to do. Also, note that the proposed changes to handling of DPI in the Report Engine for SP1 and discussed herein, did, in fact, take place. Use that version of the code in SP1 and SP2.
The source for this article is available in Spacefold downloads
SQL Server Reporting folks who want to do something similar should consult the Print a report from a console app
sample on Got ReportViewer?
to understand how the ideas here can be applied by them. Both
VFP and RS folks will find some related thoughts here
In a previous article about generating PDFs (PDF Power to the People,
June, 2005) I said that my custom Reportlistener class used some "special magic" to save and restore printer setups.
PROTECTED methods of the PDFListener class,
LoadPrinterInfo() and UnloadPrinterInfo(),
I used VFP 9.0-enhanced calls to
and I recommended that you be careful to call these methods in an appropriate "push" and "pop"
sequence when saving and restoring VFP's printer environment.
There are other scenarios in which it's better not to manipulate VFP's printer environment excessively. Instead, you can choose to control an entire print job yourself, and send the contents of a report to this print job. In this article, I'll use all three of VFP 9.0's new SYS(1037) calls, and a few very simple Windows API calls to handle a common requirement: switching printer bins, orientation, or other printer capabilities between report pages.
Before I start, let me assure you that I am not a C++ programmer.
When I say "very simple Windows API calls", I mean "very simple".
To achieve this result you need to think deeply about what VFP
is doing, and what you are trying to accomplish in your VFP
application, not about how Windows works.
How to give the printer a piece of your mind
The sample class for this article is called PrintJobListener.
Like PDFListener, it's derived from _ReportListener in the FFC
with one intermediate class layer, OLEAwareReportListener.
To perform its work, PrintJobListener runs all reports using
ListenerType 3, which tells the Report Engine to store all page
images, as it would for a Preview application. The value 3 specifies
no automatic display activity after the run, so you can use
it for an alternative Preview application or send the images
to a different target device, such as image files.
allows you to assign a sequence of print setups for a report
run. Before the report run begins, PrintJobListener sets up
both the FRX and the Report Engine to match your print job's
requirements as closely as possible, using the information in
the first set of printer instructions you've designated. After
the run, when the Report Engine has prepared the content, PrintJobListener
creates a print job and sends the pages to the printer, inserting
your printer instructions as necessary for each set of pages
Figure 1. PrintJobListener
gives you fine-grained control over printer layout settings
for multiple sets of one or more pages in the same print job.
That's the big picture. How does it all happen?
Providing instructions to PrintJobListener
To send instructions of any kind to any printer, PrintJobListener
must designate a way for you to specify what you want. The class
exposes a public property, PrinterSetupTable, which it uses
for the name of a DBF that stores multiple printer setups. You
populate the table by making calls to SYS(1037,1).
Although this table is in standard FRX format, the property
value defaults to "PrintSetups.DBF" and, if you don't supply
an extension with your alternate table name, PrintJobListener
forces the extension to DBF. I've done this to avoid anyone
thinking the table is a runnable FRX, since it contains no standard
FRX layout records. The only columns of significance to SYS(1037)
and print jobs are Expr, Tag, and Tag2, but SYS(1037)
all standard FRX columns to be in the table, as well as EXCLUSIVE
access to the table, to work.
You designate different records
in this table for retrieval by adding appropriate record identifiers
in the Name column, which SYS(1037) ignores. PrintJobListener
uses the memofield Name, rather than the 10-character field
UniqueID, for this purpose, because these identifiers should
be varied and readable.
When you're ready to run a report, you
use a second public property, FRXPrintSetups, to hold a Collection
object. The value for each item in the collection should be
the starting page number of a group of pages. The key for each
item in the collection should be a case-insensitive match for
whatever you used in the Name field in your table.
processes the items of the collection in strict collection order,
so use the collection's Add method in an appropriate sequence.
For example, the test code included in PRINTJOBLISTENER.PRG
does the following:
LOCAL m.ox, m.oy
m.ox = CREATEOBJECT("PrintJobListener")
m.oy = CREATEOBJECT("Collection")
ox.FRXPrintSetups = m.oy
Because Name is a memo field, you can include more information
about the expected usage of each print setup, such as for which
report or report groups it is suitable. For each printer setup
you add, PrintJobListener applies the appropriate printer instructions,
and then sends pages to the printer until it reaches the starting
page for the next setup. When it reaches the final setup in
its collection, it processes all pages until it reaches the
OutputPageCount value it received from the Report Engine for
this report run.
This is a simple-minded way of handling page
ranges, to be sure. You can come up with your own method of
storing and applying page range instructions, once you understand
the "main event": how to store and apply the actual setups to
a print job.
Ensuring the right conditions
As you might expect, PrintJobListener's LoadReport event code
sets up the conditions for the run:
IF NOT THIS.StripAndSaveFRXSetup(;
IF THIS.FRXPrintSetups.Count = 0
First, the class checks the FRX or LBX file you're processing
for any printer setup instructions. If there are any, it removes
them and saves them for later restoration. If the report or
label is readonly and contains printer environment data, or
if PrintJobListener is unable to remove printer environment
details from the FRX or LBX for any other reason, it does not
continue the report run.
At this stage of the report run, if you prefer, you can make
a read-write copy of the report and swap in the name of your
copy for the report run (there is an example showing how to
do this in the LoadReport topic of the VFP docs). For PrintJobListener,
I decided that if you embedded such instructions in a readonly
table, you really wanted them there – and, consequently, that
this report or label was not suitable for print job manipulation.
StripAndSaveFRXSetup(tFRXFile), the method handling any printer
environment values in your report or label form, saves printer
environment data to a protected member property. When you investigate
this method, notice that StripAndSaveFRXSetup indicates a successful
result whether it actually saved a setup information or not.
If there is no setup information to save, there's no problem
whether the report is readonly or read-write. It returns a failure
result if the report contains setup information and is readonly
Figure 2. The report run cannot continue if the report contains
printer setup information and is read-only.
As you can probably tell, this public method is designed for
use outside report runs as well as during a LoadReport event.
It has a straightforward companion method, RestoreSavedFRXSetup(tFRXFile).
You can experiment with using these methods to transfer print
setup information between FRXs, or between FRXs and print setup
storage tables, if you like.
Next, PrintJobListener's LoadReport code checks its FRXPrintSetups
collection property for appropriate contents. If you have not
provided any instructions, it adds a single item to the collection
to handle all the pages of your report.
As a final preliminary
step, it calls its MultipleSetupsOnePrinter method to confirm
availability of its printer setup table, and your instructions
in the table. MultipleSetupsOnePrinter is capable of creating
the table, if it does not exist. The subsidiary method performing
this task uses code I recommend for any similar requirement;
there is no need to maintain a "dummy" report as a model, and
the result should be free of any normal FRX records:
m.lcSafety = SET("SAFETY")
SET SAFETY OFF
CREATE CURSOR (THIS.FRXTempAlias) (onefield l)
CREATE REPORT (THIS.PrinterSetupTable) ;
USE IN (THIS.FRXTempAlias)
USE (THIS.PrinterSetupTable) ;
ALIAS (THIS.FRXPrintSourceAlias) EXCLUSIVE
IF m.lcSafety = "ON"
SET SAFETY ON
PrintJobListener attempts to get exclusive use of the printer
setup table at this point in its processing. If the table previously
existed, exclusive use may not be available, so it opens the
setup table shared.
With the table available, MultipleSetupsOnePrinter
attempts to find your specified setup names in the Name field.
If they are not available, and if it has exclusive use of the
table, the method offers an opportunity for you or your user
to add the new setups on-the-fly (Figure 3).
Figure 3. You can add new setups for the same printer on-the-fly.
Here is an excerpt showing how SYS(1037,1) performs this minor
miracle. This code processes all collection members after the
first one in a loop, after it has handled the first item in
your collection. The code handling the first item is similar:
m.lcSetup = toSetupList.GetKey(m.liIndex)
INSERT BLANK BEFORE
DO WHILE .T.
IF THIS.GetPrinterSetupDeviceInfo(Expr) == ;
REPLACE Name WITH m.lcSetup
BLANK FIELDS Expr, Tag, Tag2
I particularly want you to observe my use of the INSERT BLANK
BEFORE statement. Yes, I do find it incredibly amusing to use
this decrepit command in a modern setting, but, honestly, INSERT
BLANK BEFORE also happens to be ideal when you're coding with
SYS(1037). SYS(1037) uses only the first record in a table!
Also, notice the check for the name of the printer, inside a
DO WHILE loop. True to its name, MultipleSetupsOnePrinter requires
that all the setups you install as part of a single collection
of setups are printer setups for the same printer (Figure 4).
Your multiple setups can have any other differences you like,
but if they're not instructions for the same printer, it's hardly
worthwhile trying to send them to the same print job.
Figure 4. All setups for the print run must be for
the same printer.
MultipleSetupsOnePrinter uses a simple parsing routine, GetPrinterSetupDeviceInfo,
on the Expr field to get the name of the printer setup each
time it calls SYS(1037) so it can verify the printer against
the first item in the collection. You'll see this utility method
in use in several PrintJobListener methods.
In the LoadReport
code, you notice that MultipleSetupsOnePrinter takes the collection
as an argument, rather than just referencing THIS.FRXPrintSetups
directly. This method is public, and in non-demo-use, you can
call it outside any report runs, with a temporary collection
object or a member property of a form, if you prefer. In an
application, you can provide an appropriate user interface,
letting users know to what use, and for what reports, each setup
will be designated. You can include associated report names
or additional information as part of the Name field, or use
other unused columns in the FRX for this purpose.
can't get exclusive use of the table, or if the table is readonly,
it can't add setups on-the-fly, but it still checks out the
existence of your designated setups for this run. Note that
it does not re-verify the sequence for consistent printer information;
managing this table is your responsibility. The print job won't
fail if you make a mistake and designate print setups that don't
belong to the same printer. However, you probably won't get
the results you expect.
With preliminary hurdles cleared, LoadReport
calls the aptly-named Setup method to begin its real work:
PROTECTED PROCEDURE Setup()
THIS.OldPrinterName = SET("PRINTER",3)
USE (THIS.PrinterSetupTable) IN 0 ;
ALIAS (THIS.FRXPrintSourceAlias) AGAIN SHARED
SET PRINTER TO NAME (THIS.CurrentPrinterName)
First, Setup issues a series of DECLARE-DLL statements. You'll
see the individual Windows API calls in use shortly; for now,
I want to point out that their arguments are not complex or
Only two of them even contain a "struct" as an argument
and you don't have to worry about either one.
Next, Setup saves
the name of the original VFP printer, using SET("PRINTER",3).
It re-opens the printer setup instruction table, SHARED this
time, using the alias associated with the table for the balance
of the report run (stored in its protected FRXPrintSourceAlias
property). Then it calls its SetPrinterOptions method, passing
the key for the first print setup in its collection. SetPrinterOptions
checks the print setup table for associated instructions:
SELECT * FROM (THIS.FRXPrintSourceAlias) WHERE ;
ALLTR(UPPER(Name)) == ALLTR(UPPER(tcWhichSetup)) ;
INTO CURSOR (THIS.FRXPrintTargetAlias)
IF NOT EOF(THIS.FRXPrintTargetAlias)
IF THIS.HDC # -1
USE IN (THIS.FRXPrintTargetAlias)
In the slightly-excerpted version of SetPrinterOptions above,
you see a call to the class's GetHDC method. This method is
responsible for getting and maintaining a handle to your own
(not VFP's) print job. As you see below, it simply parses the
Expr field from your print setup instructions table to hand
the relevant information to the Windows CreateDC function. At
the same time, it provides important information to the rest
of the Setup routines by storing a value in the CurrentPrinterName
PROTECTED PROCEDURE GetHDC()
IF THIS.HDC = -1
LOCAL m.lcDevice, m.lcDriver, m.liSelect
m.liSelect = SELECT()
m.lcDevice = THIS.GetPrinterSetupDeviceInfo(Expr,"DEVICE")
m.lcDevice = SET("PRINTER",3)
THIS.CurrentPrinterName = m.lcDevice
m.lcDriver = THIS.GetPrinterSetupDeviceInfo(Expr,"DRIVER")
m.lcDriver = "winspool"
THIS.HDC = CreateDC( m.lcDriver, m.lcDevice, CHR(0), 0 )
THIS.HDC = -1
With the handle available after its call to GetHDC, SetPrinterOptions
calls Windows' ResetDC() function. It passes the handle and
your setup instructions to override the user's standard preferences
for this printer setup.
ResetDC takes setup instructions in
the form of a struct – but don't worry. The struct you need
is exactly what SYS(1037,1) beautifully and completely, though
unintelligibly, saved for you in the Tag2 field of your print
setups table. All you have to do is pass it in.
On this initial
call, Setup also passes a second argument to SetPrinterOptions,
indicating that SetPrinterOptions should save the initial state
of the printer it intends to use, before it calls ResetDC to
make any changes. SetPrinterOption calls SaveRestorePrinterSetupData
for this task.
SaveRestorePrinterSetupData uses SYS(1037,2)
on the save and SYS(1037,3), when it's called again at the end
of the report run, to perform the restore. PrintJobListener
has a second protected member property for this purpose, similar
to the one used in StripAndSaveFRXSetup.
Having called SetPrinterOptions
for the first time, and having set your designated printer for
this run to the appropriate values, PrintJobListener has arranged
your report layout the way you want it. As its final action,
Setup synchs up the Engine to obey the page size, orientation,
et cetera, in your first print setup, with the simple command
SET PRINTER TO NAME (THIS.CurrentPrinterName). Remember, this
code all runs during LoadReport, so the Report Engine hasn't
started its own work to construct the report pages yet.
almost all over except the shouting. After this mass of setup
code, PrintJobListener does absolutely nothing to customize
the Report Engine's processing of the report run, so I'm going
to move directly to UnloadReport, where you send page images
to the printer, in the next section.
But first, take a moment to consider exactly what all this setup
code buys you.
How to print a document
Once it gets to the UnloadReport event, PrintJobListener first
ascertains that there are pages to process; you might have RETURNed
.F. from the LoadReport event. Assuming it has pages ready to
print, PrintJobListener must first tell the printer to expect
a document. It uses the Windows function StartDoc for this purpose,
passing the handle it has already saved during Setup.
iterating through your FRXPrintSetups collection to send each
group of pages to the printer, of course it also tells the printer
the document is complete, using the EndDoc function and passing
the same handle. Finally, it has a Cleanup method in which it
restores all the things it saved during its long setup process.
The code for UnloadReport, and the process of "embracing" a
print run with StartDoc and EndDoc calls, is relatively straightforward,
so I won't include the UnloadReport code in the article text.
When you do investigate the method in the source code, you'll
notice that StartDoc is the second Windows call in the set (besides
ResetDC) that could potentially involve constructing a struct
as an argument – but I'm simply passing a string of null characters.
This argument sends the document to the device to which you've
already pointed the function by handing it the device context
If you want to get fancy, you can go ahead and "enrich"
the StartDoc code by adding information to its lcDocInfo argument.
For example, you could provide a name to show in the Windows
Print Queue for your print job, using the Reportlistener PrintJobName
property to get the desired value.
The truly significant activity
for controlling a print job and, in my experience, the one that
VFP developers have trouble understanding, is sending the individual
pages in the document to the printer, using the Reportlistener's
OutputPage method. Although the method name indicates that you're
sending a page to the target device, remember that OutputPage
only sends a "page" from the Report Engine's point of view.
The printer has no way of knowing that each image you send is,
in fact, a page; you have to tell it. Just as you embraced the
print job with StartDoc and EndDoc calls, you have to embrace
each page with StartPage and EndPage calls.
It might help some
of you to recall the old FoxPro PRINTJOB… ENDPRINTJOB construct
for character-based output. StartDoc() and EndDoc() perform
similar functions to these output-bracketing commands. Remember
the ON PAGE and EJECT PAGE commands you used in a PRINTJOB?
You have to do something to let the printer know what bundles
of output-producing instructions constitute a page, now, just
as you did then. The fact that OutputPage wraps the ReportEngine's
bundle into a single image is immaterial. Here's the code that
PrintJobListener uses. UnloadReport calls this method to handle
each setup-based group of pages:
PROTECTED PROCEDURE ProcessPages(;
tiSetupIndex, tiPageStart, tiPageEnd)
LOCAL m.liPage, m.l, m.t, m.w, m.h
THIS.SizePages(@m.l, @m.t, @m.w, @m.h)
FOR m.liPage = m.tiPageStart TO m.tiPageEnd
THIS.OutputPage(m.liPage, THIS.HDC, ;
m.l, m.t, m.w ,m.h)
In this method, you notice a call to SetPrinterOptions before
ProcessPages begins to send pages to the printer. For each print
setup you designated in your collection, SetPrinterOptions invokes
ResetDC with a different set of instructions on the existing
handle, using the Tag2 contents of the appropriate print setup
After it's adjusted the print job for the coming
page range, ProcessPages needs to figure out the appropriate
left, top, width, and height coordinates for OutputPages. Each
print setup in your collection may have a different page size
and orientation, so these values may be different for each page
range. This is the second area of confusion for most VFP developers.
Part of the confusion arises because VFP 9.0 RTM uses an unexpected
default value for its DPI when providing output to a device
of this type. When you pass a device type of 0 (hDC or GDI handle)
to the Report Engine, the Engine uses this handle to construct
the GDI+ handle (type 1) it needs. Windows sets a default DPI,
which turns out to be 96, in this situation, and the Report
Engine simply accepts this value. Xbase code has no opportunity
to give alternative instructions about device units or resolution.
In SP-1 or later versions, the Engine may be adjusted to interrogate
the handle you pass to the OutputPage method, and set the DPI
appropriately to the printer you're using -- which would make
for much simpler calculation code for most coordinate values
you would want to use as OutputPage arguments. I've included
both versions of the required calculation code in PrintJobListener's
SizePages method. If you are not using the RTM version and the
sizing code does not appear to be working properly when you
run the test code, adjust the DEFINEd constant USE_DEFAULT_DPI_GRAPHICS
value you see at the top of PRINTJOBLISTENER.PRG to .F.. If
you implement this technique in your applications, you can change
this evaluation to a VERSION() check after testing.
working in the right units is only a small part of the confusion
you face when you work out the coordinates. The real question
is: how should you size and scale each page?
The Report Engine
calculates page dimensions exactly once per run, no matter how
many print setups (each with its own page size, offsets, and
orientation) you decided to create for that run.
adjusts the Report Engine at the beginning of the run to use
your first set of instructions, in Setup code you saw earlier.
The SizePages method does its best to match subsequent printer
instructions as the print job progresses. It uses calls to the
Windows function GetDeviceCaps to get information about the
current print setup for each page group, and then determines
a strategy for matching those sizes.
a property, ScalePages, to allow you to tune this strategy.
For each report run, you can choose to scale-and-retain, scale-and-clip,
or scale-and-stretch pages (Figure 5). These choices are similar
to those available when you use an image control in a report
layout, and, just as you've found with images in reports, each
choice is valid for certain combinations of image and page-container
sizes. However, these three choices are by no means the only
choices you could make.
Figure 5.You can choose from three scaling options
for each report run.
For example, you could offer an option to put multiple report
page images on a single physical page. In this scenario, the
ProcessPage method would check the ScalePages property, of course,
because it would not include StartPage and EndPage calls for
each individual report page.
You could also construct a report
layout for form letters, with a title or summary band holding
the envelope layout and the rest of the pages holding the letter
contents. In this scenario, you might want to offset the top
and left positions of the envelope print setup to match special
requirements. If the form letters are all one page in length,
you could work out an "odd-even" system instead of FRXPrintSetups'
collection of page ranges. You could also read a table containing
start page, end page, and print setup name values during the
report run. In some cases, you could even use the Reportlistener's
TwoPassProcess property to construct the print setup collection
or a table of page ranges on the fly, during the report pre-pass.
PrintJobListener's ScalePages_Assign method restricts you to
three possible values for the ScalePages property, to cover
the calculation CASEs I've included in the SizePages code. The
CASE statement in the method contains a CASE .F. and some comments,
as a placeholder for additional choices you might add.
in VFP, the possibilities boggle the mind.
Move on to bigger and better things
Why stop there? All the scenarios, and the print job handling
techniques as presented by PrintJobListener, provide multiple
print setups for a single report, but you can do even more when
you use similar code to handle report pages from multiple reports.
You already know that you can use NOPAGEEJECT to chain multiple
VFP reports in a single print job. Using what you've learned
in this article, you can chain multiple reports more flexibly
than NOPAGEJECT allows. Simply use a separate Reportlistener,
set to ListenerType 3, for each report in the run. Call their
OutputPage methods, with appropriate "embracing" code, after
each report finishes, rather than within the UnloadReport event,
applying new print setup options as you go.
When you take this
approach, you have the ability to design each FRX appropriately
for the task at hand, with a different layout size. Think of
each FRX layout as a document section, similar to a Word document's
sections. Not coincidentally, Word allows you to associate a
layout and print instructions with each section; Word's "first
page – other pages" facility is merely a special case within
What will change in Sedna (a set of enhancements
to VFP that is scheduled for release in 2007)? The short answer
is "not a thing", in the sense that the code in PrintJobListener
will still work. Maybe some of this will get a bit easier, because
Sedna might provide an Xbase toolkit to manage some of the details
for you. But you have all the tools you need to start evaluating
your options, and implementing a strategy that will work, right