平时的项目中,我们可能需要做大量的报告。而这些报告中,有些是固定的格式,有些是需要自定义模板来生成。如果是固定的形式的,那么相对而言是比较好做的,但要是根据模板来生成报告,比如:word、Excel或PDF。这样的话,可能需要我们花点时间去解决了。这篇博客主要是带领大家学习一下,如何用poi技术来实现生成word报告。
我们知道,poi的技术可以做出word、Excel、PDF等文件。在网上也有大量的博客是关于如何使用poi的ftl的模板来生成word、Excel、PDF。主要是思路是将某个模板(自己定义的,可以是word或Excel)上传至服务器,然后解析成对应的ftl文件,之后服务在做报告时,就可以直接拿取对应的ftl文件来生成报告。这种方式会比较灵活,现在用的也比较多。但是对于一份简单的,不需要ftl的文件来作为中间生成文件时,那么可能会比较难了。比如,接下来我们要学习的,使用word的模板作为报告模板,然后直接生成需要的报告。但是word中的所的之间的设置,如字体大小、颜色等都不能改变。这个思路大概是:用户直接设计一个word模板,然后上传至服务器,服务器将其直接保存到对应的文件夹下;在要生成报告时,直接拿word的模板来填充数据。这样,好像也不是很难,但是想想细节上的地方,可能很多东西就不好做了。接下来,我们来说说问题所在。
我们在使用word作为一份模板时,首先要解决的时,如何向模板中定义的字段替换数据,如:{name}要换成真实的改名;其次我们如果要做表格的话,如何向word上直接画出表格;再次,表格的行列要怎么合并,多张表应该怎么复制等;最后,我们应该怎么将一比我图片插入到word中。带着这些问题,我们来一一解决。
在word的文件中,我们同一个使用“{**}”来标志需要替换的文件,如{name}就是要将name的字段替换成真实的姓名。所以我们需要用正则表达式来寻找到广本的内容是否包含了“{}”。正则表达式是:\\{.+?\\}。同时,我们需要用poi提供的XWPFRun的接口来替换文本。为了使用替换后的文本可以的换行的操作。比如,我们自己定义的一行文本需要换行,笔者用“@”符号作为标签,也就是如果遇到内容含有“@”,就要实现换行。而换行是调用XWPFRun的addBreak()的方法。部分代码如下:
public void replaceParagraph(XWPFParagraph xWPFParagraph, Map parametersMap) {
List 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 removeRunList = new ArrayList();//需要移除的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 有标签
}
为了达到一份表格的行可以循环,我们使用”##{foreachTableRow}##”,说明同一表格的行需要循环。该标签定义到表格的最上方。然而,表格中会有固定的字段名,其内容 是需要循环的,所以我们用“##{foreachRows}##”来表示,行需要循环。表格设计如下:
表格中的{project}{class}无非就是内容循环了。部分代码如下:
XWPFTable newCreateTable = document.createTable();// 创建新表格,默认一行一列
List 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();// 添加回车换行
有时候,我们需要是写一整段的内容,并这段内容的某个值是不一样的,但大部分是一样的,且要循环输出。这个时候,我们可以借用表格来实现。也就是定义个表格循环,而表格的边框设置为隐藏的,这样就可以实现我们的需求了。定义的表格样式如下:
图片中是一个表格,只是边框被笔者隐藏了。从上面的图片可以看出,“##{foreachTable}##”是需要表格循环,而其它的“{**}”都只是内容替换。部分代码如下:
for (Map map : list) {
List 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);//替换标签
}
上面我们已经介绍了同一份表格中的行循环,及一份单独表格循环。那么如果遇到即要行循环又要表格循环,那么应该怎么实现呢?拉下来我们就要实现这样的功能。现在先来看一下我们设计的模板:
该模板中的”##{foreachTableRowTable}##”就是说明表格的行都要循环,“##{foreachRows}##”是行循环。我们来看一下部分代码实现:
@SuppressWarnings("unchecked")
Map>> tableDataList = (Map>>) dataMap
.get(dataSource);
table.getRow(1).getCell(0).setText("aaa1111");
//XWPFRun endRun = runs.get(0);
Map dd = new HashMap();
dd.put("aa","ssssssssss");
List cells = table.getRow(1).getTableCells();
for (XWPFTableCell xWPFTableCell : cells) {
List paragraphs = xWPFTableCell.getParagraphs();
for (XWPFParagraph xwpfParagraph : paragraphs) {
replaceParagraph(xwpfParagraph, dd);
}
}
addTableInDocFooter(table, tableDataList, parametersMap, "aaa");
addTableInDocFooter(table, tableDataList, parametersMap, "bbb");
以下代码就是实现跨行合并。
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);
}
}
}
以下代码就是实现跨列合并。
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);
}
}
}
前面我们已经说了,对于一份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 =
"" + "" +
" " +
" " +
" " + " " + " " +
" " + " " +
" " +
" " + " " +
" " + " " +
" " + " " +
" " +
" " + " " +
" " +
" " + " " +
" " + " " +
" " + " ";
return picXml;
}
上面的代码,会先调用方法insertPicture,然后再调用方法getPicXml,就可以实现插入图片。
以上就是笔者带着大伙儿解决使用word模板时遇到的问题,希望对大伙儿有帮助。
使用poi技术,我们需要以下的jar包
这篇博客就是带大家使用poi的技术,来实现生成一份word的文档。只要对poi的接口熟悉的话,应该不是很大的难度。这里只是举出一些简单的例子,解决了项目上会常遇到的问题。当然,在实际的项目中,需要根据具体的情况来使用。