使用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將這個文檔加載進來,你可以使用下面的幾行代碼做這件事:

 

[java]  view plain  copy
 
  1. private WordprocessingMLPackage getTemplate(String name) throws Docx4JException, FileNotFoundException {  
  2.     WordprocessingMLPackage template = WordprocessingMLPackage.load(new FileInputStream(new File(name)));  
  3.     return template;  
  4. }  


這樣會返回一個表示完整的空白(在此時)文檔Java對象。現在我們可以使用Docx4J API添加、刪除以及更新這個word文檔的內容,Docx4J有一些你可以用於遍歷該文檔的工具類。我自己寫了幾個助手方法使查找指定占位符並用真實內容進行替換的操作變地很簡單。讓我們來看一下其中的一個,這個計算是幾個JAXB計算的包裝器,允許你針對一個特定的類來搜索指定元素以及它所有的孩子,例如,你可以用它獲取文檔中所有的表格、表格中所有的行以及其它類似的操作。

 

 

[java]  view plain  copy
 
  1. private static List<Object> getAllElementFromObject(Object obj, Class<?> toSearch) {  
  2.     List<Object> result = new ArrayList<Object>();  
  3.     if (obj instanceof JAXBElement) obj = ((JAXBElement<?>) obj).getValue();  
  4.   
  5.     if (obj.getClass().equals(toSearch))  
  6.         result.add(obj);  
  7.     else if (obj instanceof ContentAccessor) {  
  8.         List<?> children = ((ContentAccessor) obj).getContent();  
  9.         for (Object child : children) {  
  10.             result.addAll(getAllElementFromObject(child, toSearch));  
  11.         }  
  12.   
  13.     }  
  14.     return result;  
  15. }  


沒什么復雜的,但真的很有幫助。讓我們看一下怎樣使用這個方法。在這個例子中我們只是使用不同的值來替換簡單的文本占位符,例如你動態設置一個文檔的標題。首先,在前面創建的模版文檔中添加一個自定義占位符,我使用SJ_EX1作為占位符,我們將要用name參數來替換這個值。在docx4j中基本的文本元素用org.docx4j.wml.Text類來表示,替換這個簡單的占位符我們需要做的就是調用這個方法:

 

 

[java]  view plain  copy
 
  1. private void replacePlaceholder(WordprocessingMLPackage template, String name, String placeholder ) {  
  2.     List<Object> texts = getAllElementFromObject(template.getMainDocumentPart(), Text.class);  
  3.   
  4.     for (Object text : texts) {  
  5.         Text textElement = (Text) text;  
  6.         if (textElement.getValue().equals(placeholder)) {  
  7.             textElement.setValue(name);  
  8.         }  
  9.     }  
  10. }  


這會在文檔中查找所有的Text元素,並且與占位符匹配的Text都將被我們指定的值替換,現在我們需要做的僅是將這個文檔寫回一個文件中。

 

 

[java]  view plain  copy
 
  1. private void writeDocxToStream(WordprocessingMLPackage template, String target) throws IOException, Docx4JException {  
  2.     File f = new File(target);  
  3.     template.save(f);  
  4. }  


如你所見,並不困難。

 

按這種方式,我們也可以向word文檔添加更加復雜的內容,確定如何添加特定內容最簡單的方式就是查看word文檔的XML源碼,這會告訴你需要什么樣的包裝及Word如何編排XML。在下一個例子中我們會看一下怎樣添加一個段落。

 

向模版文檔添加段落

 

你可能想知道為什么我們需要添加段落?我們已經可以添加文本,難道段落不就是一大段的文本嗎?好吧,既是也不是,一個段落確實看起來像是一大段文本,但你需要考慮的是換行符,如果你像前面一樣添加一個Text元素並且在文本中添加換行符,它們並不會出現,當你想要換行符時,你就需要創建一個新的段落。然而,幸運的是這對於Docx4j來說也非常地容易。

做這個需要下面的幾步:

 

  1. 從模版中找到要替換的段落
  2. 將輸入文本拆分成單獨的行
  3. 每一行基於模版中的段落創建一個新的段落
  4. 移除原來的段落

