隐藏

利用NPOI将EXCEL转换成HTML的C#实现

发布:2020/2/12 13:39:41作者:管理员 来源:本站 浏览次数:1563

领导说想做一个网页打印功能,而且模板可以自定义,我考虑了三个方案,一是打印插件,二是在线 html 编辑器,三是 excel 模板,领导建议用的是打印插件的形式,我研究了一下,一个是需要下载安装,二个是模板定义其实也相当不方便,所以我想采用后两种,而在线  html 编辑器的话,直接画出来的并不真的是所见即所得,打印效果肯定需要不停的去调整,而直接 html 代码呢,对客户的要求又比较高(不可否认,很多客户都不知道 html 是什么玩意儿),所以最后选择了 excel 形式,搜了一下 npoi 官网,发现一个 java 版的 html 导出,于是辛苦了一下,把它改造成了 c# 的,在此过种中发现Java版本的没有处理合并单元格,且字体相对较大,我对此进行了一点改进,另外发现了两个NPOI的BUG,因为时间关系,也就没有去弄NPOI的源码了,等我有空了再来解决这两个BUG吧, 代码注释不多,需要看注释的直接去看 java版本的即可。

贴上效果图:

	
  1. using NPOI.HSSF.UserModel;
  2. using NPOI.POIFS.FileSystem;
  3. using NPOI.SS.Format;
  4. using NPOI.SS.UserModel;
  5. using System;
  6. using System.Collections;
  7. using System.Collections.Generic;
  8. using System.IO;
  9. using System.Linq;
  10. using System.Text;
  11. using System.Text.RegularExpressions;
  12. using System.Web;
  13.  
  14. namespace ExcelUtility
  15. {
  16. public class EXCELTOHTML
  17. {
  18. private IWorkbook wb = null;
  19.  
  20. private const String DEFAULTS_CLASS = "excelDefaults";
  21. private const String COL_HEAD_CLASS = "colHeader";
  22. private const String ROW_HEAD_CLASS = "rowHeader";
  23.  
  24. private const int IDX_TABLE_WIDTH = -2;
  25. private const int IDX_HEADER_COL_WIDTH = -1;
  26.  
  27. private int firstColumn;
  28. private int endColumn;
  29.  
  30. private bool gotBounds;
  31.  
  32. private List<KeyValuePair<HorizontalAlignment, string>> HALIGN = new List<KeyValuePair<HorizontalAlignment, string>>() {
  33. new KeyValuePair<HorizontalAlignment, string>(HorizontalAlignment.Left, "left"),
  34. new KeyValuePair<HorizontalAlignment, string>(HorizontalAlignment.Center, "center"),
  35. new KeyValuePair<HorizontalAlignment, string>(HorizontalAlignment.Right, "right"),
  36. new KeyValuePair<HorizontalAlignment, string>(HorizontalAlignment.Fill, "left"),
  37. new KeyValuePair<HorizontalAlignment, string>(HorizontalAlignment.Justify, "left"),
  38. new KeyValuePair<HorizontalAlignment, string>(HorizontalAlignment.CenterSelection, "center"),
  39. new KeyValuePair<HorizontalAlignment, string>(HorizontalAlignment.General, "left")
  40. };
  41.  
  42. private List<KeyValuePair<VerticalAlignment, string>> VALIGN = new List<KeyValuePair<VerticalAlignment, string>>() {
  43. new KeyValuePair<VerticalAlignment, string>(VerticalAlignment.Bottom, "bottom"),
  44. new KeyValuePair<VerticalAlignment, string>(VerticalAlignment.Center, "middle"),
  45. new KeyValuePair<VerticalAlignment, string>(VerticalAlignment.Top, "top")
  46. };
  47.  
  48. private List<KeyValuePair<BorderStyle, string>> BORDER = new List<KeyValuePair<BorderStyle, string>>() {
  49. new KeyValuePair<BorderStyle, string>(BorderStyle.DashDot, "dashed 1pt"),
  50. new KeyValuePair<BorderStyle, string>(BorderStyle.DashDotDot, "dashed 1pt"),
  51. new KeyValuePair<BorderStyle, string>(BorderStyle.Dashed, "dashed 1pt"),
  52. new KeyValuePair<BorderStyle, string>(BorderStyle.Dotted, "dotted 1pt"),
  53. new KeyValuePair<BorderStyle, string>(BorderStyle.Double, "double 3pt"),
  54. new KeyValuePair<BorderStyle, string>(BorderStyle.Hair, "dashed 1px"),
  55. new KeyValuePair<BorderStyle, string>(BorderStyle.Medium, "solid 2pt"),
  56. new KeyValuePair<BorderStyle, string>(BorderStyle.MediumDashDot, "dashed 2pt"),
  57. new KeyValuePair<BorderStyle, string>(BorderStyle.MediumDashDotDot, "dashed 2pt"),
  58. new KeyValuePair<BorderStyle, string>(BorderStyle.MediumDashed, "dashed 2pt"),
  59. new KeyValuePair<BorderStyle, string>(BorderStyle.None, "none"),
  60. new KeyValuePair<BorderStyle, string>(BorderStyle.SlantedDashDot, "dashed 2pt"),
  61. new KeyValuePair<BorderStyle, string>(BorderStyle.Thick, "solid 3pt"),
  62. new KeyValuePair<BorderStyle, string>(BorderStyle.Thin, "solid 1pt")
  63. };
  64.  
  65. public EXCELTOHTML(IWorkbook wb)
  66. {
  67. this.wb = wb;
  68. }
  69.  
  70. public EXCELTOHTML(string path)
  71. {
  72. using (var inputfs = new FileStream(path, FileMode.Open, FileAccess.Read))
  73. {
  74. NPOIFSFileSystem fs = new NPOIFSFileSystem(inputfs);
  75. this.wb = new HSSFWorkbook(fs.Root, true);
  76. }
  77. }
  78.  
  79. public string ToHtml(int sheetIndex = 0, bool completeHtmls = true, bool needTitle = true)
  80. {
  81. return ToHtml(wb.GetSheetName(sheetIndex), completeHtmls, needTitle);
  82. }
  83.  
  84. public string ToHtml(string sheetName, bool completeHtmls = true, bool needTitle = true)
  85. {
  86. StringBuilder sbRet = new StringBuilder();
  87.  
  88. if (completeHtmls)
  89. {
  90. sbRet.Append("<?xml version=\"1.0\" encoding=\"iso-8859-1\" ?>\n");
  91. sbRet.Append("<html>\n");
  92. sbRet.Append("<head>\n");
  93. }
  94. sbRet.Append(GetInlineStyle());
  95. if (completeHtmls)
  96. {
  97. sbRet.Append("</head>\n");
  98. sbRet.Append("<body>\n");
  99. }
  100. sbRet.Append(GetSheets(sheetName, needTitle));
  101. if (completeHtmls)
  102. {
  103. sbRet.Append("</body>\n");
  104. sbRet.Append("</html>\n");
  105. }
  106.  
  107. return sbRet.ToString();
  108. }
  109.  
  110. private string GetSheets(string sheetName, bool needTitle)
  111. {
  112. StringBuilder sbRet = new StringBuilder();
  113.  
  114. ISheet sheet = wb.GetSheet(sheetName);
  115. sbRet.Append(GetSheet(sheet, needTitle));
  116.  
  117. return sbRet.ToString();
  118. }
  119.  
  120. private string GetSheet(ISheet sheet, bool needTitle)
  121. {
  122. StringBuilder sbRet = new StringBuilder();
  123.  
  124. List<KeyValuePair<int, int>> widths = computeWidths(sheet);
  125. int tableWidth = widths.Where(o => o.Key == IDX_TABLE_WIDTH).First().Value;
  126. sbRet.Append(string.Format("<table class={0} cellspacing=\"0\" cellpadding=\"0\" style=\"width:{1}px;\">\n", DEFAULTS_CLASS, tableWidth));
  127. sbRet.Append(GetCols(widths, needTitle));
  128. sbRet.Append(GetSheetContent(sheet, needTitle));
  129. sbRet.Append("</table>\n");
  130.  
  131. return sbRet.ToString();
  132. }
  133.  
  134. private string GetColumnHeads()
  135. {
  136. StringBuilder sbRet = new StringBuilder();
  137.  
  138. sbRet.Append(string.Format("<thead>\n"));
  139. sbRet.Append(string.Format(" <tr class={0}>\n", COL_HEAD_CLASS));
  140. sbRet.Append(string.Format(" <th class={0}>◊</th>\n", COL_HEAD_CLASS));
  141. //noinspection UnusedDeclaration
  142. for (int i = firstColumn; i < endColumn; i++)
  143. {
  144. StringBuilder colName = new StringBuilder();
  145.  
  146. int cnum = i;
  147. do
  148. {
  149. colName.Insert(0, (char)('A' + cnum % 26));
  150. cnum /= 26;
  151. } while (cnum > 0);
  152.  
  153. sbRet.Append(string.Format(" <th class={0}>{1}</th>\n", COL_HEAD_CLASS, colName));
  154. }
  155. sbRet.Append(" </tr>\n");
  156. sbRet.Append("</thead>\n");
  157.  
  158. return sbRet.ToString();
  159. }
  160.  
  161. private string GetSheetContent(ISheet sheet, bool needTitle)
  162. {
  163. StringBuilder sbRet = new StringBuilder();
  164.  
  165. if (needTitle)
  166. {
  167. sbRet.Append(GetColumnHeads());
  168. }
  169.  
  170. sbRet.Append(string.Format("<tbody>\n"));
  171. IEnumerator rows = sheet.GetRowEnumerator();
  172. while (rows.MoveNext())
  173. {
  174. IRow row = (IRow)rows.Current;
  175.  
  176. sbRet.Append(string.Format(" <tr>\n"));
  177. if (needTitle)
  178. {
  179. sbRet.Append(string.Format(" <td class={0}>{1}</td>\n", ROW_HEAD_CLASS, row.RowNum + 1));
  180. }
  181.  
  182. StringBuilder sbTemp = new StringBuilder();
  183. int mergeCnt = 0;
  184. ICell preCell = null;
  185. ICell cell = null;
  186.  
  187. for (int i = firstColumn; i < endColumn; i++)
  188. {
  189. String content = " ";
  190. String attrs = "";
  191. ICellStyle style = null;
  192. bool isMerge = false;
  193.  
  194. if (i >= row.FirstCellNum && i < row.LastCellNum)
  195. {
  196. cell = row.GetCell(i);
  197. if (cell != null)
  198. {
  199. isMerge = cell.IsMergedCell;
  200. style = cell.CellStyle;
  201. attrs = tagStyle(cell, style);
  202. //Set the value that is rendered for the cell
  203. //also applies the format
  204. MyCellFormat cf = MyCellFormat.GetInstance(style.GetDataFormatString());
  205. CellFormatResult result = cf.Apply(cell);
  206. content = result.Text; //never null
  207. if (string.IsNullOrEmpty(content))
  208. {
  209. content = " ";
  210. }
  211. }
  212. }
  213.  
  214. if (isMerge == true && content == " ")
  215. {
  216. /*
  217. * 因为 NPOI 返回的 cell 没有 mergeCnt 属性,只有一个 IsMergedCell 属性
  218. * 如果有5个单元格,后面四个单元格合并成一个大单元格
  219. * 它返回的其实还是5个单元格,IsMergedCell 分别是: false,true,true,true,true
  220. * 上头这种情况还算好,我们好歹还能猜到后面四个单元格是合并单元格
  221. *
  222. * 但是如果第一个单独,后面四个每两个合并呢?
  223. * TMD返回的还是5个单元格,IsMergedCell 仍然是: false,true,true,true,true
  224. * 所以这里是有问题的,我没法知道后面的四个单元格是四个合并成一个呢,还是两个两个的分别合并
  225. * 这个是没办法的,除非从NPOI的源代码里头去解决这个问题,介于上班呢,要求的是出结果,所以公司是
  226. * 不太会允许我去干这种投入产出比较差的事情的,所以这个问题我采用了一个成本比较低的办法来绕开
  227. *
  228. * 办法就是我们在定义模板的时候,可以通过为每一个合并单元格添加内容来避免。
  229. * 比如说 cell1(内容), cell2,cell3(内容), cell4,cell5(内容)
  230. * 这样的话我就能知道 cell1 IsMergedCell = false 是一个独立的单元格
  231. * cell2, cell3, cell4, cell5 的 IsMergedCell 虽然都是 true, 但是因为 cell4 这个位置有内容了,
  232. * 那我就晓得 cell2 和 cell3 是合并的, cell4 和 cell5 也是合并的。
  233. *
  234. * 当然这里还会有个小小的问题,如果 cell4, cell5 里头是一个会被替换掉的内容,也即 $[字段] 这样的东西
  235. * 如果实际的内容为 null 那么 cell4, cell5 合并单元格的内容也就是 null 了,这又回到了之前的问题了,
  236. * 所以此处要求定义模板的时候 $[内容] 后面加一个空格,这样在生成 html 的时候,其实是不影响打印效果的。
  237. * 也即 “$[] ”注意双引号里头的 “]”后头有个空格
  238. */
  239. if (mergeCnt == 1 && preCell != null && preCell.IsMergedCell == false)
  240. {
  241. sbTemp.Append(string.Format(" <td class={0} {1}{3}>{2}</td>\n", styleName(style), attrs, content, (isMerge) ? " colspan=\"1\"" : ""));
  242. } else {
  243. mergeCnt++;
  244. }
  245. } else {
  246. sbTemp.Replace("colspan=\"1\"", string.Format("colspan=\"{0}\"", mergeCnt));
  247. mergeCnt = 1;
  248. sbTemp.Append(string.Format(" <td class={0} {1}{3}>{2}</td>\n", styleName(style), attrs, content, (isMerge) ? " colspan=\"1\"" : ""));
  249. }
  250. preCell = cell;
  251. }
  252. sbRet.Append(sbTemp.Replace("colspan=\"1\"", string.Format("colspan=\"{0}\"", mergeCnt)).ToString());
  253.  
  254. sbRet.Append(string.Format(" </tr>\n"));
  255. }
  256. sbRet.Append(string.Format("</tbody>\n"));
  257.  
  258. return sbRet.ToString();
  259. }
  260.  
  261. private String tagStyle(ICell cell, ICellStyle style)
  262. {
  263. if (style.Alignment == HorizontalAlignment.General)
  264. {
  265. switch (ultimateCellType(cell))
  266. {
  267. case CellType.String:
  268. return "style=\"text-align: left;\"";
  269. case CellType.Boolean:
  270. case CellType.Error:
  271. return "style=\"text-align: center;\"";
  272. case CellType.Numeric:
  273. default:
  274. // "right" is the default
  275. break;
  276. }
  277. }
  278. return "";
  279. }
  280.  
  281. private static CellType ultimateCellType(ICell c)
  282. {
  283. CellType type = c.CellType;
  284. if (type == CellType.Formula)
  285. {
  286. type = c.CachedFormulaResultType;
  287. }
  288. return type;
  289. }
  290.  
  291. private string GetCols(List<KeyValuePair<int, int>> widths, bool needTitle)
  292. {
  293. StringBuilder sbRet = new StringBuilder();
  294.  
  295. if (needTitle)
  296. {
  297. int headerColWidth = widths.Where(o => o.Key == IDX_HEADER_COL_WIDTH).First().Value;
  298. sbRet.Append(string.Format("<col style=\"width:{0}px\"/>\n", headerColWidth));
  299. }
  300. for (int i = firstColumn; i < endColumn; i++)
  301. {
  302. int colWidth = widths.Where(o => o.Key == i).First().Value;
  303. sbRet.Append(string.Format("<col style=\"width:{0}px;\"/>\n", colWidth));
  304. }
  305.  
  306. return sbRet.ToString();
  307. }
  308.  
  309. private List<KeyValuePair<int, int>> computeWidths(ISheet sheet)
  310. {
  311. List<KeyValuePair<int, int>> ret = new List<KeyValuePair<int, int>>();
  312. int tableWidth = 0;
  313.  
  314. ensureColumnBounds(sheet);
  315.  
  316. // compute width of the header column
  317. int lastRowNum = sheet.LastRowNum;
  318. int headerCharCount = lastRowNum.ToString().Length;
  319. int headerColWidth = widthToPixels((headerCharCount + 1) * 256);
  320. ret.Add(new KeyValuePair<int, int>(IDX_HEADER_COL_WIDTH, headerColWidth));
  321. tableWidth += headerColWidth;
  322.  
  323. for (int i = firstColumn; i < endColumn; i++)
  324. {
  325. int colWidth = widthToPixels(sheet.GetColumnWidth(i));
  326. ret.Add(new KeyValuePair<int, int>(i, colWidth));
  327. tableWidth += colWidth;
  328. }
  329.  
  330. ret.Add(new KeyValuePair<int, int>(IDX_TABLE_WIDTH, tableWidth));
  331. return ret;
  332. }
  333.  
  334. private int widthToPixels(double widthUnits)
  335. {
  336. return (int)(Math.Round(widthUnits * 9 / 256));
  337. }
  338.  
  339. private void ensureColumnBounds(ISheet sheet)
  340. {
  341. if (gotBounds) return;
  342.  
  343. IEnumerator iter = sheet.GetRowEnumerator();
  344. if (iter.MoveNext()) firstColumn = 0;
  345. else firstColumn = int.MaxValue;
  346.  
  347. endColumn = 0;
  348. iter.Reset();
  349. while (iter.MoveNext())
  350. {
  351. IRow row = (IRow)iter.Current;
  352. short firstCell = row.FirstCellNum;
  353. if (firstCell >= 0)
  354. {
  355. firstColumn = Math.Min(firstColumn, firstCell);
  356. endColumn = Math.Max(endColumn, row.LastCellNum);
  357. }
  358. }
  359. gotBounds = true;
  360. }
  361.  
  362. private string GetInlineStyle()
  363. {
  364. StringBuilder sbRet = new StringBuilder();
  365.  
  366. sbRet.Append("<style type=\"text/css\">\n");
  367. sbRet.Append(GetStyles());
  368. sbRet.Append("</style>\n");
  369.  
  370. return sbRet.ToString();
  371. }
  372.  
  373. private string GetStyles()
  374. {
  375. StringBuilder sbRet = new StringBuilder();
  376.  
  377. HashSet<ICellStyle> seen = new HashSet<ICellStyle>();
  378. for (int i = 0; i < wb.NumberOfSheets; i++)
  379. {
  380. ISheet sheet = wb.GetSheetAt(i);
  381. IEnumerator rows = sheet.GetRowEnumerator();
  382. while (rows.MoveNext())
  383. {
  384. IRow row = (IRow)rows.Current;
  385. foreach (ICell cell in row)
  386. {
  387. ICellStyle style = cell.CellStyle;
  388. if (!seen.Contains(style))
  389. {
  390. sbRet.Append(GetStyle(style));
  391. seen.Add(style);
  392. }
  393. }
  394. }
  395. }
  396.  
  397. return sbRet.ToString();
  398. }
  399.  
  400. private string GetStyle(ICellStyle style)
  401. {
  402. StringBuilder sbRet = new StringBuilder();
  403.  
  404. sbRet.Append(string.Format(".{0} .{1} {{\n", DEFAULTS_CLASS, styleName(style)));
  405. sbRet.Append(styleContents(style));
  406. sbRet.Append("}\n");
  407.  
  408. return sbRet.ToString();
  409. }
  410.  
  411. private string styleContents(ICellStyle style)
  412. {
  413. StringBuilder sbRet = new StringBuilder();
  414.  
  415. sbRet.Append(styleOut("text-align", style.Alignment));
  416. sbRet.Append(styleOut("vertical-align", style.VerticalAlignment));
  417. sbRet.Append(fontStyle(style));
  418. sbRet.Append(borderStyles(style));
  419. sbRet.Append(colorStyles(style));
  420.  
  421. return sbRet.ToString();
  422. }
  423.  
  424. private string colorStyles(ICellStyle style)
  425. {
  426. StringBuilder sbRet = new StringBuilder();
  427.  
  428. //sbRet.Append("还未实现!");
  429.  
  430. return sbRet.ToString();
  431. }
  432.  
  433. private string borderStyles(ICellStyle style)
  434. {
  435. StringBuilder sbRet = new StringBuilder();
  436.  
  437. sbRet.Append(styleOut("border-left", style.BorderLeft));
  438. /*
  439. * NPOI有BUG,合并单元格的 border-right 永远都是 None
  440. * 我们可以通过设置合并单元格后边那个单元格的左边框的解决
  441. * 如果当前合并单元格已经合并到最后一列了,我们就只能再加一列了,为了不影响打印效果
  442. * 这最后加的这一列在设置好左边框后,需要把宽度设置得很小,比如说0.1这样
  443. */
  444. sbRet.Append(styleOut("border-right", style.BorderRight));
  445. sbRet.Append(styleOut("border-top", style.BorderTop));
  446. sbRet.Append(styleOut("border-bottom", style.BorderBottom));
  447.  
  448. return sbRet.ToString();
  449. }
  450.  
  451. private string fontStyle(ICellStyle style)
  452. {
  453. StringBuilder sbRet = new StringBuilder();
  454.  
  455. IFont font = style.GetFont(wb);
  456.  
  457. if (font.Boldweight == 0)
  458. {
  459. sbRet.Append(" font-weight: bold;\n");
  460. }
  461. if (font.IsItalic)
  462. {
  463. sbRet.Append(" font-style: italic;\n");
  464. }
  465.  
  466. double fontheight = font.FontHeight / 10 - 10;
  467. if (fontheight == 9)
  468. {
  469. //fix for stupid ol Windows
  470. fontheight = 10;
  471. }
  472. sbRet.Append(string.Format(" font-size: {0}pt;\n", fontheight));
  473.  
  474. return sbRet.ToString();
  475. }
  476.  
  477. private string styleOut(string k, HorizontalAlignment p)
  478. {
  479. return k + ":" + HALIGN.Where(o => o.Key == p).First().Value + ";\n";
  480. }
  481. private string styleOut(string k, VerticalAlignment p)
  482. {
  483. return k + ":" + VALIGN.Where(o => o.Key == p).First().Value + ";\n";
  484. }
  485. private string styleOut(string k, BorderStyle p)
  486. {
  487. return k + ":" + BORDER.Where(o => o.Key == p).First().Value + ";\n";
  488. }
  489.  
  490. private string styleName(ICellStyle style)
  491. {
  492. if (style == null)
  493. {
  494. style = wb.GetCellStyleAt((short)0);
  495. }
  496. StringBuilder sb = new StringBuilder();
  497. sb.Append(string.Format("style_{0}", style.Index));
  498. return sb.ToString();
  499. }
  500. }
  501. }

javascript部分:

最后NPOI在处理日期的时候,还有一个BUG

	
  1. using NPOI.SS.UserModel;
  2. using NPOI.Util;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Drawing;
  6. using System.Text.RegularExpressions;
  7. using System.Windows.Forms;
  8. using NPOI.SS.Format;
  9.  
  10. namespace ExcelUtility
  11. {
  12. /// <summary>
  13. /// 这个东西是为了解决 NPOI CellFormat 的BUG而存在的。
  14. /// 它在读取 日期格式 的时候有时候会报错。
  15. /// </summary>
  16. public class MyCellFormat
  17. {
  18. private CellFormat cellformat = null;
  19.  
  20. private MyCellFormat(string format) {
  21. this.cellformat = CellFormat.GetInstance(format);
  22. }
  23.  
  24. public static MyCellFormat GetInstance(string format) {
  25. return new MyCellFormat(format);
  26. }
  27.  
  28. public CellFormatResult Apply(ICell cell)
  29. {
  30. try
  31. {
  32. return cellformat.Apply(cell);
  33. }
  34. catch (Exception)
  35. {
  36. var formatStr = cell.CellStyle.GetDataFormatString();
  37. var mc = new Regex(@"(yy|M|d|H|s|ms)").Match(formatStr);
  38. /*
  39. * 目前全部不能正常转换的日期格式都转换成 yyyy - MM - dd 的形式
  40. * 比如说:【[$-F800]dddd\,\ mmmm\ dd\,\ yyyy】这个格式
  41. * 稍微 google 了下( https://msdn.microsoft.com/en-us/library/dd318693(VS.85).aspx)
  42. * 这个字符串 0x0800 表示 [System default locale language]
  43. * 因时间关系,只能干完手头的活之后再慢慢研究了。
  44. */
  45. if (mc.Success)
  46. {
  47. return CellFormat.GetInstance("yyyy-MM-dd").Apply(cell);
  48. }
  49. else return cellformat.Apply(cell.ToString() + "<!-- This is the bug of NPOI, Maybe you should modify the file which name is \"MyCellFormat.cs\" -->");
  50. }
  51. }
  52.  
  53. public CellFormatResult Apply(Object v)
  54. {
  55. return cellformat.Apply(v);
  56. }
  57. }
  58. }