Trabajar con PDFTableDataSource… con datos distintos de RowSet

Algunos usuarios han preguntado sobre si es posible generar tablas en un documento PDFDocument utilizando la interface de clase PDFTableDataSource, cuando la fuente de datos no es un RowSet. Bueno, lo cierto es que no hay mucha diferencia. Continúa leyendo y te mostraré cómo puedes hacerlo mediante un ejemplo sencillo.

El “truco” consiste en manejar los datos procedentes de cualquier otra fuente mediante el uso de una estructura de datos adecuada, como por ejemplo un simple Array. En este ejemplo utilizaremos como fuente de datos un simple archivo de texto cuyo contenido está formado por varias líneas y en donde las “columnas” están separadas entre sí mediante el uso del caracter tabulador.

Para seguir este tutorial puedes descargar el proyecto de ejemplo desde este enlace.

Obtener los datos

Como puedes ver en el proyecto de ejemplo, el archivo de texto se ha añadido al proyecto utilizando un Build > Copy File Step (el nombre del archivo es “Data.txt”), de modo que se copiará en la carpeta Resources de la app compilada y estará disponible tanto para las compilaciones de depuración como para la de despliegue final.

A continuación, en el evento Pressed del control Button1 situado en la ventana por omisión Window1, accederemos a los contenidos del archivo para almacenar cada una de sus líneas en el array de tipo String Data():

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

Subclase PDFRenderer

El siguiente paso consiste en crear una clase personalizada que implementará los métodos recogidos en la interface de clase PDFTableDataSource. Nombraremos dicha subclase como “PDFRenderer”. Luego, desde el Panel Inspector, haremos clic en el botón Interfaces y seleccionaremos la interface PDFTableDatasource.

Una vez que confirmemos la selección y cerremos el diálogo veremos que los métodos de la interface se añadirán automáticamente a nuestra clase:

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

Además de estos, añadiremos otros tres nuevos métodos:

El Constructor de Clase:

Public Sub Constructor(document As PDFDocument)
  // Constructor simple… sólo asignamos el objeto recibido
  // a la propiedad "Document"
  
  Self.document = document
  proxy = new Picture(document.Graphics.Width, document.Graphics.Height)
End Sub

Realmente simple: se encarga de asignar la instancia PDFDocument recibida sobre la propiedad document. También crea una instancia de Graphics que utilizaremos como proxy para calcular la altura del texto procesado a partir de los datos (no necesitaremos dicho proxy a partir de Xojo 2024r4, ya que dicha release corregirá algunos bugs detectados en PDFDocument y relacionados con la obtención de la altura de un texto dado).

DrawTable:

Este método es el encargado de comenzar a dibujar la tabla propiamente dicha. Está a cargo de realizar la inicialización de algunas propiedades empleadas en nuestra clase, como los datos recibidos (sí, verás una propiedad llamada “DrawingFromRowSet”, pero la he dejado a propósito para que compruebes que, de hecho, este ejemplo no difiere en mucho del utilizado para generar una tabla a partir de un RowSet):

Public Sub DrawTable(data() As String, headers() As String)
  // Este es el método que se llama sobre la clase para dibujar
  // una tabla en un PDFDocument, basándose en un Array de Strings
  
  If document <> Nil And data.lastindex <> -1 Then
    
    // Assign the received Array to the "Data" property
    Self.data = data
    
    // Asigna el array recibido de cabeceras a la propiedad "Headers"
    Self.Headers = Headers
    
    // ¡Hey! Vamos a crear una tabla a partir de un Array
    // así que utilizaremos una propiedad Booleana como bandera
    Self.DrawingFromRowSet = True
    
    // ¿Cuántas columnas se van a renderizar?
    // Bueno… tantas columnas como tabuladores contemos en cualquiera de las líneas (items) del Array… por ejemplo el primer item.
    Var totalColumns As Integer = data(0).CountFields( chr(9) )
    
    // Esta será el área de dibujo en la página
    // para la tabla = ancho total de la página menos los márgenes derecho e izquierdo
    Var totalWidth As Double = document.Graphics.Width - 40 // 40 = 20 puntos de márgenes izquierdo/derecho
    
    // Aquí es donde creamos el objeto PDFTable
    Var table As New PDFTable
    
    // Queremos repetir la cabecera de la tabla en cada página
    table.HasRepeatingHeader = True
    
    // Definimos el número de columnas para la tabla
    table.ColumnCount = totalColumns
    
    // …y el ancho de cada columna.
    table.ColumnWidths = CalculateColumnWidths(TotalWidth,totalColumns)
    
    // El objeto creado a partir de esta clase será el responsable
    // de gestionar todos los métodos asociados con
    // la interface de clase PDFTableDataSouce
    table.DataSource = Self
    
    // Ajustamos los márgenes superior e inferior para
    // el dibujado de la tabla en cada página del PDF
    table.TopMargin = 20
    table.BottomMargin = 20
    
    // …y por último indicamos al objeto de PDFDocument
    // que dibuje la tabla
    document.AddTable(table,20,0)
    
    // Por último, borramos la bandera
    Self.DrawingFromRowSet = False
    
  End If
