Condividi tramite


Il presente articolo è stato tradotto automaticamente.

Frontiere dell'interfaccia utente

Principi di impaginazione

Charles Petzold

Scaricare il codice di esempio

Charles PetzoldPer decenni un paio, programmatori, specializzato in computer grafica hanno saputo che i compiti più difficili non coinvolgere le bitmap o grafica, ma buon vecchio testo vettoriale.

Testo è dura per diverse ragioni, la maggior parte dei quali riguardano il fatto che è solito auspicabile per il testo di essere leggibile. Inoltre, l'altezza attuale di un pezzo di testo solo casualmente è legato alla sua dimensione del carattere e larghezze di carattere variano a seconda del carattere. Caratteri vengono spesso combinati in parole (che devono essere tenute insieme). Parole vengono combinati in paragrafi, che devono essere separati in più righe. Paragrafi vengono combinati in documenti, che devono essere fatti scorrere o separati in più pagine.

Nell'ultimo numero ho discusso il supporto per la stampa in Silverlight 4, e ora mi piacerebbe discutere la stampa del testo. La più importante differenza tra la visualizzazione di un testo sullo schermo e la stampa di testo sulla stampante può essere riassunta in una semplice verità suitable for framing: la pagina di stampante non ha le barre di scorrimento.

Un programma che ha bisogno di stampare il testo più che in grado di inserirsi in una pagina deve separare il testo in più pagine. Questo è un compito banale di programmazione conosciuto come l'impaginazione. Lo trovo molto interessante che l'impaginazione in realtà è diventata più importante negli ultimi anni, anche se la stampa è diventata meno importante. Ecco un altro semplice verità è possibile inquadrare e messo sul vostro muro: impaginazione — non è solo per le stampanti più.

Raccogliere qualsiasi lettore di e-book — o qualsiasi piccolo dispositivo che consente di legge periodici, libri o software di lettura del libro anche desktop — e troverete i documenti che sono stati organizzati in pagine. A volte queste pagine sono preformattate e fisso (con formati di file PDF e XPS), ma in molti casi pagine può da dinamicamente adattato (come ad esempio con EPUB o formati proprietari e-book). Per i documenti che possono essere adattati, qualcosa di semplice come cambiare la dimensione del carattere può richiedere un'intera sezione del documento a reimpaginato dinamicamente mentre l'utente è in attesa, probabilmente con impazienza.

L'impaginazione al volo — e farlo rapidamente — si trasforma un lavoro di programmazione non banale in quella che può essere particolarmente impegnativo. Ma diciamo non spaventare noi stessi troppo. Io sarò accumularsi per la roba dura nel tempo e per ora inizierà molto semplicemente.

Accatastamento TextBlock

Silverlight fornisce diverse classi che consentono di visualizzare testo:

  • L'elemento Glyphs è forse una sola classe con cui la maggior parte dei programmatori di Silverlight sono meno familiari. Il tipo di carattere utilizzato in glifi deve essere specificato con un URL o un oggetto Stream, che rende l'elemento più utile in documenti pagina fissa o pacchetti di documento che si affidano pesantemente i caratteri specifici. Non discutere l'elemento Glyphs.
  • La classe paragrafo è di nuova con Silverlight 4 e imita una classe prominente nel supporto di documenti di Windows Presentation Foundation (WPF). Ma paragrafo è utilizzato principalmente in combinazione con il controllo RichTextBox, e non è supportato in Silverlight per Windows Phone 7.
  • E poi c'è TextBlock, che viene spesso utilizzato in modo semplice impostando la proprietà Text — ma anche possibile combinare testo di diversi formati con la relativa proprietà Inlines. TextBlock ha anche la capacità cruciale per disporre il testo in più righe quando il testo supera la larghezza ammissibile.

TextBlock ha il merito di essere conosciute dai programmatori di Silverlight e adatto alle nostre esigenze, quindi, che è quello che sarò utilizzando.