使用我們已經擁有的助手方法也並不困難。

 

 

[java]  view plain  copy
 
  1. private void replaceParagraph(String placeholder, String textToAdd, WordprocessingMLPackage template, ContentAccessor addTo) {  
  2.     // 1. get the paragraph  
  3.     List<Object> paragraphs = getAllElementFromObject(template.getMainDocumentPart(), P.class);  
  4.   
  5.     P toReplace = null;  
  6.     for (Object p : paragraphs) {  
  7.         List<Object> texts = getAllElementFromObject(p, Text.class);  
  8.         for (Object t : texts) {  
  9.             Text content = (Text) t;  
  10.             if (content.getValue().equals(placeholder)) {  
  11.                 toReplace = (P) p;  
  12.                 break;  
  13.             }  
  14.         }  
  15.     }  
  16.   
  17.     // we now have the paragraph that contains our placeholder: toReplace  
  18.     // 2. split into seperate lines  
  19.     String as[] = StringUtils.splitPreserveAllTokens(textToAdd, '\n');  
  20.   
  21.     for (int i = 0; i < as.length; i++) {  
  22.         String ptext = as[i];  
  23.   
  24.         // 3. copy the found paragraph to keep styling correct  
  25.         P copy = (P) XmlUtils.deepCopy(toReplace);  
  26.   
  27.         // replace the text elements from the copy  
  28.         List<?> texts = getAllElementFromObject(copy, Text.class);  
  29.         if (texts.size() > 0) {  
  30.             Text textToReplace = (Text) texts.get(0);  
  31.             textToReplace.setValue(ptext);  
  32.         }  
  33.   
  34.         // add the paragraph to the document  
  35.         addTo.getContent().add(copy);  
  36.     }  
  37.   
  38.     // 4. remove the original one  
  39.     ((ContentAccessor)toReplace.getParent()).getContent().remove(toReplace);  
  40.   
  41. }  


在這個方法中我們使用提供的文本替換了段落的內容,然后將新的段落指定為addTo方法的參數。

 

 

[java]  view plain  copy
 
  1. String placeholder = "SJ_EX1";  
  2. String toAdd = "jos\ndirksen";  
  3.   
  4. replaceParagraph(placeholder, toAdd, template, template.getMainDocumentPart());  


如果你用更多的內容針對模版文檔運行這個例子,你會注意到這些段落出現在你文檔的底部。原因是段落被添加回主文檔,如果你希望段落被添加到文檔的指定位置(你通常會希望如此),你可以將它們包到一個1X1無邊框的表格中,這個表格被視為段落的父親並且新的段落可以添加到那里。

 

 

在模版文檔中添加表格

 

我准備展示的最后一個例子是如何向一個word模版添加表格,一個更適合的表述應該是,如何填充word模版中預定義的表格。就像我們對文本和段落所做的一樣,將要替換占位符。為了本例要在你的word文檔中添加一個簡單的表格(你可以設置喜歡的樣式),在表格中添加一個“仿制行”(原文:dummy row,傀儡行?假的行?不知道怎樣翻譯,意思就是模版行)作為內容的模版。在代碼中我們將要查找到該行,復制它,並且在Java代碼中使用新行替換內容,如下:

 

  1. 找到包含其中一個關鍵字的表格
  2. 復制用作行模版的行
  3. 針對每一條的數據,向表格添加基於行模版創建的一行
  4. 移除原來的模版行

跟我們針對段落時展示的方法一樣,首先來看一下我們將要提供怎樣的替換數據。對於本例,我提供了一個hashmap的集合,其中包含要被替換替換的占位符名稱和替換它的值,同時也提供了可以在表格行中發現的替換符。

 

 

