65.9K
CodeProject 正在变化。 阅读更多。
Home

在WPF中显示Word文件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (158投票s)

2013年9月5日

CPOL

5分钟阅读

viewsIcon

109690

downloadIcon

5802

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

目录

DOCX in WPF application

引言

Word 2007文档是Office Open XML文档,它是XML架构和ZIP压缩的组合,用于将XML文件和非XML文件一起存储在单个ZIP归档中。这些文档通常具有DOCX扩展名,但宏启用文档、模板等除外。

本文将介绍如何仅使用.NET Framework 3.0(不使用任何第三方代码)在WPF中读取和查看DOCX文件。

DOCX概述

DOCX文件实际上是一个已压缩的文件和文件夹的集合,称为包。包由包部件(包含任何类型数据的文件,如文本、图像、二进制文件等)和关系文件组成。包部件具有唯一的URI名称,关系XML文件包含这些URI。

当您使用解压缩应用程序打开DOCX文件时,您可以看到文档结构及其包的部件。

DOCX content

DOCX主内容存储在包部件document.xml中,该部件通常位于word目录中,但并非总是如此。要找出document.xml的URI(位置),我们应该读取_rels目录中的关系XML文件,并查找类型为http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument的关系。

DOCX content

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检索mainDocumentPartdocument.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读取器,其路径轨迹与树数据结构中的深度优先遍历算法相同。

DOCX elements

第一条路径,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的原子化字符串作为其LocalNameNamespaceURI属性,并且因为.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);
}

此外,此类实现了设置一些ParagraphSpan属性的功能,这些属性是从段落属性元素<pPr>和运行属性元素<rPr>读取的。当XmlReader读取这些属性元素时,我们已经创建了一个新的ParagraphSpan元素,现在我们需要设置它们的属性。

由于我们正在从父元素(Paragraph)移动到子元素(Spans)然后返回到父元素,因此我们将不得不使用TextElement类型的变量(ParagraphSpan的抽象基类)来跟踪我们在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相关应用程序提供基础。

© . All rights reserved.