OpenXml和Excel单元格日期格式

22

我正在尝试使用OpenXML创建一个xlsx格式的Excel文件,因为我需要在Web服务器上使用它。

我可以填充工作表中的值,但是我无法将经典日期格式设置在单元格中。

下面是使用DocumentFormat.OpenXml和WindowsBase引用进行的快速测试。

class Program
{
    static void Main(string[] args)
    {
        BuildExel(@"C:\test.xlsx");
    }

    public static void BuildExel(string fileName)
    {
        using (SpreadsheetDocument myWorkbook =
               SpreadsheetDocument.Create(fileName,
               SpreadsheetDocumentType.Workbook))
        {
            // Workbook Part
            WorkbookPart workbookPart = myWorkbook.AddWorkbookPart();
            var worksheetPart = workbookPart.AddNewPart<WorksheetPart>();
            string relId = workbookPart.GetIdOfPart(worksheetPart);

            // File Version
            var fileVersion = new FileVersion { ApplicationName = "Microsoft Office Excel" };

            // Style Part
            WorkbookStylesPart wbsp = workbookPart.AddNewPart<WorkbookStylesPart>();
            wbsp.Stylesheet = CreateStylesheet();
            wbsp.Stylesheet.Save();

            // Sheets
            var sheets = new Sheets();
            var sheet = new Sheet { Name = "sheetName", SheetId = 1, Id = relId };
            sheets.Append(sheet);

            // Data
            SheetData sheetData = new SheetData(CreateSheetData1());

            // Add the parts to the workbook and save
            var workbook = new Workbook();
            workbook.Append(fileVersion);
            workbook.Append(sheets);
            var worksheet = new Worksheet();
            worksheet.Append(sheetData);
            worksheetPart.Worksheet = worksheet;
            worksheetPart.Worksheet.Save();
            myWorkbook.WorkbookPart.Workbook = workbook;
            myWorkbook.WorkbookPart.Workbook.Save();
            myWorkbook.Close();
        }
    }

    private static Stylesheet CreateStylesheet()
    {
        Stylesheet ss = new Stylesheet();

        var nfs = new NumberingFormats();
        var nformatDateTime = new NumberingFormat
        {
            NumberFormatId = UInt32Value.FromUInt32(1),
            FormatCode = StringValue.FromString("dd/mm/yyyy")
        };
        nfs.Append(nformatDateTime);
        ss.Append(nfs);

        return ss;
    }

    private static List<OpenXmlElement> CreateSheetData1()
    {
        List<OpenXmlElement> elements = new List<OpenXmlElement>();

        var row = new Row();

        // Line 1
        Cell[] cells = new Cell[2];

        Cell cell1 = new Cell();
        cell1.DataType = CellValues.InlineString;
        cell1.InlineString = new InlineString { Text = new Text { Text = "Daniel" } };
        cells[0] = cell1;

        Cell cell2 = new Cell();
        cell2.DataType = CellValues.Number;
        cell2.CellValue = new CellValue((50.5).ToString());
        cells[1] = cell2;

        row.Append(cells);
        elements.Add(row);

        // Line 2
        row = new Row();
        cells = new Cell[1];
        Cell cell3 = new Cell();
        cell3.DataType = CellValues.Date;
        cell3.CellValue = new CellValue(DateTime.Now.ToOADate().ToString());
        cell3.StyleIndex = 1; // <= here I try to apply the style...
        cells[0] = cell3;

        row.Append(cells);
        elements.Add(row);

        return elements;
    }
执行的代码创建了Excel文档。但是当我尝试打开该文档时,我收到以下消息:“Excel在 'test.xlsx' 中发现无法读取的内容。你想恢复此工作簿的内容吗?如果你信任此工作簿的来源,请单击 是。” 如果我删除这一行:
cell3.StyleIndex = 1;

我可以打开这个文件,但是日期没有格式化,只有日期的数字。

感谢您帮忙格式化日期。

11个回答

7
这篇博客对我非常有帮助:http://polymathprogrammer.com/2009/11/09/how-to-create-stylesheet-in-excel-open-xml/ 我的问题是想要在样式表中添加NumberingFormats而不是完全添加一个新的样式表。如果你也有这个需求,可以使用:
Stylesheet.InsertAt<NumberingFormats>(new NumberingFormats(), 0);

相比之下
Stylesheet.AppendChild<NumberingFormats>(new NumberingFormats(), 0);

意外之喜,命令很重要。

+1 这让我抓狂了,所有现有的互联网示例都假设从一个空样式表开始,并使用AppenChild()首先添加NumberingFormats,但我是在现有模板中添加,没有现有的NumberFormats,所以它会将numFmts部分添加到样式表的末尾,这是不好的。最终我在找到你的九年级答案之前偶然发现了解决方案。 - controlbox

6
另一个非常推荐的选择是:https://github.com/closedxml/closedxml
在试图从网络上散乱的代码片段(包括StackOverFlow)中构建自己的类之后,我找到了以上提到的库,并在短短几分钟内就得到了一个完全可用的Excel文件。
为了让任何有兴趣完成它的人受益,我将我的尝试粘贴在下面。这部分代码只完成了一半,并且对于创建日期和字符串单元格存在问题。
在尝试使用这个类之前,请先下载closedXML并首先尝试使用它。
请自行考虑。
    /// <summary>
    /// This class allows for the easy creation of a simple Excel document who's sole purpose is to contain some export data.
    /// The document is created using OpenXML.
    /// </summary>
    internal class SimpleExcelDocument : IDisposable
    {
        SheetData sheetData;

