jueves, 8 de enero de 2009

Imprimir el contenido de un DataGridView con PrintDocument

Igual alguna vez os habéis encontrado en la situación de querer imprimir el contenido de un DataGridView y poder presentárselos a alguien o tenerlos a mano sin necesidad de tener la aplicación en pantalla.

Está claro que hay formas muy sencillas de hacerlo, por ejemplo ese botón “Imprimir pantalla” que hay en el teclado y que mucha gente no sabe para qué sirve, crear informes con Crystal Reports u otro creador de informes, apuntarlos a mano en un papel, memorizarlos ...

Algunas de estas opciones nos mostrarán datos de más, otras de menos y otras dependerán de la cantidad de neuronas que nos queden disponibles en ese momento (bueno, y de las que cada uno traiga de fábrica :P) pero hay una opción bastante sencilla que nos puede sacar de más de un apuro: usar PrintDocument con el contenido del DataGridView.

Supongamos que tenemos un formulario con un DataGridView que se llama dataGridView1 (original, ¿a qué sí?) que tiene como origen de datos ... lo que sea, nos da igual porque lo que nos interesa es el contenido no si el origen de datos es un DataSet, un fichero XML o hemos metido los datos por código.

Bien, añadimos al formulario un objeto de tipo PrintDocument (está en el Cuadro de Herramientas en el apartado Impresión) con el nombre printDocument1 (para ser coherentes con la nomenclatura :P).

Ahora añadimos un botón (¿a qué no adivináis como se llama?) pues sí, button1 y en su evento click escribimos lo siguiente:


printDocument1.Print();



En el evento PrintPage de printDocument1 vamos a recorrer el contenido de dataGridView1 e imprimir una línea por cada fila del grid. Es muy importante saber que el evento se dispara por cada página que se vaya a imprimir, esto quiere decir que si tenemos 5 páginas el evento se disparará 5 veces y por tanto hay que saber en qué punto nos hemos quedado para continuar desde ahí y no volver a empezar.

// Variable a nivel de clase para recordar en qué punto nos hemos quedado
// la inicializamos a -1 para imprimir también las cabeceras de las columnas
private int i = -1;

private void printDocument1_PrintPage(object sender, System.Drawing.Printing.PrintPageEventArgs e)
{
// La fuente que vamos a usar para imprimir.
Font printFont = new Font("Arial", 10);
float topMargin = e.MarginBounds.Top;
float yPos = 0;
float linesPerPage = 0;
int count = 0;
string texto = "" ;
DataGridViewRow row;

// Calculamos el número de líneas que caben en cada página.
linesPerPage = e.MarginBounds.Height / printFont.GetHeight(e.Graphics);


// Recorremos las filas del DataGridView hasta que llegemos
// a las líneas que nos caben en cada página o al final del grid.
while (count < linesPerPage && i < this.dataGridView1.Rows.Count)
{
row = dataGridView1.Rows[i];
texto = "";

foreach (DataGridViewCell celda in row.Cells)
{
texto += "\t" + celda.Value.ToString();
}

// Calculamos la posición en la que se escribe la línea
yPos = topMargin + (count * printFont.GetHeight(e.Graphics));

// Escribimos la línea con el objeto Graphics
e.Graphics.DrawString(texto, printFont, Brushes.Black, 10, yPos);

count++;
i++;
}

// Una vez fuera del bucle comprobamos si nos quedan más filas
// por imprimir, si quedan saldrán en la siguente página
if (i < this.dataGridView1.Rows.Count)
e.HasMorePages = true;
else
{
// si llegamos al final, se establece HasMorePages a
// false para que se acabe la impresión
e.HasMorePages = false;

// Es necesario poner el contador a 0 porque, por ejemplo si se hace
// una impresión desde PrintPreviewDialog, se vuelve disparar este
// evento como si fuese la primera vez, y si i está con el valor de la
// última fila del grid no se imprime nada
i = 0;
}

}




Con esto conseguimos imprimir todos los datos que hay en un grid, incluidas las cabeceras de las columnas, pero ¿habría forma de imprimir únicamente lo que seleccionemos en el grid?

La respuesta es sí y es bastante sencillo, simplemente hay que cambiar:


texto += "\t" + celda.Value.ToString();



por este otro:


if (celda.Selected)
{
texto += "\t" + celda.Value.ToString();
}
if (String.IsNullOrEmpty(seleccion) == false)
{
yPos = topMargin + (count * printFont.GetHeight(e.Graphics));
e.Graphics.DrawString(texto, printFont, Brushes.Black, 10, yPos);
count++;
}



