zoukankan      html  css  js  c++  java
  • 还有什么不能做?——细谈在C#中读写Excel系列文章之四

      作为这一系列文章的最后一篇,向大家介绍下如何在Silverlight中解压和创建Excel OpenXml ZIP压缩包。由于Silverlight对本地客户端文件系统访问的安全级别要求比较高,不太容易像Windows应用程序那样可以随意地读写目录和文件,我们不得不考虑使用一些其它的办法。如使用Silverlight的OOB(Out of Browser)模式,可以允许Silverlight程序读写本地的部分目录和文件,下面这篇文章介绍了如何在Silverlight OOB模式下调用COM组件来操作Excel。

    http://www.codeproject.com/Articles/83996/Silverlight-4-Interoperability-with-Excel-using-th

      又回到使用COM组件的方式了,不过这个不是我们要讨论的问题,了解一下也无妨。先说说为什么我们要如此变态地在Silverlight中使用Excel OpenXML。

      这其实是一个需求,用户的数据存放在SharePoint List中,想通过一个程序从List读取数据然后填充到一个Excel模板中提供下载。这个需求本身其实非常简单,关键是用户要求程序不能在客户端安装,服务器端不能有Custom Code,这两点要求几乎扼杀了我们所有可以使用的方法,第一不能使用诸如Windows Form或WPF application,第二不能创建SharePoint Feature或Webpart,当然就更别提创建单独的ASP.NET应用程序了,客户根本就没有提供空间去部署站点。另外Silverlight的OOB模式也不允许,因为OOB模式也是要在本地安装的,尽管它不同于传统意义上的Windows程序安装。这样,我们只有一条路可走,那就是创建Silverlight应用程序然后通过SharePoint的Silverlight Webpart部署到页面上,在Silverlight中直接调用Excel Services把从List中读取到的数据填充到Excel中。那跟Excel OpenXML有什么关系?我们不是已经往Excel里写入数据了吗?对,没错!如果你只是单纯往Excel模板中写入数据根本不需要再做任何操作,可是修改Excel文件的样式呢?

      还记得在上一篇文章中的那个图吗?如果单元格中没有部分内容加粗,而只是单纯的换行或空格,我们可以直接通过Excel的公式或表达式来实现。

    =CONCATENATE("  Short-term investments (including securities loaned", CHAR(10), "    of $9,999 and $8,888")
    ="  Short-term investments (including securities loaned)" & CHAR(10)&"    of " & TEXT("9999", "$#,##0") & " and " & TEXT("8888", "$#,##0")

      上面两行代码分别使用了Excel中的Concatenate()函数和&连接符来填充单元格内容,其中CHAR(10)表示的就是回车符。但是我们无法在Excel中通过函数设置字符串的样式,Excel所有内置的函数都不能修改样式。或许我们可以尝试通过VBA来修改样式呢?在单独的Excel文件中这个办法是可行的,我们只需要在Workbook的Open事件或SheetChange事件中写入VBA代码,当事件被调用的时候样式会被自动修改。不过Excel Services不支持带有VBA或宏的Excel文件,当你尝试通过Excel Services读取一个带有VBA代码或宏的Excel文件时会抛出异常。所以,尝试通过VBA来修改样式是行不通的,尤其是那些特殊的样式,如上图中单元格内数字加粗和上标等。

      因此,我们会考虑通过Excel OpenXML方式将已经填充好数据的Excel文件进行样式修改。按照前面几篇文章的介绍,修改Excel内容需要首先将其解压到一个临时目录,然后修改临时目录中相应的XML文件,最后再重新打包成一个Excel文件。可是在Silverlight中不太容易操作本地文件系统,因此我们只能考虑在文件Stream中完成操作了。

      我们需要一个能支持在Silverlight工程中操作ZIP文件的类库,之前的那个开源类库http://www.icsharpcode.net/OpenSource/SharpZipLib/是使用较早的.NET Framework编写的,有许多类型和对象在Silverlight Framework中找不到无法编译通过。幸好这里我找到有人将其修改成Silverlight版本了,非常感谢!互联网是强大的。

    http://web-snippets.blogspot.com/2008/03/unpacking-zip-files-in-silverlight-2.html

      我这里也提供一个下载吧,以免原作者的空间打不开。SLSharpZipLib_Solution.zip

      这里还有一个关于ShareZipLib示例的WiKi站点,可以研究下这个类库都能干些什么。

    http://wiki.sharpdevelop.net/SharpZipLib_Updating.ashx#Updating_a_zip_file_in_memory_1

      来看一个实际应用的例子。

    /// <summary>
    /// Format exported Excel file with a stream.
    /// </summary>
    /// <param name="zipfileStream">A stream of Excel zip file.</param>
    /// <returns>Return a MemoryStream of updated Excel zip file.</returns>
    public Stream FormatExcelWithStream(Stream zipfileStream)
    {
        // copy the current stream to a new stream
        MemoryStream msZip = new MemoryStream();
        long pos = 0;
        pos = zipfileStream.Position;
        zipfileStream.CopyTo(msZip);
        zipfileStream.Position = pos;
    
        XElement eleSheet = null;
    
        // load sharedStrings xml document for updating
        XElement eleSharedStrings = null;
        using (Stream mainStream = FindEntryFromZipStream(zipfileStream, "xl/sharedStrings.xml"))
        {
            if (mainStream == null)
            {
                return msZip;
            }
            eleSharedStrings = XElement.Load(mainStream);
        }
    
        // distinct sheet xml document for searching
        IEnumerable<ExcelFormattingSetting> noduplicates = ExcelFormattingSettings.Distinct(new ExcelFormattingSettingCompare());
        foreach (ExcelFormattingSetting sheet in noduplicates)
        {
            zipfileStream.Position = 0;
            using (Stream stream = FindEntryFromZipStream(zipfileStream, sheet.SheetName))
            {
                if (stream != null)
                {
                    eleSheet = XElement.Load(stream);
                    // update sharedStrings.xml document
                    UpdateSharedStringsXMLDoc(sheet.SheetName, eleSharedStrings, eleSheet);
                }
            }
        }
    
        // update to stream
        MemoryStream msEntry = new MemoryStream();
        eleSharedStrings.Save(msEntry);
        // The zipStream is expected to contain the complete zipfile to be updated
        ZipFile zipFile = new ZipFile(msZip);
    
        zipFile.BeginUpdate();
    
        // To use the entryStream as a file to be added to the zip,
        // we need to put it into an implementation of IStaticDataSource.
        CustomStaticDataSource sds = new CustomStaticDataSource();
        sds.SetStream(msEntry);
    
        // If an entry of the same name already exists, it will be overwritten; otherwise added.
        zipFile.Add(sds, "xl/sharedStrings.xml");
    
        // Both CommitUpdate and Close must be called.
        zipFile.CommitUpdate();
        // Set this so that Close does not close the memorystream
        zipFile.IsStreamOwner = false;
        zipFile.Close();
    
        return msZip;
    }
    
    
    /// <summary>
    /// Find a specific stream with the entry name of the Excel zip file package.
    /// </summary>
    /// <param name="inputStream">The Excel zip file stream.</param>
    /// <param name="entryName">Entry name in the Excel zip file package.</param>
    /// <returns>Return the sepcific stream.</returns>
    private Stream FindEntryFromZipStream(Stream inputStream, string entryName)
    {
        ZipInputStream zipStream = new ZipInputStream(inputStream);
        ZipEntry zippedFile = zipStream.GetNextEntry();
    
        //Do until no more zipped files left
        while (zippedFile != null)
        {
            byte[] buffer = new byte[2048];
            int bytesRead;
    
            MemoryStream memoryStream = new MemoryStream();
            // read through the compressed data
            while ((bytesRead = zipStream.Read(buffer, 0, buffer.Length)) != 0)
            {
                memoryStream.Write(buffer, 0, bytesRead);
            }
            memoryStream.Position = 0;
    
            if (zippedFile.Name.Equals(entryName))
            {
                return memoryStream;
            }
    
            zippedFile = zipStream.GetNextEntry();
        }
    
        return null;
    }
    
    
    /// <summary>
    /// Update formatted strings to the sharedStrings.xml document in Excel zip file.
    /// </summary>
    /// <param name="sheetName"></param>
    /// <param name="navSharedStrings"></param>
    /// <param name="navSheet"></param>
    private void UpdateSharedStringsXMLDoc(string sheetName, XElement eleSharedStrings, XElement eleSheet)
    {
        XNamespace nsSharedStrings = eleSharedStrings.GetDefaultNamespace();
        XNamespace nsSheet = eleSheet.GetDefaultNamespace();
        int i;
        string sContent;
    
        // update each formatting settings to the sharedStrings xml document
        foreach (ExcelFormattingSetting setting in ExcelFormattingSettings.Where(s => s.SheetName.Equals(sheetName)))
        {
            // find out which si element need to update from the sheet xml document.
            var siIndex = eleSheet.Element(nsSheet + "sheetData")
                                    .Descendants(nsSheet + "c")
                                    .Where(d => d.Attribute("r").Value == setting.ExcelPositionString).FirstOrDefault();
            if (siIndex != null)
            {
                if (int.TryParse(siIndex.Value, out i))
                {
                    var siEntry = eleSharedStrings.Elements(nsSharedStrings + "si").ElementAt(i);
                    if (siEntry != null)
                    {
                        var child = siEntry.Element(nsSharedStrings + "t");
                        if (child != null)
                        {
                            setting.OriginalText = child.Value;
                            sContent = setting.ProcessFormatting(setting.processFormatting);
                            // note, cannot set empty content to the new XElement.
                            if (!sContent.Equals(string.Empty))
                            {
                                XElement newElement = XElement.Parse("<si xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">" + setting.ProcessFormatting(setting.processFormatting) + "</si>");
                                siEntry.ReplaceWith(newElement);
                            }
                        }
                    }
                }
            }
        }
    }

      不能在原Stream上进行操作。首先将文件的Stream Copy出来一份。函数FindEntryFromZipStream()通过entryName来返回ZIP压缩包中指定的部分,返回类型为Stream。将找到的Stream交给UpdateSharedStringsXMLDoc()方法进行修改,该方法会逐一读取所有工作表XML文件,找到需要修改的部分的si节点的序号(该序号存放在工作表XML文件的sheetData->c->r节点中),然后修改sharedStrings.xml文件的内容。注意sharedStrings.xml文件加载的XElement对象只有一个,传入该对象到UpdateSharedStringsXMLDoc()方法进行修改,之后将其存入到一个MemoryStream对象中。接下来的代码便是将该MemoryStream重新加到ZIP包里,使用了类库提供的方法,注意如果指定的entryName在原ZIP包中存在则会直接替换,否则就添加。CustomStaticDataSource类是自定义的一个类,按照要求该类必须继承自IStaticDataSource接口。

    public class CustomStaticDataSource : IStaticDataSource
    {
        private Stream _stream;
        // Implement method from IStaticDataSource
        public Stream GetSource()
        {
            return _stream;
        }
    
        // Call this to provide the memorystream
        public void SetStream(Stream inputStream)
        {
            _stream = inputStream;
            _stream.Position = 0;
        }
    }

      所有的操作都在一个Stream里完成,不需要临时目录存放解压后的文件。这里是上面整个类的完整下载,ExcelFormattingAdjustor.zip

       例子中将所有需要修改样式的单元定义成常量,然后在自定义类ExcelFormattingSetting中存储设置样式的一些参数,如原内容、工作表名称、样式替换字符串,以及如何修改样式的委托等。将所有ExcelFormattingSetting类的实例存放到一个集合里,遍历该集合对所有的设置项进行修改。你可以在ExcelFormattingSetting类的ProcessFormatting()方法中定义如何替换这些样式字符串,如果遇到需要特殊处理的情况,就在该类的实例中定义一个匿名函数,在匿名函数中进行处理。

      更多的应用还在不断尝试中,如果能够提供一个功能丰富且成熟的类库,我们完全可以脱离COM组件来操作Excel,包括创建一个全新的Excel文件、读取数据生成报表、导出数据到Excel文件并自定义样式等等。但所有这一切都应该归功于OpenXML,它使得Office文件从一个自封闭的环境中解脱出来了,基于XML结构的文件是开放的,因此我们做的所有工作其实就是在操作XML,如此简单!不是吗?

  • 相关阅读:
    Java实现 LeetCode 394 字符串解码
    Java实现 LeetCode 394 字符串解码
    Java实现 LeetCode 392 判断子序列
    Java实现 LeetCode 392 判断子序列
    Java实现 LeetCode 392 判断子序列
    Java实现 LeetCode 391 完美矩形
    Java实现 LeetCode 391 完美矩形
    Java实现 LeetCode 391 完美矩形
    Java实现 LeetCode 390 消除游戏
    Java实现 LeetCode 390 消除游戏
  • 原文地址:https://www.cnblogs.com/jaxu/p/2496956.html
Copyright © 2011-2022 走看看