OpenXML - 从 Datagridview 导出时更改 Excel 单元格格式(日期和数字)

OpenXML - change Excel cell format (Date and Number) when exporting from Datagridview

我使用 OpenXML 将 Datagridview 导出到 Excel。如果我导出带有 CellValues.String evertyhing 的单元格,在 Excel 文件中没有任何错误,但我需要的是将所有日期和数字数据正确转换为相应的单元格格式。我尝试使用内置格式(不是自定义格式)来更改单元格格式,但后来我的 Excel 被损坏了。

这是我到目前为止尝试过的方法:

  public void Export_to_Excel(DataGridView dgv, string path)
    {
        using (var workbook = SpreadsheetDocument.Create(path, SpreadsheetDocumentType.Workbook))
        {
            var workbookPart = workbook.AddWorkbookPart();

            workbook.WorkbookPart.Workbook = new Workbook();
            workbook.WorkbookPart.Workbook.Sheets = new Sheets();

            var sheetPart = workbook.WorkbookPart.AddNewPart<WorksheetPart>();
            var sheetData = new SheetData();
            sheetPart.Worksheet = new Worksheet(sheetData);

            Sheets sheets = workbook.WorkbookPart.Workbook.GetFirstChild<Sheets>();
            string relationshipId = workbook.WorkbookPart.GetIdOfPart(sheetPart);

            uint sheetId = 1;
            if (sheets.Elements<Sheet>().Count() > 0)
            {
                sheetId =
                    sheets.Elements<Sheet>().Select(s => s.SheetId.Value).Max() + 1;
            }

            Sheet sheet = new Sheet() { Id = relationshipId, SheetId = sheetId, Name = "List "+ sheetId};
            sheets.Append(sheet);

            Row headerRow = new Row();

            // Construct column names 
            List<String> columns = new List<string>();
            foreach (DataGridViewColumn column in dgv.Columns)
            {
                columns.Add(column.Name);

                Cell cell = new Cell
                {
                    DataType = CellValues.String,
                    CellValue = new CellValue(column.HeaderText)
                };
                headerRow.AppendChild(cell);
            }

            // Add the row values to the excel sheet 
            sheetData.AppendChild(headerRow);

            foreach (DataGridViewRow dsrow in dgv.Rows)
            {
                Row newRow = new Row();
                foreach (String col in columns)
                {

                    CellValues cell_type = new CellValues();
                    string cell_value = "";
                    UInt32 style_index;
                    if (dsrow.Cells[col].ValueType == typeof(decimal)) //numbers
                    {
                        cell_type = CellValues.Number;
                        cell_value = ((decimal)dsrow.Cells[col].Value).ToString();
                        style_index = 4; //should be #,##0.00
                    }
                    else if (dsrow.Cells[col].ValueType == typeof(DateTime)) //dates
                    {
                        cell_type = CellValues.String;
                        cell_value = ((DateTime)dsrow.Cells[col].Value).ToString("dd.mm.yyyy");
                        style_index =0; //should be General
                    }
                    else
                    {
                        cell_type = CellValues.String;
                        cell_value = dsrow.Cells[col].Value.ToString();
                        index_stila = 0; //should be General
                    }

                    Cell cell = new Cell();
                    cell.DataType = new EnumValue<CellValues>(cell_type);
                    cell.CellValue = new CellValue(cell_value);
                    cell.StyleIndex = style_index;
                    newRow.AppendChild(cell);
                }

                sheetData.AppendChild(newRow);
            }
        }
    }

所以基本上,我想要的是正确格式化这些单元格。在上面的代码中,我只尝试了数字格式,但我也需要相同的日期格式。这里还有一个 link 到 OpenXML 的内置样式。

似乎有很多此类问题的答案导致 excel 要求修复。我通常建议人们使用 ClosedXML,但如果 OpenXML 是必须的,那么这里给出的答案是: 确实有效。

