Um conjunto de dados é linhas e colunas; uma página PDF é uma grelha de coordenadas em branco sem noção de nenhuma das duas coisas. Fazer a ponte entre isso é todo o trabalho aqui. Não existe nenhuma chamada DrawTable no HotPDF que receba um dataset e entregue uma grelha formatada. O que se tem, 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 réguas. Um exportador de tabelas que funcione é a disciplina de transformar o raciocínio em linhas e colunas em coordenadas x e y explícitas, e depois manter essas coordenadas corretas quando os dados ultrapassam o fundo da página
O exemplo que se segue reporta registos de clientes, mas nada no código de desenho sabe ou se importa de onde vêm as linhas. O original usava um TTable legado; uma consulta FireDAC, um dataset em memória, ou um simples array de registos alimenta as mesmas rotinas sem alterações. O que importa é conseguir percorrer os dados uma linha de cada vez e ler quatro campos de string de cada uma. Mantenha a renderização separada da fonte de dados e conseguirá alterar qualquer um dos lados sem perturbar o outro
A geometria das colunas vem primeiro
Antes de se desenhar um único caráter, decida onde vive cada coluna. Aqui a tabela tem quatro colunas, pelo que precisa de quatro margens esquerdas e de uma margem direita conhecida. Codificar um número mágico à mão em cada chamada TextOut, como os exemplos rápidos costumam fazer, é exatamente o que torna uma tabela penosa de alargar mais tarde. Dê nome às margens uma única vez, em pontos a partir da origem no canto inferior esquerdo, e todas as chamadas de desenho referem-se a elas pelo nome:
const
ColNo = 70; // margem esquerda da coluna "N.º"
ColName = 110; // nome da empresa
ColAddr = 300; // morada
ColCity = 480; // cidade
RowLeft = 50; // moldura da tabela: régua esquerda
RowRight = 570; // moldura da tabela: régua direita
RowStep = 20; // distância vertical entre linhas de base
procedure PrintRow(Page: THPDFPage; Y: Single;
const ANo, AName, AAddr, ACity: string; Shaded: boolean);
begin
if Shaded then
begin
// Uma faixa sombreada atrás da linha. Rectangle recebe 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 pormenores merecem destaque aqui. A faixa sombreada é desenhada primeiro, depois o texto por cima, porque a ordem de pintura é a ordem-z em PDF: preencha o retângulo depois do texto e enterra a linha. E o sombreado alternado não é decoração pelo prazer de o ser. Num relatório denso, é a forma mais barata de impedir que o olhar deslize para a linha errada, razão pela qual o ciclo mais adiante inverte um booleano a cada linha e passa-o diretamente para Shaded
As posições de coluna acima são fixas, o que é honesto para um relatório cujo esquema se controla. Quando os dados são variáveis, meça em vez de adivinhar. O HotPDF expõe medição de largura de texto no objeto de página, pelo que a versão de produção de PrintRow pode receber o maior valor esperado em cada coluna, medi-lo uma vez no tamanho de letra escolhido, e derivar as margens esquerdas a partir dessas larguras mais uma calha. A forma da rotina não muda; só muda a origem das constantes
O cabeçalho, as réguas, e um único sítio que os possui
Uma tabela que desliza para fora de uma página e retoma na seguinte sem rótulos de coluna é ilegível. A correção é tratar o cabeçalho como algo que se redesenha, não como algo que se desenha uma única vez. Coloque os títulos das colunas e as réguas horizontais que os emolduram numa única rotina, e chame essa rotina tanto no início como sempre que se abre uma nova página. Como o cabeçalho e o corpo partilham as mesmas constantes de coluna, alinham-se por construção
procedure DrawHeader(Page: THPDFPage; var Y: Single; PageNo: Integer);
begin
// Esquerda: etiqueta da fonte e número de página. Direita: hora de geração.
Page.SetFont('Arial', [fsItalic], 10);
Page.TextOut(RowLeft, Y, 0, 'customer.db Page ' + IntToStr(PageNo));
Page.TextOut(ColCity, Y, 0, DateTimeToStr(Now));
// Duas réguas horizontais que emolduram os títulos das colunas.
Page.MoveTo(RowLeft, Y + 15);
Page.LineTo(RowRight, Y + 15);
Page.MoveTo(RowLeft, Y + 45);
Page.LineTo(RowRight, Y + 45);
Page.Stroke;
// Os títulos das colunas, num tipo de letra mais pesado para se lerem como cabeçalhos.
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; // avança para além do cabeçalho emoldurado antes da primeira linha do corpo
end;
Note que DrawHeader recebe Y por referência e avança-o. Quem chama a rotina nunca precisa de saber quão alto é o cabeçalho; a rotina que o desenha é a rotina que sabe. Essa regra de posse única é o que impede o layout de derivar quando mais tarde se acrescenta um logótipo ou um resumo de filtros à faixa do cabeçalho. O ciclo do corpo mantém-se alheio a isso. Limita-se a continuar a desenhar linhas a partir de onde quer que Y esteja no momento
As próprias réguas são a diferença entre uma lista e uma tabela. Os separadores verticais de coluna são a mesma ideia aplicada ao eixo dos x: um MoveTo / LineTo / Stroke em cada margem de coluna, a correr desde a régua superior até ao fundo da última linha na página. O exemplo mantém-se apenas em réguas horizontais para ficar legível, mas o passo de produção é mecânico assim que as constantes de coluna existem
O ciclo do cursor é dono da quebra de página
Desenhar é a metade fácil. A metade que separa um brinquedo de um relatório é a paginação: saber, antes de desenhar uma linha, se ela ainda cabe, e começar uma página nova com um cabeçalho novo quando não cabe. Essa decisão pertence exatamente a um único sítio, o ciclo que percorre os dados, e a mais nenhum outro
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;
// Título do relatório, uma vez, no topo da primeira página.
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
// Sem mais espaço? Abre uma nova página e repete o cabeçalho aí.
if Y < 60 then
begin
Pdf.AddPage;
Page := Pdf.CurrentPage; // AddPage avança o CurrentPage
Inc(PageNo);
Y := 760;
DrawHeader(Page, Y, PageNo);
end;
Shaded := not Shaded;
Page.SetFont('Arial', [], 10); // SetFont tem de ser reemitido em cada nova página
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 de coordenadas conduzem todo o ciclo. O PDF mede o y para cima a partir do canto inferior esquerdo, pelo que as linhas avançam para baixo na página subtraindo RowStep a Y de cada vez, e o teste de página cheia dispara quando Y desce abaixo da margem inferior, e não acima de alguma margem superior. Inverta o sentido e a primeira linha imprime-se para fora do fundo da página enquanto o ciclo pensa que tem uma página inteira de espaço
O outro facto apanha quase toda a gente uma vez. AddPage cria uma página nova e reaponta 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 Page é relido a partir de CurrentPage depois de cada AddPage, e por que SetFont é reemitido antes das linhas do corpo. Se se saltar a releitura, continua-se a desenhar na página que se acabou de deixar para trás; se se saltar o tipo de letra, a página nova é renderizada seja qual for a predefinição para a qual o visualizador recua
Os casos que partem um exportador de tabelas
A maioria dos bugs de tabelas não aparece no caminho feliz de umas dezenas de linhas arrumadas. Vivem nos limites, e os limites são baratos de testar assim que se sabe onde estão
- Datasets vazios. Um ciclo sobre zero linhas produz 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 parece uma falha. Decida qual quer antes de publicar
- A linha que cai exatamente na fronteira. Gere um relatório cuja última linha fica um passo acima da margem, depois outro cuja linha seguinte fica um passo abaixo dela. A paginação com erro de um-a-mais esconde-se até os dados terem exatamente o comprimento errado
- Valores demasiado longos. Um nome de empresa mais largo do que a sua coluna vai invadir a seguinte. Meça o campo e decida uma política: quebrar para uma segunda linha, recortar, ou truncar com reticências. O silêncio não é uma política
- Campos nulos. Ler um nulo diretamente para
TextOutpode surgir como o texto literalNullou como um espaço em branco, consoante a forma como se converte. Escolha a renderização deliberadamente em vez de deixar a conversão de variante escolher por si
Passe o resultado por mais do que um visualizador antes de o dar por concluído. A substituição de tipos de letra e o recorte comportam-se de forma diferente entre motores de renderização, e uma tabela que parece direita num leitor de PDF pode mostrar uma coluna desalinhada ou uma cidade cortada noutro. Confirme que o cabeçalho repetido, o sombreado das linhas e as margens sobrevivem à mudança, e que os números de página se mantêm contínuos depois de os dados atravessarem uma fronteira
Desenhar a grelha manualmente em vez de se apoiar num desenhador visual de relatórios é mais código, e vale a pena nomear a contrapartida com clareza: é o programador que controla cada coordenada, o que é exatamente o que se quer para tarefas em lote no lado do servidor, faturas, e exportações de auditoria que têm de ser renderizadas de forma idêntica em todas as máquinas, e exatamente o overhead que se preferiria evitar numa listagem interna pontual. No primeiro caso, o controlo paga-se a si próprio na primeira vez que um relatório tem de ter o mesmo aspeto em produção que teve na sua secretária
As réguas e as faixas sombreadas acima apoiam-se nas mesmas primitivas de vetores e cor abordadas no guia de desenho em canvas, caso se prefira ver as chamadas Rectangle, MoveTo e LineTo tratadas por si só primeiro. As primitivas de desenho aqui usadas fazem parte do HotPDF Delphi Component para Delphi e C++Builder