Compartir a través de


Fronteras de la UI

Conceptos básicos de impresión de Silverlight

Charles Petzold

Descargar el ejemplo de código

Charles PetzoldSilverlight 4 agregó la impresión a la lista de características de Silverlight, y deseo sumergirme en eso mostrando un programa muy pequeño que me hace muy feliz.

Este programa se llama PrintEllipse (impresión y elipse) y eso es precisamente todo lo que hace. El archivo XAML para la MainPage contiene un botón, y la figura 1 muestra el archivo de código subyacente de la MainPage en su totalidad.

Figura 1 El código de MainPage para PrintEllipse

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Printing;
using System.Windows.Shapes;

namespace PrintEllipse
{
  public partial class MainPage : UserControl
  {
    public MainPage()
    {
      InitializeComponent();
    }
    void OnButtonClick(object sender, RoutedEventArgs args)
    {
      PrintDocument printDoc = new PrintDocument();
      printDoc.PrintPage += OnPrintPage;
      printDoc.Print("Print Ellipse");
    }
    void OnPrintPage(object sender, PrintPageEventArgs args)
    {
      Ellipse ellipse = new Ellipse
      {
        Fill = new SolidColorBrush(Color.FromArgb(255, 255, 192, 192)),
        Stroke = new SolidColorBrush(Color.FromArgb(255, 192, 192, 255)),
        StrokeThickness = 24    // 1/4 inch
      };
      args.PageVisual = ellipse;
    }
  }
}

Observe la directiva en uso para System.Windows.Printing. Cuando hace clic en el botón, el programa crea un objeto de tipo PrintDocument y asigna un controlador para el evento PrintPage. Cuando el programa llama al método Impresión, aparece el cuadro de diálogo de impresión estándar. El usuario puede tomar esta oportunidad para definir la impresora que se va a usar y para establecer varias propiedades para la impresión, como modo vertical o modo horizontal.

Cuando el usuario hace clic en Imprimir en el cuadro de diálogo de impresión, el programa recibe una llamada al controlador de evento PrintPage. Este programa especial responde creando un elemento Ellipse y definiéndolo en la propiedad PageVisual de los argumentos del evento. (Elegí deliberadamente colores pasteles claros para que el programa no use gran cantidad de tinta). Pronto, de la impresora surgirá una página con una elipse gigante.

Puede ejecutar este programa desde mi sitio web en bit.ly/dU9B7k y comprobarlo usted mismo. Por supuesto, también se puede descargar todo el código fuente mencionado en este artículo.

Si su impresora es como la mayoría, el hardware interno evita que imprima en el borde mismo del papel. Normalmente, las impresoras tienen un margen intrínseco integrado en el que no se imprime; por el contrario, la impresión está restringida a un “área imprimible” que es menor que el tamaño completo de la página.

Lo que observará con este programa es que la elipse aparece en su totalidad dentro del área imprimible de la página y, obviamente, esto ocurre con un esfuerzo mínimo por parte del programa. El área imprimible de la página se comporta de manera muy similar a un elemento contenedor en la pantalla: solo recorta un elemento secundario cuando hay un elemento cuyo tamaño supera al área. Algunos entornos gráficos mucho más sofisticados (como Windows Presentation Foundation) no se comportan de esa manera pero, por supuesto, WPF ofrece mucho más control de impresión y mayor flexibilidad que Silverlight.

PrintDocument y eventos

Además del evento PrintPage, PrintDocument también define los eventos BeginPrint y EndPrint, pero estos no resultan ser tan importantes como PrintPage. El evento BeginPrint señala el comienzo de un trabajo de impresión. Se activa cuando el usuario sale el cuadro de diálogo de impresión estándar al presionar el botón Imprimir y da al programa la oportunidad de realizar la inicialización. Después de la llamada al controlador BeginPrint viene la primera llamada al controlador PrintPage.

