PDFTable… ¡a partir de un RowSet!

Como probablemente sepas, recientemente hemos introducido la capacidad de añadir (o “dibujar”) tablas en PDFDocument. Esto se basa en el uso de la clase PDFTable en combinación de la interface de clase PDFTableDataSource. Probablemente ya sepas cómo usarlas para los casos más comunes, pero… ¿cómo podemos crear una tabla cuando los datos provienen de un RowSet? Continúa leyendo y te mostraré cómo hacerlo.

Pero antes de que nos pongamos a escribir código… el proyecto de ejemplo necesitará para su funcionamiento la base de datos SQLite “EddiesElectronics.sqlite”. Esta se encuentra en varios de los ejemplos proporcionados con Xojo, pero para simplificar las cosas un poco más, también puedes descargarla directamente desde este enlace.

Y si lo deseas, también es posible descargar el proyecto de ejemplo Xojo (para Desktop) desde este enlace.

Estoy convencido de que podrás adaptar el proyecto de ejemplo para que se ajuste al uso de otras bases de datos o fuentes de datos.

¡Clase Helper al rescate!

Todo el trabajo de dibujar la tabla en el documento PDF estará a cargo de una clase helper o auxiliar. Es decir, una clase que vamos a utilizar para “delegar” el trabajo. De modo que una vez que tengas abierto Xojo y hayas creado un nuevo proyecto de tipo Desktop, añade una nueva Clase al proyecto (Insert > Class).

Con la clase recién añadida seleccionada en el Navegador, dirígete al Panel Inspector asociado e introduce los siguientes valores:

  • Name: PDFRenderer
  • Interfaces: Haz clic en el botón “Choose” y selecciona la interface de clase PDFTableDataSource

La clase recién añadida se poblará con todos los métodos definidos por la interface de clase PDFTableDataSource. No te preocupes por ahora de dichos métodos.

Lo próximo que haremos será añadir algunas propiedades a la clase PDFRenderer:

  • Name: data
  • Type: RowSet
  • Scope: Protected
  • Name: document
  • Type: PDFDocument
  • Scope: Protected
  • Name: DrawingFromRowSet
  • Type: Boolean
  • Scope: Protected
  • Name: Headers()
  • Type: String
  • Scope: Protected

Vamos a utilizar DrawingFromRowSet como flag o bandera, de modo que PDFRenderer sepa cuándo vamos a crear la tabla a partir de los datos de un RowSet o bien a partir de cualquier otra fuente de datos que vayamos a añadir (¡esto es algo que tendrás que implementar tú mismo! Tómalo como un ejercicio).

A continuación, añade un nuevo método a la clase PDFRenderer (aun seleccionada en el Navegador), utilizando los siguientes valores en el Panel Inspector asociado:

  • Name: Constructor
  • Parameters: document As PDFDocument
  • Scope: Public

Y escribiendo la siguiente línea de código en el Editor de Código asociado:

Self.Document = document

Sip, así de simple: nos limitamos a asignar el objeto PDFDocument recibido a la propiedad “Document” del objeto creado a partir de la clase PDFRenderer.

Añade un segundo método utilizando los siguientes valores:

  • Name: DrawTable
  • Parameters: rs As RowSet, headers() As String
  • Scope: Public

Y escribe el siguiente código en el Editor de Código asociado:

// This is the method we call on the Class to draw
// a table on a PDFDocument based on a RowSet


If document <> Nil And rs <> Nil Then

  // Assign the received RowSet to the "Data" property
  Self.data = rs

  // Assign the received headers to the "Headers" property
  Self.Headers = Headers

  // Hey! We are going to create a table from a RowSet
  // 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 in the RowSet.
  Var totalColumns As Integer = rs.ColumnCount

  // 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

Como puedes ver, aquí estamos llamando al método “CalculateColumnWidths” para obtener la cadena con los anchos de columna esperada por la propiedad PDFTable.ColumnWidths; de modo que tenemos que añadir dicho método a la clase PDFRenderer usando los siguientes valores:

  • Name: CalculateColumnWidths
  • Parameters: TotalWidth As Double, NumberOfColumns As Double
  • Return Type: String
  • Scope: Protected

