Programação Por Equipe DevBras

Capa do artigo: Relatório impresso que corta a última coluna: como paginar no PrintDocument

Relatório impresso que corta a última coluna: como paginar no PrintDocument

Imprima relatórios em .NET que cabem na folha A4: largura de coluna pela área da margem, cabeçalho repetido, quebra de página certa e número que nunca é cortado.

O gerente leva o relatório de vendas do mês para a reunião. Na tela, o grid tem nove colunas e cabe inteiro no monitor de 27 polegadas. No papel, a coluna "Total" sumiu depois da margem direita, a segunda página começa sem cabeçalho e ninguém sabe se o número da linha 48 é de itens ou de reais. A reunião discute um papel que não fecha a conta.

A resposta curta: impressão é um layout próprio, calculado a partir da folha, não uma cópia da tela. Em .NET com PrintDocument, distribua as colunas pela largura de e.MarginBounds, desenhe o cabeçalho em toda página, pare antes da margem inferior e use e.HasMorePages para continuar. Se não cabe, mude a orientação ou tire colunas de propósito. Número nunca é truncado.

Por que o que cabe na tela não cabe na folha

O monitor do gerente tem 2.560 pixels de largura. Uma folha A4 tem 210 por 297 milímetros. Em retrato, com margens de meia polegada (12,7 mm) de cada lado, sobram cerca de 185 mm úteis; em paisagem, cerca de 272 mm. Quem desenha o relatório pensando na tela está tentando enfiar uma faixa larga num papel estreito, e a impressora simplesmente descarta o que passa da área imprimível.

Há três mecanismos que pioram o resultado:

  • Coordenadas fixas. Código que desenha a coluna "Total" em x = 900 funciona na impressora do desenvolvedor e corta em outra com margens físicas diferentes.
  • Paginação esquecida. O evento PrintPage desenha uma página por chamada. Se você não controla onde parou e não liga HasMorePages, o relatório inteiro tenta caber numa folha, e o resto some.
  • Estado que não reinicia. A pré-visualização percorre todas as páginas. Se o índice da próxima linha não volta a zero antes da impressão real, a impressora recebe uma folha em branco.

Como montar o relatório que cabe na folha

  1. Escolha o papel e a orientação no código. A4 em paisagem para relatórios com mais de cinco ou seis colunas.
  2. Defina as margens. Em PrintDocument, Margins é medido em centésimos de polegada: 50 equivale a 12,7 mm.
  3. Corte colunas antes de encolher a fonte. Das nove colunas da tela, o papel do gerente precisa de cinco: data, cliente, vendedor, itens e total. Código do pedido e status ficam para o relatório detalhado.
  4. Distribua a largura por peso. Cada coluna recebe uma fração de MarginBounds.Width. Texto longo (cliente) pode ganhar reticências; número, não.
  5. Repita o cabeçalho em toda página e ponha rodapé com número da página e data de emissão.
  6. Reinicie o estado em BeginPrint, para que pré-visualização e impressão saiam iguais.
  7. Teste na pré-visualização e numa impressora real, com um relatório de pelo menos três páginas.

Exemplo: relatório de vendas em C# com PrintDocument

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Printing;
using System.Globalization;

public record LinhaVenda(DateTime Data, string Cliente, string Vendedor, int Itens, decimal Total);

public class RelatorioVendas
{
    private static readonly CultureInfo Br = new CultureInfo("pt-BR");
    private readonly IReadOnlyList<LinhaVenda> _linhas;
    private readonly Font _fonte = new Font("Segoe UI", 9f);
    private readonly Font _negrito = new Font("Segoe UI", 9f, FontStyle.Bold);

    private static readonly string[] Titulos = { "Data", "Cliente", "Vendedor", "Itens", "Total (R$)" };
    private static readonly float[] Pesos   = { 1.2f, 3.5f, 2.0f, 0.8f, 1.5f };
    private static readonly bool[] Numero   = { false, false, false, true, true };

    private int _proxima;
    private int _pagina;

    public RelatorioVendas(IReadOnlyList<LinhaVenda> linhas) => _linhas = linhas;

    public PrintDocument CriarDocumento()
    {
        var doc = new PrintDocument { DocumentName = "Vendas do mês" };
        doc.DefaultPageSettings.Landscape = true;
        doc.DefaultPageSettings.Margins = new Margins(50, 50, 50, 60); // centésimos de polegada
        doc.BeginPrint += (s, e) => { _proxima = 0; _pagina = 0; };
        doc.PrintPage += ImprimirPagina;
        return doc;
    }

    private void ImprimirPagina(object sender, PrintPageEventArgs e)
    {
        Graphics g = e.Graphics;
        Rectangle area = e.MarginBounds;
        float altura = _fonte.GetHeight(g) + 6;
        float[] larguras = CalcularLarguras(area.Width);
        _pagina++;

        float y = area.Top;
        DesenharLinha(g, area.Left, y, larguras, Titulos, _negrito);
        y += altura;
        g.DrawLine(Pens.Black, area.Left, y - 2, area.Right, y - 2);

        float limite = area.Bottom - altura;   // espaço reservado ao rodapé
        while (_proxima < _linhas.Count && y + altura <= limite)
        {
            LinhaVenda l = _linhas[_proxima];
            string[] celulas =
            {
                l.Data.ToString("dd/MM/yyyy", Br),
                l.Cliente,
                l.Vendedor,
                l.Itens.ToString("N0", Br),
                l.Total.ToString("N2", Br)
            };
            DesenharLinha(g, area.Left, y, larguras, celulas, _fonte);
            y += altura;
            _proxima++;
        }

        string rodape = "Página " + _pagina + "  |  Emitido em " + DateTime.Now.ToString("dd/MM/yyyy HH:mm", Br);
        g.DrawString(rodape, _fonte, Brushes.Black, area.Left, area.Bottom - altura + 4);

        e.HasMorePages = _proxima < _linhas.Count;
    }

