文本显示无疑是更重要的 UI 功能之一。在 WPF 界面中,您通常使用标签等控件来显示文本。但是在许多情形下,您需要的不只是简单地显示几个单词。流文档提供了一种更高级的方法,而它们实质上非常简单。它们通过类似 HTML 文档的格式定义文本流,但其功能更强大,并可提供明显更先进的布局选项。
通常使用基于 XML 的标准标记语言——可扩展应用程序标记语言 (XAML) 来定义“流文档”。XAML 对于流文档特别直观,主要是因为它与 HTML 类似。以下流文档示例创建了一段文字,并只对其中几个单词应用了粗体格式:
The quick brown fox jumps over the lazy dog.
可以看到,与 HTML 的相似性比在其他 XAML UI 中更明显。真正的元素名称是不同的,但是至少对简单的文档来说,模式非常相似。流文档一般以包含多个块的 FlowDocument 根元素开头。“块”是指流内的元素,通常是如上例所示的文本段落(当然还有其他块类型)。段落又可以包含其他元素,例如本例中的两个粗体单词。请注意,对于任何其他 XAML 文档,根元素必须包含 XAML 特定的命名空间定义,否则无法被识别。这是 XAML 特定的实现细节,与流文档无关。请注意,命名空间定义只在独立的流文档中才需要。(流文档可以是更大的 XAML UI 的一部分,在这种情况下,该 UI 的根元素中会包含命名空间定义。)
当然,用户永远不会看到流文档的 XAML(而 HTML 源则可在浏览器中查看),这与他们无法看到任何其他 UI 元素的 XAML 一样。相反,用户看到的是文档的最终呈现。对于这个特定的示例,您可通过多种方式看到结果。或许最简单的方式是将其键入 Windows SDK 附带的实用工具 XamlPad 中(请参见图 1)。
当然,这是一个非常简单的例子,文档的定义和嵌入式布局会复杂得多。流文档支持您能想到的所有格式,例如斜体、下划线、字体颜色和字体等。图 2 显示的是一个稍微高级的示例,其结果可在图 3 中看到。
本示例显示的是带有内嵌格式的若干段落。它还提供另一类型块元素的第一个示例,即列表,毫无疑问,它包含多个列表项。请注意,每个列表项反过来也只是包含更多块元素的容器。因此,我不是简单地将文本置于一个列表项中,而是向每个列表项中添加一个段落元素。就此而论,我应该已向每个列表项或任何其他块类型添加了多个段落。这可以让您在列表的单个列表项内创建高级布局,这在 HTML 等格式中一般行不通,因为此类格式只会让简单的文本字符串流向每个列表元素。
至此您已了解了一些流文档的基础知识,接下来让我们回顾一下某些基础知识。如您所见,流文档是块的集合。在内部,所有块都是从 System.Windows.Documents.Block 类派生而来的 WPF 类。块又是从 ContentElement 派生而来(沿此链向上追寻几步),ContentElement 是 WPF 中专门为文档定义优化的一个相当低级别的类。此方法有些类似于您用来定义 WPF 界面的控件,它们都从 UIElement 派生而来。两者的继承树在概念上很相似,但并不完全相同。这意味着 WPF 控件和块不能直接组合。例如,一个按钮的标题不能设为一段文本,一个段落也不能直接包含一个按钮。这些控件和块之间存在一些细微差别,这是由于内容控件内的布局和块内的布局的运作方式截然不同这一事实所致。幸运的是,这两类 WPF 元素之间需要弥合的差异非常小。就按钮而言,它可以包含由带格式的文本构成的 TextBlock 对象;而块可以通过特殊 BlockUIContainer 块类包含任何 WPF 控件。这意味着,流文档可以包含所有类型的 WPF 元素(包括交互式用户界面、媒体和三维元素),而从另一个角度看,流文档也可是任何 WPF 用户界面的一部分,例如可以是控件内容的一个高级布局元素,也可以是一个真正的元素,例如销售点应用程序中的某一项的描述。
可用块的列表理论上是可扩充的,因为开发人员可以派生他们自己的块类,然后创建他们自己的针对文档呈现引擎的增强功能。这提供了我所了解的任何其他文档呈现引擎都无法提供的自由度。但是,对一般的文档创建者公开的块数量通常有限。图 4 显示了最重要的块类型的列表。
当使用 XAML 创建 WPF 流文档时,您事实上只要实例化某些类型。请看下面的 XAML 代码段(从此处起,我将省略命名空间定义,以让示例尽量简单):
Hello World!
这会实例化一个 FlowDocument 类和 Paragraph 类(其文本设为“Hello World!”)。该段落被添加到 FlowDocument 的块集合中。请注意,对于所有 XAML 而言,元素名称都区分大小写,并且精确映射到作为 WPF 一部分而提供的类。您也可通过编程方式创建相同文档,如下所示:
FlowDocument doc = new FlowDocument(); Paragraph para = new Paragraph(); para.Inlines.Add(“Hello World!”); doc.Blocks.Add(para);
当然,这远不及 XAML 提供的声明性方法那么直观,因此编程的方法只在特殊情形下采用。(当我需要创建一个格式丰富的报告,结果要更像一份真实的文档,而非通过许多报告引擎创建的表格形式的输出时,有时会使用此方法。)
在许多情形下,段落本身带有格式丰富的内容,这也是通过实例化类实现的,如下所示:
Hello World!
在本例中,该段落包含两个文本段——“Hello”(使用默认格式)和“World!”(粗体)。这比较有趣,因为这表示此 XAML 不只是实例化一个段落,并将其文本设为一个简单的字符串;相反,它创建了含有两个子段的一个段落,每个子段包含不同格式的文本。在 WPF 中,这些段称为内嵌元素。就如一个流文档可以包含多个不同类型的块一样,段落也可以包含各种类型的内嵌元素。内嵌元素有多种变体。有些内嵌元素就是所谓的 Span,它们代表应用了特定格式选项的文本段。此例中的 Bold 元素是 Span 的一个特殊情形,其默认字体粗细设为粗体。内嵌元素的另一种类型是 Run,它是带有默认格式的文本段。因此,上面的 XAML 其实只是下例的简化:
Hello World!
当然,它要方便得多,您不必使用 XAML 定义每个内嵌元素,但是如果您要以编程方式创建相同示例,了解内嵌元素的概念就非常重要了,因为它们不可以在代码中省略。以下是前面两个 XAML 示例的对等代码段:
Paragraph para = new Paragraph(); para.Inlines.Add(new Run(“Hello “)); Bold b = new Bold(); b.Inlines.Add(“World!”); para.Inlines.Add(b);
Bold 是 Span 的特殊版本,其默认字体粗细设为粗体;Bold 类型由 Span 子类化而来,并且会覆盖 FontWeight 属性。类似特殊的 Span 还有 Italic 和 Underline。不过,这些特殊的 Span 并不是必不可少的,因为您也可以使用默认的 Span,并设置相应属性:
Hello World!
当然,通过将某一文本段包到粗体或斜体标记中,来直接指定诸如粗体和斜体等属性的功能非常方便和直观,因此通常更多的是使用
Hello World!
诸如 FontFamily 等许多属性都可以始终在所有流文档类中找到。例如,若要设置一个完整段落而非只是一个内嵌元素的字体,您不使用 Span 即可做到:
Hello World!
还有 Span 和 Run 之外的一些内嵌元素。下面就是其他一些更有趣的内嵌元素:
Figure Figure 是有些不寻常的内嵌元素,因为它们包含块。因此,从某种意义上讲,Figure 几乎就像流文档内的迷你流文档。Figure 经常用于高级布局功能,例如段落中被普通文本流包围的图像。
Floater Floater 是轻型的图形。它们不支持任何图形放置选项,但是如果您需要的只是除标准段落对齐之外还能做些简单对齐的功能,Floater 会比较有用。
LineBreak LineBreak 元素的作用与其名称所指的意义完全相同:它们会在段落内引入换行符。
InlineUIContainer InlineUIContainer 是 BlockUIContainer 的内嵌元素等同项。如果您需要将任何类型的 WPF 控件与您其他的内嵌元素组合使用(例如让一个按钮在一个段落文本内移动),InlineUIContainer 正是您所需要的。
Figure 始终用于流文档中(LineBreak 也是如此,不过它们几乎不需要详细讨论)。以下示例使用一个图形,将一个图像显示为一个更大流文档的一部分:
The quick brown fox jumps over the lazy dog. The quick brown...
请注意,WPF 流文档中没有 Image 块。相反,图像以标准的 WPF Image 控件内嵌为 BlockUIContainer。(相同的方法也用于流文档内诸如视频或交互式三维模型等内容)。图 5 显示了与此类似的一个文档的呈现。
现在,您已了解如何创建一些简单的流文档以及如何在 XamlPad 中查看它们。而目前我所忽略的是该如何在自然状态下查看流文档。毕竟,您不会期望用户打开 XamlPad,然后粘贴文档的 XAML。查看 XAML 流文档的一种方法是将其另存为一个扩展名为 .xaml 的文件,然后在 Windows 资源管理器中双击它。这会启动与 XAML 文件相关联的默认应用程序(通常是 Internet Explorer®),从而显示该文档。结果如图 6 所示。
Internet Explorer(及其他浏览器)可以显示 XAML 内容这一事实特别有趣,因为这是将流文档作为您的 Web 应用程序一部分显示的一张票证。换句话说,如果您将 XAML 流文档上传到您的 Web 服务器,而有人浏览到了该文件,他就会看到类似于图 6 的效果(假设该用户已安装 Microsoft® .NET Framework 3.0)。当然,这也是动态运作的。如果您的 ASP.NET Web 应用程序(或任何其他服务器端技术)动态生成了一个 XAML 流文档,并将其作为输出返回(假设内容类型已适当设为“application/xaml+xml”),用户就会看到作为您应用程序一部分的流文档,这在许多情形下必然相当有用。图 7 显示了一个简单的生成流文档的 ASP.NET 页面。
您可能已经注意到,每当显示流文档时(无论是在浏览器中还是在 XamlPad 中),显示的似乎不只是文档本身,还会显示其他少量内容。特别是,文档底部会呈现一些控件。如图 8 所示,流文档默认会通过 FlowDocumentReader 控件呈现,它提供了一组标准功能,例如缩放、分页、不同视图模式切换,甚至查找功能。事实上,流文档需要由一些能够显示它们的某类控件承载。流文档的默认查看器是 FlowDocumentReader 控件,除非您明确使用其他控件,否则该控件会自动实例化。WPF 目前提供三个不同的控件用于查看流文档:
FlowDocumentScrollViewer 此控件使用一个滚动条以连续的流显示文档,类似网页或 Microsoft Word 中的“Web 版式”。图 9 显示的是滚动查看器中的文档。
FlowDocumentPageViewer 此控件以单独的页面显示流文档,让页面翻转而非滚动。这与 Word 中的“阅读版式”类似。图 10 显示的是页面查看器。在这里,图 9 中的文档使用 FlowDocumentPageViewer 控件呈现,滚动条被分页机制取代。这种简单的流布局方法已被一种更高级、多列的分页布局所取代。
FlowDocumentReader 此控件组合了滚动查看器和页面查看器,让用户可以在两种方法之间切换。这是用于流文档的默认控件,而且对于以显示复杂文本为特色的应用程序通常是一个不错的选择。在图 11 中,图 9 和图 10 中显示的同一文档通过 FlowDocumentReader 呈现,它将滚动查看器和页面查看器两种方法结合在一起。此外,它还启用了其他控件中默认隐藏的搜索功能(其他查看器的确支持查找功能,通过执行 ApplicationCommands.Find 命令或从键盘上按 Ctrl+F 可实现该功能)。读取器控件还支持多页视图,这稍微改变了基于页面的呈现,以及列和图的呈现方式。
虽然 FlowDocumentReader 几乎对所有基本使用情形都很有吸引力,但选择怎样的控件还需视您的情况而定。它用途广泛且功能强大,并支持分页布局,这在许多情形下是比滚动更高级的功能。关于该主题的更详细讨论不在本文探讨范围之内,但事实证明,滚动及重合等相关效果是人们较之数字化文本更喜欢打印文本的主要原因之一。分页方法在许多情况下更为自然,有助于让数字化阅读被更普遍接受。
那么您如何定义要使用哪个控件呢?一个简单但相当强力的方法是将想要的控件添加到文档的 XAML 中:
The quick brown fox jumps over the lazy dog.
在本例中,文档根已被设为一个 FlowDocumentScrollViewer 标记。也就是说,您不再只是定义一个单纯的文档而已。相反,您在定义一个完整的 XAML 界面,而它碰巧使用滚动查看器作为其根。滚动查看器的内容是最开始示例中的流文档。(请注意,命名空间定义现在使用滚动查看器标记,而非流文档标记)。图 9 到图 11 是使用此方法创建的,不同的查看器控件用作根元素。
我为何把这称为强力方法呢?这是因为,从结构角度看,将用户界面定义与其实际数据相混合会导致一些问题。而更理想的状况是将文档与其界面分开。将读取器与文档混合在一起有点像创建一个 SQL Server™ 表,并出于某种原因定义该表只能在 Windows Forms DataGrid 中显示。有若干方法可让文档与 UI 定义分离。如果想使用上文所示的 ASP.NET 方法将流文档作为 Web 应用程序的一部分显示,您可使用所需的查看器控件定义 ASP.NET 页面,然后只要使用标准 ASP.NET 代码合并到实际内容(单独存储,可能在数据库中)即可。
另一方面,在一个典型的 WPF 应用程序中,您可以只要使用标准 WPF、Windows 和 XAML 浏览器应用程序 (XBAP) 方法来定义您的用户界面,然后动态加载您的文档即可。图 12 显示的是使用我文章中的一个虚构库的一个简单示例,这些文章显示在左上角的一个列表框中。用户从列表中选择一篇文章时,该文档会自动加载到占用大部分窗体的 Flow Document Reader 控件。请注意,诸如 alpha 值混合处理等标准 WPF 技术在此设置中也能使用。您会注意到,实际的流文档是半透明的,背景中我的照片也在闪烁。另外也请注意,应用程序使用了一个列表框、图像,一个标签和一个 FlowDocumentReader 控件来创建虚构文章的库。
这个例子最棘手的地方是将实际文档加载到查看器控件中。这通过 System.Windows.Markup.XamlReader 类实现,它允许动态加载任何 XAML 内容,包括但不限于流文档。以下是我绑定到列表框选定更改事件的一行代码:
documentReader.Document = (FlowDocument)XamlReader.Load( File.OpenRead(fileName));
Load 方法会返回一个对象,因为 XAML 文件中的根元素可以代表许多不同类型。在我的例子中,我知道返回值为 FlowDocument,因此我只要执行一个转换,并将该文档指定给 FlowDocumentReader 控件的 Document 属性即可(此例中,我将控件实例命名为 documentReader)。请记住,这只是个例子。生产品质的代码此处当然还需要一些错误处理。
请注意,您了解的关于 WPF 的所有东西都适用于本例。例如,读取器控件只是支持样式的标准 WPF 控件。也就是说,您可以完全更改所有 UI 元素的外观,例如缩放栏、视图模式切换或分页控件。(您的控制能力受到限制的唯一元素是搜索框,虽然如果您不喜欢它,就根本不必用它。)
此外,我的例子显示的是基于 Windows 的应用程序,相同的应用程序也可以作为 XBAP 部署,并在 Web 浏览器内运行(当然,我们还是假设用户已安装了 .NET Framework 3.0)。请注意,Microsoft Silverlight™(原代号为“WPF/E”)是不够的,因为 Silverlight 只支持 WPF 的子集,且并不支持流文档。
如何编写流文档?当然,开发人员始终可以使用诸如 XamlPad 等低级工具来编写流文档。但是,在现实环境下,这不大可能。通常,流文档是使用 WYSIWYG 编辑器或通过从现有文档格式进行的内容转换来创建的。由于流文档可以使用 XAML 定义,因此转换现有 XML 内容特别简单。但也可以转换 HTML 和 Word 文档,而无需付出过大的精力(尽管需要编码,因为迄今为止尚未出现现成工具)。
对于 WYSIWYG 编辑,WPF 提供了一个现成的控件。WPF RichTextBox 控件可以本机编辑 XAML 流文档。该控件名称让人误以为它是专门针对 RTF 格式。尽管这个控件也支持 RTF,但实际上它主要用于流文档。事实上,该控件实际上会反映流文档查看控件,只不过它也支持编辑。有些人甚至会说,RichTextBox 控件应该被视为显示流文档的另一种方式。
将下列示例键入 XamlPad 中,以查看运行中的 RichTextBox 控件:
The quick brown fox jumps over the lazy dog.
恰如读取器控件一样,RichTextBox 也有一个 Document 属性,您可以自动以此会话中的流文档填充其值。这实际上会创建一个与 FlowDocumentScrollViewer 控件看起来很相似的 UI,只不过其中的文本可以编辑。请注意,此文本框控件始终以滚动方式处理流文档。在分页或多列模式下,无法在 RichTextBox 中编辑流文档。不过,编辑操作的结果是一个标准流文档,该文档可以使用您已看到的任何一种查看器机制显示,其中包括多列和分页模式。
关于 RichTextBox,值得一提的其中一项功能是集成的拼写检查。您可以按如下所示启用该功能:
...
图 13 显示了运行中的拼写检查程序。
使用此控件唯一复杂的地方是加载与保存。在许多情形下,您可能不会像在之前的例子中那样,将 RichTextBox 内容编码到 UI XAML 中,而是要动态加载和保存文档。RichTextBox 中文本的加载操作与为查看器控件加载流文档相同(见上文)。保存文档本质上则完全相反:您要先拿到文档对象,然后将其序列化回 XAML,如下所示:
System.Windows.Markup.XamlWriter. Save(richTextBox.Document)
这会将 XAML 作为一个字符串返回,然后您可以将其存储到文件或数据库中,或者使用您能想到的任何其他方式。
RichTextBox 非常方便,不过在这里还是要提醒几句话。虽然流文档代表了可用于呈现屏幕文档的最复杂的技术,但 RichTextBox 控件却一点也不复杂。它是编辑小型文档和文本段的极佳选择,但是您不会用它来编写书籍、杂志或营销手册。对于这些长格式,它的呈现过于简单,因为它不支持除滚动布局之外的其他任何布局(也就是说,还没有一种很好的可视方式可用于创建我稍后将谈到的高级布局)。同样,用于保存文档的方法也经常不尽人意。XmlWriter 类只是使用实时的内存中文档,并将其转换为 XAML,但遗憾的是,对于大规模的流文档操作非常重要的许多概念(例如样式),它并未注意。结果,尽管 XAML 忠实地保存了文档的外观,但文档看起来往往不太清爽,并且很大。RichTextBox 控件当然还是很有用的,但是别指望将它作为屏幕内容的桌面出版解决方案(虽然这类应用程序非常急需)。
至此您已了解了如何编写和查看流文档,接着让我们回到文档本身,看看更多的功能。流文档非常复杂,探究所有可用功能超出了本文范围,不过我想再讨论几项功能。
其中一项一直让我着迷的功能是“最佳段落”。启用该功能后,可以在指定段落内尽可能平均地分布空白,从而带来显著改进的阅读体验。“最佳段落”特别适合与另一项内置功能“断字”搭配使用,该功能(居然)会执行动态整个流文档或者个别段落的断字。
启用最佳段落和断字功能是项非常简单的操作:
图 14 显示的是相同的文档,只是呈现时启用或禁用了这些功能。两个版本间的区别非常细微,但是非常重要。请注意,左边的版本看起来更平和,主要因为词与词之间的空白分布得更平均,且从整体上减少了。特别是在屏幕上阅读大量文本时,这个看起来细小的区别会变得极为重要。
如您所见,FlowDocumentReader 控件采取多列的方法呈现文本。这是另一项非常重要的可读性功能,因为人们不喜欢读跨越整个宽屏显示页面宽度的一行行文字。实际列宽因各种因素会有所不同,例如用于内容显示的可用总宽度、缩放系数和定义的列宽等。流文档的默认列宽为字体大小的 20 倍,默认字体大小约为 300 个与设备无关的像素(3 1/8 英寸的精确尺寸显示)。您可以很轻松地覆盖此默认设置:
这会产生宽度约 400 像素的列。不过,还有其他一些因素会影响实际宽度。举例来说,如果缩放比例是 50%,那么实际列宽就只有 200 像素。另外,到目前为止,列宽更多地会被看作最小列宽。这意味着,如果可用总宽度为 900 像素,要呈现结果包含两列,并且要充分填满这整个 900 像素的话,就要让每列的宽度都超过定义的 400 像素。通常都需要这样,因为它会让呈现结果看起来非常美观。不过,如果您不想执行该行为,而只希望列宽实际就是 400 像素的话,可以确保列宽不是灵活可变的:
现在,所有列都正好是 400 像素(100% 缩放),剩余空间就让它显示为空白。
另一个您可能想尝试的与列相关的设置是列之间的空隙。这可以通过 ColumnGap 属性调整(此设置也是基于与设备无关的像素数):
其中一个相关的设置是列规则,它允许在列之间定义一个可视元素。请看此例(其结果见图 15):
当然,在许多出版物中,文档并不只是采用简单的列布局。通常还存在从一般流中提取出来的其他元素。您已见过这样的例子,例如将图像置于文档中。图 12 显示了图形设计师常用的一种排列方式。此图像位于两列之间,周围环绕文字,图像方方正正地位于内容中间,并没有影响任何一列的文字布局。这是一种常见的布局选择,只是还不能用于流文档之前我所了解的动态屏幕阅读环境。
创建此类布局的关键是图形块,它允许定义不与文档其余部分那样布局的内容。将图像置于图形标记内部就是一例,但图形还有许多其他用途。例如,您可以使用图形来定义横跨整个文档宽度的标题:
Windows Presentation Foundation in Windows Vista provides a great set of features.
在本代码中,图形包含另一个段落,即用作标题的文本。请注意,这里有一些您可用来创建高级、灵活文档的便捷属性。例如,看一下图形的宽度。我没有将宽度设为特定像素数,而是将其设为内容的确切宽度,这会根据整个内容的宽度自动调整图形宽度。
请看图 16。其中,您会注意到标题(通过图形放置)设为横跨整个内容宽度,这就将所有四列的位置都向下推移了。该图像从垂直和水平方向看都定位于页面中央。
请注意,其宽度与内容相关的图形不必始终与内容一样宽。以下例来说,图形宽度设为内容宽度的 75%:
宽度也可与其他项相关,例如列宽。下例图形始终是两列宽(除非只显示一列,那样宽度就会减为一列):
当然,图形高度可通过类似方式定义(虽然图形通常是随着内容纵向变化)。
另一重要方面是图形的位置。在代码段中,它设为横向定位为靠左,纵向定位为靠上。也就是说,图形会出现在当前内容页面的左上角,而无论其实际如何定义。然而在本示例中,图形被定义为文档的第一个元素,但即使该标题之前已有段落,它也会由于这些设置而被上移和左移。图 12 和图 16 中的照片已按类似方式,将其横向定位为“PageCenter”,在列之间移动。(所有这些设置的可用属性值都可以在 WPF 文档中找到)。
您可能已经注意到,本文涉及了大量手动编码。例如,每当需要改变字体时,您都要将该信息添加到块或内嵌元素中。到目前为止,这还不是一个大问题,因为大部分示例都很小。但是,如果有一本每 50 页为一章的书,您要改变每一段的字体,每次都手动来改的话,无疑会很繁重。幸运的是,现在有了一个更好的办法:如 WPF 中的其他任何内容一样,流文档支持样式。样式可被定义为实际流文档中指定名称的资源。以下是定义字体信息的样式:
...
然后,该样式会通过以下方式应用到段落(和其他元素):
The quick...
由于流文档的特性,样式特别常用。建议您对于最简单情形之外的任何情形,都使用样式来定义大部分格式选项,而不是通过个别内嵌元素的属性。样式可让您的文档保持紧凑,而且更易维护。
希望本文不只让您获得对流文档及其功能的基本了解,而且也激发起您的兴趣。还有许多更高级的功能,包括查看器控件的复杂样式、子类化和延伸文档、块和内嵌元素、数字权限管理、文本和墨迹注释功能以及高级字体格式等,绝对值得您深入研究。