Y escribe a continuación el siguiente fragmento en el Editor de Código asociado:

Var ColumnWidth As Integer = TotalWidth / NumberOfColumns
Var s() As String

For n As Integer = 0 To NumberOfColumns-1
  s.Add Str(ColumnWidth)
Next

Return String.FromArray(s,",")

Métodos PDFTableDataSource… ¡con datos de un RowSet!

Vamos a rellenar ahora los métodos requeridos por la interface de clase PDFTableDataSource, de modo que funcionen en combinación con el RowSet recibido.

Método AddNewRow

Añade este fragmento de código al método “AddNewRow”:

If DrawingFromRowSet And data <> Nil Then

  // We are going to draw as many rows as rows are in the
  // "data" rowset
  Return rowCount <> data.RowCount

End If

Como puedes ver, dicho fragmento de código sólo se ejecutará cuando se cumplan ambas condiciones. Es decir, que tengamos la bandera “DrawingFromRowSet” definida a True y la propiedad “data” asignada a un RowSet que no sea Nil. Si es así, entonces indicaremos a PDFTable que configure el número de filas a representar en la tabla al mismo número de filas que contiene el RowSet.

Método Completed

Este es el método invocado una vez que se ha completado el dibujado de la tabla. Utilizaremos dicho método para dibujar los números de página en la parte inferior de cada página del documento PDF. Como seguramente ya sepas, resulta verdaderamente fácil cambiar el contexto gráfico activo al de una página dada del documento PDF, utilizando para ello la propiedad CurrentPage. Por tanto, utilizaremos esto para iterar por cada una de las páginas del documento PDF, añadiendo el número de página como pie de página.

Este es el fragmento de código responsable de hacerlo:

// 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

Método HeaderHeight

Este es el método invocado de modo que podamos indicar a PDFTable la altura deseada para el dibujado de la cabecera de la tabla. En este caso se trata de un valor fijo: 20 puntos; de modo que el código es así de sencillo:

// Fixed header height
Return 20

Método PaintCell

Este es el método responsable de dibujar cada celda en PDFTable. Recibe como parámetros el contexto gráfico para cada una de las celdas (de modo que su origen X/Y se encuentra en las coordenadas 0,0), así como los valores correspondientes a la fila y la columna para dicha celda.

En este caso dibujamos el contorno de un rectángulo en la celda, así como el texto propiamente dicho y que recuperamos del RowSet basándonos en el valor recibido en el parámetro Column. El texto se dibujará centrado verticalmente sobre la celda:

If DrawingFromRowSet And data <> Nil Then
  // Drawing the outer rectangle for the cell
  g.DrawRectangle(0,0,g.Width,g.Height)


  // retrieving the text to be drawn from the RowSet,
  // using the column parameter for that.
  Var s As String = data.ColumnAt(column)

  // 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)

  // Have we drawed all the columns for this row?
  If column = data.ColumnCount-1 Then

    // if that is the case, then move to the next row
    // in the RowSet!
    data.MoveToNextRow
  End If

End If

Método PaintHeaderContent

Este es el método invocado para dibujar la Cabecera de la tabla propiamente dicha. Es una versión más simple que la encontrada en el anterior método. En este caso el texto estará centrado en cada celda de la cabecera tanto vertical como horizontalmente:

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

Método RowHeight

Tal y como ocurre en el método HeaderHeight, este es el método invocado para que podamos definir la altura de una fila determinada. Tal y como hicimos en el método HeaderHeight, utilizaremos un valor fijo de 20 puntos, de modo que escribe la siguiente línea de código en el Editor de Código asociado:

Return 20

Probando la clase PDFRenderer

Para probar nuestra clase PDFRenderer en combinación con un objeto RowSet aun tenemos que añadir unas cuantas piezas al proyecto. En primer lugar, selecciona la ventana Window1 del proyecto en el Navegador y añade una propiedad con los siguientes valores:

  • Name: db
  • Type: SQLiteDatabase
  • Scope: Private

