基于OOXML (Office Open XML)的复杂Excel文件在线输出1(动态复制表单)

时间:2022-09-23 08:30:17
写在前面的:如果你是OOXML的反对者,本文可能不适合你。
概要
    大家一定知道微软最新的OOXML吧,虽然去年没有被设立为ISO标准,但是它的思路和做法对我们开发人员还是有很大帮助的。本问想阐述的是如何利用OOXML实现在我们项目中复杂Excel文件的在线输出。

项目需求
    导出Excel文件是我们项目的重要功能,且待导出的Excel文件构成比较复杂,特别是程序要能根据用户的请求数据自动判断表单数目,动态完成Excel表单的复制和数据绑定,最终提供用户下载。
     在尝试以往各种方法后,我们决定采用Excel2007的格式开发,即OOXML,众所周知OOXML是一个新的文件类型,确切的说是一个压缩包,而压缩包里面是组成该文件的XML部件文档,一旦面向XML文件了,所有问题就变得简单了。

实现步骤
     1、对Office 2007格式文件包操作
      由于Office 2007的文件是基于 XML 和 ZIP 归档技术创建的,所以我们可以使用任何能够处理 XML 或者 ZIP 文件的工具来访问并且修改文档内容。可使用ICSharpCode.SharpZipLib 下载地址:
