使用docx4j编程式地创建复杂的Word(.docx)文档
原文链接:Create complex Word (.docx) documents programatically with docx4j
原文作者:jos.dirksen
发表日期:2012年2月7日
两个月前,我需要用一些表格和段落创建动态的Word文档。过去我使用过POI做这些事情,但我发现它非常难用并且在我创建更加复杂的文档时它总不能很好地工作。所以在一番四处搜索之后,对于这个项目我决定使用docx4j。
根据官方网站的说法,Docx4j是一个:
"docx4j is a Java library for creating and manipulating Microsoft Open XML (Word docx, Powerpoint pptx, and Excel xlsx) files.
It is similar to Microsoft's OpenXML SDK, but for Java. "
在这篇文章中,我会向你展示几个可以用于生成word文档内容的示例,更具体地说,我们会看一下下面的两个例子:
一般的方式是首先创建一个包含你最终文档布局和主要样式的Word文档。你需要在这个文档中添加用于搜索并替换为真实内容的占位符(简单的字符串)。
例如,一个非常基本的模版看起像这样:
在这篇文章中会向你展示如何填充这个模版最终得到这个:
加载一个用于添加内容的模版word文档并保存为一个新文档
首先,我们创建一个可用作模版的简单的word文档。对于此只需打开Word,创建新文档然后保存为template.docx,这就是我们将要用于添加内容的word文档。我们需要做的第一件事是使用docx4j将这个文档加载进来,你可以使用下面的几行代码做这件事:
- private WordprocessingMLPackage getTemplate(String name) throws Docx4JException, FileNotFoundException {
- WordprocessingMLPackage template = WordprocessingMLPackage.load(new FileInputStream(new File(name)));
- return template;
- }
这样会返回一个表示完整的空白(在此时)文档Java对象。现在我们可以使用Docx4J API添加、删除以及更新这个word文档的内容,Docx4J有一些你可以用于遍历该文档的工具类。我自己写了几个助手方法使查找指定占位符并用真实内容进行替换的操作变地很简单。让我们来看一下其中的一个,这个计算是几个JAXB计算的包装器,允许你针对一个特定的类来搜索指定元素以及它所有的孩子,例如,你可以用它获取文档中所有的表格、表格中所有的行以及其它类似的操作。
- private static List<Object> getAllElementFromObject(Object obj, Class<?> toSearch) {
- List<Object> result = new ArrayList<Object>();
- if (obj instanceof JAXBElement) obj = ((JAXBElement<?>) obj).getValue();
- if (obj.getClass().equals(toSearch))
- result.add(obj);
- else if (obj instanceof ContentAccessor) {
- List<?> children = ((ContentAccessor) obj).getContent();
- for (Object child : children) {
- result.addAll(getAllElementFromObject(child, toSearch));
- }
- }
- return result;
- }
没什么复杂的,但真的很有帮助。让我们看一下怎样使用这个方法。在这个例子中我们只是使用不同的值来替换简单的文本占位符,例如你动态设置一个文档的标题。首先,在前面创建的模版文档中添加一个自定义占位符,我使用SJ_EX1作为占位符,我们将要用name参数来替换这个值。在docx4j中基本的文本元素用org.docx4j.wml.Text类来表示,替换这个简单的占位符我们需要做的就是调用这个方法:
- private void replacePlaceholder(WordprocessingMLPackage template, String name, String placeholder ) {
- List<Object> texts = getAllElementFromObject(template.getMainDocumentPart(), Text.class);
- for (Object text : texts) {
- Text textElement = (Text) text;
- if (textElement.getValue().equals(placeholder)) {
- textElement.setValue(name);
- }
- }
- }
这会在文档中查找所有的Text元素,并且与占位符匹配的Text都将被我们指定的值替换,现在我们需要做的仅是将这个文档写回一个文件中。
- private void writeDocxToStream(WordprocessingMLPackage template, String target) throws IOException, Docx4JException {
- File f = new File(target);
- template.save(f);
- }
如你所见,并不困难。
按这种方式,我们也可以向word文档添加更加复杂的内容,确定如何添加特定内容最简单的方式就是查看word文档的XML源码,这会告诉你需要什么样的包装及Word如何编排XML。在下一个例子中我们会看一下怎样添加一个段落。
向模版文档添加段落
你可能想知道为什么我们需要添加段落?我们已经可以添加文本,难道段落不就是一大段的文本吗?好吧,既是也不是,一个段落确实看起来像是一大段文本,但你需要考虑的是换行符,如果你像前面一样添加一个Text元素并且在文本中添加换行符,它们并不会出现,当你想要换行符时,你就需要创建一个新的段落。然而,幸运的是这对于Docx4j来说也非常地容易。
做这个需要下面的几步:
- 从模版中找到要替换的段落
- 将输入文本拆分成单独的行
- 每一行基于模版中的段落创建一个新的段落
- 移除原来的段落
使用我们已经拥有的助手方法也并不困难。
- private void replaceParagraph(String placeholder, String textToAdd, WordprocessingMLPackage template, ContentAccessor addTo) {
- // 1. get the paragraph
- List<Object> paragraphs = getAllElementFromObject(template.getMainDocumentPart(), P.class);
- P toReplace = null;
- for (Object p : paragraphs) {
- List<Object> texts = getAllElementFromObject(p, Text.class);
- for (Object t : texts) {
- Text content = (Text) t;
- if (content.getValue().equals(placeholder)) {
- toReplace = (P) p;
- break;
- }
- }
- }
- // we now have the paragraph that contains our placeholder: toReplace
- // 2. split into seperate lines
- String as[] = StringUtils.splitPreserveAllTokens(textToAdd, '\n');
- for (int i = 0; i < as.length; i++) {
- String ptext = as[i];
- // 3. copy the found paragraph to keep styling correct
- P copy = (P) XmlUtils.deepCopy(toReplace);
- // replace the text elements from the copy
- List<?> texts = getAllElementFromObject(copy, Text.class);
- if (texts.size() > 0) {
- Text textToReplace = (Text) texts.get(0);
- textToReplace.setValue(ptext);
- }
- // add the paragraph to the document
- addTo.getContent().add(copy);
- }
- // 4. remove the original one
- ((ContentAccessor)toReplace.getParent()).getContent().remove(toReplace);
- }
在这个方法中我们使用提供的文本替换了段落的内容,然后将新的段落指定为addTo方法的参数。
- String placeholder = "SJ_EX1";
- String toAdd = "jos\ndirksen";
- replaceParagraph(placeholder, toAdd, template, template.getMainDocumentPart());
如果你用更多的内容针对模版文档运行这个例子,你会注意到这些段落出现在你文档的底部。原因是段落被添加回主文档,如果你希望段落被添加到文档的指定位置(你通常会希望如此),你可以将它们包到一个1X1无边框的表格中,这个表格被视为段落的父亲并且新的段落可以添加到那里。
在模版文档中添加表格
我准备展示的最后一个例子是如何向一个word模版添加表格,一个更适合的表述应该是,如何填充word模版中预定义的表格。就像我们对文本和段落所做的一样,将要替换占位符。为了本例要在你的word文档中添加一个简单的表格(你可以设置喜欢的样式),在表格中添加一个“仿制行”(原文:dummy row,傀儡行?假的行?不知道怎样翻译,意思就是模版行)作为内容的模版。在代码中我们将要查找到该行,复制它,并且在Java代码中使用新行替换内容,如下:
- 找到包含其中一个关键字的表格
- 复制用作行模版的行
- 针对每一条的数据,向表格添加基于行模版创建的一行
- 移除原来的模版行
跟我们针对段落时展示的方法一样,首先来看一下我们将要提供怎样的替换数据。对于本例,我提供了一个hashmap的集合,其中包含要被替换替换的占位符名称和替换它的值,同时也提供了可以在表格行中发现的替换符。
- Map<String,String> repl1 = new HashMap<String, String>();
- repl1.put("SJ_FUNCTION", "function1");
- repl1.put("SJ_DESC", "desc1");
- repl1.put("SJ_PERIOD", "period1");
- Map<String,String> repl2 = new HashMap<String, String>();
- repl2.put("SJ_FUNCTION", "function2");
- repl2.put("SJ_DESC", "desc2");
- repl2.put("SJ_PERIOD", "period2");
- Map<String,String> repl3 = new HashMap<String, String>();
- repl3.put("SJ_FUNCTION", "function3");
- repl3.put("SJ_DESC", "desc3");
- repl3.put("SJ_PERIOD", "period3");
- replaceTable(new String[]{"SJ_FUNCTION","SJ_DESC","SJ_PERIOD"}, Arrays.asList(repl1,repl2,repl3), template);
现在,replaceTable方法如下所示:
- private void replaceTable(String[] placeholders, List<Map<String, String>> textToAdd,
- WordprocessingMLPackage template) throws Docx4JException, JAXBException {
- List<Object> tables = getAllElementFromObject(template.getMainDocumentPart(), Tbl.class);
- // 1. find the table
- Tbl tempTable = getTemplateTable(tables, placeholders[0]);
- List<Object> rows = getAllElementFromObject(tempTable, Tr.class);
- // first row is header, second row is content
- if (rows.size() == 2) {
- // this is our template row
- Tr templateRow = (Tr) rows.get(1);
- for (Map<String, String> replacements : textToAdd) {
- // 2 and 3 are done in this method
- addRowToTable(tempTable, templateRow, replacements);
- }
- // 4. remove the template row
- tempTable.getContent().remove(templateRow);
- }
- }
该方法找到表格,获取第一行并且遍历提供的map向表格添加新行,在将其返回之前删除模版行。这个方法用到了两个助手方法:addRowToTable 和 getTemplateTable。我们首先看一下后面的那个:
- private Tbl getTemplateTable(List<Object> tables, String templateKey) throws Docx4JException, JAXBException {
- for (Iterator<Object> iterator = tables.iterator(); iterator.hasNext();) {
- Object tbl = iterator.next();
- List<?> textElements = getAllElementFromObject(tbl, Text.class);
- for (Object text : textElements) {
- Text textElement = (Text) text;
- if (textElement.getValue() != null && textElement.getValue().equals(templateKey))
- return (Tbl) tbl;
- }
- }
- return null;
- }
这个方法只是查看表格是否含有我们的占位符,如果有则返回表格。addRowToTable方法也很简单:
- private static void addRowToTable(Tbl reviewtable, Tr templateRow, Map<String, String> replacements) {
- Tr workingRow = (Tr) XmlUtils.deepCopy(templateRow);
- List<?> textElements = getAllElementFromObject(workingRow, Text.class);
- for (Object object : textElements) {
- Text text = (Text) object;
- String replacementValue = (String) replacements.get(text.getValue());
- if (replacementValue != null)
- text.setValue(replacementValue);
- }
- reviewtable.getContent().add(workingRow);
- }
本方法复制模版并且使用给定的值替换模版行中的占位符,然后这个复制行被添加到表格,就这么简单。使用这块代码我们可以在保持表格样式和布局的同时填充word文档中任意的表格。
这篇文章就到这里,使用段落和表格你可以创建很多不同风格的文档,而且这与通常生成的文档风格能很好地匹配。相同的方式也适用于向word文档中添加其它类型的内容。