Um conjunto de dados (dataset) é composto por linhas e colunas; uma página PDF é uma grelha de coordenadas em branco sem qualquer noção de nenhum dos dois. Fazer a ponte para essa lacuna é todo o trabalho necessário aqui. Não existe nenhuma chamada DrawTable no HotPDF que receba um dataset e lhe entregue uma grelha formatada. O que obtém, em vez disso, são as primitivas de que uma grelha é feita: TextOut para colocar uma string num ponto, SetFont para escolher o seu tipo de letra, Rectangle e Fill para sombrear uma faixa, e MoveTo / LineTo / Stroke para desenhar linhas de grelha. Um exportador de tabelas funcional exige a disciplina de transformar o raciocínio de linhas e colunas em coordenadas x e y explícitas, e depois manter essas coordenadas corretas quando os dados ultrapassam o fim da página.
O exemplo que se segue apresenta registos de clientes, mas nada no código de desenho sabe ou quer saber de onde vêm as linhas. O original utilizava um TTable antigo; uma consulta FireDAC, um dataset em memória ou um simples array de registos alimentam as mesmas rotinas sem qualquer alteração. O que importa é que consiga percorrer os dados uma linha de cada vez e ler quatro campos de texto (string) de cada uma. Mantenha a renderização separada da origem de dados e poderá alterar qualquer um dos lados sem perturbar o outro.
A geometria das colunas vem primeiro
Antes de desenhar um único carácter, decida onde se situa cada coluna. A tabela aqui tem quatro colunas, pelo que necessita de quatro limites esquerdos e de uma margem direita conhecida. Codificar diretamente um número mágico em cada chamada a TextOut, como os exemplos rápidos costumam fazer, é exatamente o que torna uma tabela difícil de alargar mais tarde. Defina os limites uma vez, em pontos a partir da origem no canto inferior esquerdo, e cada chamada de desenho referir-se-á a eles pelo nome:
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;
Dois detalhes justificam-se aqui. A faixa sombreada é desenhada primeiro, e depois o texto por cima, porque a ordem de pintura é a ordem Z no PDF: preencha o retângulo após o texto e acabará por ocultar a linha. E a alternância de sombreado não é decoração por capricho. Num relatório denso, é a forma mais simples de evitar que a vista se desvie para a linha errada, razão pela qual o ciclo inverte um valor booleano em cada linha e o passa diretamente para Shaded.
As posições das colunas acima são fixas, o que é adequado para um relatório cujo esquema controla. Quando os dados são variáveis, meça em vez de adivinhar. O HotPDF disponibiliza a medição da largura do texto no objeto de página, pelo que a versão de produção do PrintRow pode obter o valor mais longo esperado em cada coluna, medi-lo uma vez no tamanho de letra escolhido e deduzir os limites esquerdos a partir dessas larguras mais um espaçamento (gutter). A estrutura da rotina não se altera; apenas muda a origem das constantes.
O cabeçalho, as linhas e um único local que os gere
Uma tabela que transborda de uma página e continua na seguinte sem cabeçalhos de coluna torna-se ilegível. A solução consiste em tratar o cabeçalho como algo que se redesenha, e não como algo que se desenha uma única vez. Coloque os títulos das colunas e as linhas horizontais que os enquadram numa única rotina e chame essa rotina tanto no início como sempre que abrir uma nova página. Como o cabeçalho e o corpo partilham as mesmas constantes de coluna, o alinhamento é garantido.
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 o DrawHeader recebe Y por referência e avança-o. O chamador nunca precisa de recordar a altura do cabeçalho; a rotina que o desenha é a mesma que conhece essa medida. Essa regra de gestão única é o que evita que o layout se desvie quando adicionar mais tarde um logótipo ou um resumo de filtro à faixa do cabeçalho. O ciclo do corpo permanece alheio a isso. Limita-se a desenhar linhas a partir de onde quer que Y aponte no momento.
As próprias linhas definem a diferença entre uma lista e uma tabela. Os separadores verticais de colunas seguem a mesma lógica aplicada ao eixo x: um MoveTo / LineTo / Stroke em cada limite de coluna, executado a partir da linha superior até ao final da última linha na página. O exemplo mantém-se apenas com linhas horizontais para facilitar a leitura, mas o passo para produção é mecânico assim que as constantes de coluna existirem.
O ciclo de cursor controla a quebra de página
Desenhar é a parte fácil. A parte que distingue uma ferramenta simples de um relatório real é a paginação: saber, antes de desenhar uma linha, se esta ainda cabe, e iniciar uma nova página com um novo cabeçalho quando já não couber. Essa decisão pertence a exatamente um local, o ciclo que percorre os dados, e a mais lado nenhum.
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;
Dois factos sobre coordenadas controlam todo o ciclo. O PDF mede Y para cima a partir do canto inferior esquerdo, pelo que as linhas avançam para baixo na página ao subtrair RowStep a Y de cada vez, e o teste de página cheia é acionado quando Y desce abaixo da margem inferior, em vez de subir acima de um determinado limite superior. Se inverter a direção, a primeira linha será impressa fora da margem inferior enquanto o ciclo assume que tem uma página inteira de espaço livre.
O outro facto surpreende quase toda a gente pelo menos uma vez. O AddPage cria uma nova página e aponta CurrentPage para ela, mas não transporta nada: nem o tipo de letra, nem a cor de preenchimento, nem a posição. É por isso que o Page é lido novamente a partir do CurrentPage após cada AddPage, e porque o SetFont é reemitido antes das linhas do corpo. Se ignorar a nova leitura, continuará a desenhar na página que acabou de deixar para trás; se omitir o tipo de letra, a nova página será renderizada com o tipo de letra padrão do visualizador.
Os casos que quebram um exportador de tabelas
A maioria dos problemas com tabelas não surge no cenário ideal de algumas dezenas de linhas organizadas. Manifestam-se nos limites extremos, e esses limites são fáceis de testar quando sabe onde se encontram.
- Conjuntos de dados vazios. Um ciclo sobre zero linhas gera uma página com um cabeçalho e nada por baixo, o que pelo menos parece intencional. Uma página em branco sem cabeçalho assemelha-se a uma falha. Decida qual das opções pretende antes de disponibilizar a aplicação.
- A linha que fica exatamente no limite. Gere um relatório cuja última linha fique um passo acima da margem, e depois outro cuja linha seguinte fique um passo abaixo. Os erros de paginação por uma unidade (off-by-one) permanecem ocultos até que os dados tenham exatamente o comprimento problemático.
- Valores excessivamente longos. Um nome de empresa mais largo do que a sua coluna invadirá a coluna seguinte. Meça o campo e decida uma política: quebrar para uma segunda linha, cortar (clip) ou truncar com reticências. A ausência de resposta não é uma política.
- Campos nulos. A leitura de um nulo diretamente para o
TextOutpode manifestar-se como o texto literalNullou como um espaço em branco, dependendo de como o converte. Escolha a renderização de forma deliberada, em vez de deixar que a conversão da variante decida por si.
Submeta o resultado a mais do que um visualizador antes de dar o trabalho por concluído. A substituição de tipos de letra e o recorte (clipping) comportam-se de forma diferente entre renderizadores, e uma tabela que parece perfeita num leitor de PDF pode mostrar uma coluna desalinhada ou uma cidade cortada noutro. Confirme se o cabeçalho repetido, o sombreado das linhas e as margens persistem após a transição, e se a numeração das páginas se mantém contínua depois de os dados ultrapassarem o limite da página.
Desenhar a grelha por si próprio, em vez de recorrer a um designer visual de relatórios, exige mais código, e a compensação deve ser expressa claramente: é proprietário de cada coordenada, o que é exatamente o que procura para processos em lote no servidor, faturas e exportações de auditoria que têm de ser renderizados de forma idêntica em qualquer máquina, sendo também precisamente a sobrecarga que preferiria evitar para uma listagem interna ocasional. No primeiro caso, o controlo compensa na primeira vez que um relatório tem de apresentar o mesmo aspeto em produção que apresentava no seu computador.
As linhas e as faixas sombreadas acima baseiam-se nas mesmas primitivas de vetor e cor abordadas no guia de desenho em tela (canvas), caso prefira analisar primeiro as chamadas Rectangle, MoveTo e LineTo de forma isolada. As primitivas de desenho aqui utilizadas fazem parte do HotPDF Component para Delphi e C++Builder.