http://www.icsharpcode.net/OpenSource/SharpZipLib/Download.aspx,也可以使用 System.IO.Packaging 名称空间中的类库,但需要安装Framework3.0,并引用 WindowsBase.dll。
      2、完成表单复制代码

        /// <summary>
        /// 拷贝sheet
        /// </summary>
        /// <param name="fileName">excel文件</param>
        /// <param name="sheetName">待拷贝的sheet名称</param>
        /// <param name="fileNo">流水号,用于名称后的数字</param>
        /// <returns>新产生的sheet名称</returns>
        protected string CopySheet(string fileName, string sheetName, int fileNo)
        {
            string partName = "/xl/workbook.xml";
            string relFile = "/xl/_rels/workbook.xml.rels";
            //打开包==============================================
            Package xlPackage = Package.Open(fileName, FileMode.Open, FileAccess.ReadWrite);
            
            // 出异常时返回"newSheetName",则:如果在try{}已产生名字,也可以返回

            string newSheetName = "";
            
            try
            {
                Uri documentUri = new Uri(partName, UriKind.Relative);
                PackagePart documentPart = xlPackage.GetPart(documentUri);
                //读出workbook.xml=======================================
                XmlDocument doc = new XmlDocument();
                doc.Load(documentPart.GetStream());
                XmlNamespaceManager nsManager = new XmlNamespaceManager(doc.NameTable);
                nsManager.AddNamespace("d", doc.DocumentElement.NamespaceURI);

                string searchString = string.Format("//d:sheet[@name='{0}']", sheetName);
                XmlNode node = doc.SelectSingleNode(searchString, nsManager);

                if (node == null)
                    return null; //指定的sheet不存在
                else
                {
                    string relId = node.Attributes["r:id"].Value;
                    string sheetId = node.Attributes["sheetId"].Value;
                    string name = node.Attributes["name"].Value;

                    XmlNode nodeSheets = doc.DocumentElement.SelectSingleNode("d:sheets", nsManager);

                    string relId1 = node.Attributes["r:id"].Value + "_" + fileNo.ToString();

                    int maxSheetID = 0;
                    int tempSheetID = 0;
                    foreach (XmlNode note in nodeSheets.ChildNodes)
                    {
                        tempSheetID = Convert.ToInt32(note.Attributes["sheetId"].Value);
                        if (maxSheetID < tempSheetID)
                            maxSheetID = tempSheetID;
                    }
                    string sheetId1 = Convert.ToString(maxSheetID + 1);
                    
                    newSheetName = name + "_" + fileNo.ToString();

                  //构造更改所需内容,主要针对/xl/_rels/workbook.xml.rels文件
                    string sheetFileName;
                    Uri xmlUri = new Uri(relFile, UriKind.Relative);
                    PackagePart xmlPart = xlPackage.GetPart(xmlUri);
                    XmlDocument doc1 = new XmlDocument();
                    doc1.Load(xmlPart.GetStream());

                    XmlNode nodeSheet1 = SelectOneNode(doc1.DocumentElement.ChildNodes, "Id", relId);
                    sheetFileName = nodeSheet1.Attributes["Target"].Value; //     [worksheets/sheetname.xml]
                    string sheetFileName1 = sheetFileName.Substring(sheetFileName.LastIndexOf('/') + 1, (sheetFileName.IndexOf('.') - sheetFileName.LastIndexOf('/') - 1)) + "_" + fileNo.ToString() + ".xml";

                    string xmlString = "<Relationship Id=\"" + relId1 + "\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\" Target=\"worksheets/" + sheetFileName1.ToLower() + "\" />";

                    XmlNode node1 = doc1.DocumentElement;
                    node1.InnerXml += xmlString;

                    //拷贝sheet文件(若文件已存在,则抛出异常),并修改主关系文件[content_types].xml
                    string sheetXmlToPaste = "/xl/worksheets/" + sheetFileName1.ToLower();
                    CopyXmlFile(xlPackage, "/xl/" + sheetFileName, sheetXmlToPaste);

                    //修改workbook.xml
                    nodeSheets.InnerXml += "<sheet name=\"" + newSheetName + "\" sheetId=\"" + sheetId1 + "\" r:id=\"" + relId1 + "\" />";
                    doc.Save(documentPart.GetStream(FileMode.Create, FileAccess.Write));

                    //修改/xl/_rels/workbook.xml.rels文件
                    doc1.Save(xmlPart.GetStream(FileMode.Create, FileAccess.Write));
                    xlPackage.Flush();

                    xlPackage.Close();
                    return newSheetName;
                }
            }
            catch
            {
                xlPackage.Close();
                return newSheetName;
            }
        }

        internal void CopyXmlFile(Package xlPackage, string sheetXmlToCopy, string sheetXmlToPaste)
        {
            Uri sheetUri = new Uri(sheetXmlToCopy, UriKind.Relative);
            PackagePart sheetPart = xlPackage.GetPart(sheetUri);
            XmlDocument doc = new XmlDocument();
            doc.Load(sheetPart.GetStream());
            Uri xmlUri = new Uri(sheetXmlToPaste, UriKind.Relative);
            if (xlPackage.PartExists(xmlUri))
            {
                xlPackage.Close();
                throw new InvalidOperationException("XML part is existing.");
            }
            PackagePart xmlPart = xlPackage.CreatePart(xmlUri, @"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml");

            using (Stream outputStream = xmlPart.GetStream(FileMode.Create, FileAccess.Write))
            {
                using (StreamWriter writer = new StreamWriter(outputStream))
                {
                    writer.Write(doc.InnerXml);
                    writer.Close();
                }
            }
            //修改主关系文件[content_types].xml
            string schemaRelationships = @"http://schemas.openxmlformats.org/officeDocument/2006/relationships";
            PackageRelationship rel = xlPackage.CreateRelationship(xmlUri, TargetMode.Internal, schemaRelationships + "/worksheet");
            xlPackage.Flush();
        }


    代码中CopySheet和CopyXMLFile为主要函数,负责完成表单的复制和对应XML文件的修改,后面还有三个辅助函数。

7 个解决方案

#1


下面是辅助函数: 
       public XmlNode SelectOneNode(XmlNodeList nodes, string name)
        {
            foreach (XmlNode n in nodes)
            {
                if (n.Name == name)
                    return n;
            }

            return null;
        }
        public XmlNode SelectOneNode(XmlNodeList nodes, string name, string value)
        {
            foreach (XmlNode n in nodes)
            {
                if (n.Attributes[name].Value == value)
                    return n;
            }

            return null;
        }
        internal void AddNode(Package xlPackage, string relFile, string xmlString, string singleNode)
        {
            Uri xmlUri = new Uri(relFile, UriKind.Relative);
            PackagePart xmlPart = xlPackage.GetPart(xmlUri);
            XmlDocument doc = new XmlDocument();
            doc.Load(xmlPart.GetStream());
            XmlNode node = doc.DocumentElement;
            node.InnerXml += xmlString;
            doc.Save(xmlPart.GetStream(FileMode.Create, FileAccess.Write));
            xlPackage.Flush();
        }

#2


开会了,待续。。。

#3


居然没有修改帖子和回复的权限,晕!

推荐大家几个好的地方,用来实现对Excel 2007文件的操作:
1、微软的MSDN,http://msdn2.microsoft.com/en-us/library/bb508943.aspx,上面有详细的讲解,而且还有很多例子下载;
2、国外已经有大侠完成了功能封装,而且开源的,参见:http://www.codeplex.com/ExcelPackage,当时我们也想直接使用这个包,但是唯独缺少我们最主要的功能:表单的复制,所以就自己研究,写了复制的方法;
3、如果大家有需求,我还会把微软的包做一下整理,然后进行简单封装;
4、除此之外,我们在本项目中还实现了数据的批量赋值(写好数据和excel中XML文件的映射文件)和在线转换低版本(Com实现),我会陆续整理。

#4


啊那这个东东很有用吗,我很菜滴,能不能给个DEMO啊

#5


这个东东就是用来实现在线导出excel文件的。你可以去看看现有的一些做法,然后比较一下。
Demo我还在整理中,会尽快提供的!

#6


非常感謝樓主,解決了我很大的問題.謝謝!

#7


好文章,正好需要这个

#1


下面是辅助函数: 
       public XmlNode SelectOneNode(XmlNodeList nodes, string name)
        {
            foreach (XmlNode n in nodes)
            {
                if (n.Name == name)
                    return n;
            }

            return null;
        }
        public XmlNode SelectOneNode(XmlNodeList nodes, string name, string value)
        {
            foreach (XmlNode n in nodes)
            {
                if (n.Attributes[name].Value == value)
                    return n;
            }

            return null;
        }
        internal void AddNode(Package xlPackage, string relFile, string xmlString, string singleNode)
        {
            Uri xmlUri = new Uri(relFile, UriKind.Relative);
            PackagePart xmlPart = xlPackage.GetPart(xmlUri);
            XmlDocument doc = new XmlDocument();
            doc.Load(xmlPart.GetStream());
            XmlNode node = doc.DocumentElement;
            node.InnerXml += xmlString;
            doc.Save(xmlPart.GetStream(FileMode.Create, FileAccess.Write));
            xlPackage.Flush();
        }

#2


开会了,待续。。。

#3


居然没有修改帖子和回复的权限,晕!

推荐大家几个好的地方,用来实现对Excel 2007文件的操作:
1、微软的MSDN,http://msdn2.microsoft.com/en-us/library/bb508943.aspx,上面有详细的讲解,而且还有很多例子下载;
2、国外已经有大侠完成了功能封装,而且开源的,参见:http://www.codeplex.com/ExcelPackage,当时我们也想直接使用这个包,但是唯独缺少我们最主要的功能:表单的复制,所以就自己研究,写了复制的方法;
3、如果大家有需求,我还会把微软的包做一下整理,然后进行简单封装;
4、除此之外,我们在本项目中还实现了数据的批量赋值(写好数据和excel中XML文件的映射文件)和在线转换低版本(Com实现),我会陆续整理。

#4


啊那这个东东很有用吗,我很菜滴,能不能给个DEMO啊

#5


这个东东就是用来实现在线导出excel文件的。你可以去看看现有的一些做法,然后比较一下。
Demo我还在整理中,会尽快提供的!

#6


非常感謝樓主,解決了我很大的問題.謝謝!

#7


好文章,正好需要这个