UI 前沿技术

Silverlight 中的字体标准

Charles Petzold

下载代码示例

Charles Petzold
大多数图形的编程环境有类或函数来获取字体规格。这些字体规格提供信息时使用特定字体呈现的文本字符的大小。最起码,字体规格信息包括的每个字符的和高度是通用的所有字符的宽度。在内部,这些宽度可能存储在阵列中,因此访问速度很快。字体规格很重要的段落和页面中的文本的布局。

遗憾的是,Silverlight 是一个图形化的环境中执行的向应用程序开发人员提供字体规格。如果您想要获取的文本才能使其大小,则必须使用 TextBlock,即,当然,用于呈现文本的同一个元素。在内部,TextBlock 显然有权访问字体规格。 否则,它将具有文本应该不是知道如何大。

很容易诱使 TextBlock 提供文本尺寸,而不实际呈现的文本。只需实例化 TextBlock 元素、 初始化的 FontFamily、 字号、 FontStyle、 粗细和文本属性中,然后查询的 ActualWidth 和 ActualHeight 属性。与某些 Silverlight 元素不同,您不需要进行此 TextBlock 面板或边框的子节点。也不需要调用父的测量方法。

若要加快此过程,可用于 TextBlock 生成一个字符宽度的数组,然后可以使用此数组来模仿传统字体规格。这是什么我将向您展示如何在这篇文章。

是所有这一切吗?

大多数 Silverlight 程序员不要 mourn 缺少的字体标准,因为对于许多应用程序,TextBlock 使它们不必要。TextBlock 有很多用途。如果您使用的内联属性,单个 TextBlock 元素可以呈现混合斜体和粗体文本,并使用不同的字体系列或字体大小甚至文本。TextBlock 还可以为要创建的段落的多行包装长文本。它是我要创建简单的电子书籍阅读器对于 Windows 电话 7 一直在过去的两个几期的此列中使用此文字环绕功能。

在前面的问题,我将呈现程序 (称为 MiddlemarchReader,可用于读取乔治 Eliot 小说"Middlemarch"在电话上。我希望您能够执行与该程序的一个实验: 部署实际 Windows 电话 7 设备上的全新副本。(如果有必要,请先卸载任何版本可能已在电话上。)现在请按应用程序栏按钮,以返回的章节列表。选择章 IV。章 IV 的第一页,从们手指转到最后一页的第三个章节和开始计数秒为单位) 的权限从左向右屏幕上:"一个密西西比,两个密西西比 …"

如果您的电话与我的电话一样,您会发现章 III 结尾章 IV 从头重新分页花费大约 10 秒钟。我认为我们可以全部同意 10 秒是太长的简单页面打开!

等待时间是分页的动态的特征。显示某一章节的最后一页,则需要首先对这一章中的前一页进行分页。提到在本专栏的前几期我一直使用的分页方法 grossly 效率较低,并且此特定示例证明。

我慢分页方法使用 TextBlock 的文字环绕功能呈现整个段落或部分段落,如果段落 straddles 多个页面。如果段落是太大,无法显示在页上,则我的代码将启动 lopping 关闭段落的末尾的单词,直到它与之匹配。删除每个单词后,Silverlight 必须 re-measure TextBlock,并且这需要大量的时间。

当然,我需要修订我分页逻辑。更好的分页算法分成单个的单词的每个段落、 获取每个单词的大小并执行自己 word 定位和自动换行的逻辑。

在以前的电子书读者就介绍了此列中,页上的每个段落 (或部分段落) 只是一个 TextBlock,且这些 TextBlock 元素的子级的 StackPanel。在电子书 reader 中我将介绍此列中,在页面上的每个词自己 TextBlock,且每个 TextBlock 放置在画布上的特定位置。这些多个 TextBlock 元素需要更多时间用于 Silverlight 呈现页上,但页面布局能力极大地加快。我试验显示两秒时与 TextBlock 元素,测量每个单词和字符宽度像传统的字体标准数组中的缓存时的 0.5 秒减少在 MiddlemarchReader 中有问题的 10 秒页面过渡效果。

但它是新简介册的时间。可下载的 Visual Studio 解决方案,该项目被称为 PhineasReader,并且还可以让您阅读部分的一种安 Trollope 最钟爱虚构字符,爱尔兰的成员的 Parliament,"Phineas Finn"(1869)。再一次曾用纯文本格式的文件从项目谷登堡 (gutenberg.org)。

FontMetrics 类

当首次设计计算机字体时,字体设计选择一个数字,就是所谓的"全角的大小"。术语来自 olden 天时间大写字母 M 的类型的方块和 M 的大小确定的高度和所有其他字符的相对宽度。

很多 TrueType 字体设计 em 大小为 2048"设计单位。"大小是足够大,以便将字符高度是一个整数通常大于 2048 以适应音调符号标记 — 并且所有字符的所有宽度都是整数以及。

如果创建 TextBlock 使用 Windows 电话 7 中,支持的字体和字号属性设置为 2048,您会发现 ActualWidth 返回一个整数,无论什么字符,您可以将 Text 属性设置。(ActualHeight 也是一个整数,Segoe 白皮书加粗字体和默认的可移植的用户界面字体除外。这两个名称相同的字体,请参阅,高度为 2,457.6。我不要知道这种不一致的原因。

一旦您获取字符高度和宽度根据字号属性设置为 2048,只是可缩放的高度和宽度的任何其他字体大小。

图 1显示我创建的 FontMetrics 类。如果您要处理多个字体,则将维护一个单独的 FontMetrics 实例的每个字体系列、 字体样式 (常规或倾斜) 和字体粗细 (常规或加粗)。很可能会引用这些 FontMetrics 实例从字典,因此我创建了字体类实现 IEquatable 接口,因此很适合作为字典的键。我的电子书籍阅读器只需要一个基于默认 Windows 电话 7 字体的 FontMetrics 实例。

图 1FontMetrics 类

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

最初,我认为我将利用我掌握 2048 常见全身大小和存储为整数,可能是 16 位整数的所有字符宽度。 但是,我决定为谨慎起见,并将其存储为双精度浮点值。 然后,我决定 FontMetrics 将除以 ActualWidth 和 ActualHeight 值 2048,使它真正存储适用于字号为 1 的值。 这便于对通过所需字号值进行相乘使用类的任何程序。

项目谷登堡纯文本格式的文件只包含具有 Unicode 值小于 256 个字符。 因此,FontMetrics 类可以在 256 个值的简单数组中存储所需的所有字符宽度。 由于可能大于 255 的字符代码的文本使用此类,我需要一些比,更灵活,但我知道我想要的最后一件事就是分配数组足以存储 64,536 双精度浮点值。 这是内存的.5mb 仅为字体规格 !

相反,我使用交错的数组。 名为 charWidths 的数组具有 256 元素,其中每个是 256 个双精度值的数组。 16 位字符代码分为两个 8 位索引。 高位字节索引 charWidths 阵列以获得 256 个双精度值,数组和低位字节的字符代码索引的数组。 但是,它们在需要时,以及它们在需要时才获取单个字符宽度仅创建这些阵列的双精度值。 FontMetrics 类的索引器中发生这种逻辑和同时减少了类所需的存储量减少不必要处理从不使用的字符。

两个 MeasureText 方法获取字符串的大小或较大字符串的子字符串。 这两种方法返回的值适用于为 1,然后只需通过乘以所需的字体大小进行缩放的字号。

因为 UIElement 类定义的 UseLayoutRounding 属性的默认值为通常 TextBlock 元素对齐像素边界上*,则返回 true*。 对于文本,像素对齐有助于提高可读性,因为它避免了不一致的消除锯齿。 后从 MeasureText 获得的字体大小的值相乘,您需要将这些值传递到 Math.Ceiling 方法。 这将使您的值向上舍入到下一个整数像素。

制作出比较独特格式

我以前的电子书籍阅读器) 所示大部分真正的 grunt 工作的程序出现在 PageProvider 类中。 此类有两个主要的作业: 预处理项目谷登堡文件以将此文件的单个行连接成单行段落和分页。

若要测试 FontMetrics 大于 255 的字符代码,我决定执行更正在预处理比过去。 首先,我已更换标准双引号引起来 (ASCII 代码 0x22) 与"艺术型引号"(Unicode 0x201C 和 0x201D) 通过简单地替换每个段落中的两个代码。 维多利亚式作者往往还,需要使用大量的全角破折号 — 通常以界定类似这样的短语),然后这些最大的项目谷登堡文件中为短划线对。 在大多数情况下,我替换这些对短划线 Unicode 0x2014 包围空间以便于换行。

