Un conjunto de datos son filas y columnas; una página PDF es una cuadrícula de coordenadas en blanco sin noción de ninguna de las dos. Cerrar esa brecha es todo el trabajo aquí. No hay una llamada a DrawTable en HotPDF que tome un conjunto de datos y le entregue una cuadrícula formateada. Lo que obtiene 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 su tipo de letra, 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 de filas y columnas en coordenadas x e y explícitas, y luego mantener esas coordenadas correctas cuando los datos pasan el final de la página
El ejemplo que sigue reporta registros de clientes, pero nada en el código de dibujo sabe o le importa de dónde provienen las filas. El original usaba un TTable heredado; una consulta de FireDAC, un conjunto de datos en memoria o un simple arreglo de registros alimenta las mismas rutinas sin cambios. Lo que importa es que pueda recorrer los datos fila por fila y leer cuatro campos de cadena de cada uno. Mantenga el renderizado separado de la fuente de datos y podrá cambiar cualquier lado sin alterar el otro
La geometría de la columna es lo primero
Antes de dibujar un solo carácter, decida dónde vive cada columna. Una tabla tiene cuatro columnas aquí, por lo que necesita cuatro bordes izquierdos y un margen derecho conocido. Codificar de forma rígida un número mágico en cada llamada a TextOut, como suelen hacer los ejemplos rápidos, es exactamente lo que hace que sea doloroso ensanchar una tabla más adelante. Nombre los bordes una vez, en puntos desde el origen inferior izquierdo, y cada llamada de dibujo se referirá a ellos por su 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;
Dos detalles justifican su existencia aquí. La banda sombreada se dibuja primero, luego el texto encima, porque el orden de pintura es el orden z en PDF: llene el rectángulo después del texto y enterrará la fila. Y el sombreado alterno no es decoración por el simple hecho de serlo. En un reporte denso, es la forma más económica de evitar que la vista se deslice hacia la línea equivocada, razón por la cual 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 reporte cuyo esquema usted controla. Cuando los datos son variables, mida en lugar de adivinar. HotPDF expone la medición del ancho del texto en el objeto de la página, por lo que la versión de producción de PrintRow puede tomar el valor esperado más largo en cada columna, medirlo una vez al tamaño de fuente elegido, y derivar los bordes izquierdos a partir de esos anchos más un canal. La forma de la rutina no cambia; solo cambia la fuente de las constantes
El encabezado, las reglas y un lugar que los posee
Una tabla que se desplaza fuera de una página y se reanuda en la siguiente sin etiquetas de columna es ilegible. La solución es tratar el encabezado como algo que vuelve a dibujar, no como algo que dibuja una vez. Ponga los títulos de las columnas y las reglas horizontales que los enmarcan en una sola rutina, y llame a esa rutina tanto al inicio como cada vez que abra una nueva página. Debido a que 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;
Note que DrawHeader toma Y por referencia y lo mueve hacia adelante. El que llama nunca tiene que recordar qué tan alto es el encabezado; la rutina que lo dibuja es la que lo sabe. Esa regla de propiedad única es lo que evita que el diseño se desvíe cuando más adelante agrega un logotipo o un resumen de filtro a la banda del encabezado. El bucle del cuerpo permanece ajeno. Simplemente sigue dibujando filas desde donde sea que apunte actualmente Y
Las reglas mismas 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, que se ejecuta desde la regla superior hasta el final de la última fila en la página. El ejemplo se limita a reglas horizontales para mantenerse legible, pero el paso de producción es mecánico una vez que existen las constantes de columna
El bucle del cursor posee el salto de página
Dibujar es la mitad fácil. La mitad que separa un juguete de un reporte es la paginación: saber, antes de dibujar una fila, si todavía cabe, y comenzar una página nueva con un encabezado nuevo cuando no es así. Esa decisión pertenece a un solo lugar, el bucle que recorre los datos, y a ningún otro lugar
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 las coordenadas impulsan todo el bucle. PDF mide la y hacia arriba desde la esquina inferior izquierda, por lo que las filas marchan hacia abajo en la página restando RowStep a Y cada vez, y la prueba de página llena se dispara cuando Y cae por debajo del margen inferior en lugar de por encima de algún límite superior. Obtenga la dirección al revés y su primera fila se imprimirá fuera del borde inferior mientras el bucle piensa que tiene una página entera de espacio
El otro hecho atrapa a casi todos alguna vez. AddPage crea una nueva página y reasigna CurrentPage a ella, pero no arrastra nada: ni la fuente, ni el color de relleno, ni la posición. Es por eso que Page se vuelve a leer de CurrentPage después de cada AddPage, y por qué SetFont se vuelve a emitir antes de las filas del cuerpo. Omita la relectura y seguirá dibujando en la página que acaba de dejar atrás; omita la fuente y la nueva página se renderizará en el valor predeterminado al que recurra el visor
Los casos que rompen un exportador de tablas
La mayoría de los errores de tabla 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 sabe dónde están
- Conjuntos de datos vacíos. Un bucle sobre cero filas produce una página con un encabezado y nada debajo de él, lo que al menos parece intencional. Una página en blanco sin encabezado parece un fallo. Decida qué es lo que quiere antes de enviar
- La fila que aterriza exactamente en el límite. Genere un reporte cuya última fila se sitúe un paso por encima del margen, luego uno cuya siguiente fila esté un paso por debajo de él. La paginación desfasada por uno se oculta hasta que los datos tienen exactamente la longitud incorrecta
- Valores demasiado largos. Un nombre de empresa más ancho que su columna invadirá la siguiente. Mida el campo y decida 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 aparecer como el texto literalNullo como un espacio en blanco, dependiendo de cómo lo convierta. Elija la representación deliberadamente en lugar de dejar que la conversión de variante elija por usted
Pase el resultado por más de un visor antes de darlo por terminado. La sustitución de fuentes y el recorte se comportan de manera diferente en distintos renderizadores, y una tabla que se ve cuadrada en un lector de PDF puede mostrar una columna desalineada o una ciudad recortada en otro. Confirme que el encabezado repetido, el sombreado de filas y los márgenes sobreviven al movimiento, y que los números de página permanecen continuos después de que los datos cruzan un límite
Dibujar la cuadrícula usted mismo en lugar de apoyarse en un diseñador de reportes visuales requiere más código, y vale la pena nombrar claramente el compromiso: usted es dueño de cada coordenada, que es exactamente lo que desea para trabajos por lotes del lado del servidor, facturas y exportaciones de auditoría que deben renderizarse de manera idéntica en cada equipo, y es exactamente la sobrecarga que preferiría evitar para un listado interno único. Para los primeros, el control se amortiza la primera vez que un reporte tiene que verse igual en producción que en su escritorio
Las reglas y bandas sombreadas anteriores se apoyan en los mismos primitivos de vectores y colores cubiertos en la guía de dibujo en canvas, si desea que las llamadas Rectangle, MoveTo y LineTo se traten de forma independiente primero. Los primitivos de dibujo utilizados aquí son parte del Componente HotPDF para Delphi y C++Builder