Have you ever had one of those problems that seemed simple but turned out to be anything but simple? Recently I received a request to provide the capability to combine several reports into one print job (that could be sent to any printer, including the Adobe PDF printer). I had previously completed the same request in another project with great ease (with the help of my friend Shelly who came up with the original solution) so I figured it would be a snap to implement the solution again in another project. Boy was I wrong.
Everything worked as it had in the other project except for one thing: the reports in this new project contained a mix of portrait and landscape reports (the other project had all portrait). Therein lies the problem: the first page sent to a print job dictates the orientation for all subsequent pages. In this case, the first report sent was in portrait mode and thus all of the landscape reports were getting cut off as the data was being sent to a portrait layout.
The Journey Begins
Not one to accept defeat easily, I decided to pursue a solution to combining portrait and landscape reports into one single print job. My friend Shelly found a slim lead on the Fox Wiki that mentioned a solution Lisa Slater Nicholls (LSN) had came up with for a similar problem. Shelly emailed Lisa directly for more info and was given a link to an article she had published in 2007. That article in turn led me to another of her articles titled A print job to call your own. This article was the catalyst that gave me what I needed to figure out how to solve my problem.
The solution involved a few new tools and concepts I had not explored much before: Report Listeners, manually creating and controlling a print job and dealing with API struct data types. The only reason I needed to learn more about structs was because some native HP printer drivers balk when a certain API function (StartDoc) is called with an invalid struct argument (the DOCINFO struct). More on that later.
The full source for this article can be downloaded here. Let me know if you have any trouble downloading it.
The Report Listener Class
I loosely derived my report listener off of LSN’s PrintJobListener class. The only custom method from the original PrintJobListener class I use is SizePages which determines the proper page height and width for a document. The SizePages method was critical in the implementation of my report listener. The LoadReport and UnloadReport events were also used. My custom PrintJobListener class is directly implemented by another custom class called PrintMixedRepTypes. Therefore the PrintJobListener class is not intended to be instantiated directly.
The Print Mixed Report Types Class
PrintMixedRepTypes is the interface that is used to initiate a print run. This class is responsible for the following actions:
- Creating a print job by utilizing the CreateDC and StartDoc API functions
- Obtaining the report orientation by utilizing SYS(1037,2)
- Running the report (which invokes the LoadReport and UnloadReport events of the PrintJobListener report listener)
- Closing the print job by utilizing the EndDoc and DeleteDC API functions (upon which case the job starts printing)
Steps 2 and 3 are repeated for each report that is to be included in the report run.
Getting Things Started
Instantiation of the PrintMixedRepTypes class looks something like this:
loPrintMixed = NEWOBJECT("PrintMixedRepTypes", "printmixedreptypes.prg", "", "Print Job Name")
The Init method in turn instantiates the PrintJobListener:
*!* PrintJobListener’s ListenerType is 3 (renders all pages at once, and does not explicitly call _REPORTPREVIEW)
*!* This causes the report engine to cache all pages so that we have complete control of the print job
this.Rl = NEWOBJECT("PrintJobListener", "printjoblistener.prg", "", tnPageWidth, tnPageHeight)
Once the PrintMixedRepTypes class is instantiated, it’s time to call the PrintMixed method. PrintMixed is called for each report to be included in the run. The first call to PrintMixed starts the print job. The second argument indicates whether this is the first report in the run. The third parameter indicates whether this is the last report in the run. So, the first report sets the second argument to .T. while the last report sets the third argument to .T. (and all reports in between leave those flags set to .F.). The last argument is a character string that specifies the name of the printer to send the output to (this would usually be obtained via GETPRINTER(), but here I’m using the Adobe PDF printer for simplicity).
llPrintMixedResult = loPrintMixed.printmixed("report1.frx", .T., .F., “Adobe PDF”)
Assuming 2 more reports are to be printed, the second and third calls to PrintMixed would be as follows.
llPrintMixedResult = loPrintMixed.printmixed("report2.frx", .F., .F., “Adobe PDF”)
llPrintMixedResult = loPrintMixed.printmixed("report3.frx", .F., .T., “Adobe PDF”) && this call closes the print job
The first thing PrintMixed has to do is start the print job for the selected printer.
this.Rl.HDC = CreateDC("WINSPOOL", “Adobe PDF”, NULL, NULL) && creates a device context (DC) for the desired printer
*!* Create a DOCINFO structure to pass to StartDoc
*!* Empty DOCINFO (PADR(CHR(20), 20, CHR(0))) doesn’t work
*!* with many HP printer drivers or the Adobe PDF driver
*!*
*!* Must use a valid DOCINFO struct (provided by winstruct DOCINFO class)*!* Load classlibs used to get a DOCINFO struct (for StartDoc)
*!* http://www.foxpert.com/download/struct.zipSET PATH TO "C:Toolsstruct" ADDITIVE
SET CLASSLIB TO "C:Toolsstructstruct.vcx" ADDITIVE
SET CLASSLIB TO "C:Toolsstructwinstruct.vcx" ADDITIVELOCAL loDocInfo, lnJobID
loDocInfo = CREATEOBJECT("DOCINFO")
WITH loDocInfo
.cbSize = .SizeOf()
.lpszDocName = this.JobName && This allows you to specify the name of the print job
ENDWITH*!* Call StartDoc() passing DOCINFO from loDocInfo.GetString()
lnJobID = StartDoc(this.Rl.HDC, loDocInfo.GetString())*!* Unload struct classlibs
RELEASE CLASSLIB "C:Toolsstructwinstruct.vcx"
RELEASE CLASSLIB "C:Toolsstructstruct.vcx"
The following code is executed for each report in the run:
*!* Open report FRX so we can get orientation, paper size, color, etc stored in Expr
USE report.frx ALIAS tempfrx IN 0 EXCLUSIVE && Must be opened Exclusive for Sys(1037)*!* Copy FRX to temporary cursor (otherwise SYS(1037) would populate the Tag and Tag2
*!* fields of the report file)
SELECT * ;
FROM tempfrx ;
INTO CURSOR repstruct READWRITESELECT repstruct
SYS(1037,2) && This sets the expr, tag and tag2 fields of the FIRST record to the
&& current printer settings.
&& This is necessary so we can use the tag2 data to reset the current DC
&& to that of the current report layout (i.e. page orientation, page count)USE IN tempfrx
Once the printer information is captured, it’s time to run the report.
*!* This could be enhanced to include support for a FOR clause (would require another argument)
REPORT FORM report1.frx OBJECT this.Rl && This will fire the LoadReport method of the Rl
The last report in the run is responsible for closing the print job. The following code is executed after the last report has ran:
= EndDoc(this.Rl.HDC) && This closes the print job so the document can print
= DeleteDC(this.Rl.HDC) && Deletes the specified device context (DC)
Now that we see how a report run is initiated, let’s take a closer look at the PrintJobListener’s LoadReport event. LoadReport is the critical point where we can “hot swap” the current print job’s orientation. ResetDC takes a DEVMODE struct as its second argument. The Tag2 field of the report is actually a valid DEVMODE struct that can be passed to ResetDC.
PROCEDURE LoadReport()
IF !USED("repstruct")
RETURN .F.
ENDIF
*!* Get Tag2 data (DEVMODE struct)
lnCWA = SELECT(0)
SELECT repstruct
lcTag2 = repstruct.tag2
SELECT (lnCWA)*!* ResetDC will allow changing the orientation of the current
*!* print job based on the report being printed
= ResetDC(THIS.HDC, ALLTRIM(lcTag2))RETURN DODEFAULT()
ENDPROC
Keep in mind that all pages are cached since the Report Listener type is 3. Once the report run is complete, the UnloadReport event of PrintJobListener fires. UnloadReport contains code that actually sends each page of the report to the current print job.
PROCEDURE UnloadReport()
IF THIS.ListenerType = 3 AND ;
THIS.OutputPageCount > 0 AND THIS.HDC # -1LOCAL lnPage, lnLeft, lnTop, lnWidth, lnHeight
*!* Output each page in the report one by one
FOR lnPage = 1 TO this.OutputPageCount
retval = StartPage(this.HDC)IF retval <= 0
MESSAGEBOX("Error calling StartPage" + CHR(13) + ;
" Error code : " + ALLTRIM(STR(GetLastError())),48)
RETURN
ENDIFTHIS.SizePages(@lnLeft, @lnTop, @lnWidth, @lnHeight)
*!* There seems to be some threshold in the dimmensions. If
*!* they’re not within that threshold, the data is rendered EMF*!* Subtract one from width so data is not rendered EMF
lnWidth = lnWidth – 1 && otherwise, output renders as EMFthis.OutputPage(lnPage, this.HDC, 0, ;
lnLeft, lnTop, lnWidth, lnHeight) && output the requested pageretval = EndPage(this.HDC)
IF retval <= 0
MESSAGEBOX("Error calling EndPage" + CHR(13) + ;
" Error code : " + ALLTRIM(STR(GetLastError())),48)
RETURN
ENDIF
ENDFOR
ENDIFDODEFAULT()
ENDPROC
One significant issue I ran into was the way the output was rendered (especially when printing to a PDF printer). Landscape reports rendered perfectly. The problem was with portrait reports. For some reason, the output was rendered in EMF format; each page was a poorly rendered static image. This led me down a long and lonely road of trial and error.
I eventually discovered that if I subtracted one from the calculated page width, the output would render correctly. Now, this may not work in other environments, but for my purposes it was all I needed to do to get perfectly rendered output. You may need to tweak the width and/or height depending on your situation. You can use the reportpagewidth and reportpageheight properties of the PrintJobListener to override the calculated values if you need to find out what works for you.
It is worth noting that the SizePages method is a very import part of the process. Without that code it would have had a hard time determining the page width and height. Many thanks to Lisa Slater Nicholls for enlightening me on how to tackle this problem!
All included code and samples are provided as-is and the author assumes no responsibility for the use or misuse of this information. If anyone has any suggestions or corrections, please let me know.