Il progetto SimplestPagination (disponibile con il codice scaricabile di questo articolo) è progettato per stampare documenti di testo normale. Il programma considera ogni riga di testo come un paragrafo che potrebbe aver bisogno di essere avvolto in più righe. Tuttavia, il programma presuppone implicitamente che questi paragrafi non sono molto lunghi. Questa ipotesi deriva dalla limitazione del programma di rompere tutti i paragrafi in pagine. (Che è la parte più semplice del nome SimplestPagination). Se un paragrafo è troppo lungo per rientrare in una pagina, l'intero paragrafo viene spostato alla pagina successiva, e se il paragrafo è troppo grande per una singola pagina, quindi esso è troncato.

È possibile eseguire il programma di SimplestPagination a bit.ly/elqWgU. Ha solo due pulsanti: carico e stampa. Il pulsante carica consente di visualizzare un oggetto OpenFileDialog che ti permette di selezionare un file da archiviazione locale. Stampa esso esegue l'impaginazione e stampa.

OpenFileDialog restituisce un oggetto di FileInfo. Il metodo OpenText di FileInfo restituisce uno StreamReader, che dispone di un metodo di ReadLine per la lettura di intere righe di testo. Figura 1 viene illustrato il gestore PrintPage.

Figura 1 Il gestore PrintPage di 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;
}

Come al solito, la pagina stampata è un struttura ad albero visuale. La radice di questa particolare struttura ad albero visuale è l'elemento di confine, che si è dato una proprietà Padding per ottenere margini (mezzo pollice) 48-unità, come indicato nel campo desiredMargins. La proprietà PageMargins degli argomenti dell'evento fornisce le dimensioni dei margini non stampabili della pagina, in modo che la proprietà Padding deve specificare ulteriore spazio per portare il totale fino a 48.

Un oggetto StackPanel è poi fatto un bambino del confine, e si aggiungono elementi TextBlock a StackPanel. Dopo ogni uno, viene chiamato il metodo di misura del confine con un vincolo orizzontale della larghezza stampabile della pagina e un vincolo verticale dell'infinito. La proprietà DesiredSize poi rivela quanto grande deve essere la frontiera. Se l'altezza supera l'altezza della PrintableArea, poi l'oggetto TextBlock deve essere rimosso dalla StackPanel (ma se non è l'unico).

Il campo leftOverLine memorizza il testo che non ottenere stampato sulla pagina. Io uso anche per segnalare che il documento è completo chiamando ReadLine sul StreamReader un'ultima volta. (Ovviamente se StreamReader aveva un metodo PeekLine, in questo campo sarebbe richiesto.)

Il codice scaricabile contiene una cartella di documenti con un file denominato EmmaFirstChapter.txt. Questo è il primo capitolo del romanzo di Jane Austen, "Emma", appositamente preparato per questo programma: tutti i paragrafi sono singole righe, ed essi sono separati da righe vuote. Con il font predefinito di Silverlight, è circa quattro pagine di lunghezza. Le pagine non sono facili da leggere, ma che è solo perché le linee sono troppo larghe per la dimensione del carattere.

Questo file rivela anche un piccolo problema con il programma: potrebbe essere che una delle righe vuote è il primo paragrafo su una pagina. Se è il caso, non dovrebbe essere stampato. Ma questo è solo ulteriore logica.

Per stampa testo che ha paragrafi effettivi, si potrebbero usare righe vuote tra i paragrafi, o si potrebbe preferire di avere più controllo impostando la proprietà Margin di TextBlock. È anche possibile avere un rientro della prima riga modificando l'istruzione che assegna la proprietà Text dell'oggetto TextBlock da questo:

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

Ma nessuna di queste tecniche avrebbe funzionato bene quando si stampa il codice sorgente.

Dividere l'oggetto TextBlock

Dopo la sperimentazione con il programma SimplestPagination, avrete probabilmente concludere che il suo più grande difetto è l'incapacità di rompere tutti i paragrafi in pagine.

Un approccio per risolvere questo problema è illustrato nel programma BetterPagination, che può essere eseguito a bit.ly/ekpdZb. Questo programma è molto simile SimplestPagination tranne nei casi quando un oggetto TextBlock viene aggiunto all'oggetto StackPanel, che provoca l'altezza totale di superare la pagina. In SimplestPagination, questo codice semplicemente tolto l'intero oggetto TextBlock 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 è mostrato in Figura 2. Il metodo semplicemente rimuove una parola alla volta dalla fine della proprietà Text dell'oggetto TextBlock e verifica se questo aiuta l'oggetto TextBlock adattarla alla pagina. Tutto il testo rimosso viene accumulato in un oggetto StringBuilder che il gestore PrintPage Salva come leftOverLine per la pagina successiva.

Figura 2 Il metodo di RemoveText da 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(' ');
}