Con esto sólo imprimimos las celdas que se hayan seleccionado, que pueden ser, varias filas, varias columnas o celdas sueltas, y además evitamos que se impriman filas en blanco.

Advertencia: que nadie piense que con esto vamos a sacar unos informes espectaculares con las columnas perfectamente alineadas y todo precioso, NO NO NO, esto es únicamente para hacer una impresión rápida de los valores que aparecen en un DataGridView, sin muchas florituras pero también sin muchas complicaciones.


Happy codding ;)


Bola extra: PrintDocument imprime directamente por la impresora predeterminada, para cambiar la impresiona, su configuración y la configuración del papel echarle un vistazo a PrintDialog y PageSetudDialog. Y para sacar la vista previa: PrintPreviewDialog.

28 comentarios:

Anónimo dijo...

Lo tendras en VB .Net???

Pablo Bouzada dijo...

A ver si esto te vale anónimo:

' Variable a nivel de clase para recordar en qué punto nos hemos quedado
' la inicializamos a -1 para imprimir también las cabeceras de las columnasprivate
Dim i As Integer = -1
Private Sub printDocument1_PrintPage(ByVal sender As Object, ByVal e As System.Drawing.Printing.PrintPageEventArgs)
' La fuente que vamos a usar para imprimir.
Dim printFont As System.Drawing.Font = New Font("Arial", 10)
Dim topMargin As Double = e.MarginBounds.Top
Dim yPos As Double = 0
Dim linesPerPage As Double = 0
Dim count As Integer = 0
Dim texto As String = ""
Dim row As System.Windows.Forms.DataGridViewRow

' Calculamos el número de líneas que caben en cada página.
linesPerPage = e.MarginBounds.Height / printFont.GetHeight(e.Graphics)
' Recorremos las filas del DataGridView hasta que llegemos
' a las líneas que nos caben en cada página o al final del grid.
While count < linesPerPage AndAlso i < dataGridView1.Rows.Count
row = DataGridView1.Rows(i)
texto = ""
For Each celda As System.Windows.Forms.DataGridViewCell In row.Cells
texto += vbCrLf + celda.Value.ToString()
Next
' Calculamos la posición en la que se escribe la línea
yPos = topMargin + (count * printFont.GetHeight(e.Graphics))
' Escribimos la línea con el objeto Graphics
e.Graphics.DrawString(texto, printFont, System.Drawing.Brushes.Black, 10, yPos)
count += 1
i += 1
End While
' Una vez fuera del bucle comprobamos si nos quedan más filas
' por imprimir, si quedan saldrán en la siguente página
If i < DataGridView1.Rows.Count Then
e.HasMorePages = True
Else
' si llegamos al final, se establece HasMorePages a
' false para que se acabe la impresión
e.HasMorePages = False
' Es necesario poner el contador a 0 porque, por ejemplo si se hace
' una impresión desde PrintPreviewDialog, se vuelve disparar este
' evento como si fuese la primera vez, y si i está con el valor de la
' última fila del grid no se imprime nada
i = 0
End If
End Sub

Buckman dijo...

Hola, necesito exactamente esto para vb.net 2008
he probado el codigo que has puesto de vb.net y parece que funciona...digo parece porque no me imprime nada, o sea la impresora saca una pagina en blanco.
Soy un novato total y no veo donde puede estar el fallo.
He importado el System.Drawing.Graphics por si fuera eso, pero no.
Incluso en la linea donde indica lo que debe imprimir que creo que es esta:
' Escribimos la línea con el objeto Graphics
e.Graphics.DrawString(texto, printFont, System.Drawing.Brushes.Black, 10, yPos)
...
la he sustituido por esta a ver si asi imprimia algo:
' Escribimos la línea con el objeto Graphics
e.Graphics.DrawString("texto para imprimir", printFont, System.Drawing.Brushes.Black, 10, yPos)
...
en teoria deberia imprimir "texto para imprimir" ¿no?, pero no imprime nada. :(
Me puedes orientar please??

Pablo Bouzada dijo...

Buckman, quita el control PrintDocument del form y prueba con este código (debería funcionar):


Imports System
Imports System.IO
Imports System.Drawing
Imports System.Drawing.Printing
Imports System.Windows.Forms


Public Class Form1

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim pd As New PrintDocument()
AddHandler pd.PrintPage, AddressOf Me.pd_PrintPage
pd.Print()

End Sub

Private Sub pd_PrintPage(ByVal sender As Object, ByVal e As System.Drawing.Printing.PrintPageEventArgs)
' La fuente que vamos a usar para imprimir.
Dim printFont As System.Drawing.Font = New Font("Arial", 10)
Dim topMargin As Single = e.MarginBounds.Top
Dim yPos As Single = 0
Dim linesPerPage As Single = 0
Dim count As Integer = 0
Dim texto As String = ""

' Calculamos el número de líneas que caben en cada página.
linesPerPage = e.MarginBounds.Height / printFont.GetHeight(e.Graphics)

texto = "TEXTO A IMPRIMIR"

' Calculamos la posición en la que se escribe la línea
yPos = topMargin + (count * printFont.GetHeight(e.Graphics))
' Escribimos la línea con el objeto Graphics
e.Graphics.DrawString(texto, printFont, System.Drawing.Brushes.Black, 10, yPos)

texto = Nothing

If texto Is Nothing Then e.HasMorePages = False

End Sub
End Class

Buckman dijo...

Hola de nuevo Pablo, ahora si me imprime lo de "TEXTO A IMPRIMIR", pero lo que yo queria era imprimir tambien el datagridview entre otras cosas, asi que lo he intentado adaptar a lo que necesito.
Lo he conseguido a medias. Me imprime bien la cabecera que yo quiero y luego me imprime el datagridview, pero me fone todos los datos en 1 columna. Me explico, el datagrid muestra estos datos:
Referencia Descripcion Precio
1010 Pañuelo 5,00 €

Bien, a la impresion me lo manda sin el nombre de la columna (que no importa porque lo puedo poner como la cabecera) y en este formato:
1010
Pañuelo
5,00 €

Como ves me lo pone todo en una columna y no en una fila, y si el datagridview tiene mas de una fila me lo sobreescribe todo en la primera columna.

Te mando como me ha quedado el codigo:
Private Sub pd_PrintPage(ByVal sender As Object, ByVal e As System.Drawing.Printing.PrintPageEventArgs)
' La fuente que vamos a usar para imprimir.
Dim printFont As System.Drawing.Font = New Font("Arial", 10)
Dim topMargin As Single = e.MarginBounds.Top
Dim yPos As Single = 0
Dim linesPerPage As Double = 0
Dim count As Integer = 0
Dim texto As String = ""
Dim i As Integer ' = -1

Dim row As System.Windows.Forms.DataGridViewRow
' Calculamos el número de líneas que caben en cada página.
linesPerPage = e.MarginBounds.Height / printFont.GetHeight(e.Graphics)

'AQUI YO AÑADO OTRAS COSAS A LA IMPRESION COMO CABECERA
e.Graphics.DrawLine(Pens.Black, 20, 20, 600, 20)
e.Graphics.DrawLine(Pens.Black, 20, 50, 600, 50)
'HASTA AQUI LA CABECERA
'
' Recorremos las filas del DataGridView hasta que llegemos
' a las líneas que nos caben en cada página o al final del grid.
While count < linesPerPage AndAlso i < DataGridView1.Rows.Count
row = DataGridView1.Rows(i)
texto = ""
For Each celda As System.Windows.Forms.DataGridViewCell In row.Cells
texto += vbCrLf + celda.Value.ToString()
Next
' Calculamos la posición en la que se escribe la línea
yPos = topMargin + (count * printFont.GetHeight(e.Graphics))
' Escribimos la línea con el objeto Graphics
e.Graphics.DrawString(texto, printFont, System.Drawing.Brushes.Black, 10, yPos)
count += 1
i += 1
End While

' Una vez fuera del bucle comprobamos si nos quedan más filas
' por imprimir, si quedan saldrán en la siguente página
If i < DataGridView1.Rows.Count Then
e.HasMorePages = True
Else
' si llegamos al final, se establece HasMorePages a
' false para que se acabe la impresión
e.HasMorePages = False
' Es necesario poner el contador a 0 porque, por ejemplo si se hace
' una impresión desde PrintPreviewDialog, se vuelve disparar este
' evento como si fuese la primera vez, y si i está con el valor de la
' última fila del grid no se imprime nada
i = 0
End If



'texto = Nothing

'If texto Is Nothing Then e.HasMorePages = False

End Sub

Gracias por tu inestimable ayuda y perdona por hacerte perder el tiempo.

Pablo Bouzada dijo...

Buckman, cada vez que pones ""vbCrLf"" hace un retorno de carro y pasa a la siguiente línea, creo que con eso ya lo tienes, ¿no?

Buckman dijo...

Requetemuchisimas gracias, no habia caido con el vbcrlf, ya te habia dicho que era un novato total (pero me tiene enganchado esto).
Al final he sustituido la linea siguiente:
texto += vbCrLf + celda.Value.ToString()
por esta otra:
texto += vbTab & celda.Value.ToString()

Simplemente cambio vbCrLf por vbTab, cambio el retorno de carro por una tabulacion.

Muchas gracias por tu ayuda
Tema zanjado.

Pablo Bouzada dijo...

De nada hombre ;)

Anónimo dijo...

hola como estas disculapa q te moleste pero tengo un problema al imprimir desde mi data grid view no me imprime las cabceras, inicialice en menos uno como lo hiciste tu pero me sale un error d outrangeexception, lo inicialiso en 0 y recien me imprime pero sin las cabeceras no se si me podrias ayudar

Pablo Bouzada dijo...

Si es el mismo código debería funcionar, mándame tu código y le echo un vistazo ;)

Anónimo dijo...

me aparece este error en la linea nueva que agregaste buckman,
"texto += vbTab & celda.Value.ToString()"

me avienta este error:

Referencia a objeto no establecida como instancia de un objeto.

Espero y me puedas ayudar lo mas antes posible amigo.

Buckman dijo...

A mi me funciona sin problemas. Manda el codigo completo de la funcion y lo revisamos entre todos a ver si descubrimos el fallo.
Saludos

Anónimo dijo...

Wenas Pablo, tengo un problema.

He copiado tu codigo en mi aplicacion y no me funciona. Lo que pasa es que se ejecuta el Office OneNote y no se imprime nada. Me he dado cuenta que la impresora que tengo predetermindada no es la que tengo connectada. Alguna idea?

Lo estoy haciendo en VB.

Muchas gracias por tu tiempo^^

Anónimo dijo...

Wenas Pablo, tengo un problema.

He copiado tu codigo en mi aplicacion ponindo todos los botones y eso y no me funciona. Lo que pasa es que se ejecuta el Office OneNote y no se imprime nada. Alguna idea?

Lo estoy haciendo en VB. Muchas gracias por tu tiempo

Pablo Bouzada dijo...

# Anónimo del 26 de noviembre:
si te da un error de referencia no establecida como instancia de un objeto, o la variable "texto" o "celda" no están establecidas, por favor comprueba eso.

# Anónimo del 29 de noviembre:
si te salta del Office OneNote será que en la configuración de la impresora por defecto lo tienes establecido así, comprueba eso.

Anónimo dijo...

hola...

oye muy padre tu aporte nada mas que me sale el mismo error que el compañero de arriba...si le pongo i=-1 me marca error "El índice estaba fuera del intervalo. Debe ser un valor no negativo e inferior al tamaño de la colección.
Nombre del parámetro: index" y pues lo pongo en cero y me imprime pero sin cabeceras y otro error que me aparece es que si el datagridview contiene pocos datos pues me los imprime de maravilla pero al querer imprimir en otra forma la misma consulta con un datagridview con mas de 40 datos me marca error que dice "Referencia a objeto no establecida como instancia de un objeto." y la impresora es una que tengo y no es la de OneNote 2007

estoy programando en visual c# 2005 no se si puedas ver cual es el problema...

de antemano gracias

Anónimo dijo...

ahi te va mi codigo espero ke me ayudes...soy el chavo del comentario pasado

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Data.SqlClient;
using System.Windows.Forms;

namespace Intecsa
{
public partial class Ver_Externos : Form
{
public Ver_Externos()
{
InitializeComponent();
}
SqlDataAdapter orden;
public DataSet tabla;
public SqlCommand comando;
public SqlDataReader reader;
static SqlConnection cnn = new SqlConnection("Server=(local)\\SQLEXPRESS; database=productos; integrated security=yes");

private void Ver_Externos_Load(object sender, EventArgs e)
{


cnn.Open();

string select = "SELECT * from externos";
comando = new SqlCommand(select, cnn);
orden = new SqlDataAdapter(comando);
tabla = new DataSet();
orden.Fill(tabla, "Externos");
dataGridView1.DataSource = tabla;
dataGridView1.DataMember = "Externos";
cnn.Close();
}

private void button1_Click(object sender, EventArgs e)
{
this.Close();
}
private int i=-1;
private void button2_Click(object sender, EventArgs e)
{
printDocument1.Print();
}

private void printDocument1_PrintPage(object sender, System.Drawing.Printing.PrintPageEventArgs e)
{
Font printFont = new Font("Arial", 10);
float topMargin = e.MarginBounds.Top;
float yPos = 0;
float linesPerPage = 0;
int count = 0; string texto = "" ;
DataGridViewRow row;

linesPerPage = e.MarginBounds.Height / printFont.GetHeight(e.Graphics);

while (count < linesPerPage && i < this.dataGridView1.Rows.Count)
{
row = dataGridView1.Rows[i];
texto = "";
foreach (DataGridViewCell celda in row.Cells)
{
texto += "\t" + celda.Value.ToString();
}
yPos = topMargin + (count * printFont.GetHeight(e.Graphics));
e.Graphics.DrawString(texto, printFont, Brushes.Black, 10, yPos);
count++;
i++;
}
if (i < this.dataGridView1.Rows.Count)
{
e.HasMorePages = true;
}
else {

e.HasMorePages = false;
}
i = 0;
}
}

}

