Un dataset son filas y columnas; una página PDF es una cuadrícula de coordenadas en blanco sin ninguna noción de ninguna de las dos. Salvar esa brecha es todo el trabajo aquí. No hay ninguna llamada DrawTable en HotPDF que acepte un dataset y devuelva una cuadrícula formateada. Lo que obtienes en su lugar son las primitivas de las que está hecha una cuadrícula: TextOut para colocar una cadena en un punto, SetFont para elegir la tipografía, Rectangle y Fill para sombrear una banda, y MoveTo / LineTo / Stroke para dibujar reglas. Un exportador de tablas funcional es la disciplina de convertir el pensamiento en filas y columnas en coordenadas x e y explícitas, y luego mantener esas coordenadas honestas cuando los datos superan la parte inferior de la página.
El ejemplo que sigue elabora informes de registros de clientes, pero nada en el código de dibujo sabe ni le importa de dónde provienen las filas. El original usaba un TTable heredado; una consulta FireDAC, un dataset en memoria o un array plano de registros alimentan las mismas rutinas sin cambios. Lo que importa es que puedas recorrer los datos una fila a la vez y leer cuatro campos de cadena de cada una. Mantén el renderizado separado de la fuente de datos y podrás cambiar cualquiera de los dos lados sin perturbar al otro.
La geometría de columnas va primero
Antes de dibujar un solo carácter, decide dónde vive cada columna. Una tabla tiene aquí cuatro columnas, por lo que necesita cuatro bordes izquierdos y un margen derecho conocido. Codificar un número mágico en cada llamada TextOut, como suelen hacer los ejemplos rápidos, es exactamente lo que hace doloroso ampliar una tabla más tarde. Nombra los bordes una vez, en puntos desde el origen inferior izquierdo, y cada llamada de dibujo hace referencia a ellos por nombre:
const
ColNo = 70; // left edge of the "No." column
ColName = 110; // company name
ColAddr = 300; // street address
ColCity = 480; // city
RowLeft = 50; // table frame: left rule
RowRight = 570; // table frame: right rule
RowStep = 20; // vertical distance between baselines
procedure PrintRow(Page: THPDFPage; Y: Single;
const ANo, AName, AAddr, ACity: string; Shaded: boolean);
begin
if Shaded then
begin
// A shaded band behind the row. Rectangle takes X, Y, Width, Height.
Page.SetRGBFillColor($00FFF3DD);
Page.Rectangle(RowLeft, Y - 4, RowRight - RowLeft, RowStep);
Page.Fill;
Page.SetRGBFillColor(clBlack);
end;
Page.TextOut(ColNo, Y, 0, ANo);
Page.TextOut(ColName, Y, 0, AName);
Page.TextOut(ColAddr, Y, 0, AAddr);
Page.TextOut(ColCity, Y, 0, ACity);
end;
Hay dos detalles que justifican su presencia. La banda sombreada se dibuja primero y luego el texto encima, porque el orden de pintado es el orden z en PDF: rellena el rectángulo después del texto y enterrarás la fila. Y el sombreado alternado no es decoración por sí misma. En un informe denso es la forma más barata de evitar que el ojo se deslice a la línea equivocada, razón por la que el bucle más adelante invierte un booleano en cada fila y lo pasa directamente a Shaded.
Las posiciones de columna anteriores son fijas, lo cual es honesto para un informe cuyo esquema controlas. Cuando los datos son variables, mide en lugar de adivinar. HotPDF expone la medición del ancho de texto en el objeto de página, de modo que la versión de producción de PrintRow puede tomar el valor más largo esperado en cada columna, medirlo una vez con el tamaño de fuente elegido y derivar los bordes izquierdos de esos anchos más un medianil. La forma de la rutina no cambia; solo cambia la fuente de las constantes.
El encabezado, las reglas y un único lugar que los gestiona
Una tabla que se sale de una página y continúa en la siguiente sin etiquetas de columna es ilegible. La solución es tratar el encabezado como algo que se redibuja, no como algo que se dibuja una vez. Pon los títulos de columna y las reglas horizontales que los enmarcan en una única rutina, y llama a esa rutina tanto al inicio como cada vez que abras una nueva página. Como el encabezado y el cuerpo comparten las mismas constantes de columna, se alinean por construcción.
procedure DrawHeader(Page: THPDFPage; var Y: Single; PageNo: Integer);
begin
// Left: source label and page number. Right: generation time.
Page.SetFont('Arial', [fsItalic], 10);
Page.TextOut(RowLeft, Y, 0, 'customer.db Page ' + IntToStr(PageNo));
Page.TextOut(ColCity, Y, 0, DateTimeToStr(Now));
// Two horizontal rules that box the column titles.
Page.MoveTo(RowLeft, Y + 15);
Page.LineTo(RowRight, Y + 15);
Page.MoveTo(RowLeft, Y + 45);
Page.LineTo(RowRight, Y + 45);
Page.Stroke;
// The column titles, in a heavier face so they read as headings.
Page.SetFont('Times New Roman', [fsBold], 12);
Page.SetRGBFillColor(clNavy);
PrintRow(Page, Y + 25, 'No.', 'Company', 'Address', 'City', False);
Page.SetRGBFillColor(clBlack);
Y := Y + RowStep + 45; // advance past the boxed header before the first body row
end;
Observa que DrawHeader toma Y por referencia y lo avanza. El llamador nunca tiene que recordar la altura del encabezado; la rutina que lo dibuja es la rutina que lo sabe. Esa regla de responsabilidad única es lo que evita que la maquetación se desplace cuando más adelante añades un logotipo o un resumen de filtro a la banda de encabezado. El bucle de cuerpo permanece ajeno. Simplemente sigue dibujando filas desde donde Y apunte en ese momento.
Las reglas en sí son la diferencia entre una lista y una tabla. Los separadores de columna verticales son la misma idea aplicada al eje x: un MoveTo / LineTo / Stroke en cada borde de columna, ejecutado desde la regla superior hasta la parte inferior de la última fila de la página. El ejemplo se limita a reglas horizontales para mantener la legibilidad, pero el paso a producción es mecánico una vez que existen las constantes de columna.
El bucle de cursor gestiona el salto de página
El dibujo es la mitad fácil. La mitad que separa un juguete de un informe es la paginación: saber, antes de dibujar una fila, si todavía cabe, y comenzar una nueva página con un nuevo encabezado cuando no cabe. Esa decisión pertenece a exactamente un lugar, el bucle que recorre los datos, y a ningún otro.
var
Pdf: THotPDF;
Page: THPDFPage;
Y: Single;
PageNo: Integer;
Shaded: boolean;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'CustomerReport.pdf';
Pdf.BeginDoc;
Page := Pdf.CurrentPage;
// Report title, once, at the top of the first page.
Page.SetFont('Arial', [fsBold], 24);
Page.TextOut(200, 800, 0, 'Customer Report');
PageNo := 1;
Y := 760;
DrawHeader(Page, Y, PageNo);
Shaded := False;
CustomerTable.First;
while not CustomerTable.Eof do
begin
// Out of room? Open a new page and repeat the header there.
if Y < 60 then
begin
Pdf.AddPage;
Page := Pdf.CurrentPage; // AddPage moves CurrentPage forward
Inc(PageNo);
Y := 760;
DrawHeader(Page, Y, PageNo);
end;
Shaded := not Shaded;
Page.SetFont('Arial', [], 10); // SetFont must be reissued on every new page
PrintRow(Page, Y,
VarToStr(CustomerTable['CustNo']),
VarToStr(CustomerTable['Company']),
VarToStr(CustomerTable['Addr1']),
VarToStr(CustomerTable['City']),
Shaded);
Y := Y - RowStep;
CustomerTable.Next;
end;
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Dos hechos sobre coordenadas impulsan todo el bucle. PDF mide y hacia arriba desde la esquina inferior izquierda, de modo que las filas avanzan hacia abajo por la página restando RowStep de Y cada vez, y la comprobación de página llena se activa cuando Y cae por debajo del margen inferior en lugar de por encima de algún límite superior. Si inviertes la dirección, tu primera fila se imprime fuera del borde inferior mientras el bucle cree que tiene una página completa de espacio.
El otro hecho pillan a casi todo el mundo una vez. AddPage crea una nueva página y redirige CurrentPage hacia ella, pero no transfiere nada: ni la fuente, ni el color de relleno, ni la posición. Por eso Page se vuelve a leer desde CurrentPage después de cada AddPage, y por eso SetFont se vuelve a emitir antes de las filas de cuerpo. Si omites la relectura, sigues dibujando en la página que acabas de dejar atrás; si omites la fuente, la nueva página se renderiza con el valor predeterminado al que recurra el visor.
Los casos que rompen un exportador de tablas
La mayoría de los errores de tablas no aparecen en el camino feliz de unas pocas docenas de filas ordenadas. Viven en los bordes, y los bordes son baratos de probar una vez que sabes dónde están.
- Datasets vacíos. Un bucle sobre cero filas produce una página con un encabezado y nada debajo, lo que al menos parece intencionado. Una página en blanco sin encabezado parece un fallo. Decide cuál quieres antes de distribuir.
- La fila que cae exactamente en el límite. Genera un informe cuya última fila quede un paso por encima del margen, luego uno cuya siguiente fila quede un paso por debajo. La paginación con error de uno se oculta hasta que los datos tienen exactamente la longitud equivocada.
- Valores demasiado largos. Un nombre de empresa más ancho que su columna se extenderá a la siguiente. Mide el campo y decide una política: ajustar a una segunda línea, recortar o truncar con puntos suspensivos. El silencio no es una política.
- Campos nulos. Leer un nulo directamente en
TextOutpuede mostrarse como el texto literalNullo como un espacio en blanco, según cómo lo conviertas. Elige el renderizado deliberadamente en lugar de dejar que la conversión de variante elija por ti.
Pasa el resultado por más de un visor antes de darlo por terminado. La sustitución de fuentes y el recorte se comportan de forma diferente según los renderizadores, y una tabla que parece correcta en un lector PDF puede mostrar una columna desalineada o una ciudad recortada en otro. Confirma que el encabezado repetido, el sombreado de filas y los márgenes sobreviven al cambio, y que los números de página siguen siendo continuos después de que los datos crucen un límite.
Dibujar la cuadrícula tú mismo en lugar de apoyarte en un diseñador de informes visual supone más código, y la contrapartida merece nombrarse claramente: posees cada coordenada, que es exactamente lo que quieres para trabajos por lotes en servidor, facturas y exportaciones de auditoría que deben renderizarse de forma idéntica en cada máquina, y exactamente la sobrecarga que preferirías evitar para un listado interno puntual. Para los primeros, el control se paga a sí mismo la primera vez que un informe tiene que verse igual en producción que en tu escritorio.
Las reglas y las bandas sombreadas anteriores se apoyan en las mismas primitivas vectoriales y de color tratadas en la guía de dibujo en lienzo, si quieres ver las llamadas Rectangle, MoveTo y LineTo por separado primero. Las primitivas de dibujo usadas aquí forman parte del HotPDF Component para Delphi y C++Builder.