B4J Library B4j Report Writer

This is a Class and 4 custom views that implements a basic reportwriter for B4j. I have developed this for an app I am working on, I've tried to make it generic enough to be of use to others but it is certainly not fully tested for all possibilities as they could be varied.

The 4 custom views are:

StaticField
ColumnHeaderField
ColumnField
TotalField

You can create a columnar report that implements a subtotal and total row, or totally freeform report using just static fields, or a mixture of both.

The required layout (See the layout ReportWriterTemplate in the demo app) has 3 required panes:

pnHeader, pnContent and pnFooter. Any of these can be empty or invisible, but they must be there.

pnHeader and pnFooter are self explanatory, they can contain any number of Static fields pnContent is explained below.

upload_2016-2-29_0-33-5.png


How it works

A report layout is defined using the B4j designer. It's name is passed to the initialize method of the reportwriter, along with an initial size for the report. The final size is determined by selection of a printer using the print dialog.

For StaticFields, you assign a name to each field in the designer and then in your program add the data to a map with a key of the same name. The data is then displayed in the field at runtime.

For the columnar part of a report, you do not assign names to the fields, they are populated from a list of arrays added to the data map with a key of "RowData" that contains rows of data in the order in which it is to be displayed. It sounds complicated, but have a look at the ColumnReport in the example, it's really fairly straightforward.

pnContent:

pnColumnHeader should only contain ColumnHeaderFields. It's text is displayed in the ColumnHeaderRow. The column header field also allows you to assign a format string that is applied to all of the fields in that column. The format string is used to feed options to the NumberFormat2 function and takes the form
B4X:
MinInt.MinFrac.MaxFrac,

See Numberformat2 for clarification, the trailing comma turns on Grouping.

pnRow should only contain Columnfields, you can add an identifier for a subtotal and total which must match an Identifier in a TotalField in pnSubTotal and/or pnTotal whichever is required.

pnSubTotal and pnTotal can contain labels and Totalfields. The labels are only used to display text, the total fields will display a subtotal or grandtotal depending on which pane they are in. TotalFields have their own FormatString that can be entered in the designer.

You can also add one or more styleclasses to the custom views for formatting with CSS. Each pane also has it's own styleclass (see the code for reference).

StaticFields:

You can put staticfields pretty much anywhere else in a report (see the InformationPage example), they should be contained in panes which have a unique id in a tag, these should then be registered with the reportwriter so that the fields can be added to the field map and matched to the data map when you pass data to it. You can enter a FormatString for Static fields, this will cause an error if you enter a formatstring and the data passed to the field is not a number.

There are 3 examples in the attached project (RW.Zip), ColumnReport (mainly uses columns, subtotals and totals), InformationPage (uses StaticFields) and Invoice (a mixture of the two).

This is just a quick overview, see the comments in the code and post any questions and I'll do my best to answer them.

Known Issues:
For some reason, a border is still printed if a fields borderwidth is set to 0, so you also need to set the border color to match the background color (or make it transparent) to stop it being seen.

Matching output to a printed page size can be tricky, you may need to experiment with variant sizes in the designer to get it to look exactly how you want it. The reportwriter does provide the ability to scale a page to fit the available printer page size (and optionally keep the aspect ratio) which may help, or it may not.

The demo app depends on the javafx8 print library

This should be considered an alpha release, please test it thoroughly before use in a production app.

There is currently very little data validation, you need to make sure that the number of columns passed to the reportwriter is the same as defined in the layout. Let's see what other problems are found which will depend largely on how you try to use it and we may be able to make it a bit more resilient.

I hope you find it useful.

Updated to V0.6 to work in Obsfuscated mode.
Updated to V0.7 with a ReportWriterPreview class, provides a better preview. See post #4 Depends on jNumberSpinner Lib.

upload_2016-2-29_1-43-30.png upload_2016-2-29_1-49-6.png upload_2016-2-29_1-50-16.png
 

Attachments

  • RW0.7.zip
    34.5 KB · Views: 898
Last edited:

tdocs2

Well-Known Member
Licensed User
Longtime User
Thank you for your generosity, Steve.

This feature is much needed. In the current version, it does not work on Release Obfuscated. Ok on Debug and release...

Error follows:

