Silverlight 4 Beta – Impresión de Múltiples Páginas

En el artículo anterior, vimos los fundamentos del modelo de clases para imprimir desde nuestras aplicaciones de Silverlight 4.  Recordemos que la propiedad HasMorePages indica si nuestro documento tiene más páginas que imprimir; si HasMorePages es true entonces el evento PrintPage se disparará nuevamente hasta que HasMorePages sea false.

En este artículo veremos cómo aprovechar esta característica para poder mandar a imprimir un catálogo de artículos (en este ejemplo, un catálogo de álbumes de música) ya que este escenario será bastante común en las Aplicaciones de Línea de Negocio que construyamos con Silverlight 4.

Escenario

Dado el siguiente escenario:

1_thumb1

Podemos apreciar que tenemos un ListBox el cual contiene una lista de álbumes de música.  Además estamos cambiando el DataTemplate para poder mostrar de una mejor manera cada uno de esos elementos.  El siguiente fragmento de código muestra el Xaml de la aplicación:

<UserControl x:Class=”Demo.SL4.ImpresionMultiple.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:d=”http://schemas.microsoft.com/expression/blend/2008″
xmlns:mc=”http://schemas.openxmlformats.org/markup-compatibility/2006″
xmlns:local=”clr-namespace:Demo.SL4.ImpresionMultiple”
mc:Ignorable=”d”
d:DesignHeight=”300″
d:DesignWidth=”500″>
<UserControl.Resources>
<local:Albumes x:Key=”albumes” />
<DataTemplate x:Key=”VistaDataTemplate”>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=”100″ />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Border BorderBrush=”Black”
Padding=”3″
BorderThickness=”2″
CornerRadius=”10″>
<Image Source=”{Binding Foto}”
Width=”100″ />
</Border>
<StackPanel Grid.Column=”1″>
<TextBlock Text=”{Binding Titulo}”
FontSize=”20″ />
<TextBlock Text=”{Binding Banda}” />
</StackPanel>
</Grid>
</DataTemplate>
</UserControl.Resources>
<Grid x:Name=”LayoutRoot”
Background=”White”
DataContext=”{StaticResource albumes}”>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=”Auto” />
<ColumnDefinition Width=”*” />
</Grid.ColumnDefinitions>
<ListBox Grid.Column=”1″
HorizontalAlignment=”Left”
Margin=”5″
Name=”listBox1″
VerticalAlignment=”Top”
Width=”450″
Height=”450″
ItemsSource=”{Binding}”
ItemTemplate=”{StaticResource VistaDataTemplate}” />
<StackPanel Height=”228″
HorizontalAlignment=”Left”
Name=”stackPanel1″
VerticalAlignment=”Top”
Margin=”5″>
<Button Height=”50″
Name=”button1″
Width=”120″>
<Button.Content>
<StackPanel>
<TextBlock HorizontalAlignment=”Center”>Imprimir</TextBlock>
<TextBlock HorizontalAlignment=”Center”>ListBox</TextBlock>
</StackPanel>
</Button.Content>
</Button>
<Button Height=”50″
Name=”button2″
Width=”120″>
<Button.Content>
<StackPanel>
<TextBlock HorizontalAlignment=”Center”>Impresión</TextBlock>
<TextBlock HorizontalAlignment=”Center”>Personalizada</TextBlock>
</StackPanel>
</Button.Content>
</Button>
</StackPanel>
</Grid>
</UserControl>

Impresión de Elementos

La propiedad PageVisual del objeto de argumentos en el evento PrintPage indica ultimadamente cuál es el contenido a imprimir, siendo este contenido parte del árbol de Xaml o no.  Es decir que, si establecemos el ListBox como el PageVisual…

pd.PrintPage += (s, a) =>
{
//Establecemos el ListBox como el PageVisual
a.PageVisual = listBox1;
};

…nuestro resultado de impresión será el siguiente (impresión en XPS):

2_thumb1

Tal vez este mecanismo no sea el adecuado para presentar correctamente la impresión, ya que se imprime el elemento tal y como aparece en pantalla; es decir, una “fotografía” del elemento (incluyendo las barras de scroll, los bordes, etc.).

Impresión Personalizada

Podemos lograr un grado más alto de personalización creando de manera dinámica el contenido Xaml que deseamos imprimir.  En el caso de este ejemplo generaremos dinámicamente un Canvas que contenga un encabezado, además de un ItemsControl que despliegue todos y cada uno de los elementos a imprimir. Además de lo anterior, nos aseguraremos que la impresión permita múltiples páginas y que se impriman los registros en cada página de manera correcta.

Canvas

