Este artigo foi traduzido por máquina.

Fronteiras da interface do usuário

Princípios de paginação

Charles Petzold

Baixar o código de exemplo

Charles PetzoldPor algumas décadas agora, programadores especializados em computação gráfica sabem que as tarefas mais difíceis não envolvem bitmaps ou vector graphics, mas bom texto antigo.

Texto é difícil por várias razões, a maioria dos quais referem-se ao fato de que é geralmente desejável para o texto a ser lido. Além disso, a altura real de um pedaço de texto é apenas casualmente relacionada ao tamanho de fonte, e as larguras de caracteres variam de acordo com a personagem. Caracteres são frequentemente combinadas em palavras (que devem ser mantidas juntos). Palavras são combinadas em parágrafos, que devem ser separados em várias linhas. Parágrafos são combinados em documentos, que devem ser rolados ou separados em várias páginas.

Na última edição discuti o suporte a impressão em Silverlight 4, e agora eu gostaria de discutir imprimir o texto. A diferença mais importante entre exibir texto na tela e texto de impressão da impressora pode ser resumida em uma verdade simple adequada para o enquadramento: A página de impressora possui sem barras de rolagem.

Um programa que precisa imprimir mais texto do que pode caber em uma página deve separar o texto em várias páginas. Esta é uma tarefa de programação não-trivial, conhecida como paginação. Acho que é bastante interessante que paginação realmente se tornou mais importante nos últimos anos, mesmo que impressão se tornou menos importante. Aqui está uma outra verdade simple você pode emoldurar e colocar-se em sua parede: paginação — ele não é mais apenas para impressoras.

Pegar qualquer leitor de e-book — ou qualquer pequeno dispositivo que permite que você leia periódicos, livros ou até mesmo desktop software de leitura de livro — e você encontrará documentos que foram organizados em páginas. Por vezes estas páginas são pré-formatado e fixa (com formatos de arquivo PDF e XPS), mas em muitos casos páginas pode por dinamicamente refluído (tais como com EPUB ou formatos proprietários e-book). Para documentos que podem ser refluídos, algo tão simples como alterar o tamanho da fonte pode exigir uma seção inteira do documento a ser repaginated dinamicamente enquanto o usuário está esperando, provavelmente com impaciência.

Paginação em tempo real — e fazê-lo rapidamente — transforma um trabalho não-trivial de programação que podem ser excepcionalmente desafiador. Mas vamos não assustar-nos muito. Eu vou construir para o material duro ao longo do tempo e por agora vai começar muito simplesmente.

Empilhamento TextBlocks

Silverlight fornece várias classes que exibem texto:

  • O elemento Glyphs é talvez a uma classe com que a maioria dos programadores do Silverlight estão menos familiarizados. A fonte usada em glifos deve ser especificada com uma URL ou um objeto de fluxo, que faz com que o elemento mais úteis na página fixo documentos ou pacotes de documento que dependem fortemente de fontes específicas. Não vai discutir o elemento Glyphs.
  • A classe de parágrafo é nova no Silverlight 4 e imita uma classe proeminente no suporte a documentos do Windows Presentation Foundation (WPF). Mas n º é usado principalmente em conjunto com o RichTextBox, e não é suportado no Silverlight para Windows Phone 7.
  • E depois há TextBlock, que muitas vezes é usado de maneira simples por configuração a propriedade de texto — mas ele também pode combinar texto de diferentes formatos com sua propriedade de linhas internas. TextBlock é também crucial capaz de quebrar o texto em várias linhas quando o texto excede a largura permitida.

TextBlock tem a virtude de ser familiar para programadores do Silverlight e adequado para as nossas necessidades, o que é o que eu vou estar usando.

O projeto SimplestPagination (disponível com o código para download deste artigo) é projetado para imprimir documentos de texto simples. O programa trata cada linha de texto como um parágrafo que talvez precise ser envolvido em várias linhas. No entanto, o programa assume implicitamente que esses números não são muito longos. Esta hipótese provém a limitação do programa para quebrar os parágrafos entre páginas. (Que é a parte mais simples do nome de SimplestPagination). Se um parágrafo é muito grande para caber em uma página, o parágrafo inteiro é movido para a página seguinte, e se o número for muito grande para uma única página, em seguida, será truncado.

Você pode executar o programa de SimplestPagination na bit.ly/elqWgU. Ele tem apenas dois botões: Load e impressão. O botão Carregar exibe um OpenFileDialog que permite que você escolha um arquivo de armazenamento local. Imprimir pagina ele e imprime-o.

O OpenFileDialog retorna um objeto de FileInfo. O método OpenText FileInfo retorna um StreamReader, que tem um método de ReadLine para ler linhas inteiras de texto. Figura 1 mostra o manipulador PrintPage.

Figura 1 Manipulador PrintPage de SimplestPagination

void OnPrintDocumentPrintPage(
  object sender, PrintPageEventArgs args) {

  Border border = new Border {
    Padding = new Thickness(
      Math.Max(0, desiredMargin.Left - args.PageMargins.Left),
      Math.Max(0, desiredMargin.Top - args.PageMargins.Top),
      Math.Max(0, desiredMargin.Right - args.PageMargins.Right),
      Math.Max(0, desiredMargin.Bottom - args.PageMargins.Bottom))
  };

  StackPanel stkpnl = new StackPanel();
  border.Child = stkpnl;
  string line = leftOverLine;

  while ((leftOverLine != null) || 
    ((line = streamReader.ReadLine()) != null)) {

    leftOverLine = null;

    // Check for blank lines; print them with a space
    if (line.Length == 0)
      line = " ";

    TextBlock txtblk = new TextBlock {
      Text = line,
      TextWrapping = TextWrapping.Wrap
    };

    stkpnl.Children.Add(txtblk);
    border.Measure(new Size(args.PrintableArea.Width, 
      Double.PositiveInfinity));

    // Check if the page is now too tall
    if (border.DesiredSize.Height > args.PrintableArea.Height &&
      stkpnl.Children.Count > 1) {

      // If so, remove the TextBlock and save the text for later
      stkpnl.Children.Remove(txtblk);
      leftOverLine = line;
      break;
    }
  }

  if (leftOverLine == null)
    leftOverLine = streamReader.ReadLine();

  args.PageVisual = border;
  args.HasMorePages = leftOverLine != null;
}

Como de costume, a página impressa é uma árvore visual. A raiz desta árvore visual específico é o elemento de fronteira, que é dada uma propriedade Padding para obter margens (meia polegada) de 48-unidade, conforme indicado no campo desiredMargins. A propriedade de PageMargins dos argumentos do evento fornece as dimensões das margens de impressão da página, portanto, a propriedade Padding precisa especificar espaço adicional para trazer o total de 48.

Um StackPanel, em seguida, é feito uma criança da fronteira e TextBlock elementos são adicionados a StackPanel. Depois de cada uma, o método de medida da borda é chamado com uma restrição horizontal de largura para impressão da página e uma restrição vertical de infinito. A propriedade DesiredSize então revela quão grande deve ser a fronteira. Se a altura excede a altura do PrintableArea, o TextBlock deve ser removida dos StackPanel (mas não se ele é o único).

O campo leftOverLine armazena o texto que não obter impressos na página. Eu também usá-lo para sinalizar que o documento foi concluído por chamar ReadLine sobre o StreamReader uma última vez. (Obviamente se StreamReader tinha um método PeekLine, este campo não seria necessário.)

O código para download contém uma pasta de documentos com um arquivo chamado EmmaFirstChapter.txt. Este é o primeiro capítulo do romance de Jane Austen, "Emma", especialmente preparado para este programa: todos os parágrafos são linhas simples, e eles estão separados por linhas em branco. Com a fonte padrão do Silverlight, é cerca de quatro páginas de comprimento. Páginas impressas não são fáceis de ler, mas isso é só porque as linhas são demasiado grande para o tamanho da fonte.

Este arquivo também revela um pequeno problema com o programa: pode ser que uma das linhas em branco é o primeiro parágrafo em uma página. Se for esse o caso, ele não deve ser impresso. Mas isso é apenas adicional lógica.

Para imprimir o texto que tem números reais, você poderia usar linhas em branco entre parágrafos, ou talvez você prefira ter mais controle por configuração a propriedade Margin do TextBlock. Também é possível ter um recuo na primeira linha, alterando a instrução que atribui a propriedade Text de TextBlock deste:

Text = line,
 
to this:
Text = "     " + line,

Mas nenhuma dessas técnicas funcionaria bem ao imprimir código-fonte.

Dividindo o TextBlock

Depois de testar o programa SimplestPagination, você provavelmente vai concluir que sua maior falha é a incapacidade para quebrar os parágrafos entre páginas.

Uma abordagem para corrigir este problema é ilustrada no programa BetterPagination, que você pode executar a bit.ly/ekpdZb. Este programa é muito como SimplestPagination exceto em casos quando um TextBlock é adicionado para o StackPanel, que faz com que a altura total exceda a página. Em SimplestPagination, esse código simplesmente removido o TextBlock inteira do StackPanel:

// Check if the page is now too tall
if (border.DesiredSize.Height > args.PrintableArea.Height &&
  stkpnl.Children.Count > 1) {

  // If so, remove the TextBlock and save the text for later
  stkpnl.Children.Remove(txtblk);
  leftOverLine = line;
  break;
}
BetterPagination now calls a method named RemoveText:
// Check if the page is now too tall
if (border.DesiredSize.Height > args.PrintableArea.Height) {
  // If so, remove some text and save it for later
  leftOverLine = RemoveText(border, txtblk, args.PrintableArea);
  break;
}

RemoveText é mostrado na Figura 2. O método simplesmente remove uma palavra em um momento do final da propriedade Text de TextBlock e verifica se isso ajuda o TextBlock caber na página. Todo o texto removido é acumulado em um StringBuilder que o manipulador PrintPage salva como leftOverLine para a próxima página.

Figura 2 O método de RemoveText de BetterPagination

string RemoveText(Border border, 
  TextBlock txtblk, Size printableArea) {

  StringBuilder leftOverText = new StringBuilder();

  do {
    int index = txtblk.Text.LastIndexOf(' ');

    if (index == -1)
      index = 0;

    leftOverText.Insert(0, txtblk.Text.Substring(index));
    txtblk.Text = txtblk.Text.Substring(0, index);
    border.Measure(new Size(printableArea.Width, 
      Double.PositiveInfinity));

    if (index == 0)
      break;
  }
  while (border.DesiredSize.Height > printableArea.Height);

  return leftOverText.ToString().TrimStart(' ');
}

Não é bonito, mas funciona. Tenha em mente que se você está lidando com texto formatado (diferentes fontes, tamanhos de fonte, negrito e itálico), então você estará trabalhando não com a propriedade Text de TextBlock, mas com a propriedade de linhas internas, e que complica o processo imensamente.

E sim, há definitivamente mais rápidas maneiras para fazer isso, embora eles certamente são mais complexos. Por exemplo, um algoritmo binário pode ser implementado: metade das palavras podem ser removidas e se ele se encaixa na página, metade dos quais foi removido pode ser restaurada, e se que não se encaixa na página, em seguida, metade dos quais foi restaurado pode ser removida e assim por diante.

No entanto, tenha em mente que este é o código escrito para impressão. O gargalo de impressão é a impressora em si, portanto, enquanto o código pode gastar mais uns segundos testando cada TextBlock, provavelmente não vai ser perceptível.

Mas você pode começar a se perguntar exatamente quanto está acontecendo nos bastidores quando você chama medida no elemento raiz. Certamente todos os elementos individuais do TextBlock estão recebendo chamadas de medida, e eles estão usando Silverlight internals para determinar a quantidade de espaço que Cadeia de caracteres de texto realmente ocupa com determinada fonte e tamanho da fonte.

Você pode perguntar se o código como este ainda seria tolerável para paginating um documento para um monitor de vídeo em um dispositivo lento.

Então, vamos tentar.

Paginação no telefone Windows 7

Meu objetivo (que não será concluído neste artigo) é construir um e-book reader para Windows Phone 7 adequado para leitura de arquivos de texto simples livro baixado do Project Gutenberg (gutenberg.org). Como você sabe, o Project Gutenberg remonta a 1971 e foi a primeira biblioteca digital. Por muitos anos, concentrou-se no fornecimento de livros de domínio público — muitas vezes os clássicos da literatura inglesa — em um formato de texto simples ASCII. Por exemplo, a "Emma" completa por Jane Austen é o arquivo gutenberg.org/files/158/158.txt.

Cada livro é identificado por um número inteiro positivo para o seu nome de arquivo. Como você pode ver aqui, "Emma" é 158 e sua versão de texto está no arquivo 158.txt. Em anos mais recentes Project Gutenberg forneceu outros formatos como EPUB e HTML, mas eu vou ficar com texto simples para este projeto, por razões óbvias de simplicidade.

O projeto de EmmaReader para Windows Phone 7 inclui txt 158. como um recurso e permite que você leia o livro inteiro no telefone. Figura 3 mostra o programa em execução no emulador de Windows Phone 7. Para suporte de gesto, o projeto requer o Silverlight para Windows Phone Toolkit, disponível para download do silverlight.codeplex.com. Toque ou movimento esquerda para ir para a próxima página. Percorra direita para ir para a página anterior.

EmmaReader Running on the Windows Phone 7 Emulator

Figura 3 EmmaReader em execução no Windows Phone 7 emulador

O programa não tem quase nenhum recurso exceto aquelas necessárias para torná-lo razoavelmente utilizável. Obviamente, eu vou reforçar este programa, especialmente para que você possa ler outros livros além de "Emma" — talvez até livros de sua escolha! Mas para pregar para baixo as noções básicas, é mais fácil se concentrar em um único livro.

Se você examinar 158.txt, você vai descobrir a característica mais significativa de arquivos de texto simples projeto Gutenberg: cada parágrafo consiste em uma ou mais linhas consecutivas 72 caracteres delimitadas por uma linha em branco. Para transformar isso em um formato adequado para TextBlock quebrar linhas, alguns pré-processamento é necessário para concatenar essas linhas consecutivas individuais em um. Isso é feito no método PreprocessBook em EmmaReader. O livro inteiro — incluindo linhas de comprimento zero separar parágrafos — então é armazenado como um campo chamado parágrafos do tipo lista <string>. Esta versão do programa não tenta dividir o livro em capítulos.

Como o livro é paginado, cada página é identificada como um objeto do tipo PageInfo com apenas duas propriedades inteiro: ParagraphIndex é um índice para a lista de pontos e CharacterIndex é um índice para a Cadeia de caracteres para que o ponto. Estes dois índices indicam o parágrafo e caractere que começa a página. Os dois índices para a primeira página, obviamente, são dois zero. Como cada página é paginada, são determinados os índices para a próxima página.

O programa não tenta paginate o livro inteiro de uma vez. Com o layout de página que defini e a fonte padrão do Silverlight para Windows Phone 7, "Emma" estende-se a 845 páginas e requer nove minutos para chegar ao executar em um dispositivo real. É evidente que a técnica que estou usando para paginação — que exigem o Silverlight executar um passe de medida para cada página e muito frequentemente muitas vezes se um parágrafo continua de uma página para a próxima — toma um pedágio. Eu vou explorar algumas técnicas mais rápidas nas colunas posteriores.

Mas o programa não precisa paginar o livro inteiro de uma vez. Como você começar a ler no início de um livro e Avançar página por página, o programa só precisa paginar uma página por vez.

Características e necessidades

Inicialmente pensei que esta primeira versão do EmmaReader não teria nenhuma característica em todos excepto aqueles necessário para ler o livro do começo ao fim. Mas que seria cruel. Por exemplo, suponha que você está lendo o livro, você chegou a página 100 ou assim e você desligar o ecrã para colocar o telefone no bolso. Nesse momento, o programa é para exclusão, o que significa que ele é essencialmente encerrado. Quando volta a ligar a tela, o programa é iniciado novamente e você volta em uma página. Em seguida, você teria que 99 páginas para continuar lendo onde você parou de tocar!

Por esse motivo, o programa salva o número da página atual no armazenamento isolado quando o programa é encerrado ou marcados para exclusão. Você sempre vai saltar para a página que você deixou. (Se você experimentar esse recurso ao executar o programa em Visual Studio, no emulador ou um telefone real, não se esqueça de encerrar o programa pressionando o botão Voltar, não interrompendo depuração no Visual Studio. Parar a depuração não permite que o programa finalizar corretamente e acessar o armazenamento isolado.)

Não guardar o número de página no armazenamento isolado é realmente suficiente. Se apenas o número da página foi salvo, o programa teria que paginar as primeiro 99 páginas para exibir o centésimo. O programa precisa, pelo menos, o objeto PageInfo para essa página.

Mas esse objeto PageInfo único não é suficiente, quer. Suponha que recarrega o programa, ele usa o objeto PageInfo para exibir a página 100 e, em seguida, você decide de chicotear o dedo direita para ir para a página anterior. O programa não tem o objeto PageInfo para página 99, assim que ele precisa repaginar as primeiro 98 páginas.

Por esse motivo, progressivamente ao ler o livro e cada página é paginado, o programa mantém uma lista do tipo lista <PageInfo> com todos os objetos de PageInfo que ele determinou que até agora. Esta lista inteira é salvos para armazenamento isolado. Se você experimentar com código-fonte do programa — por exemplo, alterar o layout ou o tamanho da fonte ou substituindo o livro inteiro com outro — Lembre-se que qualquer alteração que afete a paginação invalidará esta lista de objetos PageInfo. Você eliminar o programa de telefone (ou o emulador), segurando o dedo sobre o nome do programa na lista Iniciar e selecionando Uninstall. Esta é atualmente a única maneira de apagar os dados armazenados de armazenamento isolado.

Aqui é a grade de conteúdo em MainPage.xaml:

<Grid x:Name="ContentPanel" 
  Grid.Row="1" Background="White">
  <toolkit:GestureService.GestureListener>
  <toolkit:GestureListener 
    Tap="OnGestureListenerTap"
    Flick="OnGestureListenerFlick" />
  </toolkit:GestureService.GestureListener>
            
  <Border Name="pageHost" Margin="12,6">
    <StackPanel Name="stackPanel" />
  </Border>
</Grid>

Durante a paginação, o programa obtém a ActualWidth e ActualHeight do elemento Border e usa-os da mesma forma que a propriedade PrintableArea é usada em programas de impressão. Os elementos TextBlock para cada parágrafo (e as linhas em branco entre os parágrafos) são adicionados para o StackPanel.

O método Paginate é mostrado na Figura 4. Como você pode ver, é muito semelhante aos métodos usados em programas de impressão, exceto que ele está acessando uma lista de objetos de Cadeia de caracteres com base em paragraphIndex e characterIndex. O método também atualiza esses valores para a próxima página.

Figura 4 O Paginate Método em EmmaReader

void Paginate(ref int paragraphIndex, ref int characterIndex) {
  stackPanel.Children.Clear();

  while (paragraphIndex < paragraphs.Count) {
    // Skip if a blank line is the first paragraph on a page
    if (stackPanel.Children.Count == 0 &&
      characterIndex == 0 &&
      paragraphs[paragraphIndex].Length == 0) {
        paragraphIndex++;
        continue;
    }

    TextBlock txtblk = new TextBlock {
      Text = 
        paragraphs[paragraphIndex].Substring(characterIndex),
      TextWrapping = TextWrapping.Wrap,
      Foreground = blackBrush
    };

    // Check for a blank line between paragraphs
    if (txtblk.Text.Length == 0)
      txtblk.Text = " ";

    stackPanel.Children.Add(txtblk);
    stackPanel.Measure(new Size(pageHost.ActualWidth, 
      Double.PositiveInfinity));

    // Check if the StackPanel fits in the available height
    if (stackPanel.DesiredSize.Height > pageHost.ActualHeight) {
      // Strip words off the end until it fits
      do {
        int index = txtblk.Text.LastIndexOf(' ');

        if (index == -1)
          index = 0;

        txtblk.Text = txtblk.Text.Substring(0, index);
        stackPanel.Measure(new Size(pageHost.ActualWidth, 
          Double.PositiveInfinity));

        if (index == 0)
          break;
      }
      while (stackPanel.DesiredSize.Height > pageHost.ActualHeight);

      characterIndex += txtblk.Text.Length;

      // Skip over the space
      if (txtblk.Text.Length > 0)
        characterIndex++;

      break;
    }
    paragraphIndex++;
    characterIndex = 0;
  }

  // Flag the page beyond the last
  if (paragraphIndex == paragraphs.Count)
    paragraphIndex = -1;
}

Como você pode ver na Figura 3, o programa exibe um número de página. Mas observe que não apresenta um número de páginas, porque isso não pode ser determinado até que o livro inteiro é paginado. Se você estiver familiarizado com leitores de e-book comercial, você provavelmente está ciente que a exibição de números de página e o número de páginas é um grande problema.

Uma característica que os usuários acham necessários em leitores de e-book é a capacidade de alterar a fonte ou o tamanho da fonte. No entanto, do ponto de vista do programa, isto tem consequências mortais: todas as informações de paginação acumuladas até agora tem que ser descartados, e o livro tem de ser repaginated para a página atual, que ainda não é a mesma página que era antes.

Outro recurso interessante em leitores de e-book é a capacidade para navegar até o início dos capítulos. A separação de um livro em capítulos realmente ajuda o programa lidar com paginação. Cada capítulo começa em uma nova página, portanto, as páginas de cada capítulo podem ser paginadas independentemente dos outros capítulos. Saltar para o início de um novo capítulo é trivial. (No entanto, se o usuário, em seguida, de movimentos de direita para a última página do capítulo anterior, o capítulo anterior inteiro deve ser re-paginated!)

Você provavelmente vai também concorda que este programa necessita de uma melhor transição de página. Tendo a nova página apenas pop no lugar é insatisfatório porque ele não fornece feedback adequado que realmente virou a página, ou que apenas uma página se transformou em vez de várias páginas. O programa definitivamente precisa de algum trabalho. Enquanto isso, aproveite o romance.

Charles Petzold é um editor antigo MSDN Magazine*.*Seu livro recente, "Programação Windows Phone 7" (Microsoft Press, 2010), está disponível como um download gratuito em bit.ly/cpebookpdf.

Graças aos seguinte perito técnico para revisão deste artigo: Jesse Liberty