End Sub

Método CalculateColumnWidths

Este es el método a cargo de calcular el ancho de cada una de las columnas que se renderizarán en el documento PDF final.

Protected Function CalculateColumnWidths(TotalWidth As Double, NumberOfColumns As Double) As String
  // Método auxiliar para obtener como cadena los anchos de las columnas
  
  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

Propiedades de la Clase

Dado que necesitamos hacer el seguimiento de aspectos como la fila o columna actuales a renderizar, las cabeceras, etc., también hemos de añadir a nuestra clase las siguientes propiedades:

  • 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

Métodos de PDFTableDataSouce

Hora de visitar los diversos métodos añadidos por la interface de clase PDFTableDatasource. Esto son los invocados por el objeto PDFTable (https://documentation.xojo.com/api/pdf/pdftable.html#pdftable) cada vez que necesita obtener información durante el renderizado de la tabla en el documento PDF, como por ejemplo si es preciso añadir una nueva fila, la altura de la Cabecera de la tabla o de la fila actual… y también el proceso de dibujarlas.

Método AddNewRow

Este es el método llamado por el objeto PDFTable para saber si es necesario renderizar una nueva fila en la tabla:

Protected Function AddNewRow(rowCount As Integer) As Boolean
  // Parte de la interfaz de clasePDFTableDataSource.
  
  If DrawingFromRowSet And data.lastindex <> -1 then
    
    // Dibujaremos tantas filas como ítems hay
    // en el array "data"
    Return rowCount <= data.LastIndex
    
  End If
End Function

Método Completed

Este es el método invocado por el objeto PDFTable cuando ha terminado de dibujar la tabla… dando así la oportunidad de que puedas realizar cualesquiera operaciones adicionales, como por ejemplo numerar cada una de las páginas generadas tal y como haremos en nuestro ejemplo (no es posible anticipar cuántas páginas se generarán para una tabla determinada a priori):

Protected Sub Completed(x As Double, y As Double)
  // Parte de la interfaz de clasePDFTableDataSource.
  
  // Este método se invoca una vez que se ha dibujado la tabla
  // de modo que "dibujaremos" el número de página en cada página
  // del documento PDF
  
  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

Método HeaderHeight

Este es el método invocado por el objeto PDFTable para que podamos devolver la altura que deseamos a la hora de renderizar la cabecera sobre la primera página de la tabla y, opcionalmente, sobre el resto de cada nueva página añadida. En este caso utilizaremos un valor fijo:

Protected Function HeaderHeight() As Double
  // Parte de la interfaz de clasePDFTableDataSource.
  
  // Devolvemos un valor de altura fijo para las cabeceras
  Return 20
End Function

Método MergeCellsForRow

La interface de clase PDFTableDataSource también ofrece la capacidad de combinar celdas para la fila actual, de modo que puedas crear tablas más elaboradas. No vamos a hacerlo en este ejemplo, de modo que dicho método no tendrá código que ejecutar.

Método PaintHeaderContent

Este es el método a cargo de renderizar la cabecera de la tabla, de modo que recibe su propio contexto gráfico con la altura que habíamos indicado previamente mediante el método HeaderHeight… y el ancho que se ha calculado a partir de la cantidad total de columnas:

Protected Sub PaintHeaderContent(g As Graphics, column As Integer)
  // Parte de la interfaz de clasePDFTableDataSource.
  
  // Dibujamos las cabeceras de la tabla
  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

Método RowHeight

De igual modo que la interface de clase proporciona un método para que podamos indicar la altura de la cabecera, también ofrece el método RowHeight para que el objeto PDFTable pueda obtener la altura que tendrá una fila determinada antes de proceder a su renderizado, dato que probablemente quieras basar en la altura máxima del texto que ha de contener cada una de las columnas de la fila.

Protected Function RowHeight() As Double
  // Parte de la interfaz de clasePDFTableDataSource.
  
  // Tenemos que calcular la altura para cada fila de la tabla
  // de modo que lo haremos básandonos en el más largo de los textos 
  // teniendo en cuenta el ancho de la columna (wrapeado o retorno suave del texto)
  
  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

Método PaintCell

Por último, este es el método invocado por el objeto PDFTable cuando se trata de renderizar una celda determinada para la fila de la tabla. Recibe como parámetros tanto el contexto gráfico, como el ancho y altura ya definidos para los datos que se han de renderizar. También proporciona los valores de fila y columna, de modo que puedas “obtener” el dato correspondiente para una celda determinada a partir de la fuente de datos utilizada:

Protected Sub PaintCell(g As Graphics, row As Integer, column As Integer)
  // Parte de la interfaz de clasePDFTableDataSource.
  
  
  // Aquí es donde hacemos realmente el dibujado de las celdas
  // sobre el contexto gráfico "g" recibido como parámetro
  // para ello también utilizamos los parámetros Row / Column
  // de modo que podamos coger el dato correcto a mostrar
  // sobre esa celda en concretol
  // El origen de las coordenadas X / Y es 0,0
  
  If DrawingFromRowSet And data.LastIndex >= row Then
    // Drawing the outer rectangle for the cell
    g.DrawRectangle(0,0,g.Width,g.Height)
    
    // Obtenemos el texto a dibujar desde el Array
    // utilizando los parámetros row y column.
    Var s As String = data(row).NthField(Chr(9), column+1)
    
    // Centramos verticalmente el texto sobre la celda
    // mientras que el offset en X está fijado a 5 puntos.
    g.DrawText(s, 5, g.Height/2+g.FontAscent/2, ColumnWidth - 20)
    
    // y registramos cuál ha sido la última fila renderizada
    LastRow = row
    
  End If
End Sub

¡Hora de pulsar el botón de Inicio!

Ya tenemos todo lo necesario en nuestra clase PDFRenderer, de modo que volvamos al evento Pressed del control Button1 situado en la ventana por omisión Window1. Recordarás que aquí fue donde incluimos al principio del tutorial el código encargado de obtener los datos a partir del archivo de texto… de modo que ahora añadiremos a continuación el código responsable de crear una instancia de PDFDocument, una nueva instancia PDFRenderer… y de realizar el renderizado propiamente dicho de la tabla a partir de los datos:

If data.LastIndex <> -1 Then
  
  // Creamos una nueva instancia de PDFDocument
  Var d As New PDFDocument
  
  // Creamos un objeto auxiliar de renderizado para la tabla (le pasamos el objeto PDFDocumento en el Constructor)
  Var PDFRender As New PDFRenderer(d)
  
  // Estos serán los textos de cabecera dibujados en la tabla
  Var headers() As String = Array("Name", "Surname", "Address", "City", "Email")
  
  // Y le indicamos al objeto auxiliar de renderizado que dibuje la tabla
  // basándonos para ello en el array Data que hemos creado previamente
  PDFRender.DrawTable(data, headers)
  
  // Guardamos el PDF resultante en un archivo sobre el Escritorio
  Var out As FolderItem = SpecialFolder.Desktop.Child("TableFromTextFile.pdf")
  
  If out <> Nil Then
    d.Save(out)
    out.Open
  End If
End If

Conclusión

Tal y como hemos visto, y si comparas este tutorial con el ya publicado aquí, la creación de una tabla con PDFTable con una fuente de datos distinta a un RowSet no se diferencia mucho a cuando se utiliza un RowSet; todo lo que se ha de hacer, básicamente, es contar con un sistema que nos permita hacer un seguimiento sobre cuál es la fila en curso, si es preciso añadir más filas a la tabla, y la estructura de datos que queremos utilizar a la hora de recuperar los datos a renderizar para una fila/columna determinados.

Como de costumbre, puedes aprender más sobre las características proporcionadas por PDFDocument en la Documentación de Xojo.

¡Feliz Programación!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *