Fronteiras da interface do usuário

Fundamentos de impressão do Silverlight

Charles Petzold

Baixar o código de exemplo

Charles PetzoldO Silverlight 4 adicionou impressão à lista de recursos do Silverlight, e quero entrar diretamente no assunto ao mostrar um pequeno programa que colocou um grande sorriso em meu rosto.

O programa se chama PrintEllipse e imprimir elipses é tudo o que ele faz. O arquivo XAML para MainPage contém um Button, e a Figura 1 mostra o arquivo codebehind de MainPage integralmente.

Figura 1 O código de MainPage do 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 a diretiva using para System.Windows.Printing. Quando você clica no botão, o programa cria um objeto do tipo PrintDocument e atribui um manipulador para o evento PrintPage. Quando o programa chamar o método Print, a caixa de diálogo padrão de impressão será exibida. O usuário pode aproveitar essa oportunidade para definir qual impressora será usada e definir várias propriedades de impressão, como modo retrato ou paisagem.

Quando o usuário clicar em Print na caixa de diálogo de impressão, o programa receberá uma chamada para o manipulador de eventos PrintPage. Esse programa específico responde criando um elemento Ellipse e definindo-o para a propriedade PageVisual dos argumentos do evento. (Escolhi deliberadamente cores claras em tom pastel para que o programa não use muita tinta.) Em breve, uma página com uma elipse gigante sairá da sua impressora.

Você pode executar esse programa do meu site em bit.ly/dU9B7k e conferir. É claro que o código-fonte exibido neste artigo também pode ser baixado.

Se a sua impressora for como a maioria das impressoras, o hardware interno proíbe a impressão na borda do papel. As impressoras geralmente têm uma margem interna intrínseca na qual nada é impresso; portanto, a impressão fica restrita a uma “área imprimível” que corresponde a menos do que o tamanho normal da página.

Com esse programa, você perceberá que a elipse aparece integralmente dentro da área imprimível da página e, obviamente, isso acontece com o mínimo esforço por parte do programa. A área imprimível da página se comporta como um elemento de contêiner na tela: recorta um filho somente quando um elemento tem um tamanho que excede a área. Alguns ambientes gráficos muito mais sofisticados — como o Windows Presentation Foundation (WPF) — não se comportam tão bem (mas, é claro, o WPF oferece muito mais controle e flexibilidade de impressão do que o Silverlight).

PrintDocument e eventos

Além do evento PrintPage, o PrintDocument também define os eventos BeginPrint e EndPrint, mas eles não são tão importantes quanto o PrintPage. O evento BeginPrint sinaliza o início de um trabalho de impressão. Ele é acionado quando o usuário fecha a caixa de diálogo padrão de impressão pressionando o botão Print e permite que o programa tenha a oportunidade de realizar a inicialização. Depois, a chamada para o manipulador BeginPrint é seguida pela primeira chamada para o manipulador PrintPage.

Um programa que quiser imprimir mais de uma página em um trabalho de impressão específico pode fazer isso. Em todas as chamadas para o manipulador PrintPage, a propriedade HasMorePages de PrintPageEventArgs inicialmente é definida como false. Quando o manipulador conclui uma página, pode simplesmente definir a propriedade como true para sinalizar que pelo menos mais uma página deve ser impressa. Em seguida, PrintPage é chamado novamente. O objeto PrintDocument mantém uma propriedade PrintedPageCount que é incrementada depois de cada chamada para o manipulador PrintPage.

Quando o manipulador PrintPage é encerrado com HasMorePages definida como o valor padrão false, o trabalho de impressão é concluído e o evento EndPrint é acionado, permitindo que o programa tenha a oportunidade de realizar tarefas de limpeza. O evento EndPrint também é acionado quando um erro ocorre durante o processo de impressão; a propriedade Error de EndPrintEventArgs é do tipo Exception.

Coordenadas de impressão

O código mostrado na Figura 1 define StrokeThickness da Ellipse como 24, e se você medir o resultado impresso, descobrirá que a largura é de um quarto de polegada. Como você sabe, um programa do Silverlight geralmente dimensiona os objetos gráficos e os controles integralmente em unidades de pixels. No entanto, quando a impressora está envolvida, as coordenadas e os tamanhos são em unidades de 1/96 de polegada independentes de dispositivo. Independentemente da resolução real da impressora, a partir de um programa do Silverlight, a impressora sempre parece ser um dispositivo de 96 DPI.

