使用poi生成word文件(最全例子)
1.說明
平時的專案中,我們可能需要做大量的報告。而這些報告中,有些是固定的格式,有些是需要自定義模板來生成。如果是固定的形式的,那麼相對而言是比較好做的,但要是根據模板來生成報告,比如:word、Excel或PDF。這樣的話,可能需要我們花點時間去解決了。這篇部落格主要是帶領大家學習一下,如何用poi技術來實現生成word報告。
2.設計
我們知道,poi的技術可以做出word、Excel、PDF等檔案。在網上也有大量的部落格是關於如何使用poi的ftl的模板來生成word、Excel、PDF。主要是思路是將某個模板(自己定義的,可以是word或Excel)上傳至伺服器,然後解析成對應的ftl檔案,之後服務在做報告時,就可以直接拿取對應的ftl檔案來生成報告。這種方式會比較靈活,現在用的也比較多。但是對於一份簡單的,不需要ftl的檔案來作為中間生成檔案時,那麼可能會比較難了。比如,接下來我們要學習的,使用word的模板作為報告模板,然後直接生成需要的報告。但是word中的所的之間的設定,如字型大小、顏色等都不能改變。這個思路大概是:使用者直接設計一個word模板,然後上傳至伺服器,伺服器將其直接儲存到對應的資料夾下;在要生成報告時,直接拿word的模板來填充資料。這樣,好像也不是很難,但是想想細節上的地方,可能很多東西就不好做了。接下來,我們來說說問題所在。
3.問題
我們在使用word作為一份模板時,首先要解決的時,如何向模板中定義的欄位替換資料,如:{name}要換成真實的改名;其次我們如果要做表格的話,如何向word上直接畫出表格;再次,表格的行列要怎麼合併,多張表應該怎麼複製等;最後,我們應該怎麼將一比我圖片插入到word中。帶著這些問題,我們來一一解決。
4.開發
4.1普通文字替換
在word的檔案中,我們同一個使用“{**}”來標誌需要替換的檔案,如{name}就是要將name的欄位替換成真實的姓名。所以我們需要用正則表示式來尋找到廣本的內容是否包含了“{}”。正則表示式是:\\{.+?\\}。同時,我們需要用poi提供的XWPFRun的介面來替換文字。為了使用替換後的文字可以的換行的操作。比如,我們自己定義的一行文字需要換行,筆者用“@”符號作為標籤,也就是如果遇到內容含有“@”,就要實現換行。而換行是呼叫XWPFRun的addBreak()的方法。部分程式碼如下:
public void replaceParagraph(XWPFParagraph xWPFParagraph, Map<String, Object> parametersMap) { List<XWPFRun> runs = xWPFParagraph.getRuns(); String xWPFParagraphText = xWPFParagraph.getText(); String regEx = "\\{.+?\\}"; Pattern pattern = Pattern.compile(regEx); Matcher matcher = pattern.matcher(xWPFParagraphText);//正則匹配字串{****} if (matcher.find()) { // 查詢到有標籤才執行替換 int beginRunIndex = xWPFParagraph.searchText("{", new PositionInParagraph()).getBeginRun();// 標籤開始run位置 int endRunIndex = xWPFParagraph.searchText("}", new PositionInParagraph()).getEndRun();// 結束標籤 StringBuffer key = new StringBuffer(); if (beginRunIndex == endRunIndex) { // {**}在一個run標籤內 XWPFRun beginRun = runs.get(beginRunIndex); String beginRunText = beginRun.text(); int beginIndex = beginRunText.indexOf("{"); int endIndex = beginRunText.indexOf("}"); int length = beginRunText.length(); if (beginIndex == 0 && endIndex == length - 1) { // 該run標籤只有{**} XWPFRun insertNewRun = xWPFParagraph.insertNewRun(beginRunIndex); insertNewRun.getCTR().setRPr(beginRun.getCTR().getRPr()); // 設定文字 key.append(beginRunText.substring(1, endIndex)); insertNewRun.setText(getValueBykey(key.toString(),parametersMap)); xWPFParagraph.removeRun(beginRunIndex + 1); } else { // 該run標籤為**{**}** 或者 **{**} 或者{**}**,替換key後,還需要加上原始key前後的文字 XWPFRun insertNewRun = xWPFParagraph.insertNewRun(beginRunIndex); insertNewRun.getCTR().setRPr(beginRun.getCTR().getRPr()); // 設定文字 key.append(beginRunText.substring(beginRunText.indexOf("{")+1, beginRunText.indexOf("}"))); String textString=beginRunText.substring(0, beginIndex) + getValueBykey(key.toString(),parametersMap) + beginRunText.substring(endIndex + 1); insertNewRun.setText(textString); xWPFParagraph.removeRun(beginRunIndex + 1); } }else { // {**}被分成多個run //先處理起始run標籤,取得第一個{key}值 XWPFRun beginRun = runs.get(beginRunIndex); String beginRunText = beginRun.text(); int beginIndex = beginRunText.indexOf("{"); if (beginRunText.length()>1 ) { key.append(beginRunText.substring(beginIndex+1)); } ArrayList<Integer> removeRunList = new ArrayList<Integer>();//需要移除的run //處理中間的run for (int i = beginRunIndex + 1; i < endRunIndex; i++) { XWPFRun run = runs.get(i); String runText = run.text(); key.append(runText); removeRunList.add(i); } // 獲取endRun中的key值 XWPFRun endRun = runs.get(endRunIndex); String endRunText = endRun.text(); int endIndex = endRunText.indexOf("}"); //run中**}或者**}** if (endRunText.length()>1 && endIndex!=0) { key.append(endRunText.substring(0,endIndex)); } //******************************************************************* //取得key值後替換標籤 //先處理開始標籤 if (beginRunText.length()==2 ) { // run標籤內文字{ XWPFRun insertNewRun = xWPFParagraph.insertNewRun(beginRunIndex); insertNewRun.getCTR().setRPr(beginRun.getCTR().getRPr()); // 設定文字 insertNewRun.setText(getValueBykey(key.toString(),parametersMap)); xWPFParagraph.removeRun(beginRunIndex + 1);//移除原始的run }else { // 該run標籤為**{**或者 {** ,替換key後,還需要加上原始key前的文字 XWPFRun insertNewRun = xWPFParagraph.insertNewRun(beginRunIndex); insertNewRun.getCTR().setRPr(beginRun.getCTR().getRPr()); // 設定文字 String textString=beginRunText.substring(0,beginRunText.indexOf("{"))+getValueBykey(key.toString(),parametersMap); // System.out.println(">>>>>"+textString); //分行處理 if(textString.contains("@")) { String[] textStrings = textString.split("@"); for(int i = 0; i < textStrings.length;i++) { //System.out.println(">>>>>textStrings>>"+textStrings[i]); insertNewRun.setText(textStrings[i]); //insertNewRun.addCarriageReturn(); insertNewRun.addBreak();//換行 } } else if(textString.endsWith(".png")) { CTInline inline = insertNewRun.getCTR().addNewDrawing().addNewInline(); try { insertPicture(document,textString, inline, 500, 200); document.createParagraph();// 添加回車換行 } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } else { insertNewRun.setText(textString); } xWPFParagraph.removeRun(beginRunIndex + 1);//移除原始的run } //處理結束標籤 if (endRunText.length()==1 ) { // run標籤內文字只有} XWPFRun insertNewRun = xWPFParagraph.insertNewRun(endRunIndex); insertNewRun.getCTR().setRPr(endRun.getCTR().getRPr()); // 設定文字 insertNewRun.setText(""); xWPFParagraph.removeRun(endRunIndex + 1);//移除原始的run }else { // 該run標籤為**}**或者 }** 或者**},替換key後,還需要加上原始key後的文字 XWPFRun insertNewRun = xWPFParagraph.insertNewRun(endRunIndex); insertNewRun.getCTR().setRPr(endRun.getCTR().getRPr()); // 設定文字 String textString=endRunText.substring(endRunText.indexOf("}")+1); insertNewRun.setText(textString); xWPFParagraph.removeRun(endRunIndex + 1);//移除原始的run } //處理中間的run標籤 for (int i = 0; i < removeRunList.size(); i++) { XWPFRun xWPFRun = runs.get(removeRunList.get(i));//原始run XWPFRun insertNewRun = xWPFParagraph.insertNewRun(removeRunList.get(i)); insertNewRun.getCTR().setRPr(xWPFRun.getCTR().getRPr()); insertNewRun.setText(""); xWPFParagraph.removeRun(removeRunList.get(i) + 1);//移除原始的run } }// 處理${**}被分成多個run replaceParagraph( xWPFParagraph, parametersMap); }//if 有標籤 }
4.2實現表格內行迴圈
為了達到一份表格的行可以迴圈,我們使用”##{foreachTableRow}##”,說明同一表格的行需要迴圈。該標籤定義到表格的最上方。然而,表格中會有固定的欄位名,其內容 是需要迴圈的,所以我們用“##{foreachRows}##”來表示,行需要迴圈。表格設計如下:
表格中的{project}{class}無非就是內容迴圈了。部分程式碼如下:
XWPFTable newCreateTable = document.createTable();// 建立新表格,預設一行一列
List<XWPFTableRow> TempTableRows = templateTable.getRows();// 獲取模板表格所有行
int tagRowsIndex = 0;// 標籤行indexs
for (int i = 0, size = TempTableRows.size(); i < size; i++) {
String rowText = TempTableRows.get(i).getCell(0).getText();// 獲取到表格行的第一個單元格
if (rowText.indexOf("##{foreachRows}##") > -1) {
tagRowsIndex = i;
break;
}
}
/* 複製模板行和標籤行之前的行 即第一行的內容*/
for (int i = 1; i < tagRowsIndex; i++) {
XWPFTableRow newCreateRow = newCreateTable.createRow();
CopyTableRow(newCreateRow, TempTableRows.get(i));// 複製行
replaceTableRow(newCreateRow, parametersMap);// 處理不迴圈標籤的替換
}
/* 迴圈生成模板行 */
XWPFTableRow tempRow = TempTableRows.get(tagRowsIndex + 1);// 獲取到模板行
for (int i = 0; i < list.size(); i++) {
XWPFTableRow newCreateRow = newCreateTable.createRow();
CopyTableRow(newCreateRow, tempRow);// 複製模板行
replaceTableRow(newCreateRow, list.get(i));// 處理標籤替換
}
/* 複製模板行和標籤行之後的行 */
for (int i = tagRowsIndex + 2; i < TempTableRows.size(); i++) {
XWPFTableRow newCreateRow = newCreateTable.createRow();
CopyTableRow(newCreateRow, TempTableRows.get(i));// 複製行
replaceTableRow(newCreateRow, parametersMap);// 處理不迴圈標籤的替換
}
newCreateTable.removeRow(0);// 移除多出來的第一行
document.createParagraph();// 添加回車換行
4.3實現整個表格迴圈
有時候,我們需要是寫一整段的內容,並這段內容的某個值是不一樣的,但大部分是一樣的,且要迴圈輸出。這個時候,我們可以借用表格來實現。也就是定義個表格迴圈,而表格的邊框設定為隱藏的,這樣就可以實現我們的需求了。定義的表格樣式如下:
圖片中是一個表格,只是邊框被筆者隱藏了。從上面的圖片可以看出,“##{foreachTable}##”是需要表格迴圈,而其它的“{**}”都只是內容替換。部分程式碼如下:
for (Map<String, Object> map : list) {
List<XWPFTableRow> templateTableRows = templateTable.getRows();// 獲取模板表格所有行
XWPFTable newCreateTable = document.createTable();// 建立新表格,預設一行一列
for (int i = 1; i < templateTableRows.size(); i++) {
XWPFTableRow newCreateRow = newCreateTable.createRow();
CopyTableRow(newCreateRow, templateTableRows.get(i));// 複製模板行文字和樣式到新行
/*表格複製時,邊框不顯示*/
for (int j = 0; j < newCreateRow.getTableCells().size(); j++) {
CTTcBorders tblBorders = newCreateRow.getCell(j).getCTTc().getTcPr().addNewTcBorders();
tblBorders.addNewLeft().setVal(STBorder.NIL);
tblBorders.addNewRight().setVal(STBorder.NIL);
tblBorders.addNewBottom().setVal(STBorder.NIL);
tblBorders.addNewTop().setVal(STBorder.NIL);
newCreateRow.getCell(j).getCTTc().getTcPr().setTcBorders(tblBorders);
}
}
newCreateTable.removeRow(0);// 移除多出來的第一行
document.createParagraph();// 添加回車換行
replaceTable(newCreateTable, map);//替換標籤
}
4.4實現表格和行一起迴圈
上面我們已經介紹了同一份表格中的行迴圈,及一份單獨表格迴圈。那麼如果遇到即要行迴圈又要表格迴圈,那麼應該怎麼實現呢?拉下來我們就要實現這樣的功能。現在先來看一下我們設計的模板:
該模板中的”##{foreachTableRowTable}##”就是說明表格的行都要迴圈,“##{foreachRows}##”是行迴圈。我們來看一下部分程式碼實現:
@SuppressWarnings("unchecked")
Map<String,List<Map<String, Object>>> tableDataList = (Map<String,List<Map<String, Object>>>) dataMap
.get(dataSource);
table.getRow(1).getCell(0).setText("aaa1111");
//XWPFRun endRun = runs.get(0);
Map<String,Object> dd = new HashMap<String,Object>();
dd.put("aa","ssssssssss");
List<XWPFTableCell> cells = table.getRow(1).getTableCells();
for (XWPFTableCell xWPFTableCell : cells) {
List<XWPFParagraph> paragraphs = xWPFTableCell.getParagraphs();
for (XWPFParagraph xwpfParagraph : paragraphs) {
replaceParagraph(xwpfParagraph, dd);
}
}
addTableInDocFooter(table, tableDataList, parametersMap, "aaa");
addTableInDocFooter(table, tableDataList, parametersMap, "bbb");
4.5跨行合併
以下程式碼就是實現跨行合併。
public void mergeCellsVertically(XWPFTable table, int col, int fromRow, int toRow) {
for (int rowIndex = fromRow; rowIndex <= toRow; rowIndex++) {
XWPFTableCell cell = table.getRow(rowIndex).getCell(col);
if ( rowIndex == fromRow ) {
// The first merged cell is set with RESTART merge value
cell.getCTTc().addNewTcPr().addNewVMerge().setVal(STMerge.RESTART);
} else {
// Cells which join (merge) the first one, are set with CONTINUE
cell.getCTTc().addNewTcPr().addNewVMerge().setVal(STMerge.CONTINUE);
}
}
}
4.6跨列合併
以下程式碼就是實現跨列合併。
public void mergeCellsHorizontal(XWPFTable table, int row, int fromCell, int toCell) {
for (int cellIndex = fromCell; cellIndex <= toCell; cellIndex++) {
XWPFTableCell cell = table.getRow(row).getCell(cellIndex);
if ( cellIndex == fromCell ) {
// The first merged cell is set with RESTART merge value
cell.getCTTc().addNewTcPr().addNewHMerge().setVal(STMerge.RESTART);
} else {
// Cells which join (merge) the first one, are set with CONTINUE
cell.getCTTc().addNewTcPr().addNewHMerge().setVal(STMerge.CONTINUE);
}
}
}
4.7插入圖片
前面我們已經說了,對於一份word文件,我們應該怎樣實現插入一張圖片。筆者在這裡用的方法是,傳送一個路徑,這個路徑就是圖片所放置的路徑,然後插入圖片就可以了。
但是word應該怎麼插入了。筆者使用的程式碼如下:
/**
* insert Picture
* @param document
* @param filePath
* @param inline
* @param width
* @param height
* @throws InvalidFormatException
* @throws FileNotFoundException
*/
private static void insertPicture(XWPFDocument document, String filePath,
CTInline inline, int width,
int height) throws InvalidFormatException,
FileNotFoundException {
document.addPictureData(new FileInputStream(filePath),XWPFDocument.PICTURE_TYPE_PNG);
int id = document.getAllPictures().size() - 1;
final int EMU = 9525;
width *= EMU;
height *= EMU;
String blipId =
document.getAllPictures().get(id).getPackageRelationship().getId();
String picXml = getPicXml(blipId, width, height);
XmlToken xmlToken = null;
try {
xmlToken = XmlToken.Factory.parse(picXml);
} catch (XmlException xe) {
xe.printStackTrace();
}
inline.set(xmlToken);
inline.setDistT(0);
inline.setDistB(0);
inline.setDistL(0);
inline.setDistR(0);
CTPositiveSize2D extent = inline.addNewExtent();
extent.setCx(width);
extent.setCy(height);
CTNonVisualDrawingProps docPr = inline.addNewDocPr();
docPr.setId(id);
docPr.setName("IMG_" + id);
docPr.setDescr("IMG_" + id);
}
/**
* get the xml of the picture
* @param blipId
* @param width
* @param height
* @return
*/
private static String getPicXml(String blipId, int width, int height) {
String picXml =
"" + "<a:graphic xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\">" +
" <a:graphicData uri=\"http://schemas.openxmlformats.org/drawingml/2006/picture\">" +
" <pic:pic xmlns:pic=\"http://schemas.openxmlformats.org/drawingml/2006/picture\">" +
" <pic:nvPicPr>" + " <pic:cNvPr id=\"" + 0 +
"\" name=\"Generated\"/>" + " <pic:cNvPicPr/>" +
" </pic:nvPicPr>" + " <pic:blipFill>" +
" <a:blip r:embed=\"" + blipId +
"\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\"/>" +
" <a:stretch>" + " <a:fillRect/>" +
" </a:stretch>" + " </pic:blipFill>" +
" <pic:spPr>" + " <a:xfrm>" +
" <a:off x=\"0\" y=\"0\"/>" +
" <a:ext cx=\"" + width + "\" cy=\"" + height +
"\"/>" + " </a:xfrm>" +
" <a:prstGeom prst=\"rect\">" +
" <a:avLst/>" + " </a:prstGeom>" +
" </pic:spPr>" + " </pic:pic>" +
" </a:graphicData>" + "</a:graphic>";
return picXml;
}
上面的程式碼,會先呼叫方法insertPicture,然後再呼叫方法getPicXml,就可以實現插入圖片。
以上就是筆者帶著大夥兒解決使用word模板時遇到的問題,希望對大夥兒有幫助。
5.專案JAR檔案
使用poi技術,我們需要以下的jar包
6.總結
這篇部落格就是帶大家使用poi的技術,來實現生成一份word的文件。只要對poi的介面熟悉的話,應該不是很大的難度。這裡只是舉出一些簡單的例子,解決了專案上會常遇到的問題。當然,在實際的專案中,需要根據具體的情況來使用。