Eso puede hacerlo un programa que desea imprimir más de una página en un trabajo de impresión en especial. En cada llamada al controlador PrintPage, la propiedad HasMorePages de PrintPageEventArgs se define inicialmente en false. Cuando el controlador finaliza con una página, simplemente define la propiedad en true para señalar que se debe imprimir al menos una página más. Entonces se vuelve a llamar a PrintPage. El objeto PrintDocument mantiene una propiedad PrintedPageCount que se incrementa después de cada llamada al controlador PrintPage.

Cuando el controlador PrintPage sale con HasMorePages definido en su valor predeterminado de false, el trabajo de impresión finaliza y se activa el evento EndPrint, lo que da al programa la oportunidad de realizar tareas de limpieza. El evento EndPrint también se activa cuando se produce un error durante el proceso de impresión; la propiedad Error de EndPrintEventArgs es de tipo Excepción.

Impresión de coordenadas

El código que aparece en la figura 1 define el StrokeThickness de la elipse en 24, y si se mide el resultado impreso, verá que tiene un ancho de un cuarto de pulgada. Como sabe, un programa Silverlight normalmente define el tamaño de controles y objetos gráficos completamente en unidades de píxeles. Sin embargo, cuando se incluye la impresora, las coordenadas y los tamaños aparecen en unidades de 1/96 de pulgada independientes del dispositivo. Independientemente de la resolución real de la impresora, en un programa Silverlight la impresora siempre aparecerá como un dispositivo de 96 PPP (puntos por pulgada).

Como probablemente sepa, este sistema de coordenadas de 96 unidades por pulgada se usa en todo WPF, donde las unidades a menudo se conocen como “píxeles independientes del dispositivo”. Este valor de 96 PPP no se eligió de manera arbitraria: de manera predeterminada, Windows asume que la visualización de vídeo tiene 96 puntos en cada pulgada por lo que, en muchos casos, un programa WPF realmente dibuja en unidades de píxeles. La especificación de CSS supone que las visualizaciones de vídeo tienen una resolución de 96 PPP y que ese valor se usa para realizar conversión entre píxeles, pulgadas y milímetros. El valor de 96 también es un número conveniente para convertir los tamaños de fuentes, los que normalmente se especifican en puntos, o en 1/72 de pulgada. Un punto representa tres cuartos de un píxel independiente del dispositivo.

PrintPageEventArgs tiene dos útiles propiedades get-only que también informan los tamaños en unidades de 1/96 de pulgada. El PrintableArea de tipo Tamaño proporciona las dimensiones del área imprimible de la página, y PageMargins de tipo Grosor es el ancho de los márgenes no imprimibles izquierdo, superior, derecho e inferior. Sume estos dos (de la manera correcta) y obtendrá el tamaño completo del papel.

Mi impresora, cuando se carga con papel estándar de 8,5 x 11 pulgadas y se define en modo vertical, informa un PrintableArea de 791 x 993. Los cuatro valores de la propiedad PageMargins son 12 (izquierda), 6 (arriba), 12 (derecha) y 56 (abajo). Si suma los valores horizontales de 791, 12 y 12, obtendrá 815. Los valores verticales son 994, 6 y 56, los que suman 1.055. No estoy seguro de por qué hay una diferencia de una unidad entre estos valores y los valores de 816 y 1.056 que se obtienen al multiplicar el tamaño de la página en pulgadas por 96.

Cuando una impresora está definida para usar el modo horizontal, se intercambian las dimensiones horizontal y vertical que informan PrintableArea y PageMargins. De hecho, examinar la propiedad PrintableArea es la única manera en que un programa de Silverlight puede determinar si la impresora se encuentra en modo vertical o en modo horizontal. Cualquier impresión que realice el programa se alinea y gira automáticamente, dependiendo de este modo.

