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

在没有 Microsoft Word 的情况下填充 .docx 文档中的合并域

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (41投票s)

2009年7月29日

CPOL

8分钟阅读

viewsIcon

451493

downloadIcon

11029

用于在 Microsoft Word (docx) 模板文档中填充合并域(单个域和表格数据)的实用工具类,无需 Microsoft Word 本身

引言

此应用程序使用 Open XML SDK 来查找 Microsoft Word 文档中的 MERGEFIELD 并将其替换为提供的数据。此外,它还支持添加带数据的表格。这是一种在服务器端生成 Microsoft Word 文档的非常快速和稳定的方法。

主要代码仅包含1个类和几个完成所有工作的方法。我提供了一个前端来测试该类的功能。

为了能够运行该应用程序,您必须下载并安装前面提到的 SDK。由于该 SDK 是用 .NET 3.5 编写的,整个库只能在 .NET 3.5 及更高版本中工作。

背景

在一个客户项目中,我需要能够将 XML 文件中的数据注入到标准化的文档格式中。该客户仍在使用 Microsoft Office 2000,但在他所有的电脑上都安装了兼容包。

我不想通过 OLE 自动化来使用 Microsoft Word,因为它是一个在无人值守情况下运行的服务器端进程。由于微软不推荐在这种场景下使用 Microsoft Office,所以这不是一个可选项。但我记得新的 docx 格式只是一个包含可编辑的零散 XML 文件的压缩包。在网上搜索了一番后,我找到了 Open XML SDK,它在解析 Microsoft Word 文档结构方面提供了很大帮助。最终,我编写了一段代码,用 XML 文件中的数据填充 Microsoft Word docx 文件。这生成了所需的数据文档。

使用这种机制还给我带来了额外的好处,即客户可以自己编辑模板的布局。虽然这不是一个必要条件,但它在之后为我节省了很多时间。

使用前端

随源代码一起提供了一个前端应用程序,让您可以测试其功能。

wordfill_demo.jpg

这个应用程序是使用 WPF 编写的,并使用了 WPF 工具包中的 datagrid。为了能够运行这个测试应用程序,您需要从 CodePlex 下载并安装 WPF 工具包

当然,在测试任何东西之前,您需要一个 docx 模板。我在 zip 文件中添加了一个示例模板,但您也可以提供自己的模板(有关模板的详细信息,请参见下一章)。

在主窗口中,您必须首先在上面的文本框中提供模板的完整路径(只要此字段为空,“生成”按钮将处于禁用状态)。

在窗口中央的网格中添加您的字段和数据。要添加表格数据,请单击“添加表格”按钮,并定义表名和列名(最多5个)。单击“确定”并为表格提供数据。对每个表格重复此操作。

最后,单击“生成”。您的报告应该会自动出现。

docx 模板

首先,您需要一个包含多个 MERGEFIELD 的 Microsoft Word docx 文档,这些合并域作为您数据的占位符。mergefield 包含了您要添加的数据的名称(代码),例如

{MERGEFIELD CAND_NAME \* MERGEFORMAT}

此外还有3个可以使用的后缀

  • dp: 如果数据字段为空或未提供,则删除该段落
  • dr: (仅在表格中)如果数据字段为空或未提供,则删除该行
  • dt: (仅在表格中)如果数据字段为空或未提供,则删除整个表格

后缀被添加到字段名称后,并以“#”开头。例如

