所見即所得,使用Java將HTML解析為Excel,支援多級表頭巢狀、單元格合併
最近專案需要實現如題“所見即所得”的功能,之前每次生成Excel都需要重新從資料庫查詢一遍,降低效率不說,那些巢狀的表頭實在是很難用Sql巢狀拼接實現。而且這樣做還沒有通用性,不同的表格需要寫不同的Sql實現,非常繁瑣。
在網上找了很多關於HTML解析為Excel的文章,有以下兩種情況:
1、大部分用“偷懶”的辦法,使用js直接將HTML程式碼輸出為文字檔案,然後將檔案字尾改為.xls。這種方式的確簡單易行,但是有它的缺點,之後講到。
2、解析HTML然後生成Excel,網上的部落格的確可以搜到一兩篇這樣的文章,但是內容太有侷限性,適用於沒有單元格合併的情況,並不適用於複雜單元格合併(eg:多級表頭巢狀)的情況
終於找到一篇這樣的文章《所見即所得的EXCEL報表生成(二)——從HTMLTABLE到EXCEL CELL》,文章介紹了使用.NET+HtmlAgilityPack解析工具實現HTML解析為Excel,無奈對.NET沒接觸過,看的過程中只借鑑了其中的推導思路,然後自己構思Java程式碼,很感謝文章的作者。我的原創點在於使用Java+Jsoup+POI的程式碼實現。
----------------------------------------------(づ ̄3 ̄)づ╭❤~華麗的分割線----------------------------------------------
為了實現“所見即所得”的效果,即將頁面的報表直接匯出為Excel,做了兩種方法的嘗試。
方法1:JS直接輸HTML到文字檔案,更改字尾為.xls
優點:
簡單有效,程式碼量最少
缺點:
1、使用者體驗有欠缺。這種方式生成的文字檔案是HTML程式碼,不是真正的Excel檔案。但是可以被Excel解析,並且還附帶有樣式。但是每次開啟時會彈出提示“檔案安全性有問題”對話方塊,太煩人…
2、匯入Excel解析是問題。匯入Excel的功能一般只解析真正的Excel檔案,不能解析HTML。這樣還要區分不同的檔案解析,有點麻煩
方法2:將HTML程式碼解析為Excel檔案
優點:
1、生成的是真正的Excel檔案,無需擔心匯入問題
2、良好的使用者體驗,沒有任何彈窗提示
缺點:
對一些多餘的HTML程式碼,或者不規範的HTLM,解析會不能正確識別。所以使用時需要注意傳入HTML的規範。
第1種方法大家可以自己搜,本文重點介紹第2種方法,廢話說完了_(:з」∠)_,終於可以進入正題了…
過程中用到的jar包:commons-lang-2.6.jar、jsoup-1.8.1.jar、poi-3.10.1-20140818.jar
下載地址:http://pan.baidu.com/s/1c02amQk
----------------------------------------------(づ ̄3 ̄)づ╭❤~華麗的分割線----------------------------------------------
我們目的是將HTML解析為Excel檔案,需要分兩步走:
1、使用Jsoup解析HTML
Jsoup是一個第三方HTML解析工具包,你可能沒用過,但是你一看官方的介紹就明白了http://www.open-open.com/jsoup/,和DOM的操作方法特別相似。列舉幾個常用方法:
據標籤名稱獲取元素:getElementsByTag(String tagName)
移除指定元素:removeAll(Elemente)
獲取元素標籤屬性值:attr(StringattrName)
2、使用POI構造Excel檔案
POI不用多說了,Excel匯入匯出經常用到的工具包。
第一步:使用Jsoup解析HTML
以下分析部分基於原文修改,並加入自己的詳細解釋:
直觀的看,一個完整Excel的內容是由位於各個單元格(Cell)中的內容組合而成的。而每個單元格(Cell)都有相應X、Y座標來標示其位置(最左上角的單元格,座標(1,1))。也就是說,一個Excel檔案實質上就是許多Cell構成的集合,每個Cell用座標屬性確定位置,用內容屬性儲存內容。
基於此,我們得到了最基本的Cell結構:
- X座標
- Y座標
- 合併列情況
- 合併行情況
- 內容
構成Excel的最基本的結構已經確定,下一步擺在我們面前的就是將html table轉化為Excel Cell集合。
Html table中的每個td節點對應一個Excel單元格。單元格的內容可以通過解析table的td獲取,行、列的合併情況也可由td的rowspan、colspan屬性得出,轉化的關鍵點就在於如何由table的tr、td結構確定Excel單元格位置,即如何確定X、Y座標。
1、確定Y座標
Y座標容易確定,即td所在tr的行數。
例如:當前的td在table的第2行中,則當前td的Y座標為2
2、確定X座標
X座標的確定沒有那麼簡單,它要受到兩方面因素的影響:
(1)與當前td處於同一tr,但位於其之前(反映在表格視覺上即其左側td)td的佔位情況。
也就是說,如果td在第2行,那麼需要考慮同樣在第2行的td的左邊,可能存在有單元格有列的合併(佔據當前行的若干個單元格),因為合併後的單元格只有一個座標值,因此td左邊的單元格合併的越多,td的X會越小。
(2)當前td所在tr的之前tr中某些td的跨行情況。
也就是說,如果td在第3行,那麼需要考慮第1行和第2行可能存在單元格有行的合併(佔據不同行的若干單元格)。比如在第1行中,一個td跨3行1列,那麼當前第3行的td的X座標會變小,因為第3行被合併的那個單元格並不屬於第3行,屬於第1行。
基於此種考慮,定位td的X座標需經過兩個過程的推導:用於處理左側td佔位影響的橫向推導(HorizontalDeduction)和處理之前行跨行td影響的縱向推導(VerticalDeduction)。
以下圖的table為例,展示兩次推導過程,這兩次的推導過程,就是這個問題的核心:
1 |
2 |
3 |
|
4 |
5 |
||
6 |
7 |
8 |
|
9 |
10 |
1、橫向推導(HorizontalDeduction)
橫向推導的目的就是發現與當前td處在同一行,且在td單元格左邊,有單元格合併的情況。整個過程基於遞迴的原理,遞迴模型如下:
解釋:n為當前行的單元格序列,從1開始。以table圖為例,第1個單元格的X座標X1=1,第二個單元格的X座標X2=X1+colspan1=1+2=3。
也就是說,對於橫向推導,td的X座標=上個兄弟td的座標+兄弟td的合併列數。
橫向推導的java程式碼(文章裡的原點為(1,1),程式碼裡是(0,0)):
<span style="font-size:12px;"> /**
* @Title : HorizontalDeduction
* @Description : 使用遞迴,進行橫座標的第一步推導,橫向推導,同時刪除多餘的非td元素
* @author : Qinchz
* @date : 2014年12月12日 下午8:51:39
* @param e
* @return
*/
private int HorizontalDeduction(Element e) {
Element preElement=e.previousElementSibling();
if(preElement!=null){
//表示td的上一個兄弟節點不是td,則刪除這個多餘的元素
if(!preElement.tagName().equals("td")){
preElement.remove();
}else{
int nColSpan=1;//預設為1
if(StringUtils.isNotBlank(preElement.attr("colspan"))){
//前一個元素的列合併情況
nColSpan=Integer.valueOf(preElement.attr("colspan").trim());
}
return HorizontalDeduction(preElement) + nColSpan;
}
}
return 0;
}</span><span style="font-size: 14px;">
</span>
經過橫向推導,table的座標情況:
1(1,1) |
2(3,1) |
3(4,1) |
|
4(1,2) |
5(2,2) |
||
6(1,3) |
7(2,3) |
8(3,3) |
|
9(1,4) |
10(2,4) |
2、縱向推導(VerticalDeduction)
縱向推導的目的是發現當前td所在行之前的行,存在跨行合併的情況。一次縱向推導的過程可以描述為(當前推導td用A表示):
找到A所在行之前的行tr中與A具有相同X座標的td節點B
if(B.rowspan>(A.Y-B.Y))
{
X+=B.colspan,即A的X座標向後推B.colspan的位置;
同時,與A同處一tr但在其後邊的td節點均應向後推B.colspan個位移;
}
解釋:
在橫向推導之後,實際單元格的排列是有重合的(上圖中不重合是因為忽略了單元格的高度,把高度視為相等),所以縱向推導就是要單元格合理的“避開”它之前行已經佔用的位置。
例如:上圖中,單元格4的座標是(1,2),這並不是4最終的正確座標,那麼4為什麼會跑到這裡?因為橫向推導只是讓4在同一行中它的位置是對的,不能保證和4處在不同行的1對它產生的影響。
4為A節點,縱向推導先找到1為B節點。如果B跨越的行數>當前AB的Y座標之差(即,判斷A和B有重合),那麼A的X座標 =A.X+B跨越的列數(即,讓A“避開”B已經佔用的位置)。同時,A的移動影響了與A同行且在右邊的節點,A右邊的節點需要移動相同的長度。
就以節點4為例,按照上述的方法移動後的位置如圖:
1(1,1) |
2(3,1) |
3(4,1) |
|
4(1,2) |
|||
|
5(2,2) |
||
|
|||
|
|||
6(1,3) |
7(2,3) |
8(3,3) |
|
9(1,4) |
10(2,4) |
縱向推導的java程式碼(文章裡的原點為(1,1),程式碼裡是(0,0)):
/**
* @Title : verticalDeduction
* @Description : 縱向推導
* @author : Qin
* @date : 2014年12月12日 下午9:12:25
* @param headerList
* @return
*/
private List<TD> verticalDeduction(List<TD> headerList) {
int headerSize = headerList.size();
for (int i = 0; i < headerSize; i++) {
TD tdA = headerList.get(i);
boolean flag = true;
while (flag) {// 不斷排列當前節點的位置,直到它的位置絕對正確
flag = false;// 不需要移位
for (int j = i - 1; j >= 0; j--) {// 找到之前與td的橫座標相等的值
TD tdB = headerList.get(j);
if (tdA.getX() == tdB.getX()) {// A找到與其X座標相等的B
// 如果B單元格“擋住”了A單元格,才進行移位操作。即:只有B佔的行數
// 大於或等於A、B之間的距離,那麼B才會擋住A
if (tdB.getRowspan() > tdA.getY() - tdB.getY()) {
// 如果存在移位單元格,則仍然需要重新判斷當前的位置是否正確。需要移位
flag = true;
// A的X座標向後推B.colspan的位置
tdA.setX(tdA.getX() + tdB.getColspan());
int YA = tdA.getY();
// 同時,與A同處一tr但在其後邊的td節點均應向後推B.colspan位移
for (int m = i + 1; m < headerSize; m++) {
TD td = headerList.get(m);
if (td.getY() == YA) {
td.setX(td.getX() + tdB.getColspan());
}
}
}
}
}
}
}
return headerList;
}
單元格類:
/**
* @ClassName: TD
* @Description: 單元格類
* @author Qin
* @date 2014年12月12日 下午5:45:10
*/
class TD {
private int rowspan=1;
private int colspan=1;
private int x;
private int y;
private String content;
public int getRowspan() {
return rowspan;
}
public void setRowspan(int rowspan) {
this.rowspan = rowspan;
}
public int getColspan() {
return colspan;
}
public void setColspan(int colspan) {
this.colspan = colspan;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
}
以上是從HTML table構造Excel Cell的整個過程的核心程式碼(完成原始碼在文章附在文章末尾),經過整個構造過程,我們就得到了每個td的絕對座標X、Y,再結合已知的td行列合併情況、td的內容,我們完全可以在Excel中構造出需要的表格。
第二步:使用POI構造Excel檔案
燒腦的內容都在第一步,第二步很簡單。
第一步中,我們得到了List< TD >這個含有表格資訊的集合,其中含有每個表格的座標、合併情況、內容。我們需要利用這些資訊進行Excel中單元格的構造。
其實重點是進行單元格的合併,POI中HSSFSheet類有一個單元格合併的方法:
//引數:起始行,終止行,起始列,終止列。這四個引數,就要用到第一步得到的座標
addMergedRegion(newCellRangeAddress(int firstRow, int lastRow, int firstCol, int lastCol)
下是合併單元格的部分核心程式碼:
//表頭、單元格資料內容寫入
for(int i=0;i<finalHeaderList.size();i++){
TD td=finalHeaderList.get(i);
sheet.getRow(td.getY()).getCell(td.getX()).setCellValue(td.getContent());
//單元格合併
sheet.addMergedRegion(
new CellRangeAddress(//起始行,終止行,起始列,終止列
td.getY(),
td.getY()+(td.getRowspan()-1),
td.getX(),
td.getX()+(td.getColspan()-1))
);
}
既然是web專案,得實現“下載Excel”功能吧,起初想通過Struts2的下載功能實現,結果不太好用,換了一種更好用的方式。
專案中所涉及的所有原始碼貼到下面:
1、Action
頁面傳遞兩個引數:需要解析的HTML字串,下載的檔名稱
import java.net.URLEncoder;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.struts2.ServletActionContext;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
import com.opensymphony.xwork2.ActionContext;
import com.wondersgroup.qyws.common.utils.HtmlToExcel;
import com.wondersgroup.qyws.tjfx.common.BaseAction;
@Controller("htmlToExcelAction")
@Scope("prototype")
public class HtmlToExcelAction extends BaseAction{
privatestatic final long serialVersionUID = 5975906847955053344L;
/**前臺傳遞的html字串*/
privateString htmlStr;
/**檔名稱*/
privateString fileName;
publicvoid htmlToExcel() throws Exception {
if(StringUtils.isNotBlank(htmlStr)){
try{
HtmlToExcelhtmlToExcel=new HtmlToExcel(2,fileName);
HSSFWorkbookwb= htmlToExcel.readHtmlStr(htmlStr);
ActionContextcontext = ActionContext.getContext();
HttpServletResponseresponse = (HttpServletResponse)context.get(ServletActionContext.HTTP_RESPONSE);
response.setHeader("Content-Disposition","attachment; filename="
+URLEncoder.encode(fileName+".xls", "utf-8"));
ServletOutputStreamfOut = response.getOutputStream();
wb.write(fOut);
fOut.flush();
fOut.close();
}catch (Exception e) {
e.printStackTrace();
}
}
}
publicString getHtmlStr() {
returnhtmlStr;
}
publicvoid setHtmlStr(String htmlStr) {
this.htmlStr= htmlStr;
}
publicString getFileName() {
returnfileName;
}
publicvoid setFileName(String fileName) {
this.fileName= fileName;
}
}
2、HTMLTOEXCEL工具類
構造器兩個引數:生成Excel的標題所佔行數,需要解析的HTML字串
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFCellStyle;
import org.apache.poi.hssf.usermodel.HSSFFont;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.hssf.util.HSSFColor;
import org.apache.poi.ss.util.CellRangeAddress;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import com.wondersgroup.qyws.tjfx.common.BaseAction;
/**
* @ClassName:HtmlToExcel
* @Description: 傳入html程式碼字串,返回POI的工作簿物件HSSFWorkbook
* @author Qin
* @date 2014年12月13日 下午6:36:31
*/
public class HtmlToExcel extends BaseAction{
privatestatic final long serialVersionUID = 4175158575304402752L;
/**表格的列數*/
private int columnSize;
/**表格的行數*/
private int rowSize;
/**資料的行數,不含表頭*/
private int rowSize_data;
/**標題所佔行數*/
privateint rowSize_title=2;
/**表頭所佔行數*/
privateint rowSize_header;
/**工作表名稱*/
privateString sheetName;
publicHtmlToExcel(int rowSize_title,String sheetName){
this.rowSize_title=rowSize_title;
this.sheetName=sheetName;
}
/**
* @Title : readHtmlStr
* @Description : 使用jsoup解析html,得到表頭資料List,表體資料String[][]
* @author : Qin
* @date : 2014年12月13日 下午6:32:55
* @return
* @throws Exception
*/
public HSSFWorkbook readHtmlStr(String htmlStr)throws Exception{
Documentdoc = Jsoup.parseBodyFragment(htmlStr, "utf-8");
doc.select("input[type$=hidden]").remove();//刪除所有input隱藏域
doc.select("tr[style*=none]").remove();//刪除隱藏的tr
Elementscaptions=doc.getElementsByTag("caption");//查詢表頭
StringtableTitle="";// 儲存表頭標題
if(captions!=null&&captions.size()>0){
Elementcaption=captions.get(0);
tableTitle=caption.text();
}else{
rowSize_title=0;//表示,沒有表頭
}
Elementstrs_data = doc.getElementsByTag("tr");//獲取所有tr
rowSize= trs_data.size()+rowSize_title;//獲取表格的行數,外加標題行數
Elementstheads=doc.getElementsByTag("thead");//表頭thead標籤
List<TD>finalHeaderList=new ArrayList<TD>();//存放推導完畢的正確資料
List<TD>dataList = new ArrayList<TD>();//表頭1單元格List
if(theads!=null&&theads.size()>0){//表示有表頭
Elementsthead_trs=theads.get(0).getElementsByTag("tr");//表頭中的tr
rowSize_header=thead_trs.size();
trs_data.removeAll(thead_trs);//移除表頭中的的tr元素,trs中剩下資料行
List<TD>headerList = new ArrayList<TD>();//表頭1單元格List
//構造表頭
//將表頭資料存到List中。x、y座標從0開始
//確定x座標之1:橫向推導
intbasicY_thead=rowSize_title;
for(inti=0;i<thead_trs.size();i++){
Elementthead_tr=thead_trs.get(i);
Elementsthead_tr_ths=thead_tr.getElementsByTag("th");
for(intj=0;j<thead_tr_ths.size();j++){
Elemente=thead_tr_ths.get(j);
TDtd=new TD();
td.setContent(e.text());
if(StringUtils.isNotBlank(e.attr("colspan"))){
td.setColspan(Integer.valueOf(e.attr("colspan").trim()));
}
if(StringUtils.isNotBlank(e.attr("rowspan"))){
td.setRowspan(Integer.valueOf(e.attr("rowspan").trim()));
}
td.setX(HorizontalDeduction_th(e));//步驟1:橫向推導,但這個座標並不是最終座標,需要進行縱向推導
td.setY(i+basicY_thead);//y座標很簡單,就是tr的值
headerList.add(td);
}
}
//確定x座標之2:縱向推導
finalHeaderList=verticalDeduction(headerList);
if(trs_data.size()>0){//表示有表格內容資料
rowSize_data=trs_data.size();
}else{//表示只有表頭資料,沒有表格內容資料
rowSize_data=0;
}
}else{//表示沒有表頭
rowSize_header=0;
}
//迴圈每一個數據單元格
intbasicY_data=rowSize_title+rowSize_header;
for(int i = 0; i < trs_data.size(); i++) {
Elementtr = trs_data.get(i);
Elementstds = tr.getElementsByTag("td");
//迴圈每一行的所有列
for(int j = 0; j < tds.size(); j++) {
Elemente = tds.get(j);
Elementsinp=e.getElementsByTag("input");
TDtd=new TD();
if(StringUtils.isNotBlank(e.attr("colspan"))){
td.setColspan(Integer.valueOf(e.attr("colspan").trim()));
}
if(StringUtils.isNotBlank(e.attr("rowspan"))){
td.setRowspan(Integer.valueOf(e.attr("rowspan").trim()));
}
if(inp!=null&&inp.size()>0){//表示td中巢狀input
td.setContent(inp.get(0).val());
}else{//表示td中沒有巢狀input
td.setContent(e.text().trim());
}
td.setX(HorizontalDeduction_td(e));//步驟1:橫向推導,但這個座標並不是最終座標,需要進行縱向推導
td.setY(i+basicY_data);//y座標很簡單,就是tr的值
dataList.add(td);
}
}
//步驟2:縱向推導
dataList=verticalDeduction(dataList);
//表頭和表內容合併為一個List
finalHeaderList.addAll(dataList);
//對列進行賦值,找到第一個單元格計算列寬
TDlastTd=finalHeaderList.get(finalHeaderList.size()-1);
columnSize=lastTd.getX()+lastTd.getColspan();
int[][]contextSizeArr=new int[rowSize][columnSize];//記錄內容單元格內容長度,便於進行列寬自適應調整
String[][]dataArr=new String[rowSize_data][columnSize];
//將表格的長度按照該單元格的位置填入字串長度
//不能使用普通下標方式賦值,因為如果有合併單元格的情況,陣列的位置就會錯位,使用座標保證不會錯位
for(int i = 0; i < finalHeaderList.size(); i++) {
TDtd = finalHeaderList.get(i);
contextSizeArr[td.getY()][td.getX()]= getStringLength(td.getContent())+1;
}
int[]maxLengthArr = getMaxLength(contextSizeArr);
//根據解析到的資料返回POI的Excel物件
returnbuildExcel(tableTitle,finalHeaderList,dataArr,maxLengthArr);
}
/**
* @Title : getStringLength
* @Description : 中文字元與非中文字元長度計算
* @author : Qin
* @date : 2014年12月14日 下午12:30:45
* @param s
* @return
*/
privateint getStringLength(String s) {
doublevalueLength = 0;
String chinese ="[\u4e00-\u9fa5]";
// 獲取欄位值的長度,如果含中文字元,則每個中文字元長度為2,否則為1
for (int i = 0; i < s.length(); i++){
// 獲取一個字元
String temp = s.substring(i, i +1);
// 判斷是否為中文字元
if (temp.matches(chinese)) {
// 中文字元長度為1
valueLength += 1;
} else {
// 其他字元長度為0.5
valueLength += 0.5;
}
}
//進位取整
return (int) Math.ceil(valueLength);
}
/**
* @Title : getMaxLength
* @Description : 豎向遍歷二維陣列,找到每一列的最大值
* @author : Qin
* @date : 2014年12月14日 上午1:18:02
* @param contextSizeArr
* @return
*/
privateint[] getMaxLength(int[][] contextSizeArr) {
int[]maxArr=new int[columnSize];
for(inti=0;i<columnSize;i++){
intbasic=0;
for(intj=0;j<rowSize;j++){
if(contextSizeArr[j][i]>basic){//注意下標的寫法
basic=contextSizeArr[j][i];
}
}
maxArr[i]=basic;
}
returnmaxArr;
}
/**
* @Title : HorizontalDeduction
* @Description : 使用遞迴,進行橫座標的第一步推導,橫向推導,同時刪除多餘的非td元素
* @author : Qin
* @date : 2014年12月12日 下午8:51:39
* @param e
* @return
*/
private int HorizontalDeduction_td(Element e) {
ElementpreElement=e.previousElementSibling();
if(preElement!=null){
if(!preElement.tagName().equals("td")){//表示td的上一個兄弟節點不是td,則刪除這個多餘的元素
preElement.remove();
}else{
intnColSpan=1;//預設為1
if(StringUtils.isNotBlank(preElement.attr("colspan"))){
nColSpan=Integer.valueOf(preElement.attr("colspan").trim());//前一個元素的列合併情況
}
returnHorizontalDeduction_td(preElement) + nColSpan;
}
}
return0;
}
/**
* @Title : HorizontalDeduction
* @Description : 使用遞迴,進行橫座標的第一步推導,橫向推導,同時刪除多餘的非td元素
* @author : Qin
* @date : 2014年12月12日 下午8:51:39
* @param e
* @return
*/
private int HorizontalDeduction_th(Element e) {
ElementpreElement=e.previousElementSibling();
if(preElement!=null){
if(!preElement.tagName().equals("th")){//表示td的上一個兄弟節點不是td,則刪除這個多餘的元素
preElement.remove();
}else{
int nColSpan=1;//預設為1
if(StringUtils.isNotBlank(preElement.attr("colspan"))){
nColSpan=Integer.valueOf(preElement.attr("colspan").trim());//前一個元素的列合併情況
}
returnHorizontalDeduction_th(preElement) + nColSpan;
}
}
return0;
}
/**
* @Title : verticalDeduction
* @Description : 縱向推導
* @author : Qin
* @date : 2014年12月12日 下午9:12:25
* @param headerList
* @return
*/
privateList<TD> verticalDeduction(List<TD> headerList) {
intheaderSize = headerList.size();
for(int i = 0; i < headerSize; i++) {
TDtdA = headerList.get(i);
booleanflag = true;
while(flag) {// 不斷排列當前節點的位置,直到它的位置絕對正確
flag= false;// 不需要移位
for(int j = i - 1; j >= 0; j--) {// 找到之前與td的橫座標相等的值
TDtdB = headerList.get(j);
if(tdA.getX() == tdB.getX()) {// A找到與其X座標相等的B
if(tdB.getRowspan() > tdA.getY() - tdB.getY()) {// 如果B單元格“擋住”了A單元格,才進行移位操作。即:只有B佔的行數大於或等於A、B之間的距離,那麼B才會擋住A
flag= true;// 如果存在移位單元格,則仍然需要重新判斷當前的位置是否正確。需要移位
tdA.setX(tdA.getX()+ tdB.getColspan());// A的X座標向後推B.colspan的位置
intYA = tdA.getY();
for(int m = i + 1; m < headerSize; m++) {// 同時,與A同處一tr但在其後邊的td節點均應向後推B.colspan個位移
TDtd = headerList.get(m);
if(td.getY() == YA) {
td.setX(td.getX()+ tdB.getColspan());
}
}
}
}
}
}
}
returnheaderList;
}
/**
* @Title : buildExcel
* @Description : 依據傳入的資料生成Excel檔案
* @author : Qin
* @date : 2014年12月13日 下午6:32:06
* @param title
* @param finalHeaderList 表格表頭資料
* @param dataArr 表格內容資料
* @return
* @throws Exception
*/
private HSSFWorkbook buildExcel(Stringtitle,List<TD> finalHeaderList,String[][] dataArr,int[] maxLengthArr)
throwsException {
HSSFWorkbookwb=new HSSFWorkbook();
HSSFSheetsheet=null;
if(StringUtils.isNotBlank(sheetName)){
sheet=wb.createSheet(sheetName);
}elseif(StringUtils.isNotBlank(title)){
sheet=wb.createSheet("title");
}else{
sheet=wb.createSheet("Sheet1");
}
//表格樣式
//1、基礎樣式
HSSFCellStylebasicStyle=wb.createCellStyle();
basicStyle.setAlignment(HSSFCellStyle.ALIGN_CENTER);//設定水平居中
basicStyle.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER); //設定垂直居中
basicStyle.setBorderBottom(HSSFCellStyle.BORDER_THIN);// 下邊框
basicStyle.setBorderLeft(HSSFCellStyle.BORDER_THIN);//左邊框
basicStyle.setBorderTop(HSSFCellStyle.BORDER_THIN);//上邊框
basicStyle.setBorderRight(HSSFCellStyle.BORDER_THIN);//右邊框
//2、標題樣式
HSSFCellStyletitleStyle=wb.createCellStyle();
titleStyle.setAlignment(HSSFCellStyle.ALIGN_CENTER);//設定水平居中
titleStyle.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER); //設定垂直居中
HSSFFontheaderFont1 = (HSSFFont) wb.createFont();
headerFont1.setBoldweight(HSSFFont.BOLDWEIGHT_BOLD);//設定字型加粗
headerFont1.setFontHeightInPoints((short)14);//設定字型大小
titleStyle.setFont(headerFont1);
//3、偶數行樣式
HSSFCellStyleevenStyle=wb.createCellStyle();
evenStyle.setFillPattern(HSSFCellStyle.SOLID_FOREGROUND);
evenStyle.setFillForegroundColor(HSSFColor.WHITE.index);
evenStyle.setAlignment(HSSFCellStyle.ALIGN_CENTER);
HSSFCellStyleoldStyle=wb.createCellStyle();
oldStyle.setFillPattern(HSSFCellStyle.SOLID_FOREGROUND);
oldStyle.setFillForegroundColor(HSSFColor.GREY_25_PERCENT.index);
oldStyle.setAlignment(HSSFCellStyle.ALIGN_CENTER);
//構建基本空白表格
for(inti=0;i<rowSize;i++){
sheet.createRow(i);
for(intj=0;j<columnSize;j++){
sheet.getRow(i).createCell(j).setCellStyle(basicStyle);
}
}
//填充資料
if(rowSize_title!=0){
//1、標題
HSSFCellcell=sheet.getRow(0).getCell(0);
cell.setCellStyle(titleStyle);
cell.setCellValue(title);
//單元格合併
sheet.addMergedRegion(newCellRangeAddress(0,rowSize_title-1,0,columnSize-1));//起始行,終止行,起始列,終止列
}
//2、表頭、單元格資料內容寫入
for(inti=0;i<finalHeaderList.size();i++){
TDtd=finalHeaderList.get(i);
sheet.getRow(td.getY()).getCell(td.getX()).setCellValue(td.getContent());
//單元格合併
sheet.addMergedRegion(
newCellRangeAddress(//起始行,終止行,起始列,終止列
td.getY(),
td.getY()+(td.getRowspan()-1),
td.getX(),
td.getX()+(td.getColspan()-1))
);
}
//3、設定每一列寬度以該列的最長內容為準
for(inti=0;i<maxLengthArr.length;i++){
sheet.setColumnWidth(i,maxLengthArr[i]*2*235);
}
for(inti=0;i<rowSize;i++){
HSSFRowrow =sheet.getRow(i);
row.setHeightInPoints(row.getHeightInPoints()+3);
}
returnwb;
}
}
/**
* @ClassName: TD
* @Description: 單元格類
* @author Qin
* @date 2014年12月12日 下午5:45:10
*/
class TD {
privateint rowspan=1;
privateint colspan=1;
privateint x;
privateint y;
privateString content;
publicint getRowspan() {
returnrowspan;
}
publicvoid setRowspan(int rowspan) {
this.rowspan= rowspan;
}
publicint getColspan() {
returncolspan;
}
publicvoid setColspan(int colspan) {
this.colspan= colspan;
}
publicString getContent() {
returncontent;
}
publicvoid setContent(String content) {
this.content= content;
}
publicint getX() {
returnx;
}
publicvoid setX(int x) {
this.x= x;
}
publicint getY() {
returny;
}
publicvoid setY(int y) {
this.y= y;
}
}