A menudo, cuando imprime en la vida real, definirá márgenes que son algo más grandes que los márgenes no imprimibles. ¿Cómo se hace esto en Silverlight? Al principio, pensaba que sería tan fácil como definir la propiedad Margen en el elemento que se imprime. Este Margen se calcularía para comenzar con un margen total deseado (en unidades de 1/96 de pulgada) y restando los valores de la propiedad PageMargins disponible en PrintPageEventArgs. Este enfoque no funcionó bien, pero la solución correcta fue casi igual de simple. El programa PrintEllipseWithMargins (que puede ejecutar en bit.ly/fCBs3X) es igual que el primer programa, excepto que una propiedad Margen está definida en la Elipse, y luego esta se define como el elemento secundario de un Borde, que completa el área imprimible. Como alternativa, puede definir la propiedad Relleno en el borde. La figura 2 muestra el nuevo método OnPrintPage.

Figura 2 El método OnPrintPage para calcular márgenes

void OnPrintPage(object sender, PrintPageEventArgs args)
{
  Thickness margin = new Thickness
  {
    Left = Math.Max(0, 96 - args.PageMargins.Left),
    Top = Math.Max(0, 96 - args.PageMargins.Top),
    Right = Math.Max(0, 96 - args.PageMargins.Right),
    Bottom = Math.Max(0, 96 - args.PageMargins.Bottom)
  };
  Ellipse ellipse = new Ellipse
  {
    Fill = new SolidColorBrush(Color.FromArgb(255, 255, 192, 192)),
    Stroke = new SolidColorBrush(Color.FromArgb(255, 192, 192, 255)),
    StrokeThickness = 24,   // 1/4 inch
    Margin = margin
  };
  Border border = new Border();
  border.Child = ellipse;
  args.PageVisual = border;
}

El objeto PageVisual

No hay clases gráficas o métodos gráficos especiales asociados con la impresora. Se “dibuja” algo en la página de la impresora, del mismo modo que “dibuja” algo en la visualización de vídeo, que se realiza ensamblando un árbol visual de objetos que derivan de un FrameworkElement. Este árbol puede incluir elementos de panel, incluidos lienzos. Para imprimir ese árbol visual, defina el elemento superior en la propiedad PageVisual de PrintPageEventArgs. (PageVisual se define como un UIElement, que es la clase principal de FrameworkElement, pero en un sentido práctico, todo lo que defina en PageVisual derivará de FrameworkElement).

Prácticamente cada clase que deriva de FrameworkElement tiene implementaciones no triviales de los métodos MeasureOverride y ArrangeOverride para el diseño. En este método MeasureOverride, un elemento determina su tamaño deseado, a veces al determinar los tamaños deseados de sus elementos secundarios al llamar a los métodos Measure de sus elementos secundarios. En el método ArrangeOverride, un elemento organiza sus elementos secundarios respecto de sí mismo llamando a los métodos Arrange de los elementos secundarios.

Cuando define un elemento en la propiedad PageVisual de PrintPageEventArgs, el sistema de impresión de Silverlight llama a Measure respecto de ese elemento superior con el tamaño del PrintableArea. Es así como, por ejemplo, el tamaño de la elipse o del borde se ajusta automáticamente al área imprimible de la página.

Sin embargo, también puede definir esa propiedad PageVisual en un elemento que ya forma parte de un árbol visual que se muestra en la ventana del programa. En este caso, el sistema de impresión no llama a Measure por ese elemento, sino que usa las medidas y el diseño que ya están determinados para la visualización de vídeo. Esto permite imprimir desde la ventana del programa con una fidelidad razonable, pero también significa que podría recortarse lo que imprima para ajustarlo al tamaño de la página.

Por supuesto, puede definir las propiedades Ancho y Alto explícitas en los elementos que imprime y puede usar el tamaño del PrintableArea como ayuda.

Escalado y rotación

El siguiente programa resultó ser más complicado de lo previsto. El objetivo era un programa que permitiera al usuario imprimir cualquier archivo de imagen compatible con Silverlight, es decir, archivos PNG y JPEG, almacenado en la máquina local del usuario. Este programa usa la clase OpenFileDialog para cargar estos archivos. Por seguridad, OpenFileDialog solo devuelve un objeto FileInfo que permite que el programa abra el archivo. No se proporciona ningún nombre de archivo o directorio.

