You have that data, but you need to display/print it in a form that users can understand and work with it. Great reports are a critical step in the development cycle.
ReportPro is a great general purposed reporting engine.
In comes in two flavours know as the RP2xx and RP3xx series. Both have designers to produce .RPT files and RP2 is easyto produce Handcoded reports.. if you know how to do it. RP3 handcoded reports from VO GUI is difficult and this paper shows the techniques to handcode reports for both RP2 and RP3
Special thanks for Oscar Schneider and Eric Visser for the work they had done re RP3 and handcoded reports.
ReportPro was written for Visual Objects and is written completely in Visual Objects. When I first saw this product for VO1 in 1994 it convinced me what VO + Reportpro were a great team. The evolution continued through all versions of VO as the ReportPro 2 series which included 2.10 / 2.11 and 2.12. The ReportPro developers created “Classmate” and “ReportPro 3.x” and access was via an Active X component for VO GUI class users, or it could be used natively from Classmate. This article will explore by way of samples and code snippets, the techniques of “hand-coding” reports which add incredible power + flexibility to your reports.
ReportPro has a GUI report designer, why handcode your reports? You must be a masochist. The advantage of ReportPro is that you can select both types of reports but once you get a few hundred [.FRM ] reports this is a lot of report files to maintain and distributed. They also require a different skill set to program and how they will work in your application. If you want to port your reports to another system, you are locked into a proprietary format. Recently the source code for ReportPro II & III was made available by Grafx software which may get around this issue.
“Hand-coded” reports.. use specific CALLBACK methods of the RPPRINTER class to provide the Preview and Print and page formatting, including Header and Footer functionality. This class subclasses the PTRDEVICE class which is a complete low-level base class/print engine built around the Windows API. When reports are built there are “NO” .FRM files and the report(s) are compiled into your DLL or EXE. Therefore you can have say 300 reports in one DLL. Lot easier to maintain believe me.
When your handcoded report is run, Reportpro looks for callback methods in the following order. If they do not exist, NO ERRORS occur and simply they do not get executed. You build your report using callbacks to have an automated reporting system.
PrintStart() // Called once for Initialisation.
PrintSetPage() // Called before Prebody()
PrintPreBody() // Called once (used for Title Pages)
PrintSetPage() // Loop for each page.. each of the below methods are called ONCE for EACH page.
PrintPageHeader()
PrintPageBody()
PrintPageFooter() // Loop ends when PRINT_NOMORE or PRINT_ERROR is returned from PrintPageBody()
PrintPostBody() // Called once (usefull for Footnotes etc)
PrintEnd() // Called once and can be used for Cleanup
OK.. how do ReportPro Callbacks work ?
You could use say the PrintStart() method to setup some or all of your variables or even plug in your servers or indexes into the report. You can be as flexible as you like in your design.
From experience at a minimum you will use PrintSetPage() and PrintPageBody() and say PrintPageHeader() in your reports. The PrintPagebody() can be used to produce the BODY of the report and the PrintSetPage() is used to control the Skipping / Forwards and Backwards of the report in Preview mode. When a PrintPageBody() returns PRINT_OK or PRINT_NOMORE or PRINT_ERROR this lets Reportpro know what to do next. That is if PRINT_ERROR is returned then do not proceed any further. If PRINT_NOMORE is returned we have reached the end of the data and we can stop processing the data.. and PRINT_OK says there is more data coming and ReportPro should call its callback methods which include PrintPageHeader() and then call PrintPagebody(). This process continues ‘looping’ until it finally returns PRINT_NOMORE and the report is completed.
The ReportPro preview dialog window has buttons for a SKIP forward and BACK and “goto first” and “goto end” and the PrintSetPage() is your friend when programming these buttons. The technique I use is to build an array of VARIABLES at the time the PrintPageHeader() is called which record the value of variable counters for each page of the report. When PrintSetPage() is called by ReportPro your code will ‘reset’ these values so the report uses the same values to produce the same page of results.
The PrintPageHeader() is the "Header" section of the report and the PrintPageFooter() handles the "Footer" section of the report.
WORKING EXAMPLE of a REPORT from Scratch.
oPrinter := RpPrinter{ SELF:oShell, SELF }
//
IF oPrinter:IsValid // Is our Object ready.
oPrinter:SetPrinterByName(SELF:oShell:cReportPrinter)
oPrinter:UnitOfMeasure := UNIT_MM
oPrinter:LeftMargin := 9 // Sets the Preview/printing..
oPrinter:TopMargin := 10 // Sets the Preview/printing..
oPrinter:BottomMargin := 30 // Sets the Preview/printing..
IF lPreview
oPrinter:PrintPreview( cPrintJob, cPrintJob + ".PRN", cTitle,
"Printing...",TRUE,FALSE,FALSE )
ELSE
oPrinter:Print( cPrintJob, cPrintJob + ".PRN", cTitle,
"Printing...",FALSE,FALSE)
ENDIF
//
ENDIF
This example code shows the oPrinter variable instantiating the RPRINTER class object and by checking oPrinter:isValid before passing in our Printer name. This could be a UNC printer. We can set the report to be MM or INCHES and left/ right/ top/ bottom margins. We could select PrintPreview() and print from there or just select Print() and the job will be processed.
Either way the Reportpro engine will call our CALLBACK methods as you have designed them for your handcoded report. The result is a powerful and easy method of reporting.
Your ReportPro “Callbacks” in ACTION
When the Printpreview() or Print() are called in the example code the ReportPro callback system will look ‘automatically’ for the PrintStart() method first and if it exists, it will process this code and Reportpro will proceed if a PRINT_OK is returned.
If a PrintSetPage() method exists it will be processed next. At this point in time the PrintPageHeader() has not been called previously and therefore the length of SELF:_aPages will be 0. The SELF:_aPages is an empty array at this stage and is used to store results as we traverse through the report. Mainly this array will contain Counters, sub-totals, recno’s numbers for databases relating to this page. Therefore the first CASE statement will be TRUE and the ‘SetPage’ will return nToPage which is equal to 1.
If the PrintPreBody() exists it will be called and it should return PRINT_OK. The callback will then call PrintSetPage() again. If PrintPrebody() did not exist it would not call either PrintPreBody() or PrintSetpage(). Reportpro will call the PrintPageheader() method [ if it exists ] and it should also return PRINT_OK.
The technique I use stores for each page, counters and record pointers which can be “reset” in the event of skip backwards and forwards through pages. This ‘resetting’ is done by PrintSetPage() method. In this example we are saving the PAGE# and the SERVER:RECNO.. but there is no limit to what you could do. As an example you could have 5 or 6 servers and numerous counters for subgroups or complex indexes. Whatever you want.
[ i.e. AAdd( SELF:_aPages, { SELF:server:recno, nPage }) ]
The PrintPageheader() method prints the data / text / images that will appear as the header of the report page(s). Remember only PRINT_OK or PRINT_NOMORE or PRINT_ERROR can be returned or ReportPro will freeze. ReportPro will now call PrintPageBody() and we enter a recursive situation. If PRINT_OK is returned the system will call PrintPageFooter() and then PrintSetPage() and PrintPageHeader() and then recall the PrintPageBody() again. It is important that the record pointer(s) to the database you are processing do not say GOTOP again in your code using this reporting style or callbacks will put you into an endless loop.
We will look at various programming techniques later in this article. In the example code shown we will loop through the database checking for EOF status and if we fill the page and there are still database records to process you should return PRINT_OK which instructs Reportpro to call PrintPageFooter() followed by a call to PrintSetpage() followed by calls to PrintPageHeader() and then a call to PrintPageBody(). The process repeats producing pages for your report ..OR.. the PrintPageBody() should return PRINT_NOMORE status and PrintPageFooter() is called for the final time as the report is completed.
When Printpagebody() returns a PRINT_NOMORE status the PrintPageFooter() will be called followed by PrintPostBody() and then PrintEnd(). If none of these methods exists no errors will be reported and the report is considered completed. If PrintPreview() mode was selected the ReportPro dialog window will show the report data using Fonts and Colors in the report and allow you to skip forward / backwards / go to top or go to end of the report. If the Print() mode method was selected the job would work the same but the job would be automatically flushed to the printer.
Managing “Callback” recursion issues
You may be tempted in the PrintPagebody() to use a Gotop() and a DO WHILE loop to print say 30 pages of a report. Sounds easy to loop around outputting data and then check the page length and issue a PRINT_OK and you have a Footer and a then a new Header for the new page and then we are returned to PrintPageBody(). You would have a potential problem if in your ‘Pagebody’ code on method entry, it set TOP OF FILE, you would be in an endless loop. You could get around it using a STATIC variable or a class variable or you could use the PrintStart() to setup the record pointers and this is not a bad design philosophy. However as the reports get more complex you need a constant approach to the situation. You need recursive techniques which are detailed on the next page.
Basic Report using Callbacks.
METHOD PrintSetPage(oPrinter, nToPage) CLASS _ myReport
LOCAL _nPage := nToPage AS LONG
DO CASE
CASE ALen(SELF:_aPages) = 0 // If we have no info
CASE nToPage == 1
SELF:server:recno := SELF:_aPages[1][1]
CASE nToPage = 32000
SELF:server:recno := SELF:_aPages[_nPage:= ALen(SELF:_aPages)][1]
OTHERWISE
IF nToPage <= ALen(SELF:_aPages)
SELF:server:recno := SELF:_aPages[_nPage][1]
ENDIF
ENDCASE
RETURN _nPage
-----------------
METHOD PrintPageHeader(oPrinter, lPrint) ) CLASS _myReport
LOCAL nPage := oPrinter:CurrentPageNumber AS DWORD
IF oPrinter:CurrentPageNumber > ALen(SELF:_aPages)
AAdd( SELF:_aPages, { SELF:server:recno, nPage })
ENDIF
IF lPrint
oPrinter:Prow := 0
oPrinter:SetFont("Arial",16,TRUE,FALSE,FALSE)
// bold italic underline nrotation escapement
oPrinter:TextOut(oPrinter:Prow,98, "SHERLOCK TEST HEADER", ;
ALIGN_CENTER,,,Color{COLORBLUE})
ENDIF
RETURN PRINT_OK
---------------
METHOD PrintPageBody(oPrinter, lPrint) CLASS _myReport
DO WHILE ! SELF:server:EOF .AND. ;
oPrinter:Prow <= oPrinter:PrintAreaLength
oPrinter:TextOut(oPrinter:Prow, ; SELF:server:FIELDGET(#zzCreditor) )
IF oPrinter:Prow > oPrinter:PrintAreaLength
RETURN PRINT_OK
ENDIF
ENDDO
RETURN PRINT_NOMORE
-----------------
METHOD PrintPageFooter(oPrinter) CLASS _myReport
// Original footer
oPrinter:SetFont("Arial",10,TRUE,FALSE,FALSE)
oPrinter:DrawText(oPrinter:PrintAreaLength,0,”FOOTER STUFF”, ;
,,,Color{COLORBLUE})
RETURN PRINT_OK
“Callback” recursion tricks and techniques
The first rule with Hand-coded reports is to think recursion from the outset in your code design. As most reports will be greater than one page this means you will “renter” the PrintPagebody() possibly in the middle of the same routine you just left, courtesy of ReportPro and you must deal with it. How do you get to exactly the same place and record number you left from ? Well let me share the secrets ;
I use a SYMBOL variable “symStage” in my Class and I set it in the PrintStart() method to symStage := #STAGE1
In the PrintPageBody() when it processes through the “Stages” of the report and as each stage is completed, I update the SELF:symStage to say #STAGE2 or #STAGE3 or #STAGE4 etc. This way when the recursive call is made I can bypass earlier stages and move to the one I was working on.
Another trick is if you need to change ORDERS for say the Dbserver or SET a SCOPE . FILTER etc.. required for the next stage you set it in the last stage just before you exit this stage. This way it will remain the way it was when you “recursed” and not be reset on re-entry.
Recursive example ;
METHOD PrintPageBody(oPrinter, lPrint) CLASS _myReport
DO WHILE ! SELF:serverONE:EOF .AND. ;
oPrinter:Prow <= oPrinter:PrintAreaLength
if self:symStage = #STAGE1
// Do stuff.. output data .. whatever
// Set an index.. position a record
self:symStage := #STAGE2
oServerTWO:Gotop() // ß-- We set this is Stage 1 for Stage 2
endif
if self:symStage = #STAGE2
// Do other stuff.. output data .. whatever
// Set an index.. position a record
DO WHILE ! SELF:serverTWO:EOF .AND. ;
oPrinter:Prow <= oPrinter:PrintAreaLength
// Print stuff and loop around
SELF:serverTWO:Skip()
IF oPrinter:Prow > oPrinter:PrintAreaLength
RETURN PRINT_OK // We exit here and recurse back
ENDIF
ENDDO
self:symStage := #STAGE3
endif
IF oPrinter:Prow > oPrinter:PrintAreaLength
RETURN PRINT_OK
ENDIF
SELF:serverONE:Skip()
ENDDO
RETURN PRINT_NOMORE
In this code we have a STAGED approach which automatically sets up the next stage. Look at Stage2 and its job is to LOOP around printing stuff out from oServerTWO but the primary loop is oServerONE. In this example either test for page space left could invoke the PRINT_OK to be returned. When this happens we would get our Footer and new Header and then return to PrintPageBody(). If Stage2 was the exit point when we returned, STAGE1 will be bypassed and we will be positioned on the same record we left for oServerTWO as well as oServerONE. If however we had an oServerONE:Gotop() before the major loop we would have an endless loop ..or.. if we had the oServerTWO:Gotop() in the STAGE2 code we also could have an endless loop situation. Think of the repercussions of this problem.
The user selects a 10 page report and walks away from the printer and returns and 400 pages are printed until paper ran out.
Images in Hand-coded reports
To print BMP files in your reports, Reportpro has the DrawBMP() and the _DrawBMP() methods. The _DrawBMP() method requires an already initialised DIBMP object from a resource or field. The DrawBMP() automatically handles loading and unloading the Bitmap from a file.
oPrinter:_DrawBmp(5,154, SELF:oDIBMP,7,34,,,TRUE)
This syntax would position the Row and Column at 5, 154 using the resource image with a Height of 7 and a Width of 34 and the TRUE says lets “stretch“ the image to fit these co-ordinates.
Lets imagine you have an external BMP file and you need to show it on your report, this code would do the trick.
cFileBMP := cDatapath + “SHERLOCK.BMP"
IF File( cFileJPG )
oPrinter:DrawBmp( 5,154, cFileBMP, 7, 34,,,TRUE )
ENDIF
Lets imagine you have an external JPG file and you need to show it on your report, we have a problem as ReportPro only handles BMP files. This is how we get around the issue. Using FabPaint you create a DIB ( Device Independant Bitmap) from the JPG on the fly and draw it on the report surface and then delete it.
cFileJPG := cDatapath + “SHERLOCK.JPG"
cFileBMP := cDatapath + SSRandfile() + ".BMP"
IF File( cFileJPG )
hPTR := DIBCreateFromFile(String2Psz(cFileJPG))
IF hPTR != NULL_PTR
DIBSaveAs( hPTR, String2Psz(cFileBMP))
oPrinter:DrawBmp(5,154, cFileBMP, 7, 34,,,TRUE )
FErase(String2Psz(cFileBMP))
ENDIF
hPTR := NULL_PTR
ENDIF
Programming the PRINTSETPAGE()
ReportPro calls the ‘Setpage’ as part of the callback system and as its name indicates it SETS THE PAGE and it is called prior to PrintPageHeader(). When you click on the Next/Backward buttons in the report dialog window this method is called with the PAGE# you are about to goto as the variable nToPAGE. In the code we store this to variable to _nPage and we check what action to take in our CASE statements. Firstly is there is an empty SELF:_aPages array ? We can do nothing and return the nTopage value. If the user has skipped back to Page#1 or selected FIRST PAGE the nToPage will be 1 and the SELF:_aPages array element 1 or any other values in element one of the array will reset the appropriate values. If the user skips to the last page or END of REPORT button the nTopage will be set to 32000 and the last element of the SELF:_aPages array will be used to reset the values. If the nToPage is not the first and not the last a check will be made to make sure it is less than or equal to where the report system has been thus far and it will use that element of the array to reset our variables / counters etc.
METHOD PrintSetPage(oPrinter, nToPage) CLASS _ myReport
LOCAL _nPage := nToPage AS LONG
DO CASE
CASE ALen(SELF:_aPages) = 0 // If we have no info
CASE nToPage == 1
SELF:server:recno := SELF:_aPages[1][1]
CASE nToPage = 32000
SELF:server:recno := SELF:_aPages[_nPage:= ALen(SELF:_aPages)][1]
OTHERWISE
IF nToPage <= ALen(SELF:_aPages)
SELF:server:recno := SELF:_aPages[_nPage][1]
ENDIF
ENDCASE
RETURN _nPage
INHERITANCE and Handcoded reports
The greatest thing about handcoded reports and the superb ReportPro class structure is that you can build yor own classes and inherit from them. This has an advantage when you say have a similar Header and Footer for your reports. You simply build a subclass and it has a PrintPageBody() and the headers and footers will be added automatically. When you need say a different header or NO header you create a PrintPageHeader() for your subclass which overrides the parent class header.
CLASS _MasterSystemReports
PROTECT symSTAGE AS SYMBOL
PROTECT _aPages := {} AS ARRAY
PROTECT cReportTitle AS STRING
--
METHOD PrintPageFooter(oPrinter) CLASS _MasterReports
--
METHOD PrintSetPage(oPrinter,nToPage) CLASS _MasterReports
LOCAL _nPage := nToPage AS LONG
DO CASE
CASE ALen(SELF:_aPages) == 0 // Empty do nothing..
CASE nToPage == 1
SELF:ResetStatus( nToPage )
CASE nToPage = 32000
_nPage := ALen(SELF:_aPages)
SELF:ResetStatus( _nPage )
OTHERWISE
IF nToPage <= ALen(SELF:_aPages)
SELF:ResetStatus( _nPage )
ENDIF
ENDCASE
//
RETURN _nPage
METHOD PrintPageHeader(oPrinter,lPrint)CLASS _MasterReports
IF oPrinter:CurrentPageNumber > ALen(SELF:_aPages)
AAdd( SELF:_aPages, ;
{ nPage, SELF:oOwners:Recno, ;
SELF:oProperti:Recno, ;
SELF:oTenants:Recno } )
ENDIF
---
METHOD ResetStatus( _nPage ) CLASS _MasterReports
SELF:oOwners:Recno := SELF:_aPages[_nPage][2]
SELF:oProperti:Recno := SELF:_aPages[_nPage][3]
SELF:oTenants:Recno := SELF:_aPages[_nPage][4]
In this example you see that we are storing numerous Recnos and the ResetStatus() method allows us to reset the record pointers to match the page we want to reposition to.
Using subclassing we only have to supply a PrintPagebody() to get our report.
CLASS _OwnersONHOLD INHERIT _MasterOwnerReports
.. or ..
CLASS _OwnersGSTTREE INHERIT _MasterOwnerReports
Programming the PRINTPreviewDlg
The ReportPro dialog window can be “programmed“ by intercepting the buttons calls by using a coding technique like this. To introduce this code to your report you pass in the symbol name of the class in the PrintPreview method as shown below.
oPrinter:PrintPreview( cReportTitle, cReportTitle+".PRN", cReportTitle,;
"Printing...",TRUE,FALSE,FALSE,,,#SherlockPrintPreviewDlg)
In this case I am intercepting the ExportBtn to Email a ReportPro report as a PDF file. You can program those buttons to call your own code and if you purchases the source code to ReportPro you can make it do anything you want.
CLASS SherlockPrintPreviewDlg INHERIT rpPrintPreviewDlg
PROTECT oReportOwner AS OBJECT
//
METHOD ExportBtn() CLASS SherlockPrintPreviewDlg
LOCAL aEmail AS ARRAY
aEmail := SELF:oReportOwner:EmailDetails()
AAdd( aEmail, aReport )
_dlgEmailStatement{ SELF, aEmail }:Show()
---
METHOD GotoBtn() CLASS SherlockPrintPreviewDlg
InfoBox{SELF, "Development Message", "Goto button"}:Show()
----
METHOD INIT( oParent, oPrinter, cCaption, lModal, ptrPlacement, ;
nShowState, lLandScape, nZoom) CLASS SherlockPrintPreviewDlg
SUPER:Init(oParent, oPrinter, cCaption, lModal, ptrPlacement, ;
nShowState, lLandScape, nZoom)
SELF:oReportOwner := oPrinter:ReportOwner
If ! IsMethod(SELF:oReportOwner, #EmailDetails)
SELF:oCCExportBtn:hide() // They do not want to EXPORT data.
ENDIF
METHOD PrintBtn() CLASS SherlockPrintPreviewDlg
IF IsMethod( SELF:owner, #Printstart )
SELF:owner:PrintStart()
ELSEIF IsMethod(SELF:oReportOwner, #Printstart)
SELF:oReportOwner:PrintStart()
ENDIF
SUPER:PrintBtn()
RP3 and handcoded reports
Reportpro 3 was a total rewrite in ‘Classmate’, a product developed by the same authors of ReportPro and its all VO. For exising VO GUI users you needed to use the Active-X aspect of RP3 or switch to Classmate which gives you native access. This was not a great commercial success and a lot of VO GUI developers remained with the RP 2 series. An article written by Oskar Sneider in April, 2000 showed a technique to get “around” the issue of using the Active X, where the PRINTSERVER class must have a “cWINDOW” class as the owner. Basically he took the Handle() of VO window class and pumped this into the hWND of the cWindow class. The following code shows the trick and we get to this code compliments of the Report() method of StandardShellWindow. ;
Method Init( symPrintServer, oVOOwner ) CLASS reportserver
if ! isNIL( oVOOwner )
_oOwner := cWindow{} // ß Classmate
_oOwner:hWnd := oVOOwner:Handle() // <- VO GUI handle
endif
_oPrinter := cPrinter{ _oOwner } // ß Classmate
_oPrinter:previewmodal := TRUE // Create our report
_oForm := CreateInstance( symPrintServer,oVOOwner )
_oForm:Printer := _oPrinter // introduce cPrinter to Form
_Printer:printserver := _oForm // Introduce Form to Printserver
//
RETURN SELF
We pass our RP3 handcoded VO report using its symbol name via the variable [ symPrintServer ] to CreateInstance() which creates / instantiates the class via the standard INIT() mechanism.
In the INIT() you could open servers or build indexes or setup some variables but you could use the PrintStart() method for this.
METHOD Init(oParentWindow) CLASS RPro_Hard_Coded
SELF:oDetails := DBSERVER{ GetDefault() + 'detail', DBSHARED }
IF SELF:oDetails:Used
SELF:cReportTitle := "HAND CODED Report in RP3 - DevFest 2005'
SELF:cInfoLine1 := "Part" + Space(16) + "Description" + ;
Space(34)+ "Unit" + Space(9) + "Unit"
SELF:cInfoLine2 := "Num#" + Space(14) + "Item" + Space(39) + ;
"Weight" + Space(9) + "Price"
SELF:ParentWindow := oParentWindow
SELF:oBodyFont := cFont{,44,"Arial"}
SELF:oFooterFont := cFont{,40,"Arial"}
SELF:oHeaderFont := cFont{,60,"Arial"}
SELF:lPrint := TRUE
//
RETURN SELF
Note in this example we set SELF:lPrint toTRUE and this will determine that this report will be a PRINT output and not a PREVIEW and then PRINT output. The ReportServer object acts as wrapper and then calls _oForm:PrintStart() and either _oForm:Print() or _oForm:Preview().
METHOD print( ) CLASS ReportServer
IF _oForm # NULL_OBJECT
_oForm:PrintStart()
_oPrinter:print()
ENDIF
RETURN SELF
---
METHOD preview( ) CLASS ReportServer
IF _oForm # NULL_OBJECT
_oForm:PrintStart()
_oPrinter:preview()
ENDIF
RETURN SELF
---
When “ReportServer” class is instantiated it relies on the report in the “Rpro_hand_Coded” class to produce our report. The result is a call to the Init() of the Reportserver class and this calls the Init() of the “Rpro_hard_coded” class.
METHOD Report CLASS StandardShellWindow
LOCAL oReport AS ReportServer
LOCAL lDirect2Printer AS LOGIC
oReport := CreateInstance(#ReportServer, #RPro_Hard_Coded,SELF)
//
lDirect2Printer := FALSE // preview first
oReport:Landscape := TRUE
oReport:StartPrinting( lDirect2Printer )
RP3 Sample code in detail
The StartPrinting() method of report server when a job is completed will proceed after the Print() or Preview() method and this is where you can install your CLEANUP code. In this example case I close the open Dbserver.. but you could expand this to close multiple servers.
METHOD StartPrinting( lDirect AS LOGIC ) CLASS ReportServer
IF _oForm # NULL_OBJECT
IF lDirect
_oPrinter:Print()
ELSE
_oPrinter:preview()
ENDIF
ENDIF
//
IF IsMethod( _oPrinter:PrintServer, #Cleanup )
Send( _oPrinter:PrintServer,#Cleanup)
ENDIF
The rest of the code and technique is similar to the explanation on previous pages like RP2. That is PRINTSTART() is called first followed by PRINTSETPAGE() and the PRINTPAGEHEADER() and then PRINTPAGEBODY() and then PRINTPAGEFOOTER() and then the cycle continues until the report is completed.
One major difference I noticed between RP2 and RP3 is that RP2 will process one page and stop.. where the RP3 processes all pages first. Check the documentation on both systems and don’t assume the same method name has the same X/Y data or parameters passed.
What you need to run the demo code
For “RP2_HandCoded_Reports_Demo” you need “RP2RDD32.lib” in your VO repository which is the standard RP2 interface to the RP DLL’s and this code should work with RP210/211 and 212.
The “RP3_NO_CM_Reports_Demo” you need “RP3 runtime DLL” and “cm GUI 20x.dll”. I have tested it with RP203 / 205 and 206 and it all seems to work.
Handcoded report to print a Listview
Reports do not always derive their data directly from say a DBF or SQL backend but it may be XML or CSV file or even a Listview on the screen. I have in one of my applications a Diary system which is a scalable listview based on time slots and I need to print a day planner to match the list views current user selection.
This code snippet gives you the general technique to do this. In the PrintConfig() we are looping through the Listview using “columncount” and adding the Column name and Caption and width to a working array. In this example I want to add two more columns that do not exist in the listview and I even adjust the last column based on the previous columns’ width. I retrieve data from the Listview and the Dbserver which is a big advantage of Handcoded reports as you can access any VO object.
CLASS _DiaryReport
PROTECT oListView AS ListView
----
METHOD PrintConfig() CLASS _DiaryReport
LOCAL nTotWidth := 0 AS WORD
LOCAL I AS INT
LOCAL oColumn AS ListViewColumn
SELF:aListView := {}
FOR I := 1 TO SELF:oListView:ColumnCount
// Check the Listview and build an Array of Columns
oColumn:= SELF:oListView:GetColumn(I)
// column name column caption column width
AAdd(SELF:aListView, {oColumn:NameSym ,oColumn:Caption ;
,Min(oColumn:Width,28-1) })
NEXT
AEval(SELF:aListView, { |aVal | nTotWidth += aVal[3]})
// Calculate width of the Columns in Listview
AAdd(SELF:aListView,{ #ACTION, 'Action' , 5 })
// Add two more columns - Adjust width of final column
AAdd(SELF:aListView,{ #NOTES , 'Extra Notes', ;
Max((75+2)- nTotWidth, 19) })
----
METHOD PrintPageHeader(oPrinter,lPrint) CLASS _DiaryReport
LOCAL nStringLen AS FLOAT
LOCAL nX AS FLOAT
LOCAL nCharWidth AS FLOAT
LOCAL nCharHeight AS FLOAT
LOCAL cText AS STRING
LOCAL cTextStr AS STRING
LOCAL I AS INT
IF lPrint
oPrinter:Prow := 0
cText := SELF:cTitle
nStringLen := oPrinter:GetTextLength(cText)
oPrinter:SetFont("Arial",16,TRUE,TRUE,FALSE)
oPrinter:DrawText(oPrinter:Prow,((oPrinter:PrintAreaWidth-
nStringLen)/3)-8,cText,,,,Color{COLORBLUE})
ENDIF
IF lPrint
oPrinter:SetFont("Arial",10,FALSE,FALSE,FALSE)
oPrinter:Prow += oPrinter:AvgCharHeight * 3/2
cText := "Print Date: " + DToC(Today())
oPrinter:DrawText(oPrinter:Prow,0,cText)
//
cText := "Page: " + AllTrim(Str(oPrinter:CurrentPageNumber,3,0))
nStringLen := oPrinter:GetTextLength(cText)
oPrinter:DrawText(oPrinter:Prow, oPrinter:PrintAreaWidth, cText)
ENDIF
oPrinter:PRow += oPrinter:AvgCharHeight * 3/2
oPrinter:SetFont("Arial",10,TRUE,TRUE,FALSE)
nX := 0
nCharWidth := oPrinter:AvgCharWidth
nCharHeight := oPrinter:AvgCharHeight
FOR I := 1 UPTO ALen(aListView)
cTextStr := aListView[I,2]
nStringLen := FLOAT(aListView[I,3])* nCharWidth
IF lPrint
oPrinter:DrawRectangle(oPrinter:PRow, nX, ;
oPrinter:PRow+oPrinter:AvgCharHeight, ;
nX+(aListView[I,3]*oPrinter:AvgCharWidth), ;
0,PS_SOLID,,HS_SOLID,color{192,192,192})
oPrinter:DrawText(oPrinter:PRow, nX, cTextStr, NIL, ;
nStringLen, DT_CENTER)
ENDIF
nX += nStringLen
NEXT
oPrinter:PRow += nCharHeight
//
RETURN PRINT_OK
In the PrintPageBody() we loop for the number of lines /rows in the Listview and use the GetNextItem() method to step through the listview data. I store with the Listview data the RECNO of the database that the data relates to and the oServer:GOTO() allows me not have to store memo data or any unnecessary data in the listview and retrieve it in realtime during the report. I can evaluate /read the data and draw the appropriate grids and boxes on my Dailyplanner report.
METHOD PrintPageBody(oPrinter, lPrint) CLASS _DiaryReport
LOCAL nX,nWidth AS FLOAT
LOCAL xText, xValue AS USUAL
LOCAL cText, cType, cNotes AS STRING
LOCAL nAlignment AS DWORD
LOCAL oItem AS ListViewItem
LOCAL XX, nLine, nCurrline := 0 AS DWORD
oPrinter:SetFont("Arial",10,FALSE,FALSE,FALSE)
DO WHILE SELF:LineNumber <= SELF:nItems .and. ;
oPrinter:PRow <= oPrinter:PrintAreaLength
nX := 0
// relationship /disabled/droptarget/focused/selected/nItemStart
oItem := oListView:GetNextItem(LV_GNIBYITEM, FALSE, FALSE, ;
FALSE,FALSE,SELF:LineNumber)
IF oItem = NIL
SELF:LineNumber += 1
EXIT
ENDIF
FOR XX := 1 UPTO ALen(aListView)
xValue := oItem:GetValue(aListView[XX,1])
// Get Array with user ID and RECNO for position for other info
xText := oItem:GetText(aListView[XX,1])
// Get text display from Listview Item
IF IsArray(xValue)
oServer:Goto( xValue[2] ) // The second element of GETVALUE is RECNO
cType := SELF:oServer:FIELDGET(#APP_TYPE)
cNotes := SELF:oServer:FIELDGET(#APP_NOTES)
ENDIF
DO CASE
CASE aListView[XX,1] = #ACTION
DO CASE
CASE cType = '1' ; xText := 'Appnt'
CASE cType = '2' ; xText := 'Phone'
CASE cType = '3' ; xText := 'To-do'
CASE cType = '4' ; xText := 'Letter'
ENDCASE
cType := ''
CASE aListView[XX,1] = #NOTES
xText := cNotes
cNotes := ''
ENDCASE
//
cText := Proper(Transform(xText,""))
nWidth := FLOAT(aListView[XX,3]) * oPrinter:AvgCharWidth
//
IF lPrint nAlignment := iif( ValType(xText) == "N", ;
ALIGN_RIGHT, ALIGN_LEFT)
IF SLen(cText) > 59
nCurrline := 0
nLine := MLCount( cText, 59 )
//
oPrinter:DrawRectangle( oPrinter:Prow, nX, oPrinter:Prow + ;
(oPrinter:AvgCharHeight*nLine), nX +;
(aListView[XX,3] * oPrinter:AvgCharWidth ), 0.5, PS_SOLID )
oPrinter:Prow -= oPrinter:AvgCharHeight DO WHILE nCurrline <= nLine oPrinter:DrawText( oPrinter:Prow, ;
nX+oPrinter:AvgCharWidth/2, ;
MemoLine( cText, 59, nCurrline), NIL,nWidth, nAlignment )
nCurrline += 1
oPrinter:Prow += oPrinter:AvgCharHeight
ENDDO
oPrinter:Prow -= oPrinter:AvgCharHeight
ELSE
oPrinter:DrawRectangle( oPrinter:Prow, nX, oPrinter:Prow + ;
oPrinter:AvgCharHeight, nX + ;
(aListView[XX,3] * oPrinter:AvgCharWidth ), 0.5, PS_SOLID )
oPrinter:DrawText(oPrinter:Prow, nX + ;
oPrinter:AvgCharWidth/2, cText, NIL, nWidth, nAlignment )
ENDIF
ENDIF
nX += nWidth
//
IF nX > oPrinter:PrintAreaWidth
EXIT
ENDIF
NEXT
oPrinter:Prow += oPrinter:AvgCharHeight
SELF:LineNumber += 1
ENDDO
//
RETURN iif( SELF:LineNumber > SELF:nItems, ;
PRINT_NOMORE, PRINT_OK )
In Summary
There are some minor but significant differences that you should be aware of between RP2 and RP3 versions. Here we can see the X and Y data is reversed between RP2 and RP3
RP2 syntax ;
oPrinter:TextOut( oPrinter:Prow, 23, , “somedata”)
RP3 syntax ;
SELF:Printer:TextOut( 23, SELF:Printer:NextProw, “somedata” )
Handcoded reports are fantastic for speed and flexibility and reduces the .RPT clutter for individual report files. For some things they are quick and easy and recommended. For very complex and tedious reports I would use the handcoded method everytime. The great thing about ReportPro is you can have the best of both worlds and mix and match. More power to you….
DOWNLOAD THE SAMPLECODE
Bibliography
Phil McGuinness is General Manager of Sherlock Software - Australia since 1994 which is a vertical market solution provider specialising in REAL ESTATE TRUST ACCOUNTING, webdesign / intranet and Videowall technology. This writer has a long history in financial data processing systems in Bank and general business and technology solutions. A career in Electronics. Chemical processing and programming in Basic and dBase, Clipper 87, Clipper 5 and all versions of VO. He has held senior positions as General Manager Sales & Marketing and National Sales and Marketing Manager outside of the Software Industry. The Sherlock Trust Accounting system has been in continuous market use for over 25 years.
Email: sherlock@sherlock.com.au]