Dieser Artikel wurde maschinell übersetzt.

Neue Benutzeroberflächentechnologien

Font Metrics in Silverlight

Charles Petzold

Downloaden des Codebeispiels

Charles PetzoldDie meisten grafische Programmierumgebungen haben Klassen oder Funktionen zum Abrufen von Schriftarteigenschaften. Diese Schriftarteigenschaften bieten Informationen über die Größe von Textzeichen mit einer bestimmten Schriftart gerendert. Zumindest umfasst die Font Metrics Informationen die Breite der die einzelnen Zeichen und einer Höhe, die gemeinsam alle Zeichen ist. Intern, sind diese Breite wahrscheinlich in einem Array gespeichert, so dass der Zugriff sehr schnell ist. Schriftarteigenschaften sind für das Layout des Textes in Absätze und Seiten von unschätzbarem Wert.

Leider ist Silverlight eine grafische Umgebung, die nicht Schriftarteigenschaften für Anwendungsentwickler Programm bereitstellen. Wenn Sie die Größe von Text vor dem Rendern erhalten möchten, verwenden Sie TextBlock, der, natürlich das gleiche Element, die Sie für das Rendern von Text verwenden. Zugriff auf Schriftarteigenschaften von TextBlock intern offensichtlich hat; Andernfalls hätte keine Ahnung, wie groß der Text werden soll.

Es ist leicht dazu verleiten, TextBlock, Textabmessungen bieten, ohne tatsächlich den Text zu rendern. Einfach Instanziieren eines TextBlock-Elements, FontFamily, FontSize, FontStyle, FontWeight und Text-Eigenschaften zu initialisieren und dann die ActualWidth und ActualHeight-Eigenschaften Abfragen. Im Gegensatz zu einigen Silverlight-Elemente müssen Sie diese TextBlock ein untergeordnetes Element eines Panel oder Rahmen zu machen. Auch müssen Sie auf dem übergeordneten Measure-Methode aufrufen.

Um diesen Prozess zu beschleunigen, können Sie TextBlock ein Array von Zeichenbreiten zu erstellen, und dann können Sie dieses Array traditionellen Schriftarteigenschaften nachahmen. Dies ist, was ich in diesem Artikel zeigen, wie werde.

Alle ist diese wirklich notwendig?

Die meisten Silverlight-Programmierer beklagen nicht das Fehlen von Schriftarteigenschaften, da für viele Anwendungen TextBlock sie überflüssig macht. TextBlock ist sehr vielseitig. Wenn Sie die Inlines-Eigenschaft verwenden, kann ein einzelnes TextBlock-Element eine Mischung aus kursiv und fett und sogar Text mit verschiedenen Schriftfamilien oder Schriftgrößen rendern. TextBlock kann langen Text in mehrere Zeilen Absätze erstellen auch umgebrochen werden. Es ist diese Umbrechen von Text-Funktion, die in den letzten beiden Ausgaben dieser Rubrik mit denen einfache e-Book-Leser für Windows Phone 7 erstellen.

In der vorherigen Ausgabe stellte ich ein Programm namens MiddlemarchReader, die Sie George Eliot Roman "Middlemarch" auf Ihrem Handy lesen kann. I want you to perform an experiment with that program: Deploy a fresh copy on an actual Windows Phone 7 device. (Falls erforderlich, zunächst deinstallieren einer Version, die bereits auf dem Telefon möglicherweise.) Drücken Sie nun die Anwendungsschaltfläche beim Abrufen der Liste der Kapitel. Wählen Sie Kapitel IV. From the first page of chapter IV, flick your finger on the screen from left to right to go to the last page of the third chapter and start counting seconds: “One Mississippi, two Mississippi …”

Wenn Ihr Telefon etwas wie Mein Telefon ist, werden Sie feststellen, dass Paging wieder vom Anfang des Kapitels IV bis zum Ende des Kapitels III etwa 10 Sekunden dauert. Ich denke, wir alle zustimmen können, dass 10 Sekunden ist viel zu lang für eine einfache Seite Turn!

Dieser Wartetyp ist charakteristisch für on-the-Fly Paginierung. Anzeigen der letzten Seite eines Kapitels erfordert, dass alle vorhergehenden Seiten in diesem Kapitel zuerst paginiert werden. Ich habe in früheren Ausgaben dieser Rubrik erwähnt, dass die Paginierung-Technik, mit denen, ist höchst ineffizient und diesem speziellen Beispiel es beweist.

Meine Technik langsam Paginierung verwendet das Umbrechen von Text-Feature von TextBlock ganze Absätze oder Teilabsätze gerendert, wenn der Absatz über mehrere Seiten überspannt. Wenn ein Absatz zu groß, um auf eine Seite passen, startet Mein Code beschneiden off Wörter am Ende des Absatzes, bis es passt. Nach dem Entfernen jedes Wort Silverlight muss erneut messen TextBlock, und dies erfordert viel Zeit.

Sicherlich muss meine Paginierung Logik zu überarbeiten. Ein besserer Paginierung-Algorithmus teilt jedem Absatz in einzelne Wörter, erhält die Größe jedes Wort und führt seine eigene Logik für Word-Positionierung und Zeilenumbruch.

In der vorherigen e-Book-Leser, die ich in diesem Artikel gezeigt habe, jeden Absatz (oder Teilabsatz) auf einer Seite ist nur ein TextBlock, und diese TextBlock-Elemente sind untergeordnete Elemente eines StackPanel. In den e-Book-Reader, die in dieser Spalte beschreiben, jedes Wort auf der Seite eigene TextBlock, und jeder TextBlock positioniert ist an einer bestimmten Position auf einer Leinwand. Diese mehrere TextBlock-Elemente erfordern ein wenig mehr Zeit für Silverlight, um die Seite zu rendern, aber das Seitenlayout wird enorm beschleunigt. Meine Experimente zeigen, dass störende 10-sekündigen Seitenübergang in MiddlemarchReader reduziert wird, um zwei Sekunden, wenn jedes Wort mit einem TextBlock-Element gemessen wird, und 0,5 Sekunden bei Zeichenbreiten in ein Array wie herkömmliche Schriftarteigenschaften zwischengespeichert werden.

Aber es ist Zeit für ein neues Buch. Die herunterladbare Visual Studio Lösung für diesen Artikel PhineasReader aufgerufen, und es können Sie die Geschichte von Anthony Trollope beliebtesten fiktive Zeichen, der irische abgeordnete, "Phineas Finn" (1869) zu lesen. Ich habe noch einmal eine nur-Text-Datei, die von Project Gutenberg heruntergeladen verwendet (gutenberg.org).

FontMetrics-Klasse

Wenn eine Computerschriftart zunächst entworfen wird, wählt Schrift-Designer eine Zahl, die aufgerufen hat, die "Geviertgröße." Der Begriff stammt aus alten Tagen bei den Großbuchstaben M ein quadratischer Block des Typs wurde und die Größe des m bestimmt die Höhe und die relative Breite aller Zeichen.

Viele TrueType-Schriftarten werden mit einem Geviertgröße von 2.048 "Design Einheiten." entwickelt. Größe groß genug sein, damit die Zeichenhöhe eine ganze Zahl ist – in der Regel größer als 2.048 um diakritische gerecht zu werden – und die Breite aller Zeichen als auch ganze Zahlen sind.

Wenn Sie einen TextBlock mithilfe einer der unterstützten Windows Phone 7 Schriftarten erstellen und die FontSize-Eigenschaft auf 2.048 legen, entdecken Sie ActualWidth Integer unabhängig davon, welches Zeichen gibt Sie die Text-Eigenschaft festlegen. (ActualHeight ist auch eine ganze Zahl mit Ausnahme von der Segoe WP-fett und die Standardschriftart für Portable User Interface. Diese beiden Namen beziehen sich auf die gleiche Schriftart und die Höhe des 2,457.6 ist. Den Grund für diese Inkonsistenz weiß nicht.)

Sobald Sie die Zeichenhöhe erhalten und breiten Grundlage FontSize-Eigenschaft auf 2.048 festgelegt, können Sie einfach, Höhe und Breite für alle anderen Schriftgrad skalieren.

Abbildung 1 zeigt die FontMetrics-Klasse I erstellt. Wenn Sie mehrere Schriftarten beschäftigen müssen, würden Sie eine separate FontMetrics-Instanz für jedes Schriftfamilie, Schriftschnitt (regulär oder kursiv) und die Schriftbreite (regulär oder fett) pflegen. Es ist sehr wahrscheinlich würde diese FontMetrics Instanzen verwiesen werden aus einem Wörterbuch, so dass ich eine Font-Klasse erstellt, die die IEquatable-Schnittstelle implementiert, daher ist es geeignet als Wörterbuchschlüssel. Mein e-Book-Reader benötigt nur eine Grundlage der Standardschriftart Windows Phone 7 FontMetrics-Instanz.

Abbildung 1 FontMetrics-Klasse

public class FontMetrics
{
  const int EmSize = 2048;
  TextBlock txtblk;
  double height;
  double[][] charWidths = new double[256][];