这是带有日期的一些额外行的答案,包括时间单元格、数字单元格和字符串单元格。

    private static void TestExcel()
    {
        using (var Spreadsheet = SpreadsheetDocument.Create("C:\Example.xlsx", SpreadsheetDocumentType.Workbook))
        {
            // Create workbook.
            var WorkbookPart = Spreadsheet.AddWorkbookPart();
            var Workbook = WorkbookPart.Workbook = new Workbook();

            // Add Stylesheet.
            var WorkbookStylesPart = WorkbookPart.AddNewPart<WorkbookStylesPart>();
            WorkbookStylesPart.Stylesheet = GetStylesheet();
            WorkbookStylesPart.Stylesheet.Save();

            // Create worksheet.
            var WorksheetPart = Spreadsheet.WorkbookPart.AddNewPart<WorksheetPart>();
            var Worksheet = WorksheetPart.Worksheet = new Worksheet();

            // Add data to worksheet.
            var SheetData = Worksheet.AppendChild(new SheetData());
            SheetData.AppendChild(new Row(
                //Date example. Will show as dd/MM/yyyy. 
                new Cell() { CellValue = new CellValue(DateTime.Today.ToOADate().ToString(CultureInfo.InvariantCulture)), StyleIndex = 1 },

                //Date Time example. Will show as dd/MM/yyyy HH:mm
                new Cell() { CellValue = new CellValue(DateTime.Now.ToOADate().ToString(CultureInfo.InvariantCulture)), StyleIndex = 2 },

                //Number example
                new Cell() { CellValue = new CellValue(123.23d.ToString(CultureInfo.InvariantCulture)), StyleIndex = 0 },

                //String example
                new Cell() { CellValue = new CellValue("Test string"), DataType = CellValues.String }

            ));

            // Link worksheet to workbook.
            var Sheets = Workbook.AppendChild(new Sheets());
            Sheets.AppendChild(new Sheet()
            {
                Id = WorkbookPart.GetIdOfPart(WorksheetPart),
                SheetId = (uint)(Sheets.Count() + 1),
                Name = "Example"
            });

            Workbook.Save();
        }
    }

    private static Stylesheet GetStylesheet()
    {
        var StyleSheet = new Stylesheet();

        // Create "fonts" node.
        var Fonts = new Fonts();
        Fonts.Append(new Font()
        {
            FontName = new FontName() { Val = "Calibri" },
            FontSize = new FontSize() { Val = 11 },
            FontFamilyNumbering = new FontFamilyNumbering() { Val = 2 },
        });

        Fonts.Count = (uint)Fonts.ChildElements.Count;

        // Create "fills" node.
        var Fills = new Fills();
        Fills.Append(new Fill()
        {
            PatternFill = new PatternFill() { PatternType = PatternValues.None }
        });
        Fills.Append(new Fill()
        {
            PatternFill = new PatternFill() { PatternType = PatternValues.Gray125 }
        });

        Fills.Count = (uint)Fills.ChildElements.Count;

        // Create "borders" node.
        var Borders = new Borders();
        Borders.Append(new Border()
        {
            LeftBorder = new LeftBorder(),
            RightBorder = new RightBorder(),
            TopBorder = new TopBorder(),
            BottomBorder = new BottomBorder(),
            DiagonalBorder = new DiagonalBorder()
        });

        Borders.Count = (uint)Borders.ChildElements.Count;

        // Create "cellStyleXfs" node.
        var CellStyleFormats = new CellStyleFormats();
        CellStyleFormats.Append(new CellFormat()
        {
            NumberFormatId = 0,
            FontId = 0,
            FillId = 0,
            BorderId = 0
        });

        CellStyleFormats.Count = (uint)CellStyleFormats.ChildElements.Count;

        // Create "cellXfs" node.
        var CellFormats = new CellFormats();

        // StyleIndex = 0, A default style that works for most things (But not strings? )
        CellFormats.Append(new CellFormat()
        {
            BorderId = 0,
            FillId = 0,
            FontId = 0,
            NumberFormatId = 0,
            FormatId = 0,
            ApplyNumberFormat = true
        });

        // StyleIndex = 1, A style that works for DateTime (just the date)
        CellFormats.Append(new CellFormat()
        {
            BorderId = 0,
            FillId = 0,
            FontId = 0,
            NumberFormatId = 14, //Date
            FormatId = 0,
            ApplyNumberFormat = true
        });

        // StyleIndex = 2, A style that works for DateTime (Date and Time)
        CellFormats.Append(new CellFormat()
        {
            BorderId = 0,
            FillId = 0,
            FontId = 0,
            NumberFormatId = 22, //Date Time
            FormatId = 0,
            ApplyNumberFormat = true
        });

        CellFormats.Count = (uint)CellFormats.ChildElements.Count;

        // Create "cellStyles" node.
        var CellStyles = new CellStyles();
        CellStyles.Append(new CellStyle()
        {
            Name = "Normal",
            FormatId = 0,
            BuiltinId = 0
        });
        CellStyles.Count = (uint)CellStyles.ChildElements.Count;

        // Append all nodes in order.
        StyleSheet.Append(Fonts);
        StyleSheet.Append(Fills);
        StyleSheet.Append(Borders);
        StyleSheet.Append(CellStyleFormats);
        StyleSheet.Append(CellFormats);
        StyleSheet.Append(CellStyles);

        return StyleSheet;
    }

