Esses dias atrás eu recebi um comentário no meu artigo sobre impressão direta com C# perguntando se seria possível imprimirmos um formulário no Windows Forms. Eu sabia que isso era possível, então, dei uma pesquisada rápida e encaminhei este link para ele. Porém, como eu já tinha recebido esse tipo de pergunta mais de uma vez (além das diversas vezes que já me deparei com essa dúvida nos fóruns da MSDN), percebi que essa era uma demanda recorrente. Então, eu pensei: que tal escrever um artigo sobre essa funcionalidade? Quer conferir o resultado? Então, vamos lá!
Capturando uma imagem do formulário
Imprimir o formulário no Windows Forms não é uma tarefa nada difícil, principalmente se pudermos considerar a borda na do formulário. Um simples objeto do tipo Graphics em conjunto com a chamada do seu método CopyFromScreen atende facilmente essa demanda. Esse método faz a captura de uma determinada área da tela. Dessa forma, se nós simplesmente passarmos as coordenadas do formulário, teremos o resultado esperado. Vamos ver como podemos fazer isso?
Primeiramente, vamos criar um novo projeto do tipo “Windows Forms Application“. Dentro do formulário, vamos adicionar um botão (“imprimirButton“) e um componente do tipo PrintDocument (“printDocument“):
Em seguida, no evento “Click” do botão, vamos chamar o método “Print” do nosso PrintDocument:
// C# private void imprimirButton_Click(object sender, EventArgs e) { printDocument.Print(); }
' VB.NET Private Sub ImprimirButton_Click(sender As Object, e As EventArgs) Handles ImprimirButton.Click PrintDocument.Print() End Sub
A definição do que será impresso pelo componente PrintDocument é feita através do seu evento “PrintPage“. Para implementarmos o código nesse evento, vamos até o designer do formulário, clicamos no componente PrintDocument e, na janela de propriedades, nós temos que encontrar o evento “PrintPage” e, em seguida, clicamos duas vezes para criarmos um novo manipulador para esse evento:
Dentro desse evento, vamos capturar o desktop, limitando para a área onde o formulário está localizado:
// C# private void printDocument_PrintPage(object sender, System.Drawing.Printing.PrintPageEventArgs e) { var image = new Bitmap(this.Width, this.Height); var graphics = Graphics.FromImage(image); graphics.CopyFromScreen(this.Location.X, this.Location.Y, 0, 0, this.Size); e.Graphics.DrawImage(image, 20, 20); }
' VB.NET Private Sub PrintDocument_PrintPage(sender As Object, e As Printing.PrintPageEventArgs) Handles PrintDocument.PrintPage Dim Image = New Bitmap(Me.Width, Me.Height) Dim graphics = System.Drawing.Graphics.FromImage(Image) graphics.CopyFromScreen(Me.Location.X, Me.Location.Y, 0, 0, Me.Size) e.Graphics.DrawImage(Image, 20, 20) End Sub
Nota: a imagem do formulário será impressa na coordenada 20,20 da página. Se você quiser imprimir o formulário em uma outra coordenada, basta alterar os dois últimos valores dos parâmetros do método “DrawImage”.
Agora execute a aplicação e clique no botão “Imprimir“. A impressão será feita na impressora “padrão“. Para alterar a impressora, você pode configurar o nome da impressora na propriedade “PrinterSettings.PrinterName” (do componente PrintDocument) ou implementar uma funcionalidade onde o usuário possa escolher qual impressora utilizar, como eu mostrei no meu artigo sobre impressão direta na impressora. O resultado da impressão será parecido com a imagem abaixo:
Notou o problema? O método “CopyFromScreen” realmente copia tudo o que está sendo exibido na tela no momento da sua chamada, inclusive diálogos que possam estar por cima do formulário (como foi o caso do diálogo de impressão que foi capturado no nosso exemplo). Para que o diálogo não seja capturado na impressão, temos que chamar o método “CopyFromScreen” antes de chamarmos o método “Print” do “PrintDialog“, armazenando o resultado em uma variável no nível do formulário, que posteriormente será utilizada dentro do evento “PrintPage“.
Dito isso, a primeira coisa que temos que fazer é extrairmos a captura em um método (que daremos o nome de “CapturarForm“):
// C# Bitmap captura = null; private void CapturarForm() { captura = new Bitmap(this.Width, this.Height); var graphics = Graphics.FromImage(captura); graphics.CopyFromScreen(this.Location.X, this.Location.Y, 0, 0, this.Size); }
' VB.NET Private Captura As Bitmap Private Sub CapturarForm() Captura = New Bitmap(Me.Width, Me.Height) Dim graphics = System.Drawing.Graphics.FromImage(Captura) graphics.CopyFromScreen(Me.Location.X, Me.Location.Y, 0, 0, Me.Size) End Sub
Em seguida, vamos ajustar o código que faz a impressão, de forma que esse novo método seja chamado antes do “Print” do “PrintDocument” e que o atributo “Captura” seja utilizado dentro do evento “PrintPage“:
// C# private void imprimirButton_Click(object sender, EventArgs e) { CapturarForm(); printDocument.Print(); } private void printDocument_PrintPage(object sender, System.Drawing.Printing.PrintPageEventArgs e) { e.Graphics.DrawImage(captura, 20, 20); }
' VB.NET Private Sub ImprimirButton_Click(sender As Object, e As EventArgs) Handles ImprimirButton.Click CapturarForm() PrintDocument.Print() End Sub Private Sub PrintDocument_PrintPage(sender As Object, e As Printing.PrintPageEventArgs) Handles PrintDocument.PrintPage e.Graphics.DrawImage(Captura, 20, 20) End Sub
Pronto! Veja só o resultado na imagem a seguir:
Como você pode perceber, o diálogo de impressão não é mais capturado na imagem. Porém, você viu que ainda temos um outro problema para resolver? No Windows 10, temos esse efeito de transparência ao redor da janela, que faz com que o conteúdo que estiver embaixo do formulário seja impresso também.
Desconsiderando o efeito de transparência do Windows 10
Depois de procurar bastante, acabei encontrando duas threads no StackOverflow mostrando como pegar as coordenadas do formulário desconsiderando o efeito de transparência da borda. A solução consiste em chamar um método da dll “dwmapi“, que retornará as coordenadas verdadeiras da janela.
Para consertarmos esse problema, vamos criar uma nova classe no nosso projeto, a qual daremos o nome de “WindowHelper“. O conteúdo dessa classe deverá ser o seguinte:
// C# public static class WindowHelper { [Serializable, System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] public struct Rect { public int Left; public int Top; public int Right; public int Bottom; public System.Drawing.Rectangle ToRectangle() { return System.Drawing.Rectangle.FromLTRB(Left, Top, Right, Bottom); } } [System.Runtime.InteropServices.DllImport(@"dwmapi.dll")] public static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out Rect pvAttribute, int cbAttribute); public enum Dwmwindowattribute { DwmwaExtendedFrameBounds = 9 } }
' VB.NET Public Module WindowHelper <Serializable, System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)> Public Structure Rect Public Left As Integer Public Top As Integer Public Right As Integer Public Bottom As Integer Public Function ToRectangle() As System.Drawing.Rectangle Return System.Drawing.Rectangle.FromLTRB(Left, Top, Right, Bottom) End Function End Structure <System.Runtime.InteropServices.DllImport("dwmapi.dll")> Public Function DwmGetWindowAttribute(hwnd As IntPtr, dwAttribute As Integer, ByRef pvAttribute As Rect, cbAttribute As Integer) As Integer End Function Public Enum Dwmwindowattribute DwmwaExtendedFrameBounds = 9 End Enum End Module
Em seguida, temos que ajustar o nosso método que faz a captura do formulário, de forma que ele utilize os valores retornados por essa nova classe que acabamos de criar:
// C# private void CapturarForm() { WindowHelper.Rect rect; WindowHelper.DwmGetWindowAttribute(this.Handle, (int)WindowHelper.Dwmwindowattribute.DwmwaExtendedFrameBounds, out rect, System.Runtime.InteropServices.Marshal.SizeOf(typeof(WindowHelper.Rect))); var rectangle = rect.ToRectangle(); captura = new Bitmap(rectangle.Width, rectangle.Height); var graphics = Graphics.FromImage(captura); graphics.CopyFromScreen(rectangle.Left, rectangle.Top, 0, 0, rectangle.Size); }
' VB.NET Private Sub CapturarForm() Dim Rect As WindowHelper.Rect WindowHelper.DwmGetWindowAttribute(Me.Handle, CInt(WindowHelper.Dwmwindowattribute.DwmwaExtendedFrameBounds), Rect, System.Runtime.InteropServices.Marshal.SizeOf(GetType(WindowHelper.Rect))) Dim Rectangle = Rect.ToRectangle() Captura = New Bitmap(Rectangle.Width, Rectangle.Height) Dim Graphics = System.Drawing.Graphics.FromImage(Captura) Graphics.CopyFromScreen(Rectangle.Left, Rectangle.Top, 0, 0, Rectangle.Size) End Sub
Execute a aplicação, clique no botão “Imprimir” e veja o novo resultado:
Agora sim, hein?
Imprimindo o formulário sem a borda
Em algumas situações, pode ser que precisemos imprimir somente o conteúdo interno do formulário, ou seja, desconsiderando totalmente as bordas. Pesquisei um pouco sobre esse tema, encontrei algumas possibilidades para calcularmos as coordenadas da área interna do formulário, mas, todas as opções me pareceram muito “gambiarra“.
Uma alternativa bem simples nesse caso seria, antes de realizarmos a captura, nós alterarmos o estilo da janela, removendo as bordas através da propriedade FormBorderStyle. Depois da captura, nós restauramos o estilo de borda anterior. Eu sei que essa solução não é das mais bonitas, mas, eu acho que às vezes a solução mais “feia” acaba sendo a mais recomendada. Imagina se nós utilizamos um método de calcular as coordenadas internas do formulário e na próxima versão do Windows esse método para de funcionar? Melhor evitar essa situação, não é mesmo?
Dito isso, para capturarmos o formulário sem as bordas, nós poderíamos fazer o seguinte ajuste no método “CapturarForm“:
// C# private void CapturarForm() { var formBorderStyleAnterior = this.FormBorderStyle; try { this.FormBorderStyle = FormBorderStyle.None; WindowHelper.Rect rect; WindowHelper.DwmGetWindowAttribute(this.Handle, (int)WindowHelper.Dwmwindowattribute.DwmwaExtendedFrameBounds, out rect, System.Runtime.InteropServices.Marshal.SizeOf(typeof(WindowHelper.Rect))); var rectangle = rect.ToRectangle(); captura = new Bitmap(rectangle.Width, rectangle.Height); var graphics = Graphics.FromImage(captura); graphics.CopyFromScreen(rectangle.Left, rectangle.Top, 0, 0, rectangle.Size); } finally { this.FormBorderStyle = formBorderStyleAnterior; } }
' VB.NET Private Sub CapturarForm() Dim FormBorderStyleAnterior = Me.FormBorderStyle Try Me.FormBorderStyle = FormBorderStyle.None Dim Rect As WindowHelper.Rect WindowHelper.DwmGetWindowAttribute(Me.Handle, CInt(WindowHelper.Dwmwindowattribute.DwmwaExtendedFrameBounds), Rect, System.Runtime.InteropServices.Marshal.SizeOf(GetType(WindowHelper.Rect))) Dim Rectangle = Rect.ToRectangle() Captura = New Bitmap(Rectangle.Width, Rectangle.Height) Dim Graphics = System.Drawing.Graphics.FromImage(Captura) Graphics.CopyFromScreen(Rectangle.Left, Rectangle.Top, 0, 0, Rectangle.Size) Finally Me.FormBorderStyle = FormBorderStyleAnterior End Try End Sub
Nota: o bloco try-finally assegura que o FormBorderStyle anterior seja sempre restaurado, independentemente se uma exceção tiver sido disparada durante o processo de impressão.
Veja só o resultado da impressão após essas alterações:
Imprimindo uma área específica do formulário
Outra demanda que pode surgir é a necessidade de imprimirmos somente uma área específica do formulário, e não o formulário inteiro. Isso é muito fácil de ser resolvido. Basta ajustarmos os valores de tamanho e coordenadas utilizados no método “CapturarForm“.
Por exemplo, se quisermos imprimir uma área de 200 x 200 pixels do nosso formulário, começando na coordenada 15, 15, o código do método “CapturarForm” ficaria assim:
// C# private void CapturarForm() { captura = new Bitmap(200, 200); var graphics = Graphics.FromImage(captura); graphics.CopyFromScreen(this.Location.X + 15, this.Location.Y + 15, 0, 0, new Size(200, 200)); }
' VB.NET Private Sub CapturarForm() Captura = New Bitmap(200, 200) Dim Graphics = System.Drawing.Graphics.FromImage(Captura) Graphics.CopyFromScreen(Me.Location.X + 15, Me.Location.Y + 15, 0, 0, New Size(200, 200)) End Sub
E este seria o resultado:
Concluindo
Apesar de não ser tão complicado imprimir um formulário no Windows Forms, existem alguns detalhezinhos que podem acabar nos deixando com o cabelo em pé. Principalmente o efeito de transparência nas bordas dos formulários do Windows 10, que pode se tornar um pesadelo nesse processo.
No artigo de hoje você conferiu como imprimir um formulário no Windows Forms e como lidar com o problema do efeito na borda do Windows 10. Além disso, vimos também como imprimir somente o conteúdo interno do formulário (desconsiderando as bordas) e como imprimir somente uma área específica do formulário.
E você? Já precisou imprimir um formulário no Windows Forms? Também passou por esses problemas que eu mencionei nesse artigo? Como é que você conseguiu resolver essa demanda? Fico aguardando os seus comentários!
Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, 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 logo abaixo.
Até a próxima!
André Lima
Image by Pixabay used under Creative Commons
https://pixabay.com/en/menu-gui-interface-template-ui-303121/
https://pixabay.com/en/inkjet-print-printer-peripheral-147674/
The post Como imprimir um formulário no Windows Forms com C# e VB.NET? appeared first on André Alves de Lima.