Deseaba que este programa imprimiera el mapa de bits con el mayor tamaño posible en la página (excluido el margen predefinido) sin alterar la relación de aspecto del mapa de bits. Normalmente esto resulta ser muy sencillo: el modo Expandido predeterminado del elemento Imagen es Uniforme, lo que significa que el mapa de bits se expande lo máximo posible sin distorsión.

Sin embargo, decidí que no quería que el usuario tuviera que definir específicamente el modo vertical u horizontal en la impresora acorde con la imagen en especial. Si la impresora estaba definida en el modo vertical y la imagen era más ancha que alta, esperaba que la imagen se imprimiera de lado en la página vertical. Esta pequeña característica hizo de inmediato que el programa fuese mucho más complejo.

Si hubiese escrito un programa de WPF para hacer esto, el mismo programa podría haber cambiado la impresora al modo vertical u horizontal. Pero eso no es posible en Silverlight. La interfaz de la impresora está definida para que solo el usuario pueda cambiar ese tipo de configuración.

Nuevamente, si hubiese escrito un programa de WPF, de manera alternativa podría haber definido una LayoutTransform en el elemento Imagen para girarlo 90 grados. Luego, el tamaño del elemento Imagen girado se ajustaría para adecuarlo a la página, y el mapa de bits se habría ajustado para corresponder al elemento Imagen.

Pero Silverlight no es compatible con LayoutTransform. Silverlight solo es compatible con RenderTransform, por lo que si el elemento Imagen se debe girar para ajustarlo a una imagen horizontal impresa en modo vertical, también se debe ajustar manualmente el tamaño del elemento Imagen según las dimensiones de la página horizontal.

Puede probar mi primer intento en bit.ly/eMHOsB. El método OnPrintPage crea un elemento Imagen y define la propiedad Stretch en None, lo que significa que el elemento Imagen aparece en el mapa de bits en su tamaño en píxeles, que en la impresora significa que cada píxel debe ser de 1/96 de pulgada. Luego el programa gira, ajusta el tamaño y traduce el elemento Imagen al calcular una transformación que aplica a la propiedad RenderTransform del elemento Imagen.

La parte complicada de dicho código es, por supuesto, el cálculo, por lo que resultó agradable ver que el programa funcionaba con imágenes verticales y horizontales con la impresora definida en los modos vertical y horizontal.

Sin embargo, lo que no fue muy agradable fue ver que el programa no podía tratar con imágenes de gran tamaño. Puede probarlo usted mismo con imágenes con dimensiones algo mayores (cuando se divide en 96) que el tamaño de la página en pulgadas. La imagen aparece en el tamaño correcto, pero no completamente.

¿Qué ocurre aquí? Bueno, es algo que he visto antes en visualizaciones de vídeo. Tenga en cuenta que RenderTransform solo afecta la manera en que se muestra el elemento y no cómo aparece en el sistema de diseño. Para el sistema de diseño, muestro un mapa de bits en un elemento Imagen con Extendido definido en None, lo que significa que el elemento Imagen es tan grande como el mismo mapa de bits. Si el mapa de bits es mayor que la página de la impresora, no se podrá presentar parte del elemento Imagen y, de hecho, se recortará, independientemente de una RenderTransform que reduce adecuadamente el elemento Imagen.

Mi segundo intento, que puede probar en bit.ly/g4HJ1C, toma una estrategia algo distinta. El método OnPrintPage aparece en la figura 3. El elemento Imagen tiene una configuración explícita de Ancho y Alto que conforman exactamente el tamaño del área de visualización calculada. Como todo está dentro del área imprimible de la página, no se recortará nada. El modo Extendido está definido en Relleno, lo que significa que el mapa de bits rellene el elemento Imagen independientemente de la relación de aspecto. Si el elemento Imagen no se fuese a girar, una dimensión tiene el tamaño correcto y la otra dimensión debiera tener un factor de escala aplicado que disminuye el tamaño. Si el elemento Imagen también se debe girar, los factores de escala deben dar cabida a la relación de aspecto distinta del elemento Imagen girado.