我解决了上面的问题。我必须说,使用 OpenXML 有点令人沮丧,但我对最终结果很满意。

我决定 - 基于许多 OpenXML 主题 - 通过提供完整可用的代码来扩展答案,而不仅仅是我通常在许多网站上遇到的示例。

我的基本要求是将 Datagridview 数据导出到 Excel 文件,具有正确的单元格格式和比我们当前使用的 Interop 解决方案更快的导出速度。下面的代码也可以与 Datatable 或 Dataset 一起使用,只需稍作修改。我还添加了一些其他功能,在我看来应该记录在案,因为这是大多数程序员在 Excel 中需要的功能,但不幸的是,它们不是。

我不会深入研究所有内容,因为做所有这些我已经有些头疼了,所以让我们切入正题。下面完整代码的结果是 Excel 文件,其中包含从 Datagridview 导出的数据和 :

  • 列名与 Datagridview 相同 headers & 粗体;
  • 将默认字体 »Calibri« 更改为 »Arial«;
  • 基于数据表(日期、数字和字符串)中具有所需格式的实际数据的单元格格式;
  • 保存文件对话框提示;
  • 列自动调整;

正如许多其他人所说,OpenXML 中的顺序非常重要。这几乎适用于所有情况——当您创建文档或为其设置样式时。所以你在这里看到的一切在 Office 2016 中对我来说都很好,但是如果你进行一些行混合,你最终会非常快速地在 Excel 中出现一些奇怪的错误......正如所承诺的,这是我的完整代码:

public void Export_to_Excel(DataGridView dgv, string file_name)
{
  String file_path= Environment.GetFolderPath(Environment.SpecialFolder.Desktop).ToString() + "\" +file_name + ".xlsx";

  SaveFileDialog saveFileDialog = new SaveFileDialog();
  saveFileDialog.InitialDirectory = Convert.ToString(Environment.SpecialFolder.Desktop);
  saveFileDialog.Filter = "Excel Workbook |*.xlsx";
  saveFileDialog.Title = "Save as";
  saveFileDialog.FileName = file_name;
  if (saveFileDialog.ShowDialog() == DialogResult.OK)
  {
    file_path = saveFileDialog.FileName;                  
  }
  else
  {
    return;
  }

 using (var workbook = SpreadsheetDocument.Create(file_path, SpreadsheetDocumentType.Workbook))
 {
    var workbookPart = workbook.AddWorkbookPart();
    workbook.WorkbookPart.Workbook = new Workbook();
    workbook.WorkbookPart.Workbook.Sheets = new Sheets();

    var sheetPart = workbook.WorkbookPart.AddNewPart<WorksheetPart>();
    var sheetData = new SheetData();

     //Autofit comes first – we calculate width of columns based on data
     sheetPart.Worksheet = new Worksheet();
     sheetPart.Worksheet.Append(AutoFit_Columns(dgv));
     sheetPart.Worksheet.Append(sheetData);

     //Adding styles to worksheet
     Worksheet_Style(workbook);

     Sheets sheets = workbook.WorkbookPart.Workbook.GetFirstChild<Sheets>();
     string relationshipId = workbook.WorkbookPart.GetIdOfPart(sheetPart);

     uint sheetId = 1;
     if (sheets.Elements<Sheet>().Count() > 0)
     {
       sheetId = sheets.Elements<Sheet>().Select(s => s.SheetId.Value).Max() + 1;
     }

     Sheet sheet = new Sheet() { Id = relationshipId, SheetId = sheetId, Name = "List " + sheetId };
      sheets.Append(sheet);

      Row headerRow = new Row(); //Adding column headers

      for (int col = 0; col < dgv.ColumnCount; col++)
      {
         Cell cell = new Cell
         {
             DataType = CellValues.String,
             CellValue = new CellValue(dgv.Columns[col].HeaderText),
             StyleIndex = 1// bold font
         };
         headerRow.AppendChild(cell);
       }

       // Add the row values to the excel sheet 
       sheetData.AppendChild(headerRow);

       for (int row = 0; row < dgv.RowCount; row++)
       {
          Row newRow = new Row();

          for (int col = 0; col < dgv.ColumnCount; col++)
          {
              Cell cell = new Cell();

              //Checking types of data
              // I had problems here with Number format, I just can't set It to a
              // Datatype=CellValues.Number. If someone knows answer please let me know. However, Date format strangely works fine with Number datatype ?
              // Also important – whatever format you define in creating stylesheets, you have to insert value of same kind in string here – for CellValues !
              // I used cell formating as I needed, for something else just change Worksheet_Style method to your needs
              if (dgv.Columns[col].ValueType == typeof(decimal)) //numbers
              {
                 cell.DataType = new EnumValue<CellValues>(CellValues.String);
                 cell.CellValue = new CellValue(((decimal)dgv.Rows[row].Cells[col].Value).ToString("#,##0.00"));
                  cell.StyleIndex = 3;
               }
               else if (dgv.Columns[col].ValueType == typeof(DateTime)) //dates
               {
                  cell.DataType = new EnumValue<CellValues>(CellValues.Number);
                  cell.CellValue = new CellValue(((DateTime)dgv.Rows[row].Cells[col].Value).ToOADate().ToString(CultureInfo.InvariantCulture));
                  cell.StyleIndex = 2;
                }
                Else // strings
                {
                  cell.DataType = new EnumValue<CellValues>(CellValues.String);
                  cell.CellValue = new CellValue(dgv.Rows[row].Cells[col].Value.ToString());
                  cell.StyleIndex = 0;
          }
                 newRow.AppendChild(cell);
                }
                    sheetData.AppendChild(newRow);
                }
            }

 }

        private static WorkbookStylesPart Worksheet_Style (SpreadsheetDocument document)
        {
            WorkbookStylesPart create_style = document.WorkbookPart.AddNewPart<WorkbookStylesPart>();
            Stylesheet workbookstylesheet = new Stylesheet();

            DocumentFormat.OpenXml.Spreadsheet.Font font0 = new DocumentFormat.OpenXml.Spreadsheet.Font(); // Default font
            FontName arial = new FontName() { Val = "Arial" };
            FontSize size = new FontSize() { Val = 10 };
            font0.Append(arial);
            font0.Append(size);


            DocumentFormat.OpenXml.Spreadsheet.Font font1 = new DocumentFormat.OpenXml.Spreadsheet.Font(); // Bold font
            Bold bold = new Bold();
            font1.Append(bold);

            // Append both fonts
            Fonts fonts = new Fonts();     
            fonts.Append(font0);
            fonts.Append(font1);

            //Append fills - a must, in my case just default
            Fill fill0 = new Fill();        
            Fills fills = new Fills();      
            fills.Append(fill0);

            // Append borders - a must, in my case just default
            Border border0 = new Border();     // Default border
            Borders borders = new Borders();    
            borders.Append(border0);

            // CellFormats
            CellFormats cellformats = new CellFormats();

            CellFormat cellformat0 = new CellFormat() { FontId = 0, FillId = 0, BorderId = 0 }; // Default style : Mandatory | Style ID =0
            CellFormat bolded_format = new CellFormat() { FontId = 1 };  // Style with Bold text ; Style ID = 1
            CellFormat date_format = new CellFormat() { BorderId = 0, FillId = 0, FontId = 0, NumberFormatId = 14, FormatId = 0, ApplyNumberFormat = true };
            CellFormat number_format = new CellFormat() { BorderId = 0, FillId = 0, FontId = 0, NumberFormatId = 4, FormatId = 0, ApplyNumberFormat = true }; // format like "#,##0.00"

            cellformats.Append(cellformat0);
            cellformats.Append(bolded_format);
            cellformats.Append(date_format);
            cellformats.Append(number_format);

            // Append everyting to stylesheet  - Preserve the ORDER !
            workbookstylesheet.Append(fonts);
            workbookstylesheet.Append(fills);
            workbookstylesheet.Append(borders);
            workbookstylesheet.Append(cellformats);

            //Save style for finish
            create_style.Stylesheet = workbookstylesheet;
            create_style.Stylesheet.Save();

            return create_style;
        }


        private Columns AutoFit_Columns(DataGridView dgv)
        {
            Columns cols = new Columns();
            int Excel_column=0;

            DataTable dt = new DataTable();
            dt = (DataTable)dgv.DataSource;

            for (int col = 0; col < dgv.ColumnCount; col++)
            {
                double max_width = 14.5f; // something like default Excel width, I'm not sure about this

                //We search for longest string in each column and convert that into double to get desired width 
                string longest_string = dt.AsEnumerable()
                     .Select(row => row[col].ToString())
                     .OrderByDescending(st => st.Length).FirstOrDefault();

                double cell_width = GetWidth(new System.Drawing.Font("Arial", 10), longest_string);

                if (cell_width > max_width)
                {
                    max_width = cell_width;
                }

                if (col == 0) //first column of Datagridview is index 0, but there is no 0 index of column in Excel, careful with that !!!
                {
                    Excel_column = 1;
                }

                //now append column to worksheet, calculations done
                Column c = new Column() { Min = Convert.ToUInt32(Excel_column), Max = Convert.ToUInt32(Excel_column), Width = max_width, CustomWidth = true };
                cols.Append(c);

                Excel_column++;
            }
            return cols;
        }

        private static double GetWidth(System.Drawing.Font stringFont, string text)
        {
            // This formula calculates width. For better desired outputs try to change 0.5M to something else

            Size textSize = TextRenderer.MeasureText(text, stringFont);
            double width = (double)(((textSize.Width / (double)7) * 256) - (128 / 7)) / 256;
            width = (double)decimal.Round((decimal)width + 0.5M, 2);

            return width;
        }

方法,在我的例子中,可以很容易地调用 .dll,例如:

Export_to_Excel(my_dgv, »test_file«)

代码中一些内容的简短说明:

1.) 样式: 有很多方法可供选择,但这对我来说是最简单的方法。当您需要更难的东西时,请不要忘记这里的顺序也很重要。并且附加字体、填充和边框是必要的。

2.) Autofit: 我无法相信为什么没有对它进行完整的记录,我的观点是 OpenXML 应该有一些默认的方法。不管怎样,在 here 的帮助下,我通过使用 LINQ 解决了这个问题。我希望作者不介意,但有人应该大声说出来:)

最后,我的测试结果和 advantages/disadvantages 与 Interop 的比较。我在 Excel 2016 年测试了 20 万行数据:

互操作

  • 将近 3 分钟导出数据;

优点:

  • 更简单的编码(在我看来)有很多 built-in 功能,例如(当然)Autofit;
  • 您实际上可以创建尚未保存到磁盘的 Excel 文件 (object);

缺点:

  • 与 OpenXML 等任何其他库相比速度较慢,但​​我可能会将 3 分钟减少到 2 分钟;
  • 我还注意到大数据会消耗大量内存,即使我的 Interop 代码已经过优化;

OpenXML

  • 在 20 秒内导出数据(具有自动调整功能和所有样式);

优点:

  • 比 Interop 快得多,我认为我的 »垃圾« 代码可以更优化(如果您愿意,您可以提供帮助);

缺点:

  • 编码,不明显? :)
  • 虽然 OpenXML 提供了两种方法 a.k.a,但内存消耗高于 Interop。 SAX 或 DOM 方法。 SAX 甚至比提供的代码更快,几乎没有内存消耗如果您直接从 DataReaderExcel 写入数据,但是编码花了我很多时间;

我希望没有人会生气,因为我实际上所做的是将许多网站的点点滴滴放入实际有用的东西中,而不是编写没人理解的复杂示例。如果有人愿意改进以上任何内容,我将不胜感激。我并不完美,更多的人在一起通常最终会为每个人形成更好的解决方案:)