  public FontMetrics(Font font)
  {
    this.Font = font;
            
    // Create the TextBlock for all measurements
    txtblk = new TextBlock
    {
      FontFamily = this.Font.FontFamily,
      FontStyle = this.Font.FontStyle,
      FontWeight = this.Font.FontWeight,
      FontSize = EmSize
    };

    // Store the character height
    txtblk.Text = " ";
    height = txtblk.ActualHeight / EmSize;
  }

  public Font Font { protected set; get; }

  public double this[char ch]
  {
    get
    {
      // Break apart the character code
      int upper = (ushort)ch >> 8;
      int lower = (ushort)ch & 0xFF;

      // If there's no array, create one
      if (charWidths[upper] == null)
      {
        charWidths[upper] = new double[256];

        for (int i = 0; i < 256; i++)
          charWidths[upper][i] = -1;
      }

      // If there's no character width, obtain it
      if (charWidths[upper][lower] == -1)
      {
        txtblk.Text = ch.ToString();
        charWidths[upper][lower] = txtblk.ActualWidth / EmSize;
      }
      return charWidths[upper][lower];
    }
  }

  public Size MeasureText(string text)
  {
    double accumWidth = 0;

    foreach (char ch in text)
      accumWidth += this[ch];

    return new Size(accumWidth, height);
  }

  public Size MeasureText(string text, int startIndex, int length)
  {
    double accumWidth = 0;

    for (int index = startIndex; index < startIndex + length; index++)
      accumWidth += this[text[index]];

    return new Size(accumWidth, height);
  }
}

Ursprünglich dachte ich ich würde Meine Kenntnisse über die gemeinsame Geviertgröße von 2.048 nutzen und speichern alle Zeichenbreiten als Ganzzahlen, vielleicht 16-Bit-Ganzzahlen. Ich entschied mich jedoch sicher zu spielen und stattdessen als Gleitkommawerte mit doppelter Genauigkeit gespeichert. Dann entschied ich mich, dass FontMetrics ActualWidth und ActualHeight-Werte von 2.048, unterteilen, so dass es wirklich für einen FontSize 1 entsprechende Werte speichert würde. Dies erleichtert die für jedes Programm, das mithilfe der Klasse multipliziert die Werte von den gewünschten Schriftgrad.

Project Gutenberg-nur-Text-Dateien enthalten nur mit Unicode-Werte weniger als 256 Zeichen. Daher konnte die FontMetrics-Klasse in ein einfaches Array von 256 Werte alle Zeichenbreiten muss es speichern. Da diese Klasse für Text mit mehr als 255 Zeichen-Codes verwendet werden kann, ich wollte etwas flexibler als, aber ich wusste, dass das letzte, was ich wollte ein Array zum Speichern von 64,536 mit doppelter Genauigkeit Gleitkommawerte ausreichend zu reservieren. Das ist Arbeitsthreadwert Speicher nur für die Schriftarteigenschaften!

Stattdessen verwendet ein verzweigtes Array. Das Array mit dem Namen CharWidths hat 256 Elemente, von die jede ein Array von 256 double-Werte ist. Ein 16-Bit-Zeichen-Code gliedert sich in zwei 8-Bit-Indizes. Das obere Byte indiziert CharWidths Array ein Array von 256 double-Werten und das untere Byte Character Code Indizes, Array abrufen. Aber diese Arrays von double-Werten werden nur erstellt, wie sie erforderlich sind, und einzelne Zeichenbreiten sind nur als sie benötigt werden. Diese Logik findet in den Indexer der Klasse FontMetrics und verringert die Menge an Speicher, die von der Klasse erforderlich und schneidet unnötige Verarbeitung für Zeichen, die nie verwendet werden.

Die beiden MeasureText-Methoden erhalten, die Größe einer Zeichenfolge oder einer Teilzeichenfolge einer größeren Zeichenfolge. Diese beiden Methoden Rückgabewerte für ein FontSize 1, die einfach durch Multiplikation mit den gewünschten Schriftgrad skaliert werden kann.

TextBlock-Elemente werden in der Regel an Pixelbegrenzungen ausgerichtet, da die UseLayoutRounding-Eigenschaft, die von der UIElement-Klasse definiert einen Standardwert hat true. Für Text hilft Pixel Ausrichtung Lesbarkeit, da es inkonsistente Anti-Aliasing vermeidet. Nach der Multiplikation der Werte aus der MeasureText durch den Schriftgrad, sollten Sie diese Werte durch die Math.Ceiling-Methode übergeben. So geben Sie Werte aufgerundet zur nächsten ganzzahligen Pixel.

Bessere Formatierung

Wie in meiner vorherigen e-Book-Leser die meisten realen Routinearbeit des Programms tritt in der PageProvider-Klasse. Diese Klasse verfügt über zwei wichtigsten Aufträge: Vorverarbeitung der Datei Project Gutenberg zu einzelne Zeilen der Datei einzeiligen Absätzen und Paginierung verketten.

Um Zeichencodes größer als 255 FontMetrics testen, entschied ich mich ein wenig mehr Vorverarbeitung als in der Vergangenheit durchführen. Erstens I ersetzt standard doppelte Anführungszeichen (ASCII-Code 0 x 22) mit "fancy Anführungszeichen" (Unicode 0x201C und 0x201D) von abwechselnden einfach der beiden Codes innerhalb jedes Absatzes. Auch Viktorianische Autoren neigen viele Gedankenstriche verwenden – oft zum Begrenzen von Sätzen wie diesem – und diese wieder oben in der Project Gutenberg-Dateien als Paare von Strichen. In den meisten Fällen ersetzt ich diese Paare von Strichen mit Unicode 0x2014 umgeben von Leerzeichen, Zeilenumbrüche zu erleichtern.

Meine neue Vorverarbeitung Logik verarbeitet auch aufeinander folgender Zeilen mit demselben Einzug. Oft diese eingerückten Zeilen umfassen, einen Brief oder andere eingerückten Material in den Text, und ich versuchte, die in einem mehr bequeme Art zu behandeln. Beim Paginieren begann ich alle nicht eingerückt Absätze mit einem Einzug der ersten Zeile, außer für den ersten Absatz ein Kapitel, die ich nehme an, in der Regel einen Kapiteltitel ist.

Die Gesamtwirkung des Einzugs Logik ist dargestellt, Abbildung 2.

A Page from PhineasReader Showing Paragraph Indenting

Abbildung 2 einer Seite von PhineasReader zeigt Einzüge

Paginierung und Zusammensetzung

Da PageProvider viel von dem Layout durchgeführt wurden von TextBlock selbst übernommen hat, ist die Paginierung Logik ein wenig zu umfangreich für die Seiten dieses Magazins geworden. Aber es ist ziemlich einfach. Die Absätze, die den Text von Project Gutenberg umfassen werden als eine Liste von ParagraphInfo-Objekten gespeichert. Das formatierte Buch ist ein BookInfo-Objekt, die ist meist eine Liste der ChapterInfo-Objekte. Das ChapterInfo-Objekt gibt den Index des Absatzes, der das Kapitel beginnt und auch verwaltet eine Liste der PageInfo-Objekte, die erstellt werden, wie das Buch schrittweise paginiert ist.

Die PageInfo-Klasse wird angezeigt, Abbildung 3. Es gibt den Speicherort die Seite mit einem Absatz Index und ein Zeichenindex innerhalb dieses Absatzes beginnt, und auch verwaltet eine Liste von WordInfo-Objekte. WordInfo-Klasse wird angezeigt, Abbildung 4. Jedes Objekt WordInfo entspricht ein einzelnes Wort, sodass diese Klasse das Wort Koordinatenposition auf der Seite und dem Text das Wort als Teilzeichenfolge eines Absatzes angibt.

Abbildung 3 der PageInfo-Klasse repräsentiert jede paginierten Seite

public class PageInfo
{
  public PageInfo()
  {
    this.Words = new List<WordInfo>();
  }

  public int ParagraphIndex { set; get; }

  public int CharacterIndex { set; get; }

  public bool IsLastPageInChapter { set; get; }

  public bool IsPaginated { set; get; }

  public int AccumulatedCharacterCount { set; get; }

  [XmlIgnore]
  public List<WordInfo> Words { set; get; }
}

Abbildung 4 die WordInfo-Klasse stellt ein einzelnes Wort

public class WordInfo
{
  public int LocationLeft { set; get; }

  public int LocationTop { set; get; }

  public int ParagraphIndex { set; get; }

  public int CharacterIndex { set; get; }

  public int CharacterCount { set; get; }
}

Sie sehen in der PageInfo-Klasse, die Wörter-Eigenschaft wird gekennzeichnet mit XmlIgnore, was bedeutet, dass diese Eigenschaft wird nicht mit dem Rest der Klasse serialisiert werden und daher nicht im isolierten Speicher zusammen mit dem Rest der Paginierungsinformationen gespeichert. A few little calculations will convince you of the wisdom of this decision: “Phineas Finn” is more than 200,000 words in length, and WordInfo contains 20 bytes of data, so, in memory, all the WordInfo objects will occupy more than 4MB. Das ist nicht so schlecht, aber betrachten Sie diesen 200.000 WordInfo-Objekten für die Serialisierung in XML konvertiert! Außerdem ist der Anfang einer Seite bekannt, ist die Berechnung die Speicherorte der Wörter auf der Seite mithilfe der Klasse FontMetrics sehr schnell, damit diese WordInfo-Objekten ohne Performanceprobleme neu erstellt werden können.

