天天看點

使用POI操作Excel的幾點注意事項

首先說說現在我所知道的Java編輯Excel檔案的兩大開源工具:

jakarta POI和JavaExcelAPI(簡稱JXL),這兩套工具我都試用了一這段時間,感覺各有優劣吧。POI在某些細節有些小Bug并且不支援寫入圖檔,其他方面都挺不錯的;

JXL就慘了,除了支援寫入圖檔外,我暫時看不到它比POI好的地方,我碰到的主要的問題就是對公式支援不是很好,很多帶有公式的Excel檔案用JXL打開後,公式就丢失了(比如now(),today()),在網上看到其他大蝦評論說JXL寫入公式也有問題,另外,JXL操作Excel檔案的效率比POI低一點。經過比較後,我選擇了POI開發我的項目。

現在我要做的東西基本完成啦,我把這段時間使用POI的一些心得總結出來,希望能對和我遇到相同問題的朋友有所幫助,至于POI基本的使用方法,自己去看文檔吧。

1、設定分頁符的bug

POI裡的HSSFSheet類提供了setRowBreak方法可以設定Sheet的分頁符。

Bug:如果你要設定分頁符的Sheet是本來就有的,并且你沒有在裡面插入過分頁符,那麼調用setRowBreak時POI會抛出空指針的異常。

解決方法:在Excel裡給這個sheet插入一個分頁符,用POI打開後再把它删掉,然後你就可以随意插入分頁符了。

如果sheet是由POI生成的則沒有這個問題。我跟蹤了setRowBreak的源代碼,發現是Sheet.Java下的PageBreakRecord rowBreaks這個變量在搞鬼,如果Sheet裡原來沒有分頁符,開發這個子產品的那位兄台忘了為這個對象new執行個體,是以隻能我們先手工給Excel插入一個分頁符來觸發POI為rowBreaks建立執行個體。

2、如何拷貝行

我在gmane.org的POI使用者論壇翻遍了每個相關的文章,找遍了api,也沒看到一個拷貝行的方法,沒辦法,隻能自己寫:

//注:this.fWorkbook是一個HSSHWorkbook,請自行在外部new
    public void copyRows
    (String pSourceSheetName, 
    String pTargetSheetName, 
    int pStartRow, int pEndRow,
    int pPosition)
    {
    HSSFRow sourceRow = null;
    HSSFRow targetRow = null;
    HSSFCell sourceCell = null;
    HSSFCell targetCell = null;
    HSSFSheet sourceSheet = null;
    HSSFSheet targetSheet = null;
    Region region = null;
    int cType;
    int i;
    short j;
    int targetRowFrom;
    int targetRowTo;
    
    if ((pStartRow == -1) || (pEndRow == -1))
    {
      return;
    }
    sourceSheet = this.fWorkbook.getSheet(pSourceSheetName);
    targetSheet = this.fWorkbook.getSheet(pTargetSheetName);
    //拷貝合并的單元格
    for (i = 0; i < sourceSheet.getNumMergedRegions(); i++)
    {
    region = sourceSheet.getMergedRegionAt(i);
    if ((region.getRowFrom() >= pStartRow) && (region.getRowTo() <= pEndRow))
    {
    targetRowFrom = region.getRowFrom() - pStartRow + pPosition;
    targetRowTo = region.getRowTo() - pStartRow + pPosition;
    region.setRowFrom(targetRowFrom);
    region.setRowTo(targetRowTo);
    targetSheet.addMergedRegion(region);
    }
    } 
    //設定列寬
    for (i = pStartRow; i <= pEndRow; i++)
    {
    sourceRow = sourceSheet.getRow(i);
    if (sourceRow != null)
    {
    for (j = sourceRow.getFirstCellNum(); j < sourceRow.getLastCellNum(); j++)
    {
    targetSheet.setColumnWidth(j, sourceSheet.getColumnWidth(j));
    }
    break;
    }
    }
    //拷貝行并填充資料
    for (;i <= pEndRow; i++)
    {
    sourceRow = sourceSheet.getRow(i);
    if (sourceRow == null)
    {
    continue;
    }
    targetRow = targetSheet.createRow(i - pStartRow + pPosition);
    targetRow.setHeight(sourceRow.getHeight());
    for (j = sourceRow.getFirstCellNum(); j < sourceRow.getLastCellNum(); j++)
    {
    sourceCell = sourceRow.getCell(j);
    if (sourceCell == null)
    {
    continue;
    }
    targetCell = targetRow.createCell(j);
    targetCell.setEncoding(sourceCell.getEncoding());
    targetCell.setCellStyle(sourceCell.getCellStyle());
    cType = sourceCell.getCellType();
    targetCell.setCellType(cType);
    switch (cType)
    {
    case HSSFCell.CELL_TYPE_BOOLEAN: 
    targetCell.setCellValue(sourceCell.getBooleanCellValue());
    break;
    case HSSFCell.CELL_TYPE_ERROR:
    targetCell.setCellErrorValue(sourceCell.getErrorCellValue());
    break;            
    case HSSFCell.CELL_TYPE_FORMULA:
    //parseFormula這個函數的用途在後面說明
    targetCell.setCellFormula(parseFormula(sourceCell.getCellFormula()));
    break;
    case HSSFCell.CELL_TYPE_NUMERIC:
    targetCell.setCellValue(sourceCell.getNumericCellValue());
    break;
    case HSSFCell.CELL_TYPE_STRING:
    targetCell.setCellValue(sourceCell.getStringCellValue());
    break;
    }
    }
    }
    }      

這個函數有兩個問題暫時無法解決:

a、隻能在同一個Workbook裡面使用,跨Workbook總是拷不過去,不知道為什麼?

b、由于在拷貝行時也把行高也拷過去了,如果往這些單元格裡寫入的資料長度超過單元格長度,那麼他們不會自動調整行高!

3、公式的問題

POI對Excel公式的支援是相當好的,但是我發現一個問題,如果公式裡面的函數不帶參數,比如now()或today(),那麼你通過getCellFormula()取出來的值就是now(ATTR(semiVolatile))和today(ATTR(semiVolatile)),這樣的值寫入Excel是會出錯的,這也是我上面copyRow的函數在寫入公式前要調用parseFormula的原因,parseFormula這個函數的功能很簡單,就是把ATTR(semiVolatile)删掉,我把它的代碼貼出來:

private String parseFormula(String pPOIFormula)
    {
    final String cstReplaceString = "ATTR(semiVolatile)"; //$NON-NLS-1$
    StringBuffer result = null;
    int index;
    
    result = new StringBuffer();
    index = pPOIFormula.indexOf(cstReplaceString);
    if (index >= 0)
    {
    result.append(pPOIFormula.substring(0, index));
    result.append(pPOIFormula.substring(index + cstReplaceString.length()));
    }
    else
    {
    result.append(pPOIFormula);
    }
    
    return result.toString();
    }      

至于為什麼會出現ATTR(semiVolatile),還需要大家的探索精神!

4、向Excel寫入圖檔的問題。

我上POI論壇查相關文章,得到兩種結論:

1、不支援寫入圖檔;

2、支援寫入圖檔,通過EscherGraphics2d這個Class實作。于是我就去查EscherGraphics2d這個Class,發現這個Class提供了N個drawImage方法,喜出望外的我開始寫代碼,結果調了一天,一直看不到效果,黔驢技窮的我在萬般無奈下隻好跟蹤進drawImage這個函數内部,經過N個函數調用後在最底層函數發現了最終答案:

public boolean drawImage(Image image, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1,
   int sx2, int sy2, Color bgColor, ImageObserver imageobserver)
   {
   if (logger.check( POILogger.WARN ))
   logger.log(POILogger.WARN,"drawImage() not supported");
   return true;
   }      

是以我強烈建議大家,以後使用第三方開發包一定盡量下載下傳它的源代碼,這樣你在碰到問題時,看看它的的内部是怎麼實作的,很多時候就可以不必重蹈我的覆轍了。既然POI不能寫入圖檔,那我們隻能把目光投向JXL,我用JXL寫入圖檔功能是實作了,付出的代價是now()和today()這些函數丢失掉了,魚與熊掌不能兼得吧。

繼續閱讀