Como você deve saber, esse sistema de coordenadas de 96 unidades por polegada é usado em todo o WPF, onde as unidades algumas vezes são chamadas de “pixels independentes de dispositivo”. Esse valor de 96 DPI não foi escolhido arbitrariamente: por padrão, o Windows presume que o seu monitor de vídeo tem 96 pontos por polegada, por isso, em muitos casos, um programa do WPF está realmente desenhando em unidades de pixels. A especificação CSS presume que os monitores de vídeo têm uma resolução de 96 DPI, e esse valor é usado para conversões entre pixels, polegadas e milímetros. O valor de 96 também é um número conveniente para a conversão de tamanhos de fonte, que normalmente são especificados em pontos, ou 1/72 de polegada. Um ponto equivale a três quartos de um pixel independente de dispositivo.

PrintPageEventArgs tem duas propriedades somente obtenção úteis que também relatam os tamanhos em unidades de 1/96 de polegada: PrintableArea do tipo Size fornece as dimensões da área imprimível da página, e PageMargins do tipo Thickness é a largura à esquerda, na parte superior, à direita e na parte inferior das bordas não imprimíveis. Junte essas duas propriedades (da maneira correta) e você obterá o tamanho normal do papel.

Minha impressora — quando carregada com papel padrão 8,5 x 11 polegadas e definida para o modo retrato — relata uma PrintableArea de 791 x 993. Os quatro valores da propriedade PageMargins são 12 (à esquerda), 6 (na parte superior), 12 (à direita) e 56 (na parte inferior). Se você somar os valores horizontais de 791, 12 e 12, obterá 815. Os valores verticais são 994, 6 e 56, que equivalem a 1.055. Não tenho certeza do porquê há uma diferença de uma unidade entre esses valores e os valores 816 e 1.056 obtidos multiplicando o tamanho da página em polegadas por 96.

Quando uma impressora está definida para o modo paisagem, as dimensões horizontais e verticais relatadas pela PrintableArea e PageMargins são trocadas. Na verdade, a única maneira de um programa do Silverlight determinar se uma impressora está no modo retrato ou paisagem é examinando a propriedade PrintableArea. Qualquer coisa impressa pelo programa é alinhada e girada automaticamente dependendo desse modo.

Frequentemente, quando você imprime algo na vida real, define margens que são um pouco maiores do que as margens não imprimíveis. Como se faz isso no Silverlight? A princípio, pensei que seria tão fácil quanto configurar a propriedade Margin no elemento para impressão. Essa Margin seria calculada começando com uma margem total desejada (em unidades de 1/96 de polegada) e subtraindo os valores da propriedade PageMargins disponíveis em PrintPageEventArgs. Essa abordagem não funcionou muito bem, mas a solução correta era quase tão fácil. O programa PrintEllipseWithMargins (que você pode executar em bit.ly/fCBs3X) é igual ao primeiro programa, exceto que a propriedade Margin é definida na Ellipse e, em seguida, a Ellipse é definida como o filho de uma Border, que preenche a área imprimível. Como alternativa, você pode definir a propriedade Padding na Border. A Figura 2 mostra o novo método OnPrintPage.

Figura 2 O método OnPrintPage para calcular as margens

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;
}

O objeto PageVisual

Não há métodos gráficos ou classes gráficas especiais associadas à impressora. Você “desenha” algo na página da impressora da mesma forma que você “desenha” algo no monitor de vídeo, montando uma árvore visual de objetos que derivam do FrameworkElement. Essa árvore pode incluir elementos Panel, incluindo Canvas. Para imprimir essa árvore visual, defina o elemento superior para a propriedade PageVisual de PrintPageEventArgs. (PageVisual é definida como um UIElement, que é a classe pai do FrameworkElement, mas na prática, tudo que você configurar na PageVisual derivará do FrameworkElement.)

Quase todas as classes que derivam do FrameworkElement têm implementações não triviais dos métodos MeasureOverride e ArrangeOverride para fins de layout. Em seu método MeasureOverride, um elemento determina seu tamanho desejado, algumas vezes determinando os tamanhos desejados dos seus filhos ao chamar os métodos Measure dos seus filhos. No método ArrangeOverride, um elemento organiza seus filhos em relação a ele mesmo ao chamar os métodos Arrange dos filhos.

