在WPF中显示Word文件
小型WPF应用程序,用于加载DOCX文件,读取DOCX文件并将其内容显示在WPF中
目录

引言
Word 2007文档是Office Open XML文档,它是XML架构和ZIP压缩的组合,用于将XML文件和非XML文件一起存储在单个ZIP归档中。这些文档通常具有DOCX扩展名,但宏启用文档、模板等除外。
本文将介绍如何仅使用.NET Framework 3.0(不使用任何第三方代码)在WPF中读取和查看DOCX文件。
DOCX概述
DOCX文件实际上是一个已压缩的文件和文件夹的集合,称为包。包由包部件(包含任何类型数据的文件,如文本、图像、二进制文件等)和关系文件组成。包部件具有唯一的URI名称,关系XML文件包含这些URI。
当您使用解压缩应用程序打开DOCX文件时,您可以看到文档结构及其包的部件。
DOCX主内容存储在包部件document.xml中,该部件通常位于word目录中,但并非总是如此。要找出document.xml的URI(位置),我们应该读取_rels目录中的关系XML文件,并查找类型为http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument的关系。
Document.xml文件包含主要在Office Open XML规范的WordprocessingML XML命名空间中定义的XML元素。document.xml的基本结构包括一个document (<document>)元素,其中包含一个body (<body>)元素。Body元素由一个或多个块级元素组成,例如paragraph (<p>)元素。段落包含一个或多个内联级元素,例如run (<r>)元素。Run元素包含一个或多个文档的文本内容元素,例如text (<t>)、page break (<br>)和tab (<tab>)元素。
实现
简而言之,要检索和显示DOCX文本内容,应用程序将使用两个类:DocxReader
及其子类DocxToFlowDocumentConverter
。
DocxReader
将使用System.IO.Packaging
命名空间解压缩文件,通过关系找到document.xml文件,并使用XmlReader
读取它。
DocxToFlowDocumentConverter
将把XmlReader
中的XML元素转换为相应的WPF FlowDocument
元素。
DocxReader
DocxReader
构造函数首先从DOCX文件流打开(解压缩)包,并通过其PackageRelationship
检索mainDocumentPart
(document.xml)。
protected const string MainDocumentRelationshipType =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
private readonly Package package;
private readonly PackagePart mainDocumentPart;
public DocxReader(Stream stream)
{
if (stream == null)
throw new ArgumentNullException("stream");
this.package = Package.Open(stream, FileMode.Open, FileAccess.Read);
foreach (var relationship in
this.package.GetRelationshipsByType(MainDocumentRelationshipType))
{
this.mainDocumentPart =
package.GetPart(PackUriHelper.CreatePartUri(relationship.TargetUri));
break;
}
}
检索document.xml PackagePart
后,我们可以使用.NET的XmlReader
类来读取它。XmlReader
是一个快速的单向XML读取器,其路径轨迹与树数据结构中的深度优先遍历算法相同。
第一条路径,1到4,显示了从段落元素中检索文本的最简单路径。第二条路径,5 - ...,显示了更复杂的段落内容。在此路径中,我们还将读取段落属性 (<pPr>)和运行属性 (<rPr>),其中包含各种格式选项。
我们为在此路径轨迹中希望支持的每个元素创建一系列读取方法。
protected virtual void ReadDocument(XmlReader reader)
{
while (reader.Read())
if (reader.NodeType == XmlNodeType.Element && reader.NamespaceURI ==
WordprocessingMLNamespace && reader.LocalName == BodyElement)
{
ReadXmlSubtree(reader, this.ReadBody);
break;
}
}
private void ReadBody(XmlReader reader) {...}
private void ReadBlockLevelElement(XmlReader reader) {...}
protected virtual void ReadParagraph(XmlReader reader) {...}
private void ReadInlineLevelElement(XmlReader reader) {...}
protected virtual void ReadRun(XmlReader reader) {...}
private void ReadRunContentElement(XmlReader reader) {...}
protected virtual void ReadText(XmlReader reader) {...}
指出DocxReader
读取方法中的一些注意事项
- 我们使用
XmlNameTable
来存储XML命名空间、元素和属性名称。这使我们的代码看起来更好,而且性能也更好,因为现在我们可以对这些字符串进行对象(引用)比较,而不是更昂贵的字符串(值)比较,因为XmlReader
将使用来自XmlNameTable
的原子化字符串作为其LocalName
和NamespaceURI
属性,并且因为.NET使用字符串驻留并巧妙地通过首先进行引用相等性然后进行值相等性来实现字符串相等性。 - 我们使用
XmlReader.ReadSubtree
方法,同时将XmlReader
传递给特定的DocxReader
读取方法,以在该XML元素周围创建边界。现在,DocxReader
读取方法将只能访问该特定XML元素,而不是整个document.xml。使用此方法会带来一些性能损失,我们用它来换取更安全、更直观的代码。
private static void ReadXmlSubtree(XmlReader reader, Action<XmlReader> action)
{
using (var subtreeReader = reader.ReadSubtree())
{
// Position on the first node.
subtreeReader.Read();
if (action != null)
action(subtreeReader);
}
}
DocxToFlowDocumentConverter
此类继承自DocxReader
,并重写了DocxReader
的一些读取方法,以创建相应的WPF FlowDocument
元素。
例如,在读取document元素时,我们将创建一个新的FlowDocument
;在读取paragraph元素时,我们将创建一个新的Paragraph
元素;在读取run元素时,我们将创建一个新的Span
元素。
protected override void ReadDocument(XmlReader reader)
{
this.document = new FlowDocument();
this.document.BeginInit();
base.ReadDocument(reader);
this.document.EndInit();
}
protected override void ReadParagraph(XmlReader reader)
{
using (this.SetCurrent(new Paragraph()))
base.ReadParagraph(reader);
}
protected override void ReadRun(XmlReader reader)
{
using (this.SetCurrent(new Span()))
base.ReadRun(reader);
}
此外,此类实现了设置一些Paragraph
和Span
属性的功能,这些属性是从段落属性元素<pPr>
和运行属性元素<rPr>
读取的。当XmlReader
读取这些属性元素时,我们已经创建了一个新的Paragraph
或Span
元素,现在我们需要设置它们的属性。
由于我们正在从父元素(Paragraph
)移动到子元素(Spans
)然后返回到父元素,因此我们将不得不使用TextElement
类型的变量(Paragraph
和Span
的抽象基类)来跟踪我们在FlowDocument
中的当前元素。
这通过CurrentHandle
和C#的using语句的语法糖(用于try-finally结构)来完成。通过SetCurrent
方法,我们设置一个当前的TextElement
;通过Dispose
方法,我们将检索前一个TextElement
并将其设置为当前TextElement
。
private struct CurrentHandle : IDisposable
{
private readonly DocxToFlowDocumentConverter converter;
private readonly TextElement previous;
public CurrentHandle(DocxToFlowDocumentConverter converter, TextElement current)
{
this.converter = converter;
this.converter.AddChild(current);
this.previous = this.converter.current;
this.converter.current = current;
}
public void Dispose()
{
this.converter.current = this.previous;
}
}
private IDisposable SetCurrent(TextElement current)
{
return new CurrentHandle(this, current);
}
使用代码
要获取FlowDocument
,我们只需要从DOCX文件流创建一个新的DocxToFlowDocumentConverter
实例,并在该实例上调用Read
方法。
之后,我们可以使用FlowDocumentReader
控件在WPF应用程序中显示flow document内容。
using (var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
var flowDocumentConverter = new DocxToFlowDocumentConverter(stream);
flowDocumentConverter.Read();
this.flowDocumentReader.Document = flowDocumentConverter.Document;
this.Title = Path.GetFileName(path);
}
结论
DOCX Reader不是一个完整的解决方案,它旨在用于简单场景(不包含表格、列表、图片、页眉/页脚、样式等)。此应用程序可以得到增强以读取更多DOCX功能,但要获得具有所有高级功能的完整DOCX支持,需要更多时间和对DOCX文件格式的深入了解。希望本文及配套应用程序能为您提供对DOCX文件格式的一些见解,并可能为开发更复杂的DOCX相关应用程序提供基础。