        /// <summary>
        /// Constructor is nothing special because the work is done at export.
        /// </summary>
        internal SimpleExcelDocument()
        {
            sheetData = new SheetData();
        }

        #region Get Cell Reference
        public Cell GetCell(string fullAddress)
        {
            return sheetData.Descendants<Cell>().Where(c => c.CellReference == fullAddress).FirstOrDefault();
        }
        public Cell GetCell(uint rowId, uint columnId, bool autoCreate)
        {
            return GetCell(getColumnName(columnId), rowId, autoCreate);
        }
        public Cell GetCell(string columnName, uint rowId, bool autoCreate)
        {
            return getCell(sheetData, columnName, rowId, autoCreate);
        }
        #endregion

        #region Get Cell Contents
        // See: http://msdn.microsoft.com/en-us/library/ff921204.aspx
        // 
        #endregion


        #region Set Cell Contents
        public void SetValue(uint rowId, uint columnId, bool value)
        {
            Cell cell = GetCell(rowId, columnId, true);
            cell.DataType = CellValues.Boolean;
            cell.CellValue = new CellValue(BooleanValue.FromBoolean(value));
        }
        public void SetValue(uint rowId, uint columnId, double value)
        {
            Cell cell = GetCell(rowId, columnId, true);
            cell.DataType = CellValues.Number;
            cell.CellValue = new CellValue(DoubleValue.FromDouble(value));
        }
        public void SetValue(uint rowId, uint columnId, Int64 value)
        {
            Cell cell = GetCell(rowId, columnId, true);
            cell.DataType = CellValues.Number;
            cell.CellValue = new CellValue(IntegerValue.FromInt64(value));
        }
        public void SetValue(uint rowId, uint columnId, DateTime value)
        {
            Cell cell = GetCell(rowId, columnId, true);
            //cell.DataType = CellValues.Date;
            cell.CellValue = new CellValue(value.ToOADate().ToString());
            cell.StyleIndex = 1;
        }
        public void SetValue(uint rowId, uint columnId, string value)
        {
            Cell cell = GetCell(rowId, columnId, true);
            cell.InlineString = new InlineString(value.ToString());
            cell.DataType = CellValues.InlineString;
        }
        public void SetValue(uint rowId, uint columnId, object value)
        {             
            bool boolResult;
            Int64 intResult;
            DateTime dateResult;
            Double doubleResult;
            string stringResult = value.ToString();

            if (bool.TryParse(stringResult, out boolResult))
            {
                SetValue(rowId, columnId, boolResult);
            }
            else if (DateTime.TryParse(stringResult, out dateResult))
            {
                SetValue(rowId, columnId,dateResult);
            }
            else if (Int64.TryParse(stringResult, out intResult))
            {
                SetValue(rowId, columnId, intResult);
            }
            else if (Double.TryParse(stringResult, out doubleResult))
            {
                SetValue(rowId, columnId, doubleResult);
            }
            else
            {
                // Just assume that it is a plain string.
                SetValue(rowId, columnId, stringResult);
            }
        }
        #endregion

        public SheetData ExportAsSheetData()
        {
            return sheetData;
        }

        public void ExportAsXLSXStream(Stream outputStream)
        {
            // See: http://blogs.msdn.com/b/chrisquon/archive/2009/07/22/creating-an-excel-spreadsheet-from-scratch-using-openxml.aspx for some ideas...
            // See: http://stackoverflow.com/questions/1271520/opening-xlsx-in-office-2003

            using (SpreadsheetDocument package = SpreadsheetDocument.Create(outputStream, SpreadsheetDocumentType.Workbook))
            {
                // Setup the basics of a spreadsheet document.
                package.AddWorkbookPart();
                package.WorkbookPart.Workbook = new Workbook();
                WorksheetPart workSheetPart = package.WorkbookPart.AddNewPart<WorksheetPart>();
                workSheetPart.Worksheet = new Worksheet(sheetData);
                workSheetPart.Worksheet.Save();

                // create the worksheet to workbook relation
                package.WorkbookPart.Workbook.AppendChild(new Sheets());
                Sheet sheet = new Sheet { 
                    Id = package.WorkbookPart.GetIdOfPart(workSheetPart), 
                    SheetId = 1, 
                    Name = "Sheet 1" 
                };
                package.WorkbookPart.Workbook.GetFirstChild<Sheets>().AppendChild<Sheet>(sheet);
                package.WorkbookPart.Workbook.Save();
                package.Close();
            }
        }

        #region Internal Methods
        private static string getColumnName(uint columnId)
        {
            if (columnId < 1)
            {
                throw new Exception("The column # can't be less then 1.");
            }
            columnId--;
            if (columnId >= 0 && columnId < 26)
                return ((char)('A' + columnId)).ToString();
            else if (columnId > 25)
                return getColumnName(columnId / 26) + getColumnName(columnId % 26 + 1);
            else
                throw new Exception("Invalid Column #" + (columnId + 1).ToString());
        }

        // Given a worksheet, a column name, and a row index, 
        // gets the cell at the specified column 
        private static Cell getCell(SheetData worksheet,
                  string columnName, uint rowIndex, bool autoCreate)
        {
            Row row = getRow(worksheet, rowIndex, autoCreate);

            if (row == null)
                return null;

            Cell foundCell = row.Elements<Cell>().Where(c => string.Compare
                   (c.CellReference.Value, columnName +
                   rowIndex, true) == 0).FirstOrDefault();

            if (foundCell == null && autoCreate)
            {
                foundCell = new Cell();
                foundCell.CellReference = columnName;
                row.AppendChild(foundCell);
            }
            return foundCell;
        }


        // Given a worksheet and a row index, return the row.
        // See: http://msdn.microsoft.com/en-us/library/bb508943(v=office.12).aspx#Y2142
        private static Row getRow(SheetData worksheet, uint rowIndex, bool autoCreate)
        {
            if (rowIndex < 1)
            {
                throw new Exception("The row # can't be less then 1.");
            }

            Row foundRow = worksheet.Elements<Row>().Where(r => r.RowIndex == rowIndex).FirstOrDefault();

            if (foundRow == null && autoCreate)
            {
                foundRow = new Row();
                foundRow.RowIndex = rowIndex;
                worksheet.AppendChild(foundRow);
            }
            return foundRow;
        } 
        #endregion
        #region IDisposable Stuff
        private bool _disposed;
        //private bool _transactionComplete;

        /// <summary>
        /// This will dispose of any open resources.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);

            // Use SupressFinalize in case a subclass
            // of this type implements a finalizer.
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            // If you need thread safety, use a lock around these 
            // operations, as well as in your methods that use the resource.
            if (!_disposed)
            {
                if (disposing)
                {
                    //if (!_transactionComplete)
                    //    Commit();
                }

                // Indicate that the instance has been disposed.
                //_transaction = null;
                _disposed = true;
            }
        }
        #endregion
    }

6

10
如果你正在处理大量的数据,问题就出现了。我们使用这个库时,发现在处理大型数据集时存在内存问题。 - Phil Cooper
1
这并不是问题的真正回答。但是非常感谢您的建议!我能够用五行代码立即替换数天使用OpenXML所遇到的困难,实现所需功能。 - Andrew Bennett

3

以下是如何在单元格上应用自定义日期格式。 首先,我们必须查找或创建工作簿样式表中的格式:

// get the stylesheet from the current sheet    
var stylesheet = spreadsheetDoc.WorkbookPart.WorkbookStylesPart.Stylesheet;
// cell formats are stored in the stylesheet's NumberingFormats
var numberingFormats = stylesheet.NumberingFormats;

// cell format string               
const string dateFormatCode = "dd/mm/yyyy";
// first check if we find an existing NumberingFormat with the desired formatcode
var dateFormat = numberingFormats.OfType<NumberingFormat>().FirstOrDefault(format => format.FormatCode == dateFormatCode);
// if not: create it
if (dateFormat == null)
{
    dateFormat = new NumberingFormat
                {
                    NumberFormatId = UInt32Value.FromUInt32(164),  // Built-in number formats are numbered 0 - 163. Custom formats must start at 164.
                    FormatCode = StringValue.FromString(dateFormatCode)
                };
numberingFormats.AppendChild(dateFormat);
// we have to increase the count attribute manually ?!?
numberingFormats.Count = Convert.ToUInt32(numberingFormats.Count());
// save the new NumberFormat in the stylesheet
stylesheet.Save();
}
// get the (1-based) index of the dateformat
var dateStyleIndex = numberingFormats.ToList().IndexOf(dateFormat) + 1;

然后,我们可以使用已解析的styleindex将格式应用于单元格:
cell.StyleIndex = Convert.ToUInt32(dateStyleIndex);

2
当我打开电子表格时,这会导致“修复记录”警告。 - Adam Garner

3

尝试了很多方法后,我发现需要使用.ToOADate()和CellValues.Number以及cell.StyleIndex = 4才能成功。此外,所有模板日期列都必须按照默认日期格式进行格式化,才能使日期可以作为筛选条件。如果没有这些操作,打开Excel文件时会出现错误,或者日期值会显示为数字。

using DocumentFormat.OpenXml.Packaging;  
using DocumentFormat.OpenXml.Spreadsheet;  

//  IMPORTANT! All template date columns MUST be formatted to the default date style for the dates to be filterable as dates  
Cell cell = new Cell();  
dataMember = dataMember.ToOADate().ToString();  //OA Date needed to export number as Date  
cell.DataType = CellValues.Number;                
cell.CellValue = new CellValue(dataMember);  
cell.StyleIndex = 4;                            // Date format: M/d/yyyy  

3

我相信你的问题在于NumberFormatId。内置的数字格式从0到163编号,自定义格式必须从164开始。


如果我更改NumberFormatId和StyleIndex(cell3)的值为164,仍然会出现错误。StyleIndex必须具有与NumberingFormatId相同的值吗? - Dan
@daner06,不是的,StyleIndex 是指单元格样式索引。 - Samuel Neff
1
这些NumberFormatId的值是否有列表可以查阅? - David C
1
@DavidC 是的,它在ECMA-376第1部分第18.8.30节(numFmt)的1767页。http://www.ecma-international.org/publications/standards/Ecma-376.htm - Frison Alexander

1

你可以在如何判断Office Open XML单元格是否包含日期/时间值?中找到答案。

关键在于单元格的StyleIndex(s属性)实际上是您电子表格样式部分中单元格样式(XF元素)列表中的索引。每个样式将指向Samuel提到的预定义数字格式ID。如果我没记错,你要找的数字格式ID可能是14或15。


除非在<cellxfs count="?"></cellxfs>内有14或15个<xf />,其中? > 14或15,否则您将收到此消息。如果第14或15个单元格样式实际上与您要查找的格式不相等,则会出现意外结果。applyNumberFormat="1",numFmtId="X"和<numFmt numFmtId="X" formatCode="some formatting for datetime">,其中X是大于163的数字,应该有所帮助。我从未成功地使用自定义格式,而不是将其明确添加到xlsx文件中。 - TamusJRoyce

1

1
我希望以下链接能对未来的访问者有所帮助。
首先,获取标准文档
ECMA-376第4版第1部分是最有用的文档。与此问题相关的章节包括:

18.8.30

18.8.31 (这个狗屎的语义)

18.8.45 (Excel理解的样式定义)

L.2.7.3.6 (如何引用样式)


1

我曾经遇到相同的问题,最终写了一个自己的导出Excel的工具。虽然代码可以解决这个问题,但是你最好使用整个导出工具,它快速且可以对单元格进行大量格式设置。你可以在以下链接中查看:

https://openxmlexporttoexcel.codeplex.com/

我希望它有所帮助。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接