Non è abbastanza, ma funziona. Tenere presente che se hai a che fare con testo formattato (diversi tipi di carattere, dimensioni dei caratteri, grassetto e corsivo), poi lavorerete non con la proprietà Text dell'oggetto TextBlock ma con la proprietà Inlines e che complica il processo immensamente.

E sì, ci sono modi decisamente più veloci per fare questo, anche se essi sono certamente più complesse. Ad esempio, può essere implementato un algoritmo binario: metà le parole possono essere rimossi, se si inserisce nella pagina, la metà di quello che è stato rimosso possono essere ripristinata e se che non si adatta nella pagina, quindi la metà di quello che è stato restaurato può essere rimosso e così via.

Tuttavia, tenere presente che questo è il codice scritto per la stampa. Il collo di bottiglia della stampa è la stampante stessa, così mentre il codice potrebbe spendere pochi secondi più test ogni oggetto TextBlock, probabilmente non sta per essere evidente.

Ma si potrebbe iniziare chiedendo esattamente quanto sta succedendo sotto le coperte quando si chiama il provvedimento sull'elemento radice. Certamente tutti i singoli elementi TextBlock sono sempre chiamate di misura, e stanno usando internals Silverlight per determinare la quantità di spazio che stringa di testo occupa effettivamente con il particolare tipo di carattere e la dimensione del carattere.

Si potrebbe chiedere se anche codice come questo sarebbe tollerabile per l'impaginazione di un documento per una visualizzazione di video su un dispositivo lento.

Quindi cerchiamo di provarlo.

Impaginazione sul telefono Windows 7

Il mio obiettivo (che non essere completato in questo articolo) è quello di costruire un e-book reader per Windows Phone 7 adatto per leggere file di testo normale libro scaricato dal progetto Gutenberg (gutenberg.org). Come forse sapete, Project Gutenberg risale al 1971 e fu la prima biblioteca digitale. Per molti anni, è focalizzata sulla fornitura di libri di pubblico dominio — molto spesso i classici della letteratura inglese — in un formato di testo ASCII. Ad esempio, la "Emma" completa di Jane Austen è il file gutenberg.org/files/158/158.txt.

Ogni libro è identificato da un numero intero positivo per il nome del file. Come si può vedere qui, "Emma" è 158 e la sua versione di testo è nel file 158.txt. Negli anni più recenti Project Gutenberg ha fornito altri formati quali EPUB e HTML, ma ho intenzione di attaccare con testo normale per questo progetto, per ovvie ragioni di semplicità.

Il progetto EmmaReader per Windows Phone 7 include 158. txt come risorsa e permette di leggere l'intero libro sul telefono cellulare. Figura 3 mostra il programma in esecuzione nell'emulatore Windows Phone 7. Per il supporto del gesto, il progetto richiede Silverlight per Windows Phone Toolkit, scaricabile da silverlight.codeplex.com. Tap o flick lasciato per andare alla pagina successiva; Flick destra per andare alla pagina precedente.

EmmaReader Running on the Windows Phone 7 Emulator

Figura 3 EmmaReader in esecuzione su Windows Phone 7 emulatore