    private static float[] CalcularLarguras(float total)
    {
        float soma = 0;
        foreach (float p in Pesos) soma += p;
        var larguras = new float[Pesos.Length];
        for (int i = 0; i < Pesos.Length; i++) larguras[i] = total * Pesos[i] / soma;
        return larguras;
    }

    private void DesenharLinha(Graphics g, float x, float y, float[] larguras, string[] textos, Font fonte)
    {
        for (int i = 0; i < textos.Length; i++)
        {
            using var formato = new StringFormat(StringFormatFlags.NoWrap)
            {
                Alignment = Numero[i] ? StringAlignment.Far : StringAlignment.Near,
                Trimming = Numero[i] ? StringTrimming.None : StringTrimming.EllipsisCharacter
            };
            if (Numero[i] && g.MeasureString(textos[i], fonte).Width > larguras[i] - 8)
                throw new InvalidOperationException("Coluna " + Titulos[i] + " estreita demais para " + textos[i]);

            var celula = new RectangleF(x + 4, y, larguras[i] - 8, fonte.GetHeight(g) + 2);
            g.DrawString(textos[i], fonte, Brushes.Black, celula, formato);
            x += larguras[i];
        }
    }
}

Para abrir a pré-visualização num formulário Windows Forms, basta new PrintPreviewDialog { Document = relatorio.CriarDocumento() }.ShowDialog();. Como o estado reinicia em BeginPrint, o botão de imprimir da pré-visualização gera as mesmas páginas que você viu.

O cálculo que mostra se cabe

Em paisagem, a A4 tem cerca de 1.169 centésimos de polegada de largura. Tirando 50 de cada lado, sobram 1.069. A soma dos pesos é 9, então a coluna "Total" recebe 1,5/9 de 1.069, ou 178 centésimos: quase 1,8 polegada, ou 45 mm. Um valor como "1.234.567,89" em Segoe UI 9 ocupa bem menos que isso. Em retrato, a largura útil cai para 727, e a mesma coluna teria 121 centésimos, ainda suficiente. Já com as nove colunas originais dividindo 727 por igual, cada uma teria uns 80 centésimos (cerca de 20 mm), e o total de sete dígitos começaria a estourar.

Repare na exceção em DesenharLinha: se um número não cabe, o relatório falha no teste em vez de sair impresso com dígito faltando. Nome de cliente cortado com reticências é aceitável; "1.234.5" no lugar de "1.234.567,89" não é.

Área não imprimível e PDF

Toda impressora tem uma faixa física onde não consegue imprimir, exposta em PageSettings.HardMarginX e HardMarginY. Com margens de meia polegada você costuma ficar longe dela, mas teste na impressora do escritório. Para quem não tem impressora, a impressora virtual "Microsoft Print to PDF" do Windows gera um PDF com o mesmo layout.

Se o relatório for web, o raciocínio é o mesmo com outras ferramentas: CSS de impressão, @page e cabeçalho de tabela repetido, explicados em o que sai no papel não é a tela inteira. Para PDF gerado no servidor, veja gerar PDF no servidor a partir de HTML bem definido.

Erros comuns

  • Coordenadas fixas em pixels da tela. O layout funciona numa impressora e corta a última coluna em outra.
  • Não usar HasMorePages. Só a primeira página sai, e ninguém percebe que faltam 200 linhas.
  • Esquecer de reiniciar o índice em BeginPrint. Depois da pré-visualização, a impressão real sai em branco.
  • Encolher a fonte até caber. Em corpo 6, o relatório cabe e ninguém consegue ler na reunião.
  • Cabeçalho só na primeira página. Da segunda em diante, a pessoa não sabe qual coluna é itens e qual é total.
  • Depender de cor. Valor negativo em vermelho vira cinza na impressora monocromática; use sinal de menos ou parênteses.

Checklist

  • Papel, orientação e margens estão definidos no código, não herdados da impressora padrão.
  • Larguras de coluna são calculadas a partir de MarginBounds.Width.
  • Cabeçalho aparece em todas as páginas; rodapé tem número da página e data de emissão.
  • Nenhuma coluna numérica é truncada; o teste falha se um valor não couber.
  • O índice de paginação reinicia em BeginPrint.
  • O relatório foi testado com pelo menos três páginas, na pré-visualização e em papel.
  • Nada depende de cor para ter significado.

Imprima agora o relatório mais longo do seu sistema: a última coluna aparece inteira na segunda página, com o cabeçalho em cima?

Avalie este artigo

Referência: http://www.macoratti.net/08/04/vbn8_imp.htm