Figura 3 Impresión de una imagen en PrintImage

void OnPrintPage(object sender, PrintPageEventArgs args)
{
  // Find the full size of the page
  Size pageSize = 
    new Size(args.PrintableArea.Width 
    + args.PageMargins.Left + args.PageMargins.Right,
    args.PrintableArea.Height 
    + args.PageMargins.Top + args.PageMargins.Bottom);

  // Get additional margins to bring the total to MARGIN (= 96)
  Thickness additionalMargin = new Thickness
  {
    Left = Math.Max(0, MARGIN - args.PageMargins.Left),
    Top = Math.Max(0, MARGIN - args.PageMargins.Top),
    Right = Math.Max(0, MARGIN - args.PageMargins.Right),
    Bottom = Math.Max(0, MARGIN - args.PageMargins.Bottom)
  };

  // Find the area for display purposes
  Size displayArea = 
    new Size(args.PrintableArea.Width 
    - additionalMargin.Left - additionalMargin.Right,
    args.PrintableArea.Height 
    - additionalMargin.Top - additionalMargin.Bottom);

  bool pageIsLandscape = displayArea.Width > displayArea.Height;
  bool imageIsLandscape = bitmap.PixelWidth > bitmap.PixelHeight;

  double displayAspectRatio = displayArea.Width / displayArea.Height;
  double imageAspectRatio = (double)bitmap.PixelWidth / bitmap.PixelHeight;

  double scaleX = Math.Min(1, imageAspectRatio / displayAspectRatio);
  double scaleY = Math.Min(1, displayAspectRatio / imageAspectRatio);

  // Calculate the transform matrix
  MatrixTransform transform = new MatrixTransform();

  if (pageIsLandscape == imageIsLandscape)
  {
    // Pure scaling
    transform.Matrix = new Matrix(scaleX, 0, 0, scaleY, 0, 0);
  }
  else
  {
    // Scaling with rotation
    scaleX *= pageIsLandscape ? displayAspectRatio : 1 / 
      displayAspectRatio;
    scaleY *= pageIsLandscape ? displayAspectRatio : 1 / 
      displayAspectRatio;
    transform.Matrix = new Matrix(0, scaleX, -scaleY, 0, 0, 0);
  }

  Image image = new Image
  {
    Source = bitmap,
    Stretch = Stretch.Fill,
    Width = displayArea.Width,
    Height = displayArea.Height,
    RenderTransform = transform,
    RenderTransformOrigin = new Point(0.5, 0.5),
    HorizontalAlignment = HorizontalAlignment.Center,
    VerticalAlignment = VerticalAlignment.Center,
    Margin = additionalMargin,
  };

  Border border = new Border
  {
    Child = image,
  };

  args.PageVisual = border;
}

El código claramente es complicado, y sospecho que podría haber simplificaciones no inmediatamente obvias para mí, pero funciona para los mapas de bits de todos los tamaños.

Otro enfoque es girar el mapa de bits mismo en lugar del elemento Imagen. Crear un WriteableBitmap desde el objeto BitmapImage cargado y un segundo WritableBitmap con dimensiones horizontal y vertical intercambiadas. Luego copie todos los píxeles del primer WriteableBitmap al segundo con filas y columnas intercambiadas.

Varias Calendar Pages

Hay una técnica muy popular que deriva de UserControl en la programación de Silverlight para crear un control reutilizable sin muchas complicaciones. Gran parte de un UserControl es un árbol visual definido en XAML.

También puede derivar de UserControl para definir un árbol visual para impresión. Esta técnica se ilustra en el programa PrintCalendar, que puede probar en bit.ly/dIwSsn. Escriba un mes de inicio y un mes final y el programa imprime todos los meses dentro de ese intervalo, un mes en cada página. Puede pegar con cinta las páginas en las paredes y marcarlas, tal como lo haría con un calendario de pared real.

Después de mi experiencia con el programa PrintImage, no quería ser una molestia con los márgenes o la orientación; en lugar de eso, incluí un botón que pone la responsabilidad en el usuario, tal como aparece en la figura 4.

The PrintCalendar Button

Figura 4 El botón PrintCalendar

El UserControl que define la página de calendario se llama CalendarPage y el archivo XAML aparece en la figura 5. Un TextBlock cerca de la parte superior aparece el mes y el año. Después de esto, hay una segunda cuadrícula con siete columnas para los días de la semana y seis filas para hasta seis semanas o semanas parciales en un mes.

Figura 5 El diseño de CalendarPage

<UserControl x:Class="PrintCalendar.CalendarPage"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  FontSize="36">  
  <Grid x:Name="LayoutRoot" Background="White">
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <TextBlock Name="monthYearText" 
      Grid.Row="0"
       FontSize="48"
       HorizontalAlignment="Center" />
    <Grid Name="dayGrid" 
      Grid.Row="1">
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
      </Grid.RowDefinitions>
    </Grid>
  </Grid>
</UserControl>

A diferencia de la mayoría de los derivados de UserControl, CalendarPage define un constructor con un parámetro, tal como aparece en la figura 6.

Figura 6 El constructor de código subyacente de CalendarPage

public CalendarPage(DateTime date)
{
  InitializeComponent();
  monthYearText.Text = date.ToString("MMMM yyyy");
  int row = 0;
  int col = (int)new DateTime(date.Year, date.Month, 1).DayOfWeek;
  for (int day = 0; day < DateTime.DaysInMonth(date.Year, date.Month); day++)
  {
    TextBlock txtblk = new TextBlock
    {
      Text = (day + 1).ToString(),
      HorizontalAlignment = HorizontalAlignment.Left,
      VerticalAlignment = VerticalAlignment.Top
    };
    Border border = new Border
    {
      BorderBrush = blackBrush,
      BorderThickness = new Thickness(2),
      Child = txtblk
    };
    Grid.SetRow(border, row);
    Grid.SetColumn(border, col);
    dayGrid.Children.Add(border);
    if (++col == 7)
    {
      col = 0;
      row++;
    }
  }
  if (col == 0)
    row--;
  if (row < 5)
    dayGrid.RowDefinitions.RemoveAt(0);
  if (row < 4)
    dayGrid.RowDefinitions.RemoveAt(0);
}

El parámetro es DateTime y el constructor usa las propiedades Mes y Año para crear un Borde que contiene TextBlock para cada.día del mes. A cada uno de estos elementos se les asignan las propiedades adjuntas Grid.Row y Grid.Column y luego se agregan a la cuadrícula. Como sabe, a menudo los meses solo abarcan cinco semanas y, en ocasiones, febrero solo tiene cuatro, por lo que los objetos RowDefinition en realidad se quitan de la cuadrícula si no son necesarios.

Los derivados de UserControl normalmente no tienen constructores con parámetros, porque por lo general forman partes de árboles visuales de mayor tamaño. Pero CalendarPage no se usa de esa manera. En lugar de eso, el controlador PrintPage simplemente asigna una instancia nueva de CalendarPage a la propiedad PageVisual de PrintPageEventArgs. A continuación aparece el cuerpo completo del controlador, y se ilustra claramente cuánto trabajo realiza CalendarPage:

args.PageVisual = new CalendarPage(dateTime);
args.HasMorePages = dateTime < dateTimeEnd;
dateTime = dateTime.AddMonths(1);

Agregar una opción de impresión a un programa a menudo se ve como un trabajo complicado que implica mucho código. Poder definir la mayor parte de una página impresa en un archivo XAML hace que todo sea mucho menos aterrador.        

Charles Petzold ha sido editor colaborador durante largo tiempo de MSDN Magazine. Su nuevo libro, “Programming Windows Phone 7” (Microsoft Press, 2010) está disponible como una descarga gratuita en bit.ly/cpebookpdf.

Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Saied Khanahmadi y Robert Lyon