B4X:
Program started.
utils._csdt_tick (java line: 98)
java.lang.RuntimeException: java.lang.RuntimeException: java.lang.Exception: Sub parsedata was not found.
    at anywheresoftware.b4a.keywords.Common.CallSub4(Common.java:471)
    at anywheresoftware.b4a.keywords.Common.CallSubNew(Common.java:409)
    at b4j.example.utils._csdt_tick(utils.java:98)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at anywheresoftware.b4a.BA.raiseEvent2(BA.java:93)
    at anywheresoftware.b4a.objects.Timer$TickTack$1.run(Timer.java:118)
    at com.sun.javafx.application.PlatformImpl.lambda$null$173(PlatformImpl.java:295)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.application.PlatformImpl.lambda$runLater$174(PlatformImpl.java:294)
    at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
    at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at com.sun.glass.ui.win.WinApplication.lambda$null$148(WinApplication.java:191)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.RuntimeException: java.lang.Exception: Sub parsedata was not found.
    at anywheresoftware.b4a.BA.raiseEvent2(BA.java:114)
    at anywheresoftware.b4a.keywords.Common.CallSub4(Common.java:462)
    ... 15 more
Caused by: java.lang.Exception: Sub parsedata was not found.
    at anywheresoftware.b4a.BA.raiseEvent2(BA.java:100)
    ... 16 more

It would be great to have a Preview function.

Thanks again.

Sandy
 

stevel05

Expert
Licensed User
Longtime User
Updated in the first post to work in obfuscated mode.

You can preview a page at a time by calling
B4X:
RW.ShowPreview(PageNumber)
although this doesn't take into account page margins and non printable page areas.

to get the page count, add this sub to the ReportWriter class:
B4X:
'Get the page count
Public Sub PageCount As Int
    Return ContentPages.Size
End Sub
 
Last edited:

stevel05

Expert
Licensed User
Longtime User
Update to v0.7, added a better report preview in a new class. You will have to set the margins at something that will approximate the printer margins., or just so it's centred. Depends on JNumberSpinner Lib
 

tdocs2

Well-Known Member
Licensed User
Longtime User
Thank you, Steve.

THIS IS AMAZING.....

It would make sense (and this is a WISH) for the preview to allow selection of page to preview and to issue the Print command....

Your generosity is to be commended.

Best regards.

Sandy
 

stevel05

Expert
Licensed User
Longtime User
Hi Sandy,

Yes it would be useful, but unfortunately not simple. To implement this properly would mean replacing most of the print dialog functionality so it can be incorporated into the preview. It would be something I'd probably use, so I'll continue to think about it. But it's not going to be a quick fix.

Steve
 

stevel05

Expert
Licensed User
Longtime User
You can generate the library from the project yourself (Project/Compile To Library)
 

Harris

Expert
Licensed User
Longtime User
Hi Sandy,

Yes it would be useful, but unfortunately not simple. To implement this properly would mean replacing most of the print dialog functionality so it can be incorporated into the preview. It would be something I'd probably use, so I'll continue to think about it. But it's not going to be a quick fix.

Steve

Any advancement on printing direct to (default) printer?
I have reports which must be printed (to assigned printer (default) ) without any user intervention (preview before print).

Thanks
 

Harris

Expert
Licensed User
Longtime User
Any advancement on printing direct to (default) printer?

Of course it can do this!

As I play with the source, I have discovered many things.
This should make a great little stand alone report server!

The way I see it, I shall create a timer watching for new records in table (every 60 seconds).
When one arrives (processed = False), get the values (pre-process and normalize many data rows) and call the appropriate layout file to print directly.

Thanks for this Steve.
 

Harris

Expert
Licensed User
Longtime User
Two reports (same report, many pages) created with this.

These show what can be done with RW.

Java 1 page.pdf
Single page with two tables creating RW.DATA

Java 3 page.pdf
Three pages with two tables creating RW.DATA
 

Attachments

  • Java 1 page.pdf
    32.5 KB · Views: 572
  • Java 3 page.pdf
    41.4 KB · Views: 472

Harris

Expert
Licensed User
Longtime User
Java page 3 does not eliminate the header or footer of page 2 and 3.
I had to force the non-printing of the column titles for page 2 and 3.

My app checks the source table every 30 seconds to see what has NOT been printed.
It will retrieve 1 row (regardless of how many), print it, and wait (x) 30 seconds to check again.
This was required cause many new printed reports would produce corruption in printed results (???).

Currently...
Since there is only 1 Rowdata, everything needs to be added to the report in Rowdata, including col-headers.

Excellent for producing a single - simple page, but me thinks we could expand it to a "banded" report generator.
ie. - Many col headers, and data rows in a report.

Things we can now do...
Can traverse source table(s) data and create a temp table, creating a normalized row that doesn't need much processing to print, using a language we know and understand.
RW doesn't need heavy formatter's to print results - rows and cols are already formatted.
Can make the engine "light".

