【问题标题】:OpenXML - change Excel cell format (Date and Number) when exporting from DatagridviewOpenXML - 从 Datagridview 导出时更改 Excel 单元格格式(日期和数字)
【发布时间】:2019-04-10 12:15:46
【问题描述】:

我使用 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 的内置样式。

【问题讨论】:

  • 在这里提出了类似的问题 - stackoverflow.com/questions/6033376/…。 OpenXML 实现看起来像往常一样乱七八糟。与该线程中的其他人一样,我强烈建议改用 ClosedXML。
  • @VBDidNothingWrong,我听说过 ClosedXML 并且会尝试一下,虽然很多程序员告诉它它正在吃内存并且存在大数据问题,而这正是我所需要的,我们是谈论 500k-100 万行。
  • 我明白了,我现在将发布 OpenXML 答案。请注意,我在那里的链接线程中尝试了几个答案,但都没有奏效。
  • @VBDidNothingWrong,期待看到您的解决方案。我自己几乎解决了问题,但我无法让数字格式正常工作(日期格式已解决)。我也将很快发布我的解决方案,一个完整的代码,用于解决有关 OpenXML 的更多烦人问题。这确实是一个混乱的解决方案,但我也尝试过 ClosedXML,但也遇到了很多问题。

标签: c# openxml


【解决方案1】:

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

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

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

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

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

正如许多其他人所说,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 默认情况下应该有一些方法。无论如何,我通过使用 LINQ 并在here 的帮助下解决了这个问题。我希望作者不介意,但应该有人大声说出来:)

最后,我的测试结果和与 Interop 相比的优势/劣势。我在 Excel 2016 上测试了 20 万行数据:

互操作

  • 近 3 分钟即可导出数据;

优点:

  • 更容易编码(在我看来)具有许多内置功能,例如(当然)Autofit;
  • 您实际上可以创建尚未完全保存到磁盘的 Excel 文件(对象);

缺点:

  • 与任何其他库(如 OpenXML)相比,速度较慢,但​​我可以将 3 分钟缩短到 2 分钟;
  • 我还注意到大数据消耗大量内存,尽管我的互操作代码已经非常优化;

OpenXML

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

优点:

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

缺点:

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

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

【讨论】:

    【解决方案2】:

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

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

        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;
        }
    

    【讨论】:

    • 很有趣,这与我正在研究并正确解决日期格式的示例相同。但是,请注意数字格式,它不包含“CellValues.Number”数据类型。
    • 数字示例对我有用,虽然字符串没有,我为此添加了一行。使用 Excel 2013 测试
    • 它不适用于数字。它确实将一个值写入 Excel,没有任何错误,但是如果您在 Excel 中签入,您会注意到单元格的格式设置为常规而不是数字。至少在 Office 2016 中是这样。但是我现在解决了所有问题,并将发布完整的一体式代码以及过去一周我一直在努力解决的其他有用的事情。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-07-12
    • 2018-08-24
    • 1970-01-01
    • 2012-01-03
    • 1970-01-01
    • 2011-11-18
    • 2017-02-22
    相关资源
    最近更新 更多