Il programma non ha quasi nessuna funzionalità ad eccezione di quelle necessarie per rendere ragionevolmente utilizzabile. Ovviamente io sarò rafforzare questo programma, in particolare per consentire di leggere altri libri oltre a "Emma" — forse anche libri di vostra scelta! Ma per inchiodare giù le nozioni di base, è più facile concentrarsi su un unico libro.

Se si esaminano 158.txt, scoprirete la caratteristica più significativa dei file di testo normale Project Gutenberg: ogni paragrafo è costituito da uno o più caratteri 72 righe consecutive delimitati da una riga vuota. Per trasformare questo in un formato adatto a TextBlock avvolgere le linee, alcuni pre-trattamento è necessaria per concatenare queste singole righe consecutive in uno. Questa operazione viene eseguita nel metodo PreprocessBook in EmmaReader. L'intero libro — comprese le linee di lunghezza zero che separa i paragrafi — viene quindi memorizzato come un campo denominato paragrafi di tipo List <string>. Questa versione del programma non cerca di dividere il libro in capitoli.

Come il libro viene impaginato, ogni pagina viene identificato come un oggetto di tipo PageInfo con solo due proprietà integer: ParagraphIndex è un indice nell'elenco dei paragrafi e CharacterIndex è un indice nella stringa per quel paragrafo. Questi due indici indicano il paragrafo e carattere che inizia nella pagina. I due indici per la prima pagina sono ovviamente entrambi zero. Come ogni pagina viene impaginato, sono determinati gli indici per la prossima pagina.

Il programma non tenta di impaginare il libro intero in una sola volta. Con il layout della pagina che ho definito e il tipo di carattere predefinito Silverlight per Windows Phone 7, "Emma" si estende alle 845 pagine e richiede nove minuti per arrivare lì quando in esecuzione su un dispositivo reale. Ovviamente la tecnica sto usando per l'impaginazione — che richiedono Silverlight l'esecuzione di un provvedimento pass per ogni pagina e molto spesso molte volte se un paragrafo continua da una pagina alla successiva — prende un tributo. Potrai esplorare alcune tecniche più veloce nelle colonne successive.

Ma il programma non ha bisogno di impaginare il libro intero in una sola volta. Come iniziare la lettura all'inizio di un libro e procedere pagina per pagina, il programma deve solo impaginare una pagina alla volta.

Caratteristiche ed esigenze

Originariamente pensavo che questa prima versione di EmmaReader non avrebbe nessuna funzionalità a tutti ad eccezione di quelli necessari per leggere il libro dall'inizio alla fine. Ma che sarebbe crudele. Si supponga, ad esempio, stai leggendo il libro, che hai ottenuto a pagina 100 o giù di lì e si spegne lo schermo per mettere il telefono in tasca. A quel tempo, il programma è eliminati definitivamente, il che significa che è essenzialmente terminato. Quando si accende lo schermo indietro, il programma si avvia da capo e siete indietro alla pagina uno. Poi dovete toccare 99 pagine per continuare a leggere in cui hai lasciato!

Per questo motivo, il programma salva il numero di pagina corrente nell'archiviazione isolata quando il programma è definitivamente o terminati. Vi salti sempre torna alla pagina in cui che hai lasciato. (Se è sperimentare questa funzionalità quando si esegue il programma in Visual Studio, sia su emulatore o un telefono reale, assicurarsi di terminare il programma premendo il pulsante indietro, non smettendo di debug in Visual Studio. Arresto di debug non consentire al programma di terminare correttamente e accedere a un'archiviazione isolata.)

Non salvare il numero di pagina nell'archiviazione isolata è in realtà abbastanza. Se solo il numero di pagina è stato salvato, il programma avrebbe impaginare il primo 99 pagine in modo da visualizzare il centesimo. Il programma deve almeno l'oggetto PageInfo per quella pagina.

Ma quel singolo oggetto PageInfo non è sufficiente, sia. Supponiamo che il programma di ricarica, utilizza l'oggetto PageInfo per visualizzare la pagina 100 e poi si decide di passare rapidamente il dito destro per andare alla pagina precedente. Il programma non ha l'oggetto PageInfo per pagina 99, quindi ha bisogno di reimpaginare il primo 98 pagine.

Per questo motivo, mentre leggete progressivamente il libro e ogni pagina viene impaginato, il programma gestisce un elenco di tipo List <PageInfo> con tutti gli oggetti di PageInfo che essa ha determinato finora. Questo intero elenco viene salvato all'archiviazione isolata. Se sperimentare con il codice sorgente del programma — ad esempio, modificare il layout, o la dimensione del carattere o sostituendo l'intero libro con un altro — tenere a mente che qualsiasi modifica che influisce sulla paginazione invaliderà questa lista di oggetti PageInfo. Ti consigliamo di eliminare il programma da telefono cellulare (o l'emulatore) tenendo il dito sul nome del programma nell'elenco di avvio, e selezionando la disinstallazione. Questo è attualmente l'unico modo per cancellare i dati memorizzati dall'archiviazione isolata.

Ecco la griglia contenuta in MainPage:

<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 l'impaginazione, il programma ottiene la proprietà ActualWidth e ActualHeight dell'elemento Border e che utilizza nello stesso modo che la proprietà PrintableArea è utilizzata nei programmi di stampa. Gli elementi TextBlock per ogni paragrafo (e le linee vuote tra i paragrafi) vengono aggiunti a StackPanel.

Il metodo Paginate è mostrato in Figura 4. Come potete vedere, è molto simile ai metodi utilizzati nei programmi di stampa, ad eccezione del fatto che esso è l'accesso a un elenco di oggetti string basato su paragraphIndex e characterIndex. Il metodo aggiorna anche questi valori per la prossima pagina.

Figura 4 l'impaginare metodo in 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;
}

Come si può vedere Figura 3, il programma visualizza un numero di pagina. Ma si noti che non viene visualizzato un numero di pagine, perché questo non può essere determinato fino a quando l'intero libro viene impaginato. Se hai familiarità con lettori e-book commerciali, siete probabilmente consapevoli del fatto che la visualizzazione dei numeri di pagina e il numero di pagine è un grande problema.

Una caratteristica che gli utenti a trovare necessari in lettori e-book è la capacità di cambiare il tipo di carattere o la dimensione del carattere. Tuttavia, dal punto di vista del programma, questo ha conseguenze mortali: tutte le informazioni di impaginazione accumulate finora ha da scartare, e il libro ha bisogno di reimpaginato alla pagina corrente, che non è nemmeno la stessa pagina com'era prima.

Un'altra caratteristica in lettori e-book è la capacità di passare agli inizi dei capitoli. Che separa un libro in capitoli aiuta effettivamente il programma occuparsi di impaginazione. Ogni capitolo inizia su una nuova pagina, così le pagine in ogni capitolo possono essere impaginate indipendentemente da altri capitoli. Saltando all'inizio di un nuovo capitolo è banale. (Tuttavia, se l'utente flicks poi a destra per l'ultima pagina del capitolo precedente, l'intero capitolo precedente deve essere re-paginated!)

Probabilmente anche d'accordo che questo programma ha bisogno di un passaggio di pagina migliore. Avendo la nuova pagina solo pop in luogo non è soddisfacente perché non fornisce un feedback adeguato che la pagina si è trasformato in realtà, o che solo una pagina ha trasformato invece di più pagine. Il programma deve sicuramente qualche lavoro. Nel frattempo, godetevi il romanzo.

Charles Petzold è un redattore che contribuisce a MSDN Magazine*.*Il suo libro recente, "Programmazione Windows Phone 7" (Microsoft Press, 2010), è disponibile come download gratuito presso bit.ly/cpebookpdf.

Grazie all'esperto tecnica seguente per la revisione di questo articolo: Jesse Liberty