André Alves de Lima

Talking about Software Development and more…

Imprimindo direto na impressora com o Report Viewer

Em algumas situações, faz mais sentido imprimirmos informações direto na impressora, ao invés de exibirmos uma janela de pré-visualização para o usuário. Esse tipo de otimização é muito importante quando o usuário precisa de muita rapidez na operação. Uns tempos atrás eu mostrei como imprimir informações direto na impressora através da classe PrintDocument. Em uma outra oportunidade, eu mostrei como exportar relatórios do Report Viewer. Mas, até hoje eu ainda não mostrei como imprimir relatórios direto na impressora com o Report Viewer. É justamente isso que eu vou mostrar para você neste vídeo:

Referências

O conteúdo desse vídeo / artigo foi baseado no código apresentado pelo Brian Hartman no blog dele em 2009. Além disso, eu tomei também como base essa entrada na documentação do MSDN, que mostra um passo a passo simplificado para atingirmos esse objetivo. Misturando esses dois códigos, eu criei uma terceira versão mais simplificada, que funcionou perfeitamente nos testes que eu fiz e espero que funcione também com relatórios da sua aplicação.

Para imprimirmos relatórios direto na impressora com o Report Viewer, nós temos que dividir a operação em dois passos: primeiro exportamos os relatórios em memória (no formato “imagem“), depois imprimimos cada uma das imagens em uma página separada (utilizando a classe PrintDocument).

Dito isso, o “esqueleto” do nosso código de impressão direta ficaria assim:

        // C#
        private DataSetRelatorio CriarDataSet()
        {
            var ds = new DataSetRelatorio();

            for (int c = 1; c <= 200; c++)
            {
                ds.Item.AddItemRow(c, string.Format("Item {0}", c));
            }

            return ds;
        }

        private void btImprimir_Click(object sender, EventArgs e)
        {
            using (var ds = CriarDataSet())
            using (var relatorio = new Microsoft.Reporting.WinForms.LocalReport())
            {
                relatorio.ReportPath = "Relatorio.rdlc";
                relatorio.DataSources.Add(new Microsoft.Reporting.WinForms.ReportDataSource("DataSetRelatorio", (DataTable)ds.Item));

                Exportar(relatorio);
                Imprimir(relatorio);
            }
        }
    ' VB.NET
    Private Function CriarDataSet() As DataSetRelatorio
        Dim Ds = New DataSetRelatorio()

        For C As Integer = 1 To 200
            Ds.Item.AddItemRow(C, String.Format("Item {0}", C))
        Next

        Return Ds
    End Function

    Private Sub btImprimir_Click(sender As Object, e As EventArgs) Handles btImprimir.Click
        Using ds = CriarDataSet()
            Using Relatorio = New Microsoft.Reporting.WinForms.LocalReport()
                Relatorio.ReportPath = "Relatorio.rdlc"
                Relatorio.DataSources.Add(New Microsoft.Reporting.WinForms.ReportDataSource("DataSetRelatorio", DirectCast(ds.Item, DataTable)))

                Exportar(Relatorio)
                Imprimir(Relatorio)
            End Using
        End Using
    End Sub

Note que temos um método que retorna um DataSet para o nosso relatório. Nesse exemplo, esse método está simplesmente gerando algumas linhas em uma DataTable, que será enviada para o relatório. Obviamente, você deverá substituir esse método com o carregamento correto dos dados para o seu relatório, dependendo de como você estiver trabalhando com os dados na sua aplicação.

Exportando as páginas em memória

Como mencionei anteriormente, o primeiro passo na impressão direta do Report Viewer é a exportação das páginas em memória. A ideia nessa etapa é gerarmos uma MemoryStream para cada página do nosso relatório, sendo que cada uma dessas Streams armazenará a imagem de uma página que deverá ser impressa.

Para implementarmos essa funcionalidade, utilizaremos o método “Render” do nosso relatório. Esse é o método que utilizamos para exportarmos o relatório nos mais diversos formatos suportados, como demonstrei neste outro artigo.

