不吹不黑,学完这篇,Word导出就没问题了
写在前头:本篇仅记录作者开发过程中使用的导出策略(B/S)。
导出Word的类库与方法也有挺多,今天主要是介绍使用NPOI根据Word模板进行导出(说是根据模板,其实到最后,就跟自己手写样式一样,模板都没用到了,具体怎么回事呢,且听我细细道来)。网上对npoi操作Word的教程还是比较少的,主要是因为NPOI 本身的问题,对Word的支持还不是特别完善。如果是根据模板导出的话,就需要用到特殊字符替换法,请自行准备好导出模板,然后将需要替换的字段(内容)换成特殊字符,例如:
跟导出Excel一样,也是需要引入Npoi的dll,
using NPOI; using NPOI.OpenXmlFormats.Wordprocessing; using NPOI.XWPF.UserModel;
既然咱们是使用替换法,就需要有替换关系,这里我使用的是字典形式,
Dictionary<string, string> datas = new Dictionary<string, string>(); datas.Add("${customerName}$", contractModel.CusName); datas.Add("${customerAddress}$", contractModel.CusAddress); datas.Add("${customerFaren}$", contractModel.CusLPerson); datas.Add("${customerWeituo}$", "");//去掉客户的委托代理人赋值 datas.Add("${customerPhone}$", contractModel.LinkPhone); datas.Add("${customerRegTel}$", InvoicePhone);//注册电话 datas.Add("${customerZipCode}$", contractModel.CusPostCode); datas.Add("${customerFax}$", contractModel.CusFax);
好,接下来是获取咱们的模板地址并打开,
string tempFile = string.Format(@"{0}bin\Template{1}", AppDomain.CurrentDomain.BaseDirectory, "销售合同模板.docx"); // 模板文件位置 XWPFDocument doc; FileStream fs = File.OpenRead(tempFile); doc = new XWPFDocument(fs); fs.Close();//关闭fs文件流,防止多人同时操作这个模板(项目中导出的话很常见的问题,因为这个项目不是一个人在用,一定会存在这种情况)。
doc 就是获取的整个word文件了,在word中的文本,没有包含在表格里的,就是段落Paragraph,表格就是Table,所以获取到word后先处理段落Paragraph,
IList paragraphs = doc.Paragraphs; foreach (var par in paragraphs) { changeValue(par, datas); }
///匹配传入信息集合与模板 /// @param value 模板需要替换的区域 /// @param textMap 传入信息集合 ///@return 模板需要替换区域信息集合对应值 private XWPFParagraph changeValue(XWPFParagraph paragraph, Dictionary<String, String> textMap) { string par = paragraph.Text; try { foreach (var date in textMap) { string oldPar = paragraph.Text; if (par.Contains(date.Key)) { par = par.Replace(date.Key, date.Value); paragraph.ReplaceText(oldPar, par); } } } catch (Exception ex) { return paragraph; } return paragraph; }
以上就可以解决Word中的段落替换问题。说完段落,咱们说一下表格,在表格中,每一个单元格内都是一个段落。首先遍历表格,然后对表格的每一行进行遍历,在遍历每一行的每个单元格,这样就可以获取到单元格内的段落进行替换,上代码:
IList tables = doc.Tables; foreach (var table in tables) { IList rows = table.Rows; //遍历表格,并替换 foreach (var row in rows) { eachTable(row, datas); } }
/// 遍历表格 ///@param rows 表格行对象 ///@param textMap 需要替换的信息集合 private void eachTable(XWPFTableRow row, Dictionary<String, String> textMap) { List<XWPFTableCell> cells = row.GetTableCells(); foreach (var cell in cells) { //是否需要替换,是则替换 if (checkText(cell.GetText())) { //表格内的每格都是一个段落 IList<XWPFParagraph> paragraphs = cell.Paragraphs; foreach (var par in paragraphs) { changeValue(par, textMap); } } } }
///判断文本中是否包含特殊字符 /// @param text 文本 ///@return 包含返回true,不包含返回false private bool checkText(String text) { bool check = false; if (text.Contains("${")) { check = true; } return check; }
导出避免不了的还有不定数量的产品行的导出替换问题,
这时候在模板上做出一行,以这一行作为不定行的模板进行复制,
//获取新增行的样式(产品表头) XWPFTableRow rowTemp = proTable.GetRow(3); //删除第三行样式行 proTable.RemoveRow(3);//这里删除样式行,因为rowTemp已经是样式行模板了 for (int i = 0; i < lstProduct.Count; i++)//lstProduct是全部产品集合 { Copy(proTable, rowTemp, i + 3);//复制出这一行 Dictionary<string, string> productMap = GetProductMap(lstProduct[i]);//获取产品行的替换字典 XWPFTableRow row = proTable.GetRow(i + 3);//获取刚刚复制出的行 eachTable(row, productMap);//遍历这一行的单元格,替换 }
/// <summary> /// 复制行 /// </summary> /// <param name="table"></param> /// <param name="sourceRow"></param> /// <param name="rowIndex"></param> private void Copy(XWPFTable table, XWPFTableRow sourceRow, int rowIndex) { //在表格指定位置新增一行 XWPFTableRow targetRow = table.InsertNewTableRow(rowIndex); //复制行属性 targetRow.GetCTRow().trPr = sourceRow.GetCTRow().trPr; List<XWPFTableCell> cellList = sourceRow.GetTableCells(); if (cellList == null || cellList.Count <= 0) { return; } //复制列及其属性和内容 XWPFTableCell targetCell = null; foreach (var sourceCell in cellList) { targetCell = targetRow.AddNewTableCell(); //列属性 targetCell.GetCTTc().tcPr = sourceCell.GetCTTc().tcPr; //段落属性 if (sourceCell.Paragraphs != null && sourceCell.Paragraphs.Count > 0) { targetCell.Paragraphs[0].Alignment = ParagraphAlignment.CENTER; if (sourceCell.Paragraphs[0].Runs != null && sourceCell.Paragraphs[0].Runs.Count > 0) { XWPFRun cellR = targetCell.Paragraphs[0].CreateRun(); cellR.SetText(sourceCell.GetText()); cellR.IsBold = true; } else { targetCell.SetText(sourceCell.GetText()); } } else { targetCell.SetText(sourceCell.GetText()); } } }
全部内容替换完成后,就是将咱们替换之后的word内容写入到全新的word文档,
MemoryStream ms = new MemoryStream(); doc.Write(ms); string FileName = string.Format(@"{0}ContractFile\{1}", AppDomain.CurrentDomain.BaseDirectory, "销售合同.docx"); // 中间模板位置 //通过中间文件进行导出。直接导出文件-->打开报错 //这就是为什么要写入全新的word文档再导出 using (FileStream filestream = new FileStream(FileName, FileMode.Create, FileAccess.Write)) { wordData = ms.ToArray(); filestream.Write(wordData, 0, wordData.Length); filestream.Flush(); ms.Close(); } File.Delete(FileName);//将新建的word文档删除,因为此文档就是一次性的
以上 基本就能满足大部分的模板替换法导出Word了。在我的项目里,我除了表格是这样做的,我要导出的合同条款,也是按照表格制作的,也就是说,条款我也是做成了表格模板,以进行复制导出(主要是因为我们的条款是业务员做合同时随便可以改的,无法做成固定模板,如果导出的文字是固定不变的或者很少会改动的,建议做成固定模板),但是这样的话,导出的word文档要盖电子签章就会有问题,也就是无法在复制方式制作的表格上显示电子签章(我们用的金格软件)。出现问题,总要解决的嘛,既然条款不能用表格了,那就用段落呗,我的想法是直接把拼接好的条款段落放在指定位置,当然也是利用特殊字符替换的方法,但是这样的话,格式就太丑了,要知道一篇文档最先吸引人的就是它的格式,所以行距以及文字段落样式至关重要,一次替换不行的话,就没法用特殊字符替换的方法了,怎么办,那就直接利用创建法进行段落创建,然后插入条款文字,但是因为要调整样式,所以要使用
CT_P m_p = doc.Document.body.AddNewP();//新建段落有问题 GetTermPar(m_p, lstTerm, model, doc);
/// <summary> /// 设置条款段落的文字及样式 /// </summary> /// <param name="paragraph"></param> /// <param name="lstTerm"></param> /// <param name="model"></param> /// <returns></returns> private void GetTermPar(CT_P paragraph, List<T> lstTerm, T_Table model,XWPFDocument doc) { XWPFParagraph par = new XWPFParagraph(paragraph, doc); paragraph.AddNewPPr().AddNewSpacing().line = "400";//行间距固定值20磅 paragraph.AddNewPPr().AddNewSpacing().lineRule = ST_LineSpacingRule.exact;//行间距应用咱们设置的值,也就是使咱们设置的值生效 foreach (var term in lstTerm.Where(p=>p.SEQ!=1)) { string str = GetParagraphStr(model, term);//条款信息 XWPFRun xwpfRun = par.CreateRun(); //段落下是run作为文字对象 xwpfRun.SetText(str);//设置值 xwpfRun.FontSize = 11;//文字大小 xwpfRun.SetFontFamily("等线", FontCharRange.None);//文字格式 xwpfRun.IsBold = true;//是否加粗 xwpfRun.AddCarriageReturn(); } }
这样条款段落就创建完成了,但是不管是AddNewP()方法 还是CreateParagraph()方法,都是在word文档最后创建段落,也就是本应在这些段落之后的文字表格等都需要重建了,下边是创建表格的代码:
/// <summary> /// 创建客户信息表 /// </summary> /// <param name="doc"></param> private void CreatCusTable(XWPFDocument doc,Dictionary<string,string> datas,bool IsMiddleCustomer) { //创建Table,只包含两行不含合并单元格行 2行 四列 CT_Tbl m_CTTbl = doc.Document.body.AddNewTbl(); XWPFTable cusTable = new XWPFTable(m_CTTbl, doc, 2, 4); //2.1 设置表格样式 m_CTTbl.AddNewTblPr().jc = new CT_Jc(); m_CTTbl.AddNewTblPr().jc.val = ST_Jc.center;//表在页面水平居中 m_CTTbl.AddNewTblPr().AddNewTblW().w = "10480";//表宽度 m_CTTbl.AddNewTblPr().AddNewTblW().type = ST_TblWidth.dxa; //添加合并单元格的行 //这里添加三行 for(int i = 0; i < 3; i++) { XWPFTableRow m_Row = cusTable.InsertNewTableRow(i);//在下标为i的位置插入 行,若i存在 for (int q = 0; q < 2; q++) { //创建单元格,并设置为合并两列(这两列是上边创建的四列中的两列) XWPFTableCell cell = m_Row.CreateCell(); CT_Tc cttc = cell.GetCTTc(); CT_TcPr ctPr = cttc.AddNewTcPr(); ctPr.gridSpan = new CT_DecimalNumber(); ctPr.gridSpan.val = "2"; //合并2列 } m_Row.GetCTRow().AddNewTrPr().AddNewTrHeight().val = (ulong)500;//设置行高 } //如上添加完之后,表格共5行,i最大为4,若要在这5行下边添加行,则无法使用InsertNewTableRow(i) //XWPFTableRow m_Row5 = cusTable.InsertNewTableRow(5);//会报错 CT_Row m_NewRow5 = new CT_Row(); XWPFTableRow m_Row5 = new XWPFTableRow(m_NewRow5, cusTable); m_Row5.GetCTRow().AddNewTrPr().AddNewTrHeight().val = (ulong)500; cusTable.AddRow(m_Row5); XWPFTableCell cell1 = m_Row5.CreateCell(); XWPFTableCell cell2 = m_Row5.CreateCell(); XWPFTableCell cell3 = m_Row5.CreateCell(); CT_Tc cttc3 = cell3.GetCTTc(); CT_TcPr ctPr3 = cttc3.AddNewTcPr(); ctPr3.gridSpan = new CT_DecimalNumber(); ctPr3.gridSpan.val = "2"; //合并2列
}
表格也添加完啦,这样基本上导出的word就没什么大问题啦。
如有问题,或者不正确的地方,敬请留言交流。
仅供学习交流,欢迎留言指正!