zoukankan      html  css  js  c++  java
  • 不吹不黑,学完这篇,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}binTemplate{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就没什么大问题啦。

     

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

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

    好好学习,认真笔记
  • 相关阅读:
    php 验证码生成方法 及使用
    idea的jar文件,“java.lang.SecurityException: Invalid signature file digest for Manifest main attribute
    Ubuntu下Java JDK安装
    Ubuntu 忘记密码
    为 ubuntu 切换更新源
    使用Java开发桌面即时通讯程序遇到的问题
    MySQL 1093
    Java中命名Dao、Bean、conn等包的含义(不定期补充)
    通过导入Jar包的方式使用JSONObject
    IM开发通信协议基础知识(一)---TCP、UDP、HTTP、SOCKET
  • 原文地址:https://www.cnblogs.com/xuanyuandai/p/12199667.html
Copyright © 2011-2022 走看看