[java]  view plain  copy
 
  1. Map<String,String> repl1 = new HashMap<String, String>();  
  2. repl1.put("SJ_FUNCTION", "function1");  
  3. repl1.put("SJ_DESC", "desc1");  
  4. repl1.put("SJ_PERIOD", "period1");  
  5.   
  6. Map<String,String> repl2 = new HashMap<String, String>();  
  7. repl2.put("SJ_FUNCTION", "function2");  
  8. repl2.put("SJ_DESC", "desc2");  
  9. repl2.put("SJ_PERIOD", "period2");  
  10.   
  11. Map<String,String> repl3 = new HashMap<String, String>();  
  12. repl3.put("SJ_FUNCTION", "function3");  
  13. repl3.put("SJ_DESC", "desc3");  
  14. repl3.put("SJ_PERIOD", "period3");  
  15.   
  16. replaceTable(new String[]{"SJ_FUNCTION","SJ_DESC","SJ_PERIOD"}, Arrays.asList(repl1,repl2,repl3), template);  


現在,replaceTable方法如下所示:

 

 

[java]  view plain  copy
 
  1. private void replaceTable(String[] placeholders, List<Map<String, String>> textToAdd,  
  2.             WordprocessingMLPackage template) throws Docx4JException, JAXBException {  
  3.     List<Object> tables = getAllElementFromObject(template.getMainDocumentPart(), Tbl.class);  
  4.   
  5.     // 1. find the table  
  6.     Tbl tempTable = getTemplateTable(tables, placeholders[0]);  
  7.     List<Object> rows = getAllElementFromObject(tempTable, Tr.class);  
  8.   
  9.     // first row is header, second row is content  
  10.     if (rows.size() == 2) {  
  11.         // this is our template row  
  12.         Tr templateRow = (Tr) rows.get(1);  
  13.   
  14.         for (Map<String, String> replacements : textToAdd) {  
  15.             // 2 and 3 are done in this method  
  16.             addRowToTable(tempTable, templateRow, replacements);  
  17.         }  
  18.   
  19.         // 4. remove the template row  
  20.         tempTable.getContent().remove(templateRow);  
  21.     }  
  22. }  


該方法找到表格,獲取第一行並且遍歷提供的map向表格添加新行,在將其返回之前刪除模版行。這個方法用到了兩個助手方法:addRowToTable 和 getTemplateTable。我們首先看一下后面的那個:

 

 

[java]  view plain  copy
 
  1. private Tbl getTemplateTable(List<Object> tables, String templateKey) throws Docx4JException, JAXBException {  
  2.     for (Iterator<Object> iterator = tables.iterator(); iterator.hasNext();) {  
  3.         Object tbl = iterator.next();  
  4.         List<?> textElements = getAllElementFromObject(tbl, Text.class);  
  5.         for (Object text : textElements) {  
  6.             Text textElement = (Text) text;  
  7.             if (textElement.getValue() != null && textElement.getValue().equals(templateKey))  
  8.                 return (Tbl) tbl;  
  9.         }  
  10.     }  
  11.     return null;  
  12. }  


這個方法只是查看表格是否含有我們的占位符,如果有則返回表格。addRowToTable方法也很簡單:

 

 

[java]  view plain  copy
 
  1. private static void addRowToTable(Tbl reviewtable, Tr templateRow, Map<String, String> replacements) {  
  2.     Tr workingRow = (Tr) XmlUtils.deepCopy(templateRow);  
  3.     List<?> textElements = getAllElementFromObject(workingRow, Text.class);  
  4.     for (Object object : textElements) {  
  5.         Text text = (Text) object;  
  6.         String replacementValue = (String) replacements.get(text.getValue());  
  7.         if (replacementValue != null)  
  8.             text.setValue(replacementValue);  
  9.     }  
  10.   
  11.     reviewtable.getContent().add(workingRow);  
  12. }  


本方法復制模版並且使用給定的值替換模版行中的占位符,然后這個復制行被添加到表格,就這么簡單。使用這塊代碼我們可以在保持表格樣式和布局的同時填充word文檔中任意的表格。

 

這篇文章就到這里,使用段落和表格你可以創建很多不同風格的文檔,而且這與通常生成的文檔風格能很好地匹配。相同的方式也適用於向word文檔中添加其它類型的內容。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM