領導說想做一個網頁打印功能,而且模板可以自定義,我考慮了三個方案,一是打印插件,二是在線 html 編輯器,三是 excel 模板,領導建議用的是打印插件的形式,我研究了一下,一個是需要下載安裝,二個是模板定義其實也相當不方便,所以我想采用后兩種,而在線 html 編輯器的話,直接畫出來的並不真的是所見即所得,打印效果肯定需要不停的去調整,而直接 html 代碼呢,對客戶的要求又比較高(不可否認,很多客戶都不知道 html 是什么玩意兒),所以最后選擇了 excel 形式,搜了一下 npoi 官網,發現一個 java 版的 html 導出,於是辛苦了一下,把它改造成了 c# 的,在此過種中發現Java版本的沒有處理合並單元格,且字體相對較大,我對此進行了一點改進,另外發現了兩個NPOI的BUG,因為時間關系,也就沒有去弄NPOI的源碼了,等我有空了再來解決這兩個BUG吧, 代碼注釋不多,需要看注釋的直接去看 java版本的即可。
貼上效果圖:
using NPOI.HSSF.UserModel; using NPOI.POIFS.FileSystem; using NPOI.SS.Format; using NPOI.SS.UserModel; using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Web; namespace ExcelUtility { public class EXCELTOHTML { private IWorkbook wb = null; private const String DEFAULTS_CLASS = "excelDefaults"; private const String COL_HEAD_CLASS = "colHeader"; private const String ROW_HEAD_CLASS = "rowHeader"; private const int IDX_TABLE_WIDTH = -2; private const int IDX_HEADER_COL_WIDTH = -1; private int firstColumn; private int endColumn; private bool gotBounds; private List<KeyValuePair<HorizontalAlignment, string>> HALIGN = new List<KeyValuePair<HorizontalAlignment, string>>() { new KeyValuePair<HorizontalAlignment, string>(HorizontalAlignment.Left, "left"), new KeyValuePair<HorizontalAlignment, string>(HorizontalAlignment.Center, "center"), new KeyValuePair<HorizontalAlignment, string>(HorizontalAlignment.Right, "right"), new KeyValuePair<HorizontalAlignment, string>(HorizontalAlignment.Fill, "left"), new KeyValuePair<HorizontalAlignment, string>(HorizontalAlignment.Justify, "left"), new KeyValuePair<HorizontalAlignment, string>(HorizontalAlignment.CenterSelection, "center"), new KeyValuePair<HorizontalAlignment, string>(HorizontalAlignment.General, "left") }; private List<KeyValuePair<VerticalAlignment, string>> VALIGN = new List<KeyValuePair<VerticalAlignment, string>>() { new KeyValuePair<VerticalAlignment, string>(VerticalAlignment.Bottom, "bottom"), new KeyValuePair<VerticalAlignment, string>(VerticalAlignment.Center, "middle"), new KeyValuePair<VerticalAlignment, string>(VerticalAlignment.Top, "top") }; private List<KeyValuePair<BorderStyle, string>> BORDER = new List<KeyValuePair<BorderStyle, string>>() { new KeyValuePair<BorderStyle, string>(BorderStyle.DashDot, "dashed 1pt"), new KeyValuePair<BorderStyle, string>(BorderStyle.DashDotDot, "dashed 1pt"), new KeyValuePair<BorderStyle, string>(BorderStyle.Dashed, "dashed 1pt"), new KeyValuePair<BorderStyle, string>(BorderStyle.Dotted, "dotted 1pt"), new KeyValuePair<BorderStyle, string>(BorderStyle.Double, "double 3pt"), new KeyValuePair<BorderStyle, string>(BorderStyle.Hair, "dashed 1px"), new KeyValuePair<BorderStyle, string>(BorderStyle.Medium, "solid 2pt"), new KeyValuePair<BorderStyle, string>(BorderStyle.MediumDashDot, "dashed 2pt"), new KeyValuePair<BorderStyle, string>(BorderStyle.MediumDashDotDot, "dashed 2pt"), new KeyValuePair<BorderStyle, string>(BorderStyle.MediumDashed, "dashed 2pt"), new KeyValuePair<BorderStyle, string>(BorderStyle.None, "none"), new KeyValuePair<BorderStyle, string>(BorderStyle.SlantedDashDot, "dashed 2pt"), new KeyValuePair<BorderStyle, string>(BorderStyle.Thick, "solid 3pt"), new KeyValuePair<BorderStyle, string>(BorderStyle.Thin, "solid 1pt") }; public EXCELTOHTML(IWorkbook wb) { this.wb = wb; } public EXCELTOHTML(string path) { using (var inputfs = new FileStream(path, FileMode.Open, FileAccess.Read)) { NPOIFSFileSystem fs = new NPOIFSFileSystem(inputfs); this.wb = new HSSFWorkbook(fs.Root, true); } } public string ToHtml(int sheetIndex = 0, bool completeHtmls = true, bool needTitle = true) { return ToHtml(wb.GetSheetName(sheetIndex), completeHtmls, needTitle); } public string ToHtml(string sheetName, bool completeHtmls = true, bool needTitle = true) { StringBuilder sbRet = new StringBuilder(); if (completeHtmls) { sbRet.Append("<?xml version=\"1.0\" encoding=\"iso-8859-1\" ?>\n"); sbRet.Append("<html>\n"); sbRet.Append("<head>\n"); } sbRet.Append(GetInlineStyle()); if (completeHtmls) { sbRet.Append("</head>\n"); sbRet.Append("<body>\n"); } sbRet.Append(GetSheets(sheetName, needTitle)); if (completeHtmls) { sbRet.Append("</body>\n"); sbRet.Append("</html>\n"); } return sbRet.ToString(); } private string GetSheets(string sheetName, bool needTitle) { StringBuilder sbRet = new StringBuilder(); ISheet sheet = wb.GetSheet(sheetName); sbRet.Append(GetSheet(sheet, needTitle)); return sbRet.ToString(); } private string GetSheet(ISheet sheet, bool needTitle) { StringBuilder sbRet = new StringBuilder(); List<KeyValuePair<int, int>> widths = computeWidths(sheet); int tableWidth = widths.Where(o => o.Key == IDX_TABLE_WIDTH).First().Value; sbRet.Append(string.Format("<table class={0} cellspacing=\"0\" cellpadding=\"0\" style=\"width:{1}px;\">\n", DEFAULTS_CLASS, tableWidth)); sbRet.Append(GetCols(widths, needTitle)); sbRet.Append(GetSheetContent(sheet, needTitle)); sbRet.Append("</table>\n"); return sbRet.ToString(); } private string GetColumnHeads() { StringBuilder sbRet = new StringBuilder(); sbRet.Append(string.Format("<thead>\n")); sbRet.Append(string.Format(" <tr class={0}>\n", COL_HEAD_CLASS)); sbRet.Append(string.Format(" <th class={0}>◊</th>\n", COL_HEAD_CLASS)); //noinspection UnusedDeclaration for (int i = firstColumn; i < endColumn; i++) { StringBuilder colName = new StringBuilder(); int cnum = i; do { colName.Insert(0, (char)('A' + cnum % 26)); cnum /= 26; } while (cnum > 0); sbRet.Append(string.Format(" <th class={0}>{1}</th>\n", COL_HEAD_CLASS, colName)); } sbRet.Append(" </tr>\n"); sbRet.Append("</thead>\n"); return sbRet.ToString(); } private string GetSheetContent(ISheet sheet, bool needTitle) { StringBuilder sbRet = new StringBuilder(); if (needTitle) { sbRet.Append(GetColumnHeads()); } sbRet.Append(string.Format("<tbody>\n")); IEnumerator rows = sheet.GetRowEnumerator(); while (rows.MoveNext()) { IRow row = (IRow)rows.Current; sbRet.Append(string.Format(" <tr>\n")); if (needTitle) { sbRet.Append(string.Format(" <td class={0}>{1}</td>\n", ROW_HEAD_CLASS, row.RowNum + 1)); } StringBuilder sbTemp = new StringBuilder(); int mergeCnt = 0; ICell preCell = null; ICell cell = null; for (int i = firstColumn; i < endColumn; i++) { String content = " "; String attrs = ""; ICellStyle style = null; bool isMerge = false; if (i >= row.FirstCellNum && i < row.LastCellNum) { cell = row.GetCell(i); if (cell != null) { isMerge = cell.IsMergedCell; style = cell.CellStyle; attrs = tagStyle(cell, style); //Set the value that is rendered for the cell //also applies the format MyCellFormat cf = MyCellFormat.GetInstance(style.GetDataFormatString()); CellFormatResult result = cf.Apply(cell); content = result.Text; //never null if (string.IsNullOrEmpty(content)) { content = " "; } } } if (isMerge == true && content == " ") { /* * 因為 NPOI 返回的 cell 沒有 mergeCnt 屬性,只有一個 IsMergedCell 屬性 * 如果有5個單元格,后面四個單元格合並成一個大單元格 * 它返回的其實還是5個單元格,IsMergedCell 分別是: false,true,true,true,true * 上頭這種情況還算好,我們好歹還能猜到后面四個單元格是合並單元格 * * 但是如果第一個單獨,后面四個每兩個合並呢? * TMD返回的還是5個單元格,IsMergedCell 仍然是: false,true,true,true,true * 所以這里是有問題的,我沒法知道后面的四個單元格是四個合並成一個呢,還是兩個兩個的分別合並 * 這個是沒辦法的,除非從NPOI的源代碼里頭去解決這個問題,介於上班呢,要求的是出結果,所以公司是 * 不太會允許我去干這種投入產出比較差的事情的,所以這個問題我采用了一個成本比較低的辦法來繞開 * * 辦法就是我們在定義模板的時候,可以通過為每一個合並單元格添加內容來避免。 * 比如說 cell1(內容), cell2,cell3(內容), cell4,cell5(內容) * 這樣的話我就能知道 cell1 IsMergedCell = false 是一個獨立的單元格 * cell2, cell3, cell4, cell5 的 IsMergedCell 雖然都是 true, 但是因為 cell4 這個位置有內容了, * 那我就曉得 cell2 和 cell3 是合並的, cell4 和 cell5 也是合並的。 * * 當然這里還會有個小小的問題,如果 cell4, cell5 里頭是一個會被替換掉的內容,也即 $[字段] 這樣的東西 * 如果實際的內容為 null 那么 cell4, cell5 合並單元格的內容也就是 null 了,這又回到了之前的問題了, * 所以此處要求定義模板的時候 $[內容] 后面加一個空格,這樣在生成 html 的時候,其實是不影響打印效果的。 * 也即 “$[] ”注意雙引號里頭的 “]”后頭有個空格 */ if (mergeCnt == 1 && preCell != null && preCell.IsMergedCell == false) { sbTemp.Append(string.Format(" <td class={0} {1}{3}>{2}</td>\n", styleName(style), attrs, content, (isMerge) ? " colspan=\"1\"" : "")); } else { mergeCnt++; } } else { sbTemp.Replace("colspan=\"1\"", string.Format("colspan=\"{0}\"", mergeCnt)); mergeCnt = 1; sbTemp.Append(string.Format(" <td class={0} {1}{3}>{2}</td>\n", styleName(style), attrs, content, (isMerge) ? " colspan=\"1\"" : "")); } preCell = cell; } sbRet.Append(sbTemp.Replace("colspan=\"1\"", string.Format("colspan=\"{0}\"", mergeCnt)).ToString()); sbRet.Append(string.Format(" </tr>\n")); } sbRet.Append(string.Format("</tbody>\n")); return sbRet.ToString(); } private String tagStyle(ICell cell, ICellStyle style) { if (style.Alignment == HorizontalAlignment.General) { switch (ultimateCellType(cell)) { case CellType.String: return "style=\"text-align: left;\""; case CellType.Boolean: case CellType.Error: return "style=\"text-align: center;\""; case CellType.Numeric: default: // "right" is the default break; } } return ""; } private static CellType ultimateCellType(ICell c) { CellType type = c.CellType; if (type == CellType.Formula) { type = c.CachedFormulaResultType; } return type; } private string GetCols(List<KeyValuePair<int, int>> widths, bool needTitle) { StringBuilder sbRet = new StringBuilder(); if (needTitle) { int headerColWidth = widths.Where(o => o.Key == IDX_HEADER_COL_WIDTH).First().Value; sbRet.Append(string.Format("<col style=\"width:{0}px\"/>\n", headerColWidth)); } for (int i = firstColumn; i < endColumn; i++) { int colWidth = widths.Where(o => o.Key == i).First().Value; sbRet.Append(string.Format("<col style=\"width:{0}px;\"/>\n", colWidth)); } return sbRet.ToString(); } private List<KeyValuePair<int, int>> computeWidths(ISheet sheet) { List<KeyValuePair<int, int>> ret = new List<KeyValuePair<int, int>>(); int tableWidth = 0; ensureColumnBounds(sheet); // compute width of the header column int lastRowNum = sheet.LastRowNum; int headerCharCount = lastRowNum.ToString().Length; int headerColWidth = widthToPixels((headerCharCount + 1) * 256); ret.Add(new KeyValuePair<int, int>(IDX_HEADER_COL_WIDTH, headerColWidth)); tableWidth += headerColWidth; for (int i = firstColumn; i < endColumn; i++) { int colWidth = widthToPixels(sheet.GetColumnWidth(i)); ret.Add(new KeyValuePair<int, int>(i, colWidth)); tableWidth += colWidth; } ret.Add(new KeyValuePair<int, int>(IDX_TABLE_WIDTH, tableWidth)); return ret; } private int widthToPixels(double widthUnits) { return (int)(Math.Round(widthUnits * 9 / 256)); } private void ensureColumnBounds(ISheet sheet) { if (gotBounds) return; IEnumerator iter = sheet.GetRowEnumerator(); if (iter.MoveNext()) firstColumn = 0; else firstColumn = int.MaxValue; endColumn = 0; iter.Reset(); while (iter.MoveNext()) { IRow row = (IRow)iter.Current; short firstCell = row.FirstCellNum; if (firstCell >= 0) { firstColumn = Math.Min(firstColumn, firstCell); endColumn = Math.Max(endColumn, row.LastCellNum); } } gotBounds = true; } private string GetInlineStyle() { StringBuilder sbRet = new StringBuilder(); sbRet.Append("<style type=\"text/css\">\n"); sbRet.Append(GetStyles()); sbRet.Append("</style>\n"); return sbRet.ToString(); } private string GetStyles() { StringBuilder sbRet = new StringBuilder(); HashSet<ICellStyle> seen = new HashSet<ICellStyle>(); for (int i = 0; i < wb.NumberOfSheets; i++) { ISheet sheet = wb.GetSheetAt(i); IEnumerator rows = sheet.GetRowEnumerator(); while (rows.MoveNext()) { IRow row = (IRow)rows.Current; foreach (ICell cell in row) { ICellStyle style = cell.CellStyle; if (!seen.Contains(style)) { sbRet.Append(GetStyle(style)); seen.Add(style); } } } } return sbRet.ToString(); } private string GetStyle(ICellStyle style) { StringBuilder sbRet = new StringBuilder(); sbRet.Append(string.Format(".{0} .{1} {{\n", DEFAULTS_CLASS, styleName(style))); sbRet.Append(styleContents(style)); sbRet.Append("}\n"); return sbRet.ToString(); } private string styleContents(ICellStyle style) { StringBuilder sbRet = new StringBuilder(); sbRet.Append(styleOut("text-align", style.Alignment)); sbRet.Append(styleOut("vertical-align", style.VerticalAlignment)); sbRet.Append(fontStyle(style)); sbRet.Append(borderStyles(style)); sbRet.Append(colorStyles(style)); return sbRet.ToString(); } private string colorStyles(ICellStyle style) { StringBuilder sbRet = new StringBuilder(); //sbRet.Append("還未實現!"); return sbRet.ToString(); } private string borderStyles(ICellStyle style) { StringBuilder sbRet = new StringBuilder(); sbRet.Append(styleOut("border-left", style.BorderLeft)); /* * NPOI有BUG,合並單元格的 border-right 永遠都是 None * 我們可以通過設置合並單元格后邊那個單元格的左邊框的解決 * 如果當前合並單元格已經合並到最后一列了,我們就只能再加一列了,為了不影響打印效果 * 這最后加的這一列在設置好左邊框后,需要把寬度設置得很小,比如說0.1這樣 */ sbRet.Append(styleOut("border-right", style.BorderRight)); sbRet.Append(styleOut("border-top", style.BorderTop)); sbRet.Append(styleOut("border-bottom", style.BorderBottom)); return sbRet.ToString(); } private string fontStyle(ICellStyle style) { StringBuilder sbRet = new StringBuilder(); IFont font = style.GetFont(wb); if (font.Boldweight == 0) { sbRet.Append(" font-weight: bold;\n"); } if (font.IsItalic) { sbRet.Append(" font-style: italic;\n"); } double fontheight = font.FontHeight / 10 - 10; if (fontheight == 9) { //fix for stupid ol Windows fontheight = 10; } sbRet.Append(string.Format(" font-size: {0}pt;\n", fontheight)); return sbRet.ToString(); } private string styleOut(string k, HorizontalAlignment p) { return k + ":" + HALIGN.Where(o => o.Key == p).First().Value + ";\n"; } private string styleOut(string k, VerticalAlignment p) { return k + ":" + VALIGN.Where(o => o.Key == p).First().Value + ";\n"; } private string styleOut(string k, BorderStyle p) { return k + ":" + BORDER.Where(o => o.Key == p).First().Value + ";\n"; } private string styleName(ICellStyle style) { if (style == null) { style = wb.GetCellStyleAt((short)0); } StringBuilder sb = new StringBuilder(); sb.Append(string.Format("style_{0}", style.Index)); return sb.ToString(); } } }
javascript部分:
最后NPOI在處理日期的時候,還有一個BUG
using NPOI.SS.UserModel; using NPOI.Util; using System; using System.Collections.Generic; using System.Drawing; using System.Text.RegularExpressions; using System.Windows.Forms; using NPOI.SS.Format; namespace ExcelUtility { /// <summary> /// 這個東西是為了解決 NPOI CellFormat 的BUG而存在的。 /// 它在讀取 日期格式 的時候有時候會報錯。 /// </summary> public class MyCellFormat { private CellFormat cellformat = null; private MyCellFormat(string format) { this.cellformat = CellFormat.GetInstance(format); } public static MyCellFormat GetInstance(string format) { return new MyCellFormat(format); } public CellFormatResult Apply(ICell cell) { try { return cellformat.Apply(cell); } catch (Exception) { var formatStr = cell.CellStyle.GetDataFormatString(); var mc = new Regex(@"(yy|M|d|H|s|ms)").Match(formatStr); /* * 目前全部不能正常轉換的日期格式都轉換成 yyyy - MM - dd 的形式 * 比如說:【[$-F800]dddd\,\ mmmm\ dd\,\ yyyy】這個格式 * 稍微 google 了下( https://msdn.microsoft.com/en-us/library/dd318693(VS.85).aspx) * 這個字符串 0x0800 表示 [System default locale language] * 因時間關系,只能干完手頭的活之后再慢慢研究了。 */ if (mc.Success) { return CellFormat.GetInstance("yyyy-MM-dd").Apply(cell); } else return cellformat.Apply(cell.ToString() + "<!-- This is the bug of NPOI, Maybe you should modify the file which name is \"MyCellFormat.cs\" -->"); } } public CellFormatResult Apply(Object v) { return cellformat.Apply(v); } } }