Professional Report Writers have gone overboard trying to meet everyone's needs. I don't think we need this here.
Please help me understand how we can achieve a simple banded report writer.

Thanks
 

Fr Simon Rundell

Member
Licensed User
Longtime User
I know that is an old thread, but I need some help on exactly this... you seem to have made my life so much easier but I can't get the report to take my own data:

My query returns data on books currently out from this school library system. I have adopted the ColumnReport code, but an error
java.lang.ClassCastException: java.lang.String cannot be cast to [Ljava.lang.String;
in the ReportWriter.bas module.

What am I missing/not understanding? Why can't I just add the array of data to the List L and then put that to RowData?


Sub BorrowedByClassReport:
Sub BorrowedByClassReport
    Private RW As ReportWriter
    Private dtCurrent As String
  
    RW.Initialize("ColumnReport",487,733)
  
    'Position the working form off the screen
    RW.PositionForm(-800,0)
    'If we want to see the working form
'    RW.PositionForm(100,100)

    'Set subtotals to display on change of data in column 0
    RW.GroupBy(0)
  
    'Set totals
    RW.ShowTotal = False
  
    'Setup some test data
    Dim Data As Map
    Data.Initialize
  
    'Static field header
    Data.Put("Header","List by Class of Books Borrowed")
  
    'Set up the row data (List of arrays)
    Dim L As List
    L.Initialize
  
    Private RW As ReportWriter
    Private dtCurrent As String
  
    RW.Initialize("ColumnReport",487,733)
  
    'Position the working form off the screen
    RW.PositionForm(-800,0)
    'If we want to see the working form
'    RW.PositionForm(100,100)

    'Set subtotals to display on change of data in column 0
    RW.GroupBy(0)
  
    'Set totals
    RW.ShowTotal = False
  
    'Setup some test data
    Dim Data As Map
    Data.Initialize
  
    'Static field header
    Data.Put("Header","List by Class of Books Borrowed")
  
    'Set up the row data (List of arrays)
    Dim L As List
    L.Initialize
  
    Private sQuery As String
    Private RS As ResultSet

' sQuery returns books out from library and how long they have been out

    sQuery=" SELECT tblBorrow.ID, tblBorrow.`User`, tblBorrow.Book,     tblBorrow.DateOut,     tblBorrow.`Status`, "  & _
            "tblUser.FirstName, tblUser.Surname, tblUser.ID, tblUser.Class, "  & _
            "tblBook.Title, tblBook.Author, tblBook.BabcockCode, datediff( CURDATE( ), tblBorrow.DateOut) As DaysBorrowed " & _
            "FROM tblBorrow INNER JOIN tblUser ON tblBorrow.`User` = tblUser.ID " & _
            "INNER JOIN tblBook ON tblBorrow.Book = tblBook.BabcockCode " & _
            "WHERE     tblBorrow.`Status` = 1 " &  _
            "ORDER BY tblUser.Class Asc, tblUser.Surname Asc"
          
    RS = SQL.ExecQuery(sQuery)
    Do While RS.NextRow
        Dim rData As String = Array As String(RS.GetString("Class"),  RS.GetString("FirstName"),  RS.GetString("Surname"),  RS.GetString("Title"),  RS.GetString("Author"),  RS.GetString("DaysBorrowed"))
        L.Add(rData)
    Loop
    RS.Close
  
    Data.Put("RowData",L)
  
    DateTime.DateFormat="dd/MM/yyyy HH:mm:ss"
    dtCurrent=DateTime.Date(DateTime.Now)
  
    'Static field footer
    Data.Put("Footer",cSchool & " " & dtCurrent)
  
    'Assign the data
    RW.Data = Data
  
    RW.PreviewSetup(30,30,30,30,False,False)
    RW.PreviewShow(0)
  
    'Call the print dialog directly
'    RW.Print(False,False)

End Sub
 

stevel05

Expert
Licensed User
Longtime User
What line does the error occur on?
 

stevel05

Expert
Licensed User
Longtime User
I think I've got it:
B4X:
 Dim rData As String
Should be
B4X:
 Dim rData() As String

It needs to be an array otherwise it is treated as a single string.
 

Fr Simon Rundell

Member
Licensed User
Longtime User
Steve - thank you so much!

The errors now move to

Error occurred on line: 419 (ReportWriter)
java.lang.NumberFormatException: For input string: "SomeSurname"

<SomeSurname> is the 3rd column in that row...

Do I need to define each column to be displayed somewhere because I seem to have missed that?
Is it in the form or in code?

Thank you for your time
 
Top