不吹不黑,学完这篇,Word导出就没问题了

 

写在前头:本篇仅记录作者开发过程中使用的导出策略(B/S)。

 本篇仅记录Word,查阅其他请跳转

导出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就没什么大问题啦。

 

如有问题,或者不正确的地方,敬请留言交流。

仅供学习交流,欢迎留言指正!

posted @ 2020-01-16 11:14  轩·尘  阅读(1017)  评论(0编辑  收藏  举报