Uma das sobrecargas do método “Render” recebe um call-back de geração de Streams (CreateStreamCallback). Com essa sobrecarga, nós teremos as imagens armazenadas em Streams. Para ficar mais fácil de explicar, vamos ver como é que fica o código:

        // C#
        private void Exportar(Microsoft.Reporting.WinForms.LocalReport relatorio)
        {
            Microsoft.Reporting.WinForms.Warning[] warnings;
            LimparStreams();
            relatorio.Render("image", CriarDeviceInfo(relatorio), CreateStreamCallback, out warnings);
        }

        private List<System.IO.Stream> _streams = new List<System.IO.Stream>();
        public System.IO.Stream CreateStreamCallback(string name, string extension, Encoding encoding, string mimeType, bool willSeek)
        {
            var stream = new System.IO.MemoryStream();
            _streams.Add(stream);
            return stream;
        }

        private void LimparStreams()
        {
            foreach (var stream in _streams)
            {
                stream.Dispose();
            }
            _streams.Clear();
        }
    ' VB.NET
    Private Sub Exportar(Relatorio As Microsoft.Reporting.WinForms.LocalReport)
        Dim Warnings As Microsoft.Reporting.WinForms.Warning()
        LimparStreams()
        Relatorio.Render("image", CriarDeviceInfo(Relatorio), AddressOf CreateStreamCallback, Warnings)
    End Sub

    Private Streams As New List(Of System.IO.Stream)()
    Public Function CreateStreamCallback(Name As String, Extension As String, Encoding As System.Text.Encoding, MimeType As String, WillSeek As Boolean) As System.IO.Stream
        Dim Stream = New System.IO.MemoryStream()
        Streams.Add(Stream)
        Return Stream
    End Function

    Private Sub LimparStreams()
        For Each Stream In Streams
            Stream.Dispose()
        Next
        Streams.Clear()
    End Sub

Note que essa sobrecarga do método “Render” recebe o formato que queremos utilizar (nesse caso, “image“), uma string com um “device info” (que eu explicarei logo mais), um CreateStreamCallback e um array de Warnings (não utilizado no nosso exemplo). A ideia é que, para cada página do relatório, o Report Viewer chamará o método de call-back, que deverá retornar uma Stream onde a página atual deverá ser armazenada. No nosso caso, nós criamos e retornamos uma MemoryStream, mantendo a referência na nossa lista interna.

Um detalhe importante é que, para cada chamada do método “Imprimir“, nós temos que limpar a nossa lista de Streams (com o método “LimparStreams“), senão a lista interna acumulará as Streams que tiverem sido geradas em chamadas anteriores desse método.

Gerando o DeviceInfo

Como você pode perceber, um dos parâmetros do método “Render” é o “DeviceInfo“. E o que seria isso? O parâmetro “DeviceInfo” é um XML contendo as informações dos tamanhos de página e margens que devem ser consideradas na exportação. Veja um exemplo desse XML na documentação do MSDN:

Não sei se você sabe, mas os arquivos rdlc nada mais são que arquivos XML com a definição do relatório. Dentro do XML do arquivo rdlc, nós encontramos as informações de página e margem, veja só:

Isso quer dizer que nós podemos ler as propriedades de tamanho de página e margem do nosso relatório (utilizando o método “GetDefaultPageSettings“) para gerarmos o XML do “DeviceInfo” dinamicamente. O código para fazer essa geração dinâmica ficaria assim:

        // C#
        private string CriarDeviceInfo(Microsoft.Reporting.WinForms.LocalReport relatorio)
        {
            var pageSettings = relatorio.GetDefaultPageSettings();
            return string.Format(
                System.Globalization.CultureInfo.InvariantCulture,
                @"<DeviceInfo>
                    <OutputFormat>EMF</OutputFormat>
                    <PageWidth>{0}in</PageWidth>
                    <PageHeight>{1}in</PageHeight>
                    <MarginTop>{2}in</MarginTop>
                    <MarginLeft>{3}in</MarginLeft>
                    <MarginRight>{4}in</MarginRight>
                    <MarginBottom>{5}in</MarginBottom>
                </DeviceInfo>",
                pageSettings.PaperSize.Width / 100m, pageSettings.PaperSize.Height / 100m, pageSettings.Margins.Top / 100m, pageSettings.Margins.Left / 100m, pageSettings.Margins.Right / 100m, pageSettings.Margins.Bottom / 100m);
        }
    ' VB.NET
    Private Function CriarDeviceInfo(Relatorio As Microsoft.Reporting.WinForms.LocalReport) As String
        Dim PageSettings = Relatorio.GetDefaultPageSettings()
        Return String.Format(
            System.Globalization.CultureInfo.InvariantCulture,
            "<DeviceInfo><OutputFormat>EMF</OutputFormat><PageWidth>{0}in</PageWidth><PageHeight>{1}in</PageHeight><MarginTop>{2}in</MarginTop><MarginLeft>{3}in</MarginLeft><MarginRight>{4}in</MarginRight><MarginBottom>{5}in</MarginBottom></DeviceInfo>",
            PageSettings.PaperSize.Width / 100D, PageSettings.PaperSize.Height / 100D, PageSettings.Margins.Top / 100D, PageSettings.Margins.Left / 100D,
            PageSettings.Margins.Right / 100D, PageSettings.Margins.Bottom / 100D)
    End Function

Imprimindo as imagens geradas em memória