我新的预处理逻辑还处理与同一个缩进的连续行。 这些缩进的行通常包含字母或其他缩进的材料,在文本中,并试图处理那些更正常的方式。 同时分散,我开始用首行缩进,想必您通常是章节标题的一章的第一段除外的所有非缩进段落。

此缩进逻辑的整体效果所示图 2

A Page from PhineasReader Showing Paragraph Indenting

图 2PhineasReader 显示缩进段落中的页面

分页和组合

因为 PageProvider 已被接管很多以前由 TextBlock 本身的布局,分页逻辑变得有点太长该杂志的页面。 但它是相当简单。 为列表中的 ParagraphInfo 对象存储构成项目谷登堡文本的所有段落。 带格式的简介册是多数情况下列表中的 ChapterInfo 对象的 BookInfo 对象。 ChapterInfo 对象表示的段落的开始一章和还维护变得越来越对书籍进行分页时创建的列表中的 PageInfo 对象的索引。

PageInfo 类所示图 3。 它指示页开始一个段落的索引与该段落中的字符索引,还维护列表的 WordInfo 对象。 WordInfo 类所示图 4。 因此,此类指示 word 的坐标位置上页和单词的文本为一个段落的子字符串,每个 WordInfo 对象对应于单个单词。

图 3PageInfo 类表示每个分页的页面

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

图 4WordInfo 类表示单个单词

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

您会注意到在 PageInfo 类中单词属性 XmlIgnore,这意味着与其他成员的类中,将不会被序列化该属性来标记,因此不能保存在一起的分页信息的其余部分的独立存储中。 一些小的计算将说服您明智的决定:"Phineas Finn"的长度,超过 200000 次的单词和 WordInfo 包含 20 个字节的数据,因此,在内存中,WordInfo 的所有对象将都占用多个 4 MB。 这不算太复杂,但考虑到 XML 转换为序列化这些 200000 WordInfo 对象! 此外,如果知道某页的开头,则计算使用 FontMetrics 类该页面上的文字的位置是非常快,因此可以重新创建这些 WordInfo 对象,而性能问题。

图 5显示基本上是一个 PageInfo 对象转换为包含一组 TextBlock 元素在画布上的 PageProvider 中的 BuildPageElement 方法。 它是此实际呈现在屏幕上的画布。

图 5PageProvider 中的 BuildPageElement 方法

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

实际的分页和布局代码不会按用户界面。 撰写页的 BuildPageElement 方法创建用户界面对象。 从页面排版的分页的分离是电子书籍阅读器中,此版本中的新功能,这意味着在后台线程中可能出现的分页和布局。 不这样做在此程序中,但要记住的东西。

而不只是为了性能

我最初决定放弃 TextBlock 布局出于性能方面的考虑。 但是,至少两个理由相当的每个字采用不同 TextBlock 元素。

首先,如果您曾希望两端对齐段落,这是非常重要的第一步。 对于 Windows 电话 7 Silverlight 不支持 TextAlignment.Justify 枚举成员。 但如果每个单词是单独 TextBlock,原因是只是一种分发的各单词间的额外空间。

第二个原因是选择文本的问题。 您可能希望允许用户选择用于不同目的的文本: 可能是为了将备注或批注添加到文档中,或要查找的单词或短语在字典或 Bing 或维基百科,或只是将文本复制到剪贴板。 您需要向用户提供一些方法来选择文本,并以不同颜色显示所选的文本。

单个 TextBlock 可以用不同颜色显示文本的各个部分吗? 是的可以使用内联属性和所选文字的一个单独的运行对象。 很杂乱,但却可能发生。

更难的问题使用户开始选择文本。 用户应该能够单击或按特定的单词,然后拖动以选择多个单词。 但是,如果单个 TextBlock 元素显示整个段落,您如何知道是什么单词呢? 您可以执行命中测试 TextBlock 本身,而不是单独的运行对象。

每个单词时自己 TextBlock,命中测试工作变得容易得多。 当然,其他的挑战出现时接听电话。 吵闹的手指必须选择细小文本,这意味着可能需要放大文本前所选内容的用户。

像往常一样,由于介绍每个程序中的新功能,它提供建议甚至更多的功能。

Charles Petzold 《MSDN 杂志》*的长期特约编辑。*他的新书《Programming Windows Phone 7》(Microsoft Press,2010 年)可从 bit.ly/cpebookpdf 免费下载获得。

衷心感谢以下技术专家对本文的审阅: Chipalo Street