{MERGEFIELD CAND_NAME#dp \* MERGEFORMAT}

如果您想向 Word 文档中添加表格数据,您必须在 docx 文档中添加一个表格。表格的单元格包含合并域,用于指示必须放置在此处的数据字段。这些 Mergefield 的格式为:TBL_表格名_字段名。例如

{MERGEFIELD TBL_LANG_NAME \* MERGEFORMAT}

上面的 mergefield 告诉应用程序,此单元格包含 Lang 数据表中选定记录的 Name 列的值。应用程序将为数据表中找到的每条记录向表格中添加一行。(后缀不支持表格数据。每个表格单元格只能包含1个合并域。)

注意:应用程序会填充放置在文档页眉/页脚中的单个 mergefield,但不支持页眉/页脚中的表格数据。

Using the Code

FormFiller 类上只有一个(public)方法可以调用:GetWordReport

此方法接受3个参数

  • filename: 模板 docx 文件的完整路径
  • dataset: 一个包含必须添加到模板中的表格数据的 DataSetdataset 中的每个 datatable 都必须根据模板中使用的名称进行命名(见上文)。如果模板包含一个字段 TBL_LANG_NAME,那么 datatable 必须被命名为“LANG”并且必须包含一个名为“NAME”的列。如果没有表格数据,此参数可以为 null
  • values: 这是一个 string 类型的 Dictionary,其中键是字段名,值是必须放置在 Microsoft Word 文档中的数据。

如果一切顺利,填充好的模板将以字节数组的形式返回。

代码中的一些亮点

打开模板

使用 SDK 打开 docx 文件非常容易。只需要以下代码

using (MemoryStream stream = new MemoryStream(filebytes))
{
    // Create a Wordprocessing document object.
    using (WordprocessingDocument docx = WordprocessingDocument.Open(stream, true))
    {
        ...
    }
}

(filebytes 变量是读入的 docx 模板。)

为数据提供一个 Run 对象

OpenXML 文档中,您不能只添加包含普通硬回车或制表符的文本。这些必须被替换为正确的 XML 标签,才能在 Microsoft Word 中正确显示。

wordfill_xml.jpg

OpenXML 中的 mergefield 表示为 SIMPLEFIELD (<fldsimple>) 元素,并且可以包含子 RUN (<r>) 元素。字段的文本表示为 RUN 元素内的子 TEXT (<t>) 元素。一个 RUN 元素还可以有一个 RUNPROPERTIES (<rpr>) 元素,其中包含有关显示文本的附加布局信息,我们不希望丢失这些信息,因为我们希望我们的数据保持与模板中合并域相同的布局。

所以,如果我们想用我们的文本替换一个合并域,我们必须确保

  1. 我们数据中的制表符和回车符能正确呈现,并且
  2. 保留合并域的格式

FormFiller.GetRunElementForText 中的代码正是做了这件事

internal static Run GetRunElementForText(string text, SimpleField placeHolder)
{
    string rpr = null;
    if (placeHolder != null)
    {
        foreach (RunProperties placeholderrpr in placeHolder.Descendants<RunProperties>())
        {
            rpr = placeholderrpr.OuterXml;
            break;  // break at first
        }
    }

    Run r = new Run();
    if (!string.IsNullOrEmpty(rpr))
        r.Append(new RunProperties(rpr));

    if (string.IsNullOrEmpty(text)) return r;
    //  first process line breaks
    string[] split = text.Split(new string[] { "\n" }, StringSplitOptions.None);
    bool first = true;
    foreach (string s in split)
    {
        if (!first) r.Append(new Break());
        first = false;

        //  then process tabs
        bool firsttab = true;
        string[] tabsplit = s.Split(new string[] { "\t" }, StringSplitOptions.None);
        foreach (string tabtext in tabsplit)
        {
            if (!firsttab) r.Append(new TabChar());

            r.Append(new Text(tabtext));
            firsttab = false;
        }
    }
    return r;
}

此方法检查给定的合并域中是否存在 RUNPROPERTIES 元素。如果存在,则保留其内容 (.OuterXml) 并将其添加到新实例化的 RUN 元素中。然后检查数据中是否有制表符/回车符,并向数据中添加正确的元素(BREAKTABCHAR 元素)。

保存模板

一旦所有字段都已填充完毕,必须将更改显式保存回文档中(它不会自动发生)。

docx.MainDocumentPart.Document.Save();  // save main document back in package

处理页眉和页脚

页眉和页脚不与主文档放在同一个 XML 文件中(它们是包中不同的“文档部分”)。上面讨论的代码将找不到放置在页眉或页脚中的 MERGEFIELD。为此,需要对页眉和页脚部分进行循环。下面是一个遍历文档页眉的循环示例

foreach (HeaderPart hpart in docx.MainDocumentPart.HeaderParts)
{
    ... // process fields
    hpart.Header.Save();    // save header back in package
}

关注点

后缀(见上文)允许删除段落、行和表格。如果在遍历元素时执行此操作,循环会突然停止(而不会抛出任何错误)。例如:如果文档中有10个合并域,您正在使用以下语句遍历它们

foreach (var field in docx.MainDocumentPart.Document.Descendants<SimpleField>())
{
    ...
}

假设您决定删除元素5。例如,以下代码搜索 mergefield 的父 PARAGRAPH (<p>) 元素,并删除它(同时也删除了字段本身)

    Paragraph p = GetFirstParent<Paragraph>(field);
    if (p != null)
        p.Remove();

您将永远无法到达元素6到10。循环将在没有任何迹象表明您错过了4个元素的情况下退出。

为了解决这个问题,您会注意到代码中有两个循环:第一个循环用数据填充 mergefield。这个循环会维护一个空 mergefield 的列表,第二个循环将删除所有这些空的 mergefield

由 M. Chale 提供的更新

该库现在支持 UPPERLOWERFirstCapCaps 标签。UPPERLOWER 将整个 string 分别转换为大写或小写,FirstCap 将首字母大写,而其他所有字母都变为小写;Caps 则将单词标题化,即每个单词的首字母大写。请注意,Caps 例程有点简单,只将紧跟在空格后的字母大写。该库还支持应出现在数据之前或之后的文本。如果字段不为空且未标记为 #dp,它们将以与 MergeField 其余部分相同的格式插入。

带格式的示例字段:MERGEFIELD MYFIELD \ UPPER \b before \f after

感谢 Michael Chale 的这次更新。

针对 Microsoft Word 2010 的更新

自 Microsoft Word 2010 起,不再使用 SimpleField 元素。它已被多个 Run 元素所取代,其中一个(或多个)包含带有字段指令的 FieldCode 元素。该库的代码已被修改,以将这些新格式替换为旧式的 SimpleField,从而保持与 Microsoft Word 2007 文档的向后兼容性。

历史

  • 2009-07-29: 提交到 CodeProject
  • 2009-08-12: 现在页眉和页脚中的 Mergefield 也会被处理
  • 2009-08-14: 源代码小更新:表格中的 mergefield 格式现在也会被复制(粗体、斜体等)
  • 2009-09-15: 更新源代码:MemoryStream 不可扩展以及表格行属性未被复制。修复了这两个问题。
  • 2010-06-14: Michael Chale 添加了对字段格式化的支持。我已将解决方案更新至 VS2010。
  • 2010-08-02: 更新库以支持 Microsoft Word 2010 生成的文档
  • 2011-05-30: 向库中添加了几个错误修复
© . All rights reserved.