jxl & poi & jacob

一、jxl 的用法:

  缺点:仅限 excel2003

  特点:代码中,没有用到 FileInputStream

    <dependency>
      <groupId>net.sourceforge.jexcelapi</groupId>
      <artifactId>jxl</artifactId>
      <version>2.6.3</version>
    </dependency>

jxl - EXCEL2003
步骤

a. 创建输出流

OutputStream os = response.getOutputStream();
response.setHeader("Content-Disposition", 
"attachment; filename="+new String(fileName.getBytes("gb2312"), "iso-8859-1"));

b. 创建 WritableWorkbook (也可选择加载模板)

WritableWorkbook wwb = Workbook.createWorkbook(os);
/** 加载模板
Workbook wb = Workbook.getWorkbook(new File(path));
WritableWorkbook wwb = Workbook.createWorkbook(os, wb)
*/

c. 创建 WritableSheet (或者选取指定已有sheet)

WritableSheet sheet = wwb.createSheet(fileName, 0);
/** 指定 sheet
WritableSheet sheet = wwb.getSheet("Sheet1");
*/

d. 创建 WritableFont

WritableFont wf = new WritableFont(WritableFont.createFont("Arial Unicode MS"), 9);

e. 创建 WritableCellFormat 用于 excel 单元格的格式

WritableCellFormat wcf = new WritableCellFormat(wf);

f. 创建 Label(列 行 从 0 开始)

Label label = new Label(column_num, row_num, value, wcf);

g. 添加 cell

sheet.addCell(label) 

h. 输出,并关闭各文件,各流

wwb.write(); // 写入数据
wwb.close(); // 关闭文件
os.flush(); 
os.close(); // 关闭数据流 

二、poi 处理(雷很多多多多多多。。。)

  1. 生成 excel2003 & excel2007

<dependency>
  <groupId>org.apache.poi</groupId>
  <artifactId>poi-scratchpad</artifactId>
  <version>3.9</version>
</dependency>

poi - EXCEL
步骤

a. 创建输出流

OutputStream os = response.getOutputStream();
response.setHeader("Content-Disposition", 
"attachment; filename="+new String(fileName.getBytes("gb2312"), "iso-8859-1"));

b. 创建 HSSFWorkbook XSSFWorkbook(也可加载模板)

Workbook workbook = new HSSFWorkbook();
// Workbook workbook = new XSSFWorkbook();// 用于2007
/** 加载模板(需要创建输入流)
FileInputStream fis = new FileInputStream(new File(path));
Workbook workbook = new HSSFWorkbook(fis);
// Workbook workbook = new XSSFWorkbook(fis);// 用于2007
*/

c. 创建 Sheet(或者选取指定已有 sheet)

Sheet sheet = workbook.createSheet("testdata");
/** 指定 sheet
Sheet sheet = workbook.getSheetAt(0);
*/

d. 创建 Row(或者指定已有 row)

   创建 Cell(或者指定已有 Cell)

(**** 注:1. 赋值之前一定要判断空值,实际中,无缘无故说 cell 为空,非常无语。O__O "…)

Row row = sheet.getRow(row_num);
if (row == null) {
  row = sheet.createRow(row_num);
}
Cell cell = row.getCell(col_num);
if (cell == null) {
  cell = row.createCell(col_num);	
} 
cell.setCellValue(value);

e. 合并单元格

(注:报“修复”的错误,是如下的原因。网上说的什么方法过时,什么导错包,都是扯淡!!!

  ①:excel 模板中的坐标已经合并了单元格,程序中,在相同坐标又重复合并了单元格

  ②:在循环中用到了合并单元格,那么可能重复合并了单元格,用 debug 查下

  ③:代码:sheet.addMergedRegion(new CellRangeAddress(12, 12,0, 0))的意思是 从 13 行 1 列到 13 行 1 列合并,也就是说值合并了一个单元格,也是错!!

根据上面三个原因,终归是因为一个原因:重复合并单元格!!!)

/** 这个方法有点怪:先 行 后 列 */
sheet.addMergedRegion(new CellRangeAddress(rowStart, rowEnd, columnStart, columnEnd))

f. 设定单元格值

cell.setCellValue(value);

g. 输出,并关闭各流

workbook.write(outputStream); // 写入数据
outputStream.flush();
outputStream.close();
fileInputStream.close();

  2. 生成 word2003(弊端:图片只能跟文字在一个层级上,而且不能旋转。最好用 jacob)

poi - WORD2003(以使用模板形式为例)
步骤

a. 创建输出流

OutputStream os = response.getOutputStream();
response.setHeader("Content-Disposition", 
"attachment; filename="+new String(fileName.getBytes("gb2312"), "iso-8859-1"));

b. 导入模板(需要使用 FileInputStream)

FileInputStream fis = new FileInputStream(new File(path));
HWPFDocument doc = new HWPFDocument(fis);

c. 创建 Rang:

Range range = hwpfDocument.getRange();

d. 替换 指定文字:

(注:网上有很多的 通过 paragraph 和 table 来替换的,但是我试了下,如果你有指定的key(比如自定义 @key@),那么可以直接替换)

range.replaceText(key, value); 

e. 输出,并关闭各流:

hwpfDocument.write(outputStream);
doc.write(os); outputStream.flush(); outputStream.close(); fileInputStream.close();

  3. 生成 word2007

<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.9</version>
</dependency>

poi - WORD2007(以使用模板形式为例)
步骤

a. 创建输出流

OutputStream os = response.getOutputStream();
response.setHeader("Content-Disposition", 
"attachment; filename="+new String(fileName.getBytes("gb2312"), "iso-8859-1"));

b. 加载模板:

(注:网上打开模板的方式如下,但是这个方式会直接修改模板文件。也就是说,当你运行完一次以后,模板也会被修改。当第二次运行时,模板已经变成了第一次运行之后的文件了。因此要慎用)

XWPFDocument xwpfDocument = new XWPFDocument(new File(path));
/** 网上打开模板的方式如下
OPCPackage opcPackage = POIXMLDocument.openPackage(path);
xwpfDocument = new XWPFDocument(opcPackage); 
*/

c. 获得 paragraph list,并替换 pargraph 中的关键字

for (XWPFParagraph xwpfParagraph : paragraphList) {
    xwpfParagraph = replaceKeyInParagraph(xwpfParagraph, paraList);
}
public XWPFParagraph replaceKeyInParagraph(XWPFParagraph xwpfParagraph,         
    List<RPTBean> paraList) {
        List<XWPFRun> xwpfRunsList = xwpfParagraph.getRuns();
        if (xwpfRunsList != null && !xwpfRunsList.isEmpty()) {
            for (XWPFRun xwpfRun : xwpfRunsList) {
                int testPosition = xwpfRun.getTextPosition();
                String text = xwpfRun.getText(testPosition);
                if (text !=null && !"".equals(text)){
                    for (RPTBean rptBean : paraList) {
                        if (text.contains(rptBean.getFieldName())){
                            int startIndex = text.indexOf(rptBean.getFieldName()); 
                            xwpfRun.setText(rptBean.getValue(), startIndex);
                        }
                    }
                }
            }
        }
    return xwpfParagraph;
}

d. 获得 table list,并替换 table 中的关键字
(注:1. 修改 cell 时,要先本来是通过 cell.removeParagraph(0) 清空cell。
2. 开始本人用循环数据组,多次修改cell值,但是试验证明,cell 被替换过一次以后,再次 cell.getText();就会获得到空值。因此就创建了临时字符串,先获得 cell 内容,再修改 临时字符串,最后 setText()。)

for (XWPFTable xwpfTable : xwpfTables) {
  xwpfTable = replaceKeyInTable(xwpfTable, paraList);
}
public XWPFTable replaceKeyInTable(XWPFTable xwpfTable, List<RPTBean> paraList) {
  for (int i = 0; i<xwpfTable.getNumberOfRows(); i++) {
    XWPFTableRow row = xwpfTable.getRow(i);
    List<XWPFTableCell> cellList = row.getTableCells();
    if (cellList != null && !cellList.isEmpty()) {
      for (XWPFTableCell cell : cellList) {
        if (cell != null) {
          // 下面有另一种方法
          String text
= cell.getText();           for (RPTBean rptBean : paraList) {             String key = rptBean.getFieldName();             String value = rptBean.getValue();             if (text.contains(key)) {               StringBuffer newText = new StringBuffer();               int startIndex = text.indexOf(key);               text = newText.append(text.substring(0, startIndex))                       .append(value)                       .append(text.substring(startIndex+key.length(), text.length()))                       .toString();               cell.removeParagraph(0);
              cell.setText(text);
            }           }
          cell.getText();         }       }     }   } }

上述方法生成的 表中没有格式。要生成格式 两种方法:

1. 先获取格式,然后修改内容,最后添加格式

2. 用 paragraph 来做

List<XWPFParagraph> paragraphList = cell.getParagraphs();
for (XWPFParagraph xwpfParagraph : paragraphList) {
  replaceKeyInParagraph(xwpfParagraph, paraList);
}

e. 输出,并关闭各流

xwpfDocument.write(outputStream);

outputStream.flush(); 
outputStream.close();
fileInputStream.close();
/** 如果用网上办法,连模板都修改的话,那么最好再关闭下面两个
opcPackage.flush();
opcPackage.close();
*/

ps1. 获取文本框(没测试过)(参考:http://www.cnblogs.com/liaokunhong/p/5403279.html)

List<XWPFParagraph> paragraphs = document.getParagraphs();
// 遍历得到doc中的段落
for ( XWPFParagraph paragraph : paragraphs) {
  String text = paragraph.getText();
  if (StringUtils.isBlank(text)) {
    continue;
  }
  List<CTR> rList = paragraph.getCTP().getRList();
  for (CTR ctr : rList) {
  // xml文件对象读取
  XmlObject xmlObject = ctr.copy();
  // 拿到xml文件节点
  Node domNode = xmlObject.getDomNode();
  // 拿到子节点List列表对象
  NodeList nodeList = domNode.getChildNodes();
  for (int idx = 0 ; idx < nodeList.getLength(); idx++) {
    Node item = nodeList.item(idx);
    // 注:文本框被当做一种图片的形式存在     if ("w:pict".equals(item.getNodeName())) {       NodeList pictChildNodes = item.getChildNodes();       for (int i = 0; i < pictChildNodes.getLength(); i++) {         // 获得图片框下面的文本框List
        Node vShapeNode = pictChildNodes.item(i);         if (!"v:shape".equals(vShapeNode.getNodeName())) {           continue;         }
        // 获得文本框         NodeList vShapeChildNodes = vShapeNode.getChildNodes();         Node textboxNode = vShapeChildNodes.item(0);         NodeList textboxChildNodes = textboxNode.getChildNodes();         Node textContentNode = textboxChildNodes.item(0);         NodeList txbxContentChildNodes = textContentNode.getChildNodes();         Node wpNode = txbxContentChildNodes.item(0);         NodeList wpNodeChildNodes = wpNode.getChildNodes();         for (int j = 0; j < wpNodeChildNodes.getLength(); j++) {           Node wrNode = wpNodeChildNodes.item(j);           if (!"w:r".startsWith(wrNode.getNodeName())) {             continue;           }           NodeList wrNodeChildNodes = wrNode.getChildNodes();           Node wtNode = wrNodeChildNodes.item(1);           Node targetNode = wtNode.getChildNodes().item(0);           if (targetNode == null) {             continue;           }           String targetNodeValue = targetNode.getNodeValue();           Object value = tags.get(targetNodeValue);           if (value != null) {             targetNode.setNodeValue(value.toString());           }           ctr.set(xmlObject);
        }
      }
    }
  }
}

ps2. 简化 ps1,用递归,获得<w:t>(没测过)(参考:http://blog.csdn.net/calance_h/article/details/52808778)

public void replaceParagraphText(final int paragraphPos, final Entry<String,String> text) throws IndexOutOfBoundsException {  
  if(this.ctTc.sizeOfPArray() < paragraphPos){  
    throw new IndexOutOfBoundsException();  
    }  
    final CTP ctP = this.ctTc.getPArray(paragraphPos);  
    final XWPFParagraph par = new XWPFParagraph(ctP, this);  
    List<XWPFRun> runs = par.getRuns();  
      for(XWPFRun run : runs){  
            for(int i = 0; i < run.getCTR().sizeOfTArray(); i++){  
                if(run.getText(i).equals(text.getKey())){  
                    String replaceBy = run.getText(i).replaceAll(text.getKey(), text.getValue());  
                    run.setText(replaceBy, i);  
                }  
            }  
        }  
    } 
}

ps3. 处理书签:

https://wenku.baidu.com/view/2206d072f342336c1eb91a37f111f18583d00c7e.html

http://elim.iteye.com/blog/2031335

ps4:通过xml来处理word

http://www.infoq.com/cn/articles/cracking-office-2007-with-java

三、jacob(在服务器端要安装 office )

  参考:

  1. http://men4661273.iteye.com/blog/2097871

  2. https://wenku.baidu.com/view/ba3cb447fe4733687e21aa2e.html

  3. http://blog.csdn.net/songbaojie/article/details/1842756

  Java COM Bridge:即 java 和 com 组件间的桥梁,com 一般表现为 dll 或者 exe 等二进制文件。

            这些文件合称为接口 api。

  1. 需要2个文件,而且分 32 & 64 版本

    下载地址:http://downloads.sourceforge.net/jacob-project/jacob_1.9.zip?modtime=1109437002&big_mirror=0

文件名称 存放位置
jacob.jar 项目lib中
jacob-1.18-x64.dll jdl的lib中
jacob-1.18-x86.dll

  2. 代码

jacob 以模板形式为例


Dispatch:调度处理类,封装了一些操作来操作 office ,里面所有的可操作对象基本都是这种类型,所以 jacob 是一种链式操作模式,就像 StringBuilder 对象,调用 append() 方法之后返回的还是 StringBuilder 对象。

Dispatch的几种静态方法:这些方法就是要用来操作office的。这些方法中有的有很多重载方法,调用不同的方法时需要放置不同的参数,至于哪些参数代表什么意思,具体放什么值,就需要参考vba代码了,仅靠jacob是无法进行变成的。

•call( ) / callN( ): 调用 com 对象的方法,返回 Variant 类型值。 
•invoke( ):          和 call( ) 作用相同,但是不返回值。 
•get( ):               获取 com 对象属性,返回 variant 类型值。 
•put( ):               设置 com 对象属性。 

call() / callN():

this.document = Dispatch.call(this.documents, "Open", templateFile).toDispatch();
this.document = Dispatch.callN(this.documents, "Open", new Object[]{templateFile}).toDispatch();

Variant:封装参数数据类型,因为操作 office 是的一些方法参数(可能是字符串类型,可能是数字类型)。可以通过 Variant 来进行转换通用的参数类型,new Variant(1),new Variant("1")。

Variant 对象中的 toDispatch():将以上方法返回的 Variant 类型转换为 Dispatch,进行下一次链式操作。 

1. 初始化com线程:大概意思是打开冰箱门,准备放大象。。。

ComThread.InitSTA();

2. 创建office的一个应用,比如你操作的是 word 还是 excel 

ActiveXComponent word = new ActiveXComponent("Word.Application");
// ActiveXComponent wordApp = new ActiveXComponent("Excel.Application"); 

3. 设置编辑器是否可见:设置应用操作的文档不在明面上显示,只在后台静默处理。  

word.setProperty("Visible", new Variant(false));

4. 获取文档属性

Dispatch documents = word.getProperty("Documents").toDispatch();
5. 打开激活文档
// Dispatch doc  = Dispatch.call(documents, "Open",new Variant(inputPath)).toDispatch();
Dispatch doc = Dispatch.invoke(documents, "Open", Dispatch.Method, new Object[]{inputPath, new Variant(false)}, new int[1]).toDispatch();
 Selection:代表当前光标位置或者所选范围。该对象代表窗口或窗格中的当前所选内容。若文档中没有所选内容,则代表插入点。每个文档窗格只能有一个活动的 Selection对象,并且整个应用程序中只能有一个活动的 Selection对象。

6. 选定的范围或插入点

Dispatch selection = Dispatch.get(word, "Selection").toDispatch();

7. 从选定内容或插入点开始查找文本

Dispatch find = Dispatch.call(this.selection, "Find").toDispatch();

8. 查找字符串(替换)

Dispatch.put(find, "Text", "name");      //查找字符串"name"  
Dispatch.call(find, "Execute");          //执行查询  
Dispatch.put(selection, "Text", "111");  //替换为"111"

 或者

Boolean f = new Boolean(false);
Boolean t  = new Boolean(true);
int wdReplaceTime = 2; //1:查找替换一次;2:查找替换全部
int wdFindContinue = 1;
Object[] args={"被替换的值",t,f,f,f,f,t,new Integer(wdFindContinue),f,"替换为",new Integer(wdReplaceTime),f,f,f,f};
Dispatch.callN(this.find, "Execute",args);

9. 文本框,只能用书签形式(替换)

Dispatch bookMarks = Dispatch.get(doc, "Bookmarks").toDispatch();
int bCount = Dispatch.get(bookMarks, "Count").getInt();  //获取书签数
  for (int i = 1; i <= bCount; i++) {
  // 读取书签命名
  Dispatch items = Dispatch.call(bookMarks, "Item", i).toDispatch();
  // Dispatch bookMarks = app.call(doc, "Bookmarks").toDispatch();   String bookMarkKey = String.valueOf(Dispatch.get(items, "Name").getString()).replaceAll("null", "");   // 读取书签文本   Dispatch range = Dispatch.get(items, "Range").toDispatch();   String bookMarkValue = String.valueOf(Dispatch.get(range, "Text").getString()).replaceAll("null", "");     if (bookMarkKey.equal("标签值")) {       Dispatch.put(range, "Text", new Variant("被替换值"));
    }   }
}

10. 文件另存为

Dispatch.invoke(doc, "SaveAs", Dispatch.Method, new Object[] {"目标文件路径", new Variant(fileType)} , new int[1]);

  或者以流方式输出:我能想到的就是,保存在本地的文件,用POI重新打开,然后以流方式输出。再将本地保存的文件删掉

  删除文件:

File file=new File("目标文件路径");
  if (file.exists()) {
    printWord();
    file.delete();
}

11. 关闭模板文件(电脑任务管理器 - 前台应用)

  val 的可选值:0/false 不保存修改 -1 保存修改 -2 提示是否保存修改

Dispatch.call(doc, "Close", new Variant(val));

12. 关闭 office 应用(电脑任务管理器 - 后台进程)

word.invoke("Quit", new Variant[] {});

13. 关闭线程

ComThread.Release();

  3. 类似上面,第 9 步中,jacob 还能 获得的 com

  Dispatch content = Dispatch.call(document, "content").getDispatch();

Open 打开文档
ActiveXComponent.Visible 设置编辑器是否可见
Tables 获得所有的表格
Bookmarks 所有标签
Selection 光标所在处或选中的区域
select 选中
typeParagraph 设置为一个段落
ParagraphFormat 段落格式,用alignment设置
alignment 1、居中,2、靠右,3、靠左
Add 新建一个word文档
Close

关闭文档:

0/false 不保存,-1保存,-2弹出框确认

SaveAS 另存为
save 保存
printOut 打印
Application 得到ActiveXComponent的实例
WindowState

Application的属性,表示窗口的大小,

0、default,1、maximize,2、minimize

top、left、height、width application的属性,表示窗口的位置
ActiveXComponent.Quit 关闭所有word文档,但是不退出整个word程序
Range

表示文档中的一个连续范围。

由一个起始字符位置和一个终止字符位置定义,进而可以得到格式的信息

Item 得到指定的表格
Rows 得到表格的所有行
Cell 表格的一个单元格
Text word的文本内容
InsertFile 插入文件
InsertRowsBelow 在指定的行下面插入一行
InsertAfter 在指定对象后插入
Delete 删除,可以是表格的行
Count 返回数目,比如Rows、Tables的数目
Height 返回高度,比如行高、表格行的高
Split 拆分单元格,要指定行数和列数
Merge 合并单元格
Exists 指定的对象是否存在,返回bool值
Copy 复制
Paste 粘贴
Font 字体
Name 字体的名字
Bold 字体是否为粗体
Italic 字体是否为斜体
Underline 字体是否有下划线
Color 颜色
Size 大小
Borders

指定边框:

-1为上边框,-2左边框,-3为下边框,-4有右边框,-5为横向边框,

-6为纵向边框,-7从左上角开始的斜线,-8从左下角开始的斜线

AutoFitBehavior

自动调整大小:

1为内容自动调整大小,2为窗口自动调整大小

Content 去的内容
InLineShapes  
AddPicture 增加一张图片,需要制定路径
homeKey 光标移到开头
moveDown 光标往下一行
moveUp 光标往上一行
moveRight 光标往左一列
moveLeft 光标往右一列
find 要查找的文本
Forward 向前查找
Format 查找的文本格式
MatchCase 大小写匹配
MatchWholeWord 全字匹配
Execute 开始执行查找
LineSpacingRule 行间距

  4. 遇到的错误

1. 

com.jacob.com.ComFailException: 

Can't map name to dispid: Open

或者,一直都卡在那里。

因为电脑的任务管理器中积压了很多 office 进程/应用程序,没有内存打开新 office 文件了。

方案:除了关闭 任务管理器中的 进程/应用程序外,程序:

finally {
    Dispatch.call(doc, "Close", new Variant(false)); // 关闭前台应用
    this.word.invoke("Quit", new Variant[] {}); // 关闭后台进程
    ComThread.Release(); // 关闭进程
 }

2.

在开着 tomcat 时,调试程序,会报如下错:

java.lang.NoClassDefFoundError:

com.jacob.com.JacobObject

方案:重启 tomcat

3.

java.lang.IllegalStateException:

Dispatch not hooked to windows memory

 我没遇到,网上办法:http://blog.sina.com.cn/s/blog_49cc672f0100pp0p.html

然后每次操作完成后都会调用ComThread.Release()去释放,但释放后word和documents并不为null,所以每次使用jacob都只有第一次是正常的,后面就要报错,然后必须重启tomcat才行。

if (this.word == null || this.word.m_pDispatch==0) {
  this.word = new ActiveXComponent("Word.Application");
  this.word.setProperty("Visible", new Variant(false));
  this.word.setProperty("DisplayAlerts", new Variant(false));
}
if (documents == null||documents.m_pDispatch==0) {
  this.documents = this.word.getProperty("Documents").toDispatch();
} 

4.

com.jacob.com.ComFailException:

Invoke of: SaveAs

 因为保存的类型出错了

Dispatch.invoke(doc, "SaveAs", Dispatch.Method, new Object[] {output, new Variant(fileType)} , new int[1]);

fileType:数字表示不同类型

5.

com.jacob.com.ComFailException: Invoke of: Item
Source: Microsoft Word
Description: 集合所要求的成员不存在。

原因:将选中一段内容来作为标签,类似将“word”作为标签

方案:以光标形式作为标签

原文地址:https://www.cnblogs.com/MissRabbit/p/6879452.html