Quando você define um elemento para a propriedade PageVisual de PrintPageEventArgs, o sistema de impressão do Silverlight chama Measure no elemento superior com o tamanho da PrintableArea. É assim que (por exemplo) a Ellipse ou Border é dimensionada automaticamente de acordo com a área imprimível da página.

No entanto, você também pode definir essa propriedade PageVisual para um elemento que já faz parte de uma árvore visual em exibição na janela do programa. Nesse caso, o sistema de impressão não chama Measure para esse elemento, mas usa as medidas e o layout determinados para o monitor de vídeo. Isso permite imprimir algo da janela do programa com razoável fidelidade, mas também significa que o que você imprimir pode ser recortado para se ajustar ao tamanho da página.

Você pode, é claro, definir propriedades Width e Height explícitas nos elementos que você imprimir, bem como usar o tamanho da PrintableArea para ajudar.

Dimensionamento e rotação

O próximo programa que escolhi se tornou mais desafiador do que eu previa. O objetivo era um programa que permitisse a impressão de qualquer arquivo de imagem com suporte no Silverlight — ou seja, arquivos PNG e JPEG — armazenado na máquina local do usuário. Esse programa usa a classe OpenFileDialog para carregar esses arquivos. Por razões de segurança, a OpenFileDialog retorna somente um objeto FileInfo que permite que o programa abra o arquivo. Nenhum nome de arquivo ou diretório é fornecido.

Queria que esse programa imprimisse o bitmap com o maior tamanho possível na página (excluindo uma margem predefinida) sem alterar a taxa de proporção do bitmap. Geralmente, isso é muito fácil: o modo Stretch padrão do elemento Image é Uniform, o que significa que o bitmap é esticado ao máximo sem distorção.

No entanto, decidi que não queria exigir que o usuário definisse especificamente o modo retrato ou paisagem na impressora de acordo com a imagem específica. Se a impressora estivesse definida para o modo retrato, e a imagem fosse maior do que a altura, queria que a imagem fosse impressa de lado na página retrato. Esse pequeno recurso imediatamente tornou o programa muito mais complexo.

Se estivesse escrevendo um programa do WPF para fazer isso, o próprio programa poderia alternar a impressora para o modo retrato ou paisagem. Mas isso não é possível no Silverlight. A interface da impressora é definida para que somente o usuário possa alterar configurações como essa.

Mais uma vez, se estivesse escrevendo um programa do WPF, outra opção seria definir um LayoutTransform no elemento Image para girá-lo 90 graus. Em seguida, o elemento Image girado seria redimensionado para caber na página, e o próprio bitmap seria ajustado para caber no elemento Image.

Mas o Silverlight não tem suporte para LayoutTransform. O Silverlight oferece suporte somente para RenderTransform, portanto, se o elemento Image precisar ser girado para acomodar uma imagem paisagem impressa no modo retrato, o elemento Image também deve ser ajustado manualmente de acordo com as dimensões da página paisagem.

Você pode testar minha primeira tentativa em bit.ly/eMHOsB. O método OnPrintPage cria um elemento Image e define a propriedade Stretch para None, o que significa que o elemento Image exibe o bitmap no tamanho em pixels, o que na impressora significa que cada pixel é considerado 1/96 de polegada. Em seguida, o programa gira, dimensiona e converte esse elemento Image, calculando uma transformação que aplica à propriedade RenderTransform do elemento Image.

É claro que a parte difícil desse código é a matemática, por isso, foi agradável ver o programa funcionar com imagens retrato e paisagem com a impressora definida para os modos retrato e paisagem.

No entanto, foi particularmente desagradável ver o programa falhar para imagens grandes. Você pode testá-lo com imagens que têm dimensões um pouco maiores (quando divididas por 96) do que o tamanho da página em polegadas. A imagem é exibida no tamanho correto, mas não integralmente.

O que está acontecendo aqui? Bem, é algo que já vi antes em monitores de vídeo. Lembre-se de que o RenderTransform afeta somente a maneira como o elemento é exibido, e não como ele aparece para o sistema de layout. Para o sistema de layout, estou exibindo um bitmap em um elemento Image com Stretch definido como None, o que significa que o elemento Image é tão grande quanto o próprio bitmap. Se o bitmap for maior do que a página da impressora, uma parte daquele elemento Image não precisará ser renderizada, e será, de fato, recortada, independentemente de um RenderTransform que estiver reduzindo o elemento Image de maneira adequada.