Abbildung 5 zeigt die BuildPageElement-Methode in PageProvider, die im Grunde ein PageInfo-Objekt in eine Leinwand, enthält eine Reihe von TextBlock-Elemente konvertiert. Es ist diese Leinwand, die tatsächlich auf dem Bildschirm dargestellt wird.

Abbildung 5 Die BuildPageElement-Methode in PageProvider

FrameworkElement BuildPageElement(ChapterInfo chapter, PageInfo pageInfo)
{
  if (pageInfo.Words.Count == 0)
  {
    Paginate(chapter, pageInfo);
  }

  Canvas canvas = new Canvas();

  foreach (WordInfo word in pageInfo.Words)
  {
    TextBlock txtblk = new TextBlock
    {
      FontFamily = fontMetrics.Font.FontFamily,
      FontSize = this.fontSize,
      Text = paragraphs[word.ParagraphIndex].Text.
Substring(word.CharacterIndex, 
        word.CharacterCount),
        Tag = word
    };

    Canvas.SetLeft(txtblk, word.LocationLeft);
    Canvas.SetTop(txtblk, word.LocationTop);
    canvas.Children.Add(txtblk);
  }
  return canvas;
}

Der tatsächliche Paginierung und Layout-Code nicht die Benutzeroberfläche nicht berühren. Nur die BuildPageElement-Methode, die die Seite komponiert werden UI-Objekte erstellt. Die Trennung der Paginierung aus Seite Komposition ist neu in dieser Version von e-Book-Reader, und es bedeutet, dass die Paginierung und das Layout in einem Hintergrundthread auftreten könnten. Ich bin, die nicht in diesem Programm tun, aber es ist etwas im Auge zu behalten.

Nicht nur für Leistung

Ich beschloss ursprünglich TextBlock für Layout aus Leistungsgründen zu verwerfen. Aber es gibt mindestens zwei weitere zwingende Gründe für die Verwendung von separaten TextBlock-Elemente für jedes Wort.

Wenn Sie Ihre Absätze ausrichten wollen, ist dies zunächst ein wichtiger erster Schritt. Silverlight für Windows Phone 7 unterstützt keine TextAlignment.Justify-Enumerationsmember. Aber wenn jedes Wort eine separate TextBlock, Rechtfertigung ist einfach eine Frage der Zwischenraum zwischen den einzelnen Wörtern zu verteilen.

Der zweite Grund betrifft das Problem der Text auswählen. You might want to allow the user to select text for different purposes: perhaps to add notes or annotations to a document, or to look up words or phrases in a dictionary or Bing or Wikipedia, or to simply copy text to the clipboard. Sie müssen dem Benutzer bieten einige Möglichkeit, den Text markieren und diese markierte Text in einer anderen Farbe angezeigt.

Kann ein einzelnen TextBlock verschiedene Teile des Textes in verschiedenen Farben anzeigen? Ja, das ist möglich, mit der Inlines-Eigenschaft und eine separate Run-Objekt für den ausgewählten Text. Es ist chaotisch, aber es ist möglich.

Schwierige Problem besteht darin, dass den Benutzer den Text zunächst markieren. Der Benutzer sollte klicken oder tippen Sie auf ein bestimmtes Wort, und ziehen mehrere Wörter auswählen können. Aber wenn Sie ein ganzen Absatz durch ein einzelnes TextBlock-Element angezeigt wird, wie Sie wissen was Wort, das ist? Sie können die Hit-testing für den TextBlock selbst, aber nicht auf die einzelnen Run-Objekte ausführen.

Wenn jedes Wort eigene TextBlock ist, wird der Auftrag Hit-testing viel einfacher. Natürlich ergeben sich weitere Herausforderungen auf dem Telefon. Grobe Finger müssen kleine Text markieren, was, dass es wahrscheinlich für den Benutzer bedeutet, um den Text zu vergrößern, vor Beginn der Auswahl erforderlich ist.

Jede neue Funktion in ein Programm eingeführt wird, schlägt es wie üblich, noch mehr Funktionen.

Charles Petzold ist ein langjähriger redaktionelle Beiträge für MSDN Magazin*.*Sein aktuelles Buch "Programming Windows Phone 7" (Microsoft Press, 2010), steht als kostenloser Download unter bit.ly/cpebookpdf.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Chipalo Street