Uma vez geradas as Streams contendo as páginas do nosso relatório em memória, chegou a hora de efetivamente imprimirmos essas Streams. Para isso, nós utilizaremos um PrintDocument, que é uma classe disponível no namespace System.Drawing.Printing que implementa a possibilidade de imprimirmos informações direto na impressora (nesse caso, as imagens representando as páginas do nosso relatório).

O código para fazermos a impressão não é muito complicado. Primeiramente, temos que configurar mais uma vez as informações de página e margens do PrintDocument, copiando essas informações do relatório. Em seguida, nós configuramos o evento “PrintPage” do nosso PrintDocument e chamamos o método “Print” para iniciarmos a impressão.

Dentro da implementação do evento “PrintPage“, nós temos que imprimir a informação de cada uma das páginas do nosso relatório. Esse método será chamado para cada página que deverá ser impressa, por isso, temos que utilizar uma variável de controle para sabermos qual página estamos imprimindo no ciclo atual.

Para imprimirmos a Stream que foi gerada anteriormente, nós utilizamos um objeto do tipo “Metafile“, que é o tipo mais genérico de imagem. Como o construtor dessa classe recebe uma Stream, nós simplesmente passamos a Stream que foi exportada anteriormente e tudo deve funcionar perfeitamente. Nós só não podemos esquecer de rebobinar a Stream (utilizando o método “Seek“), senão a impressão sairá em branco!

Por fim, a última operação dentro do evento “PrintPage” será a configuração da propriedade “HasMorePages“. É através dessa propriedade que o PrintDocument saberá se o evento “PrintPage” deverá ser chamado mais uma vez (para imprimir a próxima página) ou não.

O código final para fazermos a impressão das Streams é o seguinte:

        // C#
        private void Imprimir(Microsoft.Reporting.WinForms.LocalReport relatorio)
        {
            using (var pd = new System.Drawing.Printing.PrintDocument())
            {
                pd.PrinterSettings.PrinterName = "PrimoPDF";
                var pageSettings = new System.Drawing.Printing.PageSettings();
                var pageSettingsRelatorio = relatorio.GetDefaultPageSettings();
                pageSettings.PaperSize = pageSettingsRelatorio.PaperSize;
                pageSettings.Margins = pageSettingsRelatorio.Margins;
                pd.DefaultPageSettings = pageSettings;

                pd.PrintPage += Pd_PrintPage;
                _streamAtual = 0;
                pd.Print();
            }
        }

        private int _streamAtual;
        private void Pd_PrintPage(object sender, System.Drawing.Printing.PrintPageEventArgs e)
        {
            var stream = _streams[_streamAtual];
            stream.Seek(0, System.IO.SeekOrigin.Begin);

            using (var metadata = new System.Drawing.Imaging.Metafile(stream))
            {
                e.Graphics.DrawImage(metadata, e.PageBounds);
            }

            _streamAtual++;
            e.HasMorePages = _streamAtual < _streams.Count;
        }
    ' VB.NET
    Private Sub Imprimir(Relatorio As Microsoft.Reporting.WinForms.LocalReport)
        Using Pd = New System.Drawing.Printing.PrintDocument()
            Pd.PrinterSettings.PrinterName = "PrimoPDF"
            Dim PageSettings = New System.Drawing.Printing.PageSettings()
            Dim PageSettingsRelatorio = Relatorio.GetDefaultPageSettings()
            PageSettings.PaperSize = PageSettingsRelatorio.PaperSize
            PageSettings.Margins = PageSettingsRelatorio.Margins
            Pd.DefaultPageSettings = PageSettings

            AddHandler Pd.PrintPage, AddressOf Pd_PrintPage
            StreamAtual = 0
            Pd.Print()
        End Using
    End Sub

    Private StreamAtual As Integer
    Private Sub Pd_PrintPage(sender As Object, e As System.Drawing.Printing.PrintPageEventArgs)
        Dim Stream = Streams(StreamAtual)
        Stream.Seek(0, System.IO.SeekOrigin.Begin)

        Using metadata = New System.Drawing.Imaging.Metafile(Stream)
            e.Graphics.DrawImage(metadata, e.PageBounds)
        End Using

        StreamAtual += 1
        e.HasMorePages = StreamAtual < Streams.Count
    End Sub

Utilizando a classe ReportPrintDocument

Se você achou todo esse código muito complicado, não se preocupe. Como eu mencionei no início do artigo, o Brian Hartman implementou uma classe que encapsula toda essa parafernalha que eu mostrei até agora. Para utilizá-la, basta baixar o arquivo correspondente a adicioná-lo no seu projeto. Você pode baixar a versão em C# aqui ou a versão em VB.NET aqui, e depois você pode adicionar no seu projeto utilizando a opção “Add -> Existing Item“:

