【问题标题】:Get Formatted Cell Values efficiently有效地获取格式化的单元格值
【发布时间】:2015-07-11 16:38:31
【问题描述】:

我希望能够从 Excel 中高效地检索格式化单元格值的多维数组。当我说格式化值时,我的意思是我希望得到它们与它们在 Excel 中显示的完全一致,并应用了所有单元格 NumberFormat。

Range.Value 和 Range.Value2 属性非常适合将大量单元格的单元格值检索到多维数组中。但这些是实际的单元格值(至少 Range.Value2 是这样,我不太确定 Range.Value 对某些值做了什么)。

如果我想检索单元格中显示的实际文本,我可以使用 Range.Text 属性。这有一些警告。首先,您需要自动调整单元格,否则如果不是所有文本都以当前单元格宽度可见,您可能会得到类似 #### 的内容。其次,Range.Text 一次不能用于多个单元格,因此您必须循环遍历该范围内的所有单元格,这对于大型数据集可能非常慢。

我尝试的另一种方法是将范围复制到剪贴板,然后将剪贴板文本解析为制表符分隔的数据流,并将其传输到多维数组中。这似乎工作得很好,虽然它比获取 Range.Value2 慢,但对于大型数据集来说,它比获取 Range.Text 快得多。但是,我不喜欢使用系统剪贴板的想法。如果这是一个非常长的操作,需要 60 秒,并且在该操作运行时,用户可能会决定切换到另一个应用程序,并且会非常不高兴地发现他们的剪贴板要么不起作用,要么里面有神秘的数据。

有没有一种方法可以有效地将格式化的单元格值检索到多维数组中?

我添加了一些从 VSTO 应用程序中的几个功能区按钮运行的示例代码。第一个设置了一些良好的测试值和数字格式,第二个按钮将显示在 MessageBox 中使用这些方法之一检索它们时的样子。

我系统上的示例输出是(由于区域设置,您的系统可能会有所不同):

Output using Range.Value
1/25/2008 3:19:32 PM    5.12345
2008-01-25 15:19:32 0.456

Output using Range.Value2
39472.6385648148    5.12345
2008-01-25 15:19:32 0.456

Output using Clipboard Copy
1/25/2008 15:19 5.12
2008-01-25 15:19:32 45.60%

Output using Range.Text and Autofit
1/25/2008 15:19 5.12
2008-01-25 15:19:32 45.60%

Range.Text 和 Clipboard 方法产生正确的输出,但如上所述,它们都有问题:Range.Text 很慢,Clipboard 是不好的做法。

    private void SetSampleValues()
    {
        var sheet = (Microsoft.Office.Interop.Excel.Worksheet) Globals.ThisAddIn.Application.ActiveSheet;

        sheet.Cells.ClearContents();
        sheet.Cells.ClearFormats();

        var range = sheet.Range["A1"];

        range.NumberFormat = "General";
        range.Value2 = "2008-01-25 15:19:32";

        range = sheet.Range["A2"];
        range.NumberFormat = "@";
        range.Value2 = "2008-01-25 15:19:32";

        range = sheet.Range["B1"];
        range.NumberFormat = "0.00";
        range.Value2 = "5.12345";

        range = sheet.Range["B2"];
        range.NumberFormat = "0.00%";
        range.Value2 = ".456";
    }

    private string ArrayToString(ref object[,] vals)
    {

        int dim1Start = vals.GetLowerBound(0); //Excel Interop will return index-1 based arrays instead of index-0 based
        int dim1End = vals.GetUpperBound(0);
        int dim2Start = vals.GetLowerBound(1);
        int dim2End = vals.GetUpperBound(1);

        var sb = new StringBuilder();
        for (int i = dim1Start; i <= dim1End; i++)
        {
            for (int j = dim2Start; j <= dim2End; j++)
            {
                sb.Append(vals[i, j]);
                if (j != dim2End)
                    sb.Append("\t");
            }
            sb.Append("\n");
        }
        return sb.ToString();
    }

    private void GetCellValues()
    {
        var sheet = (Microsoft.Office.Interop.Excel.Worksheet)Globals.ThisAddIn.Application.ActiveSheet;

        var usedRange = sheet.UsedRange;

        var sb = new StringBuilder();

        sb.Append("Output using Range.Value\n");
        var vals = (object [,]) usedRange.Value; //1-based array
        sb.Append(ArrayToString(ref vals));

        sb.Append("\nOutput using Range.Value2\n");
        vals = (object[,])usedRange.Value2; //1-based array
        sb.Append(ArrayToString(ref vals));

        sb.Append("\nOutput using Clipboard Copy\n");
        string previousClipboardText = Clipboard.GetText();
        usedRange.Copy();
        string clipboardText = Clipboard.GetText();
        Clipboard.SetText(previousClipboardText);
        vals = new object[usedRange.Rows.Count, usedRange.Columns.Count]; //0-based array
        ParseClipboard(clipboardText,ref vals);
        sb.Append(ArrayToString(ref vals));


        sb.Append("\nOutput using Range.Text and Autofit\n");
        //if you dont autofit, Range.Text may give you something like #####
        usedRange.Columns.AutoFit();
        usedRange.Rows.AutoFit();
        vals = new object[usedRange.Rows.Count, usedRange.Columns.Count];
        int startRow = usedRange.Row;
        int endRow = usedRange.Row + usedRange.Rows.Count - 1;
        int startCol = usedRange.Column;
        int endCol = usedRange.Column + usedRange.Columns.Count - 1;
        for (int r = startRow; r <= endRow; r++)
        {
            for (int c = startCol; c <= endCol; c++)
            {
                vals[r - startRow, c - startCol] = sheet.Cells[r, c].Text;
            }
        }
        sb.Append(ArrayToString(ref vals));


        MessageBox.Show(sb.ToString());
    }

    //requires reference to Microsoft.VisualBasic to get TextFieldParser
    private void ParseClipboard(string text, ref object[,] vals)
    {
        using (var tabReader = new TextFieldParser(new StringReader(text)))
        {
            tabReader.SetDelimiters("\t");
            tabReader.HasFieldsEnclosedInQuotes = true;

            int row = 0;
            while (!tabReader.EndOfData)
            {
                var fields = tabReader.ReadFields();
                for (int i = 0; i < fields.Length; i++)
                    vals[row, i] = fields[i];
                row++;
            }
        }
    }


    private void button1_Click(object sender, RibbonControlEventArgs e)
    {
        SetSampleValues();
    }

    private void button2_Click(object sender, RibbonControlEventArgs e)
    {
        GetCellValues();
    }

