Skip to content

Working with PDFTableDataSource from Sources Other Than RowSet

Some users on the Xojo Forum recently asked how to render tables on a PDFDocument using the PDFTableDataSource class interface when the dat source does not comes from a RowSet. Well, the truth is that it doesn’t differ too much from when it does! Continue reading and I will show you how using a simple example.

The trick is to handle the data coming from any source with the right data structure, for example, an Array. This example uses a plain text file with several lines and columns separated by the Tab character as the source of the data.

In order to follow this tutorial, download the example project.

Getting the Data

As you can see in the example project, the text file has been added to the project using a Build > Copy Files Step (the file name is “Data.txt”), so it is copied to the Resources folder of the compiled app and available for both debugging and release builds.

Then, in the Pressed event of the Button1 control placed on the default Window1, we will access the contents of the file in order to store each of the lines in the Data Array of strings:

Var f As FolderItem = SpecialFolder.Resource("data.txt")
If f <> Nil And f.Exists Then
  Var tis As TextInputStream = TextInputStream.Open(f)
  Var s As String = tis.ReadAll
  data = s.ToArray(EndOfLine.macOS)
End If

PDFRenderer Subclass

The next step is to create a custom class that will implement the methods provided by the PDFTableDataSource Class Interface. We will name the class “PDFRenderer”. Then, from the Inspector Panel, click on the Interfaces button, check the PDFTableDataSource interface and confirm the selection.

Once done, the methods from the class interface will be automatically added to the class:

  • AddNewRow
  • Completed
  • HeaderHeight
  • MergeCellsForRow
  • PaintCell
  • PaintHeaderContent
  • RowHeight

Over these we will add three more methods:

The class Constructor:

Public Sub Constructor(document As PDFDocument)
  // Simple constructor… just assign the received object
  // to the "Document" property
  
  Self.document = document
  proxy = New Picture(document.Graphics.Width, document.Graphics.Height)
End Sub

Assign the received PDFDocument instance to the document property. It also creates a graphics instance that will be used as a proxy for calculating the height of the processed text from the data (that won’t be necessary starting with Xojo 2024r4 where some PDFDocument bugs related with that are already fixed).

DrawTable:

This method is the one that will be called to start drawing the table. It will initialize some properties from the received Data (yeah, you can see a property named as “DrawingFromRowSet”, but I left that one on purpose because this project is based on the one dealing with the data coming from a RowSet, so you can see how similar all the process is):

Public Sub DrawTable(data() As String, headers() As String)
  // This is the method we call on the Class to draw
  // a table on a PDFDocument based on an Array of Strings
  
  If document <> Nil And data.LastIndex <> -1 Then
    
    // Assign the received Array to the "Data" property
    Self.data = data
    
    // Assign the received headers to the "Headers" property
    Self.Headers = Headers
    
    // Hey! We are going to create a table from an Array
    // so we use a Boolean property as a flag for that
    // (Yeah, we can do it using other techniques, but this
    // is easy enough for this purpose… while leave this
    // helper class "open" enough for drawing tables based on other
    // data sources).
    Self.DrawingFromRowSet = True
    
    // How many columns are we going to draw?
    // Well… as many as columns (TABs) in any of the Array items (for example, the first one).
    Var totalColumns As Integer = data(0).CountFields(Chr(9))
    
    // This is going to be the "drawable area" on the page
    // for the table = total page width less the left and right margins
    Var totalWidth As Double = document.Graphics.Width - 40 // 40 = 20 points left/right margins
    
    // Creating the PDFTable object here!
    Var table As New PDFTable
    
    // We want to repeat the headers on every page
    table.HasRepeatingHeader = True
    
    // Setting the column count for the table
    table.ColumnCount = totalColumns
    
    // …and the width for every column.
    table.ColumnWidths = CalculateColumnWidths(totalWidth, totalColumns)
    
    // The object from this class will be the responsible
    // of handling all the methods associated with the
    // PDFTableDataSouce Class Interface
    table.DataSource = Self
    
    // Setting the Top and Bottom margins for the drawing
    // of the table on every PDF page
    table.TopMargin = 20
    table.BottomMargin = 20
    
    // …and finally we instruct the PDFDocument object
    // to draw the table!
    document.AddTable(table, 20, 0)
    
    // Lastly, clearing the flag
    Self.DrawingFromRowSet = False
    
  End If