otra ves gracias

Pablo Bouzada dijo...

#Anónimo del 2 de diciembre:

A ver, el código que puse no es perfecto, era únicamente un ejemplo, pero viendo el código que me envías, puede fallar por 2 cosas:

- la inicialización de la variable "i" a -1. Prueba inicializando a 0.

- que la consulta a base de datos no te devuelva ningún resultado, por lo que al intentar hacer esta asignación: row = dataGridView1.Rows[i]; puede fallar.

Comprueba esas 2 cosas.

Unknown dijo...

me da error en esta linea...

texto += vbCrLf + celda.Value.ToString()

Referencia a objeto no establecida como instancia de un objeto.

no se que hacer, no soy muy bueno y necesito imprimir mi datagrid, favor ayuda

Pablo Bouzada dijo...

A tod@s:

he creado una nueva entrada con el código en VB.NET y un par de errores corregidos: http://programandoenpuntonet.blogspot.com/2009/12/imprimir-el-contenido-de-un_09.html


A Oscar:

El error es bastante obvio, "celda.Value" es Nothing, sólo tienes que comprobar que eso no sea cierto. Seguro que tienes la propiedad AllowUserToAddRows a true. Igualmente te remito a la nueva entrada en la que se comprueba eso.

Anónimo dijo...

Buenas tardes,

Mira he conseguido imprimir mi DataGrid. Pero tengo dos problemas. El primero es que los valors no me caben en la hoja y no me los imprime, supongo que la solucion es imprimir la pagina en horizontal. Como lo hago?

Después yo queria que también se imprimiera el cabezal de la tabla. Me puedes ayudar?

Muchas gracias de antemano.

Pablo Bouzada dijo...

#Anónimo del 27 de diciembre:

te emplazo a este otro post para ver cómo se imprimen las cabeceras:

http://programandoenpuntonet.blogspot.com/2009/12/imprimir-el-contenido-de-un_09.html

Con lo de girar la página, al final de este posts tienes un par de links, uno de ellos es mostrar el PageSetudDialog, seguro que con eso lo sacas ;)

Unknown dijo...

Agradezco inmensamente el código aportado me ha sacado de un problema de ya varios días.

Alejandra Hernandez dijo...

q tal amigo... tengo el mismo problema de uno xD... q dice q se sale de la ora los parametros.... seria genial si pusieras como usar los Printdialog y el printpreview control y para q me imprima los encabezados es igaul q en VB? yo ocupo el c# :P grax de antema ^^!

Pablo Bouzada dijo...

A todos los que preguntan sobre cómo imprimir las cabeceras:

El código más importante de este ejemplo es esta línea:

e.Graphics.DrawString(texto, printFont, Brushes.Black, 10, yPos);

donde los 2 últimos parámetros son la posición X y la posición Y donde se escribirá el contenido de la variable "texto", si no os cuadra con lo que queréis imprimir simplemente cambiar los valores de esas variables en el momento de escribir las cabeceras, o directamente ponerlas a mano.

Jose Luis dijo...

Aca hay un codigo muy bueno y util para imprimir un datagridview http://www.codeproject.com/KB/grid/PrintDataGridView.aspx

Anónimo dijo...

Antes que nada agradezco a las personas que se toman la molestia en subir codigo propio para que los demas aprendamos un poco mas, yo utilizo vb .net y este codigo me servido para darme una idea de imprimir un datagridview(el contenido) y lo que yo puedo aportar al revisar los errores es que efectivamente i tienes que iniciarlo en 0 y 2 yo utilize en el botn que dispara la carga de informacion en el datagridview la sig linea
Me.DataGridView1.AllowUserToAddRows = False
y con esto pude sacar la impresion ahora lo que me falta es imprimir los encabezados y alinear bien las lineas si alguien me puede orientar de antemano muchas gracias

Anónimo dijo...

hola mira tengo este problema
cuando imprimo me marca en amarillo esta parte del codigo texto += "\t" + celda.Value.ToString();
y me sale un mensaje con eso: Referencia a objeto no establecida como instancia de un objeto.
que solucion tiene?
gracias