【问题讨论】:

  • 试试这个:将格式化的工作表导出到 .csv 文件。创建一个新工作表,然后导入(不要打开).csv 文件。执行此操作时,文本导入向导将打开,您将每列指定为文本。然后,您可以通过一个步骤将这个新工作表的 UsedRange 放入变量数组中。
  • 谢谢,但我认为该方法会非常缓慢且容易出错,因为 Excel 不是最强大的 csvs 程序。我永远不会相信 Excel 能够正确导出包含 Unicode 字符、前导零、日期、单元格值中的分隔符、单元格值中的换行符等的 csv。
  • 没有意识到您对 Unicode 字符的要求。我刚刚使用了您提供的示例数据,效果很好。

标签: c# excel vba vsto clipboard


【解决方案1】:

您的问题的一个解决方案是使用:

Range(XYZ).Value(11) = Range(ABC).Value(11) 

here 所述,这将:

以 XML 格式返回指定 Range 对象的记录集表示形式。

假设您的 excel 配置为 OpenXML 格式,这将复制范围 ABC 的值/公式和格式,并将其注入范围 XYZ。

另外,this answer 解释了 Value 和 Value2 之间的区别。

.Value2 为您提供单元格的基础值(可以是空、字符串、错误、数字(双精度)或布尔值)

.Value 为您提供与 .Value2 相同的信息,除非单元格被格式化为货币或日期,它为您提供 VBA 货币(可能会截断小数位)或 VBA 日期。

【讨论】:

    【解决方案2】:

    我找到了部分解决方案。将 NumberFormat 值应用于 Value2 的已解析双精度。这仅适用于单个单元格,因为在数组中返回具有不同格式的 NumberFormat 数组返回 System.DBNull。

    double.Parse(o.Value2.ToString()).ToString(o.NumberFormat.ToString())
    

    但日期不适用于此。如果您知道哪些列包含某些内容,例如格式化日期,则可以在双精度上使用 DateTime.FromOADate,然后在 NumberFormat 上使用 value.ToString(format)。下面的代码接近但不完整。

    <snip>
    sb.Append("\nOutput using Range.Value2\n");
    vals = (object[,])usedRange.Value2; //1-based array
    var format = GetFormat(usedRange);
    sb.Append(ArrayToString(ref vals, format));
    </snip>
    
    private static object[,] GetFormat(Microsoft.Office.Interop.Excel.Range range)
    {
        var rows = range.Rows.Count;
        var cols = range.Columns.Count;
        object[,] vals = new object[rows, cols];
        for (int r = 1; r <= rows; ++r)
        {
            for (int c = 1; c <= cols; ++c)
            {
                vals[r-1, c-1] = range[r, c].NumberFormat;
            }
        }
        return vals;
    }
    
    private static string ArrayToString(ref object[,] vals, object[,] numberformat = null)
    {
        int dim1Start = vals.GetLowerBound(0); //Excel Interop will return index-1 based arrays instead of index-0 based
        int dim1End = vals.GetUpperBound(0);
        int dim2Start = vals.GetLowerBound(1);
        int dim2End = vals.GetUpperBound(1);
    
        var sb = new StringBuilder();
        for (int i = dim1Start; i <= dim1End; i++)
       {
            for (int j = dim2Start; j <= dim2End; j++)
            {
                if (numberformat != null)
                {
                    var format = numberformat[i-1, j-1].ToString();
                    double v;
                    if (double.TryParse(vals[i, j].ToString(), out v))
                    {
                        if (format.Contains(@"/") || format.Contains(":"))
                        {// parse a date
                            var date = DateTime.FromOADate(v);
                            sb.Append(date.ToString(format));
                        }
                        else
                        {
                            sb.Append(v.ToString(format));
                        }
                    }
                    else
                    {
                        sb.Append(vals[i, j].ToString());
                    }
                }
                else
                {
                    sb.Append(vals[i, j]);
                }
                if (j != dim2End)
                    sb.Append("\t");
            }
            sb.Append("\n");
        }
        return sb.ToString();
    }
    

    【讨论】:

    • 谢谢,但就像你说的 NumberFormat 仅在整个范围具有相同格式的情况下才有效,否则,您必须逐个查看单元格。如果您的数据在范围段中具有一致的数字格式并且您应用算法来获取其中的一部分(例如检查整个列是否具有相同的 NumberFormat),这可以相对有效地工作,但如果数据相对随机,您必须得到每个单元格的 NumberFormat 并且您不会看到使用 Text 属性的任何性能提升,实际上它会更慢。
    猜你喜欢
    • 1970-01-01
    • 2021-05-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-09
    • 1970-01-01
    相关资源
    最近更新 更多