End Sub

CalculateColumnWidths Method

This one will calculate the widths for each column to be rendered in the final PDF document.

Protected Function CalculateColumnWidths(TotalWidth As Double, NumberOfColumns As Double) As String
  // Helper method to get a string for the column widths
  
  ColumnWidth = TotalWidth / NumberOfColumns
  Var s() As String
  
  For n As Integer = 0 To NumberOfColumns - 1
    s.Add(Str(ColumnWidth))
  Next
  
  Return String.FromArray(s, ",")
End Function

Class Properties

Because we need to track things like what the current row or column is to be rendered, the headers, etc., our class needs to add some properties:

  • Protected Property ColumnWidth As Integer
  • Public Property CurrentColumn As Integer
  • Protected Property CurrentHeight As Integer
  • Protected Property CurrentRow As Integer
  • Protected Property data() As String
  • Protected Property document As PDFDocument
  • Protected Property DrawingFromRowSet As Boolean
  • Public Property Headers() As String
  • Protected Property LastRow As Integer
  • Protected Property proxy As Picture

PDFTableDataSouce Methods

Time to visit the methods added by the PDFTableDataSouce class interface. These are the methods called by the PDFTable object each time it needs to retrieve a piece of information during the rendering on the PDFDocument, as for example if it needs to add a new row, the height for the Header or the current row… and the painting of the row itself!

AddNewRow Method

This one is called by the PDFTable object in order to know if it needs to render a new row in the table:

Protected Function AddNewRow(rowCount As Integer) As Boolean
  // Part of the PDFTableDataSource interface.
  
  If DrawingFromRowSet And data.LastIndex <> -1 then
    
    // We are going to draw as many rows as rows are in the
    // "data" array
    Return rowCount <= data.LastIndex
    
  End If
End Function

Completed method

This is the method invoked by the PDFTable object when it finished drawing the table, so it is your chance to take additional operations, for example, numbering every page of the PDFDocument (we can’t anticipate that before drawing the table itself):

Protected Sub Completed(x As Double, y As Double)
  // Part of the PDFTableDataSource interface.
  
  // This method is called once the table has been drawed
  // so let's "print" the page number on every page
  // of the PDF Document
  
  Static pageNumber As String = "Page: "
  
  If document <> Nil Then
    Var g As Graphics = document.Graphics
    
    For n As Integer = 1 To document.PageCount
      document.CurrentPage = n
      g.DrawText(pageNumber + Str(n), g.Width - g.TextWidth(pageNumber + Str(n)) - 20, g.Height - g.FontAscent)
    Next
    
  End If
End Sub

HeaderHeight Method

This is the method called by the PDFTable object the returns the height we want to use for rendering the header on the first page and, optionally, on every new page required (added to the document) by rendering the table. In this case we will return a fixed value:

Protected Function HeaderHeight() As Double
  // Part of the PDFTableDataSource interface.
  
  // Returning a fixed height value for the headers
  Return 20
End Function

MergeCellsForRow Method

The PDFTableDataSouce class interface also has the ability to merge cells in the current row, so you can create more elaborated tables. We are not going to use that feature in this example project so it will be empty, no code to be executed.

PaintHeaderContent Method

This is the method in charge of rendering the header for the table, so it receives its own Graphic context with the provided height from the HeaderHeight method and the width calculated from the total of columns (headers) needed to be rendered:

Protected Sub PaintHeaderContent(g As Graphics, column As Integer)
  // Part of the PDFTableDataSource interface.
  
  // Painting the headers for the table
  If column <= Self.Headers.LastIndex Then
    
    Var s As String = headers(column)
    g.DrawingColor = Color.Black
    g.FillRectangle(0, 0, g.Width, g.Height)
    g.DrawingColor = Color.White
    g.DrawText(s, g.Width / 2 - g.TextWidth(headers(column)) / 2, g.Height / 2 + g.FontAscent / 2)
    
  End If