Minha segunda tentativa, que você pode testar em bit.ly/g4HJ1C, usa uma estratégia um pouco diferente. O método OnPrintPage é mostrado na Figura 3. O elemento Image recebe configurações Width e Height explícitas que o tornam exatamente do tamanho da área de exibição calculada. Como tudo está dentro da área imprimível da página, nada será recortado. O modo Stretch está definido como Fill, o que significa que o bitmap preenche o elemento Image independentemente da taxa de proporção. Se o elemento Image não for girado, uma dimensão está ajustada corretamente, e a outra dimensão deve ter um fator de dimensionamento aplicado que reduz o tamanho. Se o elemento Image também precisar ser girado, os fatores de dimensionamento devem acomodar a taxa de proporção diferente do elemento Image girado.

Figura 3 Impressão de uma imagem no 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;
}

O código certamente é confuso — e suspeito que possa haver simplificações que não são evidentes para mim agora — mas funciona para bitmaps de todos os tamanhos.

Outra abordagem é girar o próprio bitmap, e não o elemento Image. Crie um WriteableBitmap do objeto BitmapImage carregado, e um segundo WritableBitmap com as dimensões horizontal e vertical trocadas. Em seguida, copie todos os pixels do primeiro WriteableBitmap no segundo com as linhas e colunas trocadas.

Várias páginas de calendário

A derivação de UserControl é uma técnica muito popular em programação do Silverlight para criar um controle reutilizável sem muito trabalho. Grande parte de um UserControl é uma árvore visual definida em XAML.

Você também pode derivar de UserControl para definir uma árvore visual para impressão! Essa técnica é ilustrada no programa PrintCalendar, que você pode testar em bit.ly/dIwSsn. Você digita um mês inicial e um mês final, e o programa imprime todos os meses nesse intervalo, um mês em cada página. Você pode colar as páginas na parede e marcá-las, como um verdadeiro calendário de parede.

Depois da minha experiência com o programa PrintImage, não queria me preocupar com margens ou orientação; em vez disso, inclui um Button que atribui a responsabilidade ao usuário, como mostrado na Figura 4.

The PrintCalendar Button

Figura 4 O Button PrintCalendar

O UserControl que define a página do calendário é chamado de CalendarPage, e o arquivo XAML é mostrado na Figura 5. Um TextBlock próximo à parte superior exibe o mês e o ano. Ele é seguido por uma segunda Grid com sete colunas para os dias da semana e seis linhas para até seis semanas ou semanas parciais em um mês.

Figura 5 O layout do 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>

Ao contrário da maioria dos derivados do UserControl, o CalendarPage define um construtor com um parâmetro, como mostrado na Figura 6.

Figura 6 O construtor codebehind do 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);
}

O parâmetro é um DateTime, e o construtor usa as propriedades Month e Year para criar uma Border contendo um TextBlock para cada dia do mês. A cada um deles são atribuídas propriedades anexadas Grid.Row e Grid e, em seguida, eles são adicionados à Grid. Como você sabe, os meses geralmente tem apenas cinco semanas, e ocasionalmente fevereiro tem quatro semanas, portanto, os objetos RowDefinition são removidos da Grid quando não são necessários.

Os derivados do UserControl geralmente não têm construtores com parâmetros porque eles costumam fazer parte de árvores virtuais maiores. Mas o CalendarPage não é usado dessa maneira. Em vez disso, o manipulador do PrintPage simplesmente atribui uma nova instância do CalendarPage à propriedade PageVisual de PrintPageEventArgs. Aqui está o corpo completo do manipulador, ilustrando claramente a quantidade de trabalho sendo realizada pelo CalendarPage:

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

A adição de uma opção de impressão a um programa geralmente é considerado um trabalho cansativo e que envolve vários códigos. Conseguir definir grande parte de uma página impressa em um arquivo XAML torna tudo menos assustador.        

Charles Petzold é um editor colaborador da MSDN Magazine há muito tempo. Seu novo livro, “Programming Windows Phone 7” (Microsoft Press, 2010), está disponível gratuitamente para download em bit.ly/cpebookpdf.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Saied Khanahmadi e Robert Lyon