Tal como en el artículo anterior, creamos un Canvas que tenga el mismo tamaño del área de impresión disponible, según la impresora seleccionada:

pd.PrintPage += (s, a) =>
{
canvas = new Canvas() { Width = a.PrintableArea.Width, Height = a.PrintableArea.Height };

Encabezado

Para cada página a imprimir, se agregará un encabezado que contenga un logo a la izquierda, el título del encabezado centrado, y un texto que indique el número de página actual.

Nota: Hay un issue con Silverlight 4 Beta que impide la impresión de imágenes creadas dinámicamente.

Lista de Elementos

Esta es la parte más importante de este escenario.  Necesitamos soportar el hecho de que habrá más de una página al momento de imprimir.  En el ejemplo de este artículo tenemos un objeto llamado Albumes (List<Album>) el cual tiene un total de 42 álbumes.

Para la impresión personalizada crearemos otro DataTemplate llamado ImpresionDataTemplate el cual desplegará de manera diferente los mismos elementos de la pantalla.  Por ejemplo, quitaremos la imagen de cada álbum, además desplegaremos los elementos dentro de un Grid con cuatro columnas.  Las columnas que mostraremos serán Titulo, Banda, Inventario y FechaLanzamiento.  El siguiente fragmento de Xaml muestra el DataTemplate para la impresión:

<DataTemplate x:Key=”ImpresionDataTemplate”>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=”150″ />
<ColumnDefinition Width=”120″/>
<ColumnDefinition Width=”100″ />
<ColumnDefinition Width=”100″ />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height=”40″ />
</Grid.RowDefinitions>
<TextBlock Text=”{Binding Titulo}” FontWeight=”Bold” Grid.Column=”0″ />
<TextBlock Text=”{Binding Banda}” Grid.Column=”1″ />
<TextBlock Text=”{Binding Inventario}” Grid.Column=”2″ />
<TextBlock Text=”{Binding FechaLanzamiento}”
Grid.Column=”3″ />
</Grid>
</DataTemplate>

En el método de creación del listado por página (método CrearLista()) crearemos un ItemsControl el cual su DataTemplate sea el descrito arriba.  Asimismo, estableceremos como su fuente de datos (propiedad ItemsSource) únicamente los registros que quepan en la página actual.  Con los métodos de extensión Skip() y Take() podemos fácilmente leer cierto número de registros del objeto Albumes según la página actual.  El siguiente fragmento de código muestra este concepto:

void CrearLista(int pagina)
{
ItemsControl lista = new ItemsControl();
lista.Width = canvas.Width;
lista.ItemTemplate = this.Resources[“ImpresionDataTemplate”] as DataTemplate;

//Obtiene únicamente los registros que van en la página actual
//identificada en el argumento ‘pagina’ del método
lista.ItemsSource = fuenteDatos.Skip((int)(pagina*lineasPorPagina)).Take((int)lineasPorPagina);
//Agregamos la lista al Canvas
canvas.Children.Add(lista);

//Respetamos el alto del encabezado
Canvas.SetTop(lista, altoEncabezado);

//Incrementamos el contador
paginaActual++;
}

Finalmente, en el evento PrintPage invocamos los métodos de creación del encabezado y el de la creación de elementos por página:


CrearEncabezado(“Lista de Álbumes”);

CrearLista(paginaActual);

a.PageVisual = canvas;

a.HasMorePages = !(paginaActual == totalPaginas);

Muy importante en el código anterior es el valor de la propiedad HasMorePages.  En este ejemplo estamos indicando si hay más páginas siempre y cuando el contador (variable paginaActual) sea diferente al número total de páginas (variable totalPaginas).  Recordemos que si HasMorePages es true, el evento PrintPage nuevamente se disparará, provocando así la impresión de todas y cada una de las páginas del listado.

El resultado:

3_thumb1

Cabe mencionar que el DataTemplate específico para la impresión no es el único mecanismo para hacerlo de manera personalizada, también pudimos haber hecho un StackPanel o Grid de manera dinámica y leer programáticamente (un foreach tal vez) cada Album de Albumes programáticamente e ir agregando al contenedor los elementos manualmente.

Resumen

A través de la propiedad HasMorePages tenemos el control para indicar si nuestro documento a imprimir tiene múltiples páginas.  Asimismo vimos que un DataTemplate específico para la impresión es un mecanismo que nos facilita la manera en la que queremos que se imprima una lista de elementos en un reporte.

Pueden descargar el código fuente aquí

Pueden ver el demo en vivo aquí (requieres Silverlight 4 Beta instalado)

Silverlight 4 Beta – Impresión

Una de las características más esperadas y solicitadas por la comunidad de Silverlight es el soporte de impresión dentro de las aplicaciones, esto es, la capacidad de poder mandar a imprimir el contenido que estemos viendo en la aplicación ya sea una lista de datos de un DataGrid o ListBox o simplemente tomarle una “foto” a la pantalla y enviarla a papel o alguna de las impresoras virtuales instaladas en nuestros equipos (por ejemplo: OneNote, XPS, PDF, etc.).  Silverlight 4 Beta incluye esta característica a través de un modelo de clases muy sencillo.  De hecho este modelo me recuerda al modelo de impresión que tenemos en las aplicaciones Windows Forms desde hace ya mucho tiempo.

Clase System.Windows.Printing.PrintDocument

Las clases para el soporte de impresión dentro de Silverlight han sido incorporadas al espacio de nombres System.Windows.Printing, el cual contiene entre otras cosas la clase PrintDocument.  Esta clase PrintDocument es la clase responsable de mandar a imprimir el contenido que deseemos.

La clase PrintDocument no expone alguna propiedad para que podamos determinar el contenido que deseemos imprimir.  No obstante, el método Print() al ser ejecutado abre la caja de diálogo de impresión de nuestro sistema operativo.

1_thumb

Una vez seleccionada la impresora destino (física o virtual), disparará la siguiente secuencia de eventos:

  1. StartPrint
  2. PrintPage
  3. EndPrint
Evento StartPrint

El evento StartPrint se dispara una vez cerrada la caja de diálogo de selección de impresora.  Inmediatamente después se dispara el siguiente evento: PrintPage.

Evento PrintPage

Seguido del evento StartPrint se disparará el evento PrintPage.  Este es el evento más importante en el modelo de impresión ya que es en los argumentos donde debemos establecer efectivamente el contenido que deseamos imprimir.  Esto es definido en la propiedad PageVisual que es de tipo UIElement.  Ya que es de tipo UIElement podemos establecer como contenido en realidad cualquier cosa: ya sea que esté presente en el árbol de Xaml o que sea contenido creado de manera dinámica (a través de código o usando XamlReader.Load(), etc.).  Por ejemplo, si deseamos tomar una “fotografía” a la aplicación actual e imprimirla, podemos establecer como PageVisual el objeto contenedor raíz (ej. LayoutRoot).  O de lo contrario, si queremos crear una impresión personalizada podemos generar contenedores y elementos de manera dinámica tal y como lo muestra este artículo.

Además de la propiedad PageVisual contamos con un par de propiedades: HasMorePages y PrintableArea.

HasMorePages es una propiedad que indica si todavía hay más páginas que imprimir, por ejemplo en el caso de documentos largos, listas, catálogos etc.  Si establecemos esta propiedad a true, entonces el evento PrintPage volverá a dispararse.

PrintableArea indica el tamaño que tiene el área de impresión según la impresora seleccionada.  Por ejemplo no es el mismo tamaño si mandamos a imprimir a OneNote que a un PDF o a una impresora física.  Esta propiedad es de tipo Size y nos servirá también para calcular márgenes, posiciones de elementos, etc.

Cabe mencionar que, si la impresora seleccionada requiere todavía de la interacción del usuario, esta continuará (para indicar el nombre de un archivo .xps o .pdf por ejemplo).

Evento EndPrint

El evento EndPrint se disparará una vez terminado el proceso de impresión.

Ejemplo

Comencemos con crear un nuevo proyecto de Silverlight 4 a través de Visual Studio 2010 (Beta 2 es la versión más reciente en el momento de escribir este artículo).  A este proyecto le pondremos de nombre Demo.SL4.Impresion.  Visual Studio creará la solución junto con el proyecto adecuado tal y como lo esperamos.

Agregaremos a nuestro MainPage.xaml el siguiente código:

Los botones nos servirán para mandar a imprimir por medio de la ejecución del método Print() de la clase PrintDocument y el TextBlock nos servirá para identificar el estatus de la impresión.

En el clic del primer botón crearemos una nueva instancia de PrintDocument y estableceremos su propiedad DocumentName de manera adecuada.  El valor de esta propiedad será lo que aparezca en la ventana de estado de la impresora.

PrintDocument pd = new PrintDocument() { DocumentName = “Ejemplo de Impresión” };

Ahora bien, manejaremos el evento PrintPage.  En este caso usaremos una expresión lambda pero pudiera ser también un método:

En el código anterior suceden varias cosas:

Creamos un objeto de tipo Canvas, el cual nos servirá como lienzo para dibujar en él los elementos visuales que queramos imprimir (en este ejemplo el contenido será un TextBlock creado en el método CrearTitulo()).  El ancho y alto del objeto Canvas los establecemos a partir de la propiedad PrintableArea, la cual, nos indica el tamaño de impresión según la impresora seleccionada (por ejemplo, el área de impresión es diferente en OneNote que un .xps).

Finalmente, establecemos este contenido que creamos dinámicamente en la propiedad PageVisual.  Esta propiedad indica el contenido visual que vamos a imprimir.  El siguiente fragmento de código contiene todo el código completo de este primer ejemplo:

Canvas canvas = null;
int pagina = 0;

private void button1_Click(object sender, RoutedEventArgs e)
{
PrintDocument pd = new PrintDocument() { DocumentName = “Ejemplo de Impresión” };
pd.PrintPage += (s, a) =>
{
//canvas es una variable a nivel de clase
//Establecemos el ancho y alto de canvas a usando la propiedad PrintableArea
canvas = new Canvas() { Width = a.PrintableArea.Width, Height = a.PrintableArea.Height };

//Agregamos el título deseado para la página
CrearTitulo(“Esto es una Prueba de Impresión”);

//Establecemos el contenido en la propiedad PageVisual
a.PageVisual = canvas;
};
pd.StartPrint += (s, a) => textBlock1.Text = “Iniciando impresión…”;
pd.EndPrint += (s, a) => textBlock1.Text = “Impresión finalizada”;
pd.Print();
}

void CrearTitulo(string titulo)
{
//Crea un TextBlock que servirá como encabezado
TextBlock contenido = new TextBlock() { Text = titulo, FontSize = 20 };
contenido.Effect = new DropShadowEffect();
canvas.Children.Add(contenido);
Canvas.SetLeft(contenido, canvas.Width / 2 – (contenido.ActualWidth / 2));
}

Y el resultado, tanto en un archivo .xps como en OneNote:

2_thumb

3_thumb

Noten cómo también soporta Efectos visuales.  Tal es el caso del DropShadowEffect que se está estableciendo en el título.

En el segundo botón tendremos una ligera variante: mandaremos a imprimir varias páginas.  Esto es logrado a partir de establecer la propiedad HasMorePages = true.  En el caso de este ejemplo hemos establecido una variable a nivel de clase llamada pagina que nos servirá de bandera.  Esta bandera la estaremos revisando hasta que se completen 4 páginas en total.

pd.PrintPage += (s, a) =>
{
canvas = new Canvas() { Width = a.PrintableArea.Width, Height = a.PrintableArea.Height };
CrearTitulo(“Esto es una Prueba de Impresión”);
CrearSubtitulo(string.Format(“Página: {0}”, pagina+1));
a.PageVisual = canvas;

a.HasMorePages = !(pagina == 3);
pagina++;

};

Cabe recordar que al establecer la propiedad HasMorePages a true, el evento PrintPage se vuelve a disparar, de esta manera, podemos tener el control del contenido a imprimir en cada página.  El código completo se muestra a continuación:

void CrearSubtitulo(string titulo)
{
//Crea un TextBlock que servirá como subtitulo
TextBlock contenido = new TextBlock() { Text = titulo, FontSize = 14 };
canvas.Children.Add(contenido);
Canvas.SetLeft(contenido, canvas.Width / 2 – (contenido.ActualWidth / 2));
Canvas.SetTop(contenido, 40);
}

private void button2_Click(object sender, RoutedEventArgs e)
{
PrintDocument pd = new PrintDocument() { DocumentName = “Ejemplo de Impresión” };
pd.PrintPage += (s, a) =>
{
canvas = new Canvas() { Width = a.PrintableArea.Width, Height = a.PrintableArea.Height };
CrearTitulo(“Esto es una Prueba de Impresión”);
CrearSubtitulo(string.Format(“Página: {0}”, pagina+1));
a.PageVisual = canvas;

a.HasMorePages = !(pagina == 3);
pagina++;

};
pd.StartPrint += (s, a) => textBlock1.Text = “Iniciando impresión…”;
pd.EndPrint += (s, a) => textBlock1.Text = “Impresión finalizada”;
pd.Print();
}

El resultado final de la impresión de este segundo botón se muestra en la siguiente figura (las flechas rojas se añadieron posteriormente para indicar el número de cada página):

4_thumb

Resumen

Silverlight 4 Beta incluye la funcionalidad de impresión, a través de la clase System.Windows.Printing.PrintDocument.  Esta clase incluye el método Print(), que al ser ejecutado dispara una serie de eventos, entre ellos el evento PrintPage en donde podemos establecer el contenido deseado para imprimir, conocer el tamaño del área de impresión así como determinar si hay más páginas para imprimir (para escenarios de documentos de más de 1 página).

Puedes descargar el código aquí.

Puedes ver el demo en vivo aquí (requiere Silverlight 4 Beta instalado).