End Sub

RowHeight Method

As the class interface provides a method so we can provide the height for the header, we also have a RowHeight method that the PDFTable object will call before it calls the method in charge of doing the row drawing itself. That way we can have rows of different height based on the height of the own text to be rendered!

Protected Function RowHeight() As Double
  // Part of the PDFTableDataSource interface.
  
  // We need to calculate the height for every row in the Table
  // so let's calculate that based on the taller of the texts (columns)
  // based on text wrapping
  
  If CurrentRow <= data.LastIndex Then
    CurrentHeight = 0
    Var s() As String = data(CurrentRow).Split(Chr(9))
    Var g As Graphics = proxy.Graphics
    Var itemHeight As Integer
    For Each item As String In s
      itemHeight = g.TextHeight(item, ColumnWidth - 40)
      If itemHeight > CurrentHeight Then
        CurrentHeight = itemHeight
      End If
    Next
    
    CurrentRow = CurrentRow  + 1 
    
    Return CurrentHeight + 20
  End If
End Function

PaintCell Method

Lastly, this is the method called by the PDFTable object when it is time to render a given cell for the current row in the table. It receives as parameters both a graphic context with the width and height already suited for the data to be rendered, and also the row and column values so you can get the appropriate data for a given cell from the data source:

Protected Sub PaintCell(g As Graphics, row As Integer, column As Integer)
  // Part of the PDFTableDataSource interface.
  
  // Here is where we really do the drawing
  // the received "g" parameter is for a particula table cell
  // based on the row / column parameters
  // so the origin for X/Y coordinates is at 0,0
  
  If DrawingFromRowSet And data.LastIndex >= row Then
    // Drawing the outer rectangle for the cell
    g.DrawRectangle(0, 0, g.Width, g.Height)
    
    // retrieving the text to be drawn from the Array,
    // using the row and column parameters for that.
    Var s As String = data(row).NthField(Chr(9), column + 1)
    
    // Centering vertically the text on the table cell
    // while the X offset is fixed at 5 points.
    g.DrawText(s, 5, g.Height / 2 + g.FontAscent / 2, ColumnWidth - 20)
    
    // Let's keep track of the last row drawed
    LastRow = row
    
  End If
End Sub

Time to push the Start button!

Everything is setup now in our PDFRenderer class, so let’s get back to the Pressed event of the Button1 control on the default Window1 window. You will remember from the beginning of this tutorial that this is the place where we retrieved the data from the text file. So let’s add the code to create a PDFDocument instance, a new PDFRenderer instance and do the rendering of the table itself from the data:

If data.LastIndex <> -1 Then
  
  // Creating a new PDFDocument Instance
  Var d As New PDFDocument
  
  // Creating the PDFTable renderer helper object (we pass the PDFDocument object to it in the Constructor)
  Var PDFRender As New PDFRenderer(d)
  
  // These will be the headers drawed in the Table
  Var headers() As String = Array("Name", "Surname", "Address", "City", "Email")
  
  // And let's instruct the PDFTable renderer helper to draw the table
  // based in the SQLite database rowset we got in the previous step
  PDFRender.DrawTable(data, headers)
  
  // …and save the resulting PDF File to the Desktop
  Var out As FolderItem = SpecialFolder.Desktop.Child("TableFromTextFile.pdf")
  
  If out <> Nil Then
    d.Save(out)
    out.Open
  End If
End If

Conclusion

As you see, and if this tutorial is compared with PDFTable from a RowSet, creating a PDFTable doesn’t differ too much when using a data source other than a RowSet. All you need, basically, is to keep track of the current row, know if you need to render more rows and a base data structure that can be used to retrieve the data to be rendered in a given Row/Column.

As always, you can learn more about other features provided by PDFDocument in the Xojo Documentation.

Happy Coding!

Javier Menendez is an engineer at Xojo and has been using Xojo since 1998. He lives in Castellón, Spain and hosts regular Xojo hangouts en español. Ask Javier questions on Twitter at @XojoES or on the Xojo Forum.