Luego, haz clic en la ventana Window1 en el Navegador de modo que se muestre en el Editor de Diseño. A continuación, arrastra una DesktopLabel desde la Librería y suéltala en el diseño de la ventana sobre la esquina superior izquierda respetando los márgenes indicados por las guías de alineamiento, de modo que reserve los márgenes esperados por la guía de interfaz de usuario. Utiliza los manejadores que permiten cambiar el tamaño del control de modo que ocupe el ancho máximo permitido (nuevamente, observando los márgenes indicados por las guías de alineamiento). Utiliza los siguientes valores en el Panel Inspector asociado:

  • Locking: Izquierda, Arriba y Derecha cerrados. Abajo, abierto.
  • Multiline: activado.
  • Text: This example project needs to use the EddiesElectronics.sqlite database file. Click on the button in order to select it!

En este punto, el diseño debería de tener un aspecto similar al mostrado en la siguiente captura de pantalla:

Arrastra ahora un DesktopButton desde la Librería y suéltalo bajo la anterior etiqueta para que esté centrado sobre el eje vertical, utilizando los siguientes valore en el Panel Inspector asociado:

  • Caption: Select EddiesElectronics database file

El diseño final debería de tener un aspecto similar al mostrado en la siguiente captura de pantalla:

Haz doble clic en el botón y añade el Manejador de Evento “Pressed”. A continuación, añade a continuación el siguiente código en el Editor de Código asociado:

Try


  // Assigning SQLite object to property
  db = New SQLiteDatabase

  // Setting the database file to the SQLite object
  db.DatabaseFile = FolderItem.ShowOpenFileDialog(".sqlite")

  // Just a simple check to see if this is the expected
  // file based on the name = "EddiesElectronics.sqlite")

  If db.DatabaseFile = Nil Or db.DatabaseFile.Name <> "EddiesElectronics.sqlite" Then Return

  //…and "connecting" to it!
  db.Connect

  // Let's select all the rows from the Customers table
  Var rs As RowSet = db.SelectSQL("Select FirstName,LastName,Address from Customers")

  // 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")

  // 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(rs, headers)

  // …and save the resulting PDF File to the Desktop
  Var f As FolderItem = SpecialFolder.Desktop.Child("TableFromRowSet.pdf")

  If f <> Nil Then
    d.Save(f)
    f.Open
  End If

Catch e As DatabaseException
  System.DebugLog e.Message
Catch e As NilObjectException
  System.DebugLog e.Message
End Try

Como puedes ver, todo lo que hacemos es crear un nuevo objeto SQLiteDatabase y asignarlo a la propiedad “db”. Luego, definimos la propiedad “DatabaseFile” al objeto del archivo seleccionado por el usuario. El código se encarga de realizar una comprobación sencilla para ver si el archivo seleccionado es el archivo de base de datos “EddiesElectronics” esperado. Si es así, entonces conectará la base de datos obteniendo un RowSet a partir de la consulta sencilla en la que obtenemos los campos para las columnas “name”, “surname” y “address” de la tabla “Customers”.

Tras crear una instancia de PDFDocument el código se encarga de crear una nueva instancia de nuestra clase PDFRenderer y llamar al método “DrawTable” sobre dicho objeto, pasando como argumentos tanto el RowSet como las cabeceras que queremos utilizar para generar la tabla.

Por último, se guardar el PDFDocument como archivo a disco y se abre con la aplicación por omisión para la lectura de archivos PDF.

Ejecuta la app, selecciona el archivo de base de datos SQLite “EddiesElectronics”… y se abrirá el archivo PDF resultante transcurridos unos pocos segundos, mostrando la tabla generada a partir del RowSet utilizado como fuente de datos.

Conclusión

Como puedes ver, también es posible generar tablas en un documento PDFDocument a partir de los datos de un RowSet; todo se lleva a cabo, prácticamente, mediante el uso de una clase auxiliar. Por supuesto, se puede mejorar más dicha clase de varios modos. Por ejemplo, estamos utilizando un valor fijo para la altura de las filas de la tabla… pero un caso más realista probablemente necesites utilizar una altura diferente para cada fila basándote en la cantidad de texto que ha de incluir, por ejemplo, una celda determinada. Eso es algo que también se puede hacer pero, nuevamente, algo que dejo como ejercicio por si te animas.

Related Posts / Entradas Relacionadas

Deja un comentario

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