Uma vez que você tiver adicionado essa classe no projeto, a utilização é muito simples. Basta criarmos uma instância da classe “ReportPrintDocument” (que fica no namespace “PrintReportSample“) passando o relatório e, em seguida, chamamos o método “Print“:

        // C#
        private void btImprimir_Click(object sender, EventArgs e)
        {
            using (var ds = CriarDataSet())
            using (var relatorio = new Microsoft.Reporting.WinForms.LocalReport())
            {
                relatorio.ReportPath = "Relatorio.rdlc";
                relatorio.DataSources.Add(new Microsoft.Reporting.WinForms.ReportDataSource("DataSetRelatorio", (DataTable)ds.Item));

                using (var rpd = new PrintReportSample.ReportPrintDocument(relatorio))
                {
                    rpd.Print();
                }
            }
        }
    ' VB.NET
    Private Sub btImprimir_Click(sender As Object, e As EventArgs) Handles btImprimir.Click
        Using ds = CriarDataSet()
            Using Relatorio = New Microsoft.Reporting.WinForms.LocalReport()
                Relatorio.ReportPath = "Relatorio.rdlc"
                Relatorio.DataSources.Add(New Microsoft.Reporting.WinForms.ReportDataSource("DataSetRelatorio", DirectCast(ds.Item, DataTable)))

                Using Rpd = New PrintReportSample.ReportPrintDocument(Relatorio)
                    Rpd.Print()
                End Using
            End Using
        End Using
    End Sub

Bem mais simples, não é mesmo? Porém, é muito importante sabermos o que está por trás de todo o código implementado nessa classe. Dessa forma, caso algo não funcione 100% corretamente para algum relatório específico, fica mais fácil de procurarmos a origem do erro. Foi justamente por isso que eu decidi mostrar primeiramente um passo a passo de como podemos fazer essa impressão de forma manual, sem utilizar essa classe utilitária do Brian Hartman.

Baixe o projeto de exemplo

Para baixar o projeto de exemplo desse artigo, assine a minha newsletter. Ao fazer isso, além de ter acesso ao projeto, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário no final do artigo.

Concluindo

O processo de impressão direta de relatórios do Report Viewer não é nem tão simples, nem tão complicado. Como nós não temos um método que implementa esse tipo de impressão nativamente, nós precisamos primeiramente exportar as páginas do nosso relatório em memória e, em seguida, imprimimos cada página utilizando um PrintDocument.

Como você pode observar, se você não estiver a fim de aprender o que está por trás de toda essa implementação, não tem problema. Basta baixar e utilizar a classe “ReportPrintDocument” implementada pelo Brian Hartman, que eu disponibilizei tanto em C# (original do Brian Hartman) e VB.NET (que eu converti por conta própria).

E você, já precisou imprimir relatórios do Report Viewer direto na impressora? Você já tinha conhecimento dessa classe do Brian Hartman ou você fez a implementação “na mão” utilizando o tutorial do MSDN? No final das contas deu tudo certo ou teve algum relatório em que a impressão direta não funcionou? Fico aguardando os seus comentários logo abaixo!

Até a próxima!

André Lima

Newsletter do André Lima

* indicates required



Powered by MailChimp

6 thoughts on “Imprimindo direto na impressora com o Report Viewer

  • Elcio Esteves disse:

    Muito interessante este post, vai me ajudar em um monte de casos. Aproveitando para te fazer uma pergunta, pois estou com um grave problema aqui. Há um tempo atrás me ajudou muito um dos seus post e até conversamos trocando informações. Aqui na empresa tínhamos vários relatórios que estavam com página A3 e quando imprimíamos em impressora, o documento era reduzido e impresso em folha A4 automático sem cortar nada, mas de um tempo para cá não tem acontecido isso os documentos só são reduzidos agora quando impressos em PDF e depois enviados a impressora. Sabe o que pode ter sido alterado para isso não acontecer mais?

    • andrealveslima disse:

      Olá Elcio!

      Infelizmente, não faço ideia.. Não teve nenhuma alteração de hardware? A impressora é a mesma, os drivers são os mesmos? Se vocês não alteraram nada na aplicação, acredito que só possa ser algo no ambiente mesmo..

      Abraço!
      André Lima

  • Airton Barros disse:

    André,
    Sempre um bom trabalho.

  • Diego disse:

    Olá Andre muto bom o vídeo e de muita utilidade, gostaria de baixar o projeto como devo proceder? Ja tenho assinatura da página

    • andrealveslima disse:

      Olá Diego! Muito obrigado pelo comentário, fico feliz que você tenha gostado.. :)

      Mandei o link para baixar os projetos de exemplo no seu email.. Qualquer coisa é só entrar em contato..

      Abraço!
      André Lima

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *