C# 高效地获取格式化的单元格值

C# 高效地获取格式化的单元格值,c#,excel,vba,vsto,clipboard,C#,Excel,Vba,Vsto,Clipboard,我希望能够从Excel中高效地检索格式化单元格值的多维数组。当我说格式化值时,我的意思是,我希望得到的值与Excel中显示的值完全相同,并应用所有单元格编号格式 Range.Value和Range.Value2属性非常适合将大量单元格的单元格值检索到多维数组中。但这些都是实际的单元格值(至少对于Range.Value2是这样,我不太确定Range.Value对一些值做了什么) 如果要检索单元格中显示的实际文本,可以使用Range.text属性。这有一些警告。首先,您需要自动调整单元格,否则,如果

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

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

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

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

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

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

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

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();
    }
private void设置采样值()
{
var sheet=(Microsoft.Office.Interop.Excel.Worksheet)Globals.ThisAddIn.Application.ActiveSheet;
sheet.Cells.ClearContents();
sheet.Cells.ClearFormats();
var范围=板材范围[“A1”];
range.NumberFormat=“常规”;
range.Value2=“2008-01-25 15:19:32”;
范围=板材。范围[“A2”];
range.NumberFormat=“@”;
range.Value2=“2008-01-25 15:19:32”;
范围=板材。范围[“B1”];
range.NumberFormat=“0.00”;
range.Value2=“5.12345”;
范围=板材。范围[“B2”];
range.NumberFormat=“0.00%”;
range.Value2=“.456”;
}
专用字符串ArrayToString(参考对象[,]VAL)
{
int dim1Start=vals.GetLowerBound(0);//Excel互操作将返回基于索引1的数组,而不是基于索引0的数组
int dim1End=vals.GetUpperBound(0);
int dim2Start=vals.GetLowerBound(1);
int dim2End=vals.GetUpperBound(1);
var sb=新的StringBuilder();

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

double.Parse(o.Value2.ToString()).ToString(o.NumberFormat.ToString())
但是,日期与此无关。如果您知道哪些列包含某些内容,例如格式化日期,则可以在双精度上使用DateTime.FromOADate,然后在数字格式上使用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();
}

sb.Append(“\n使用Range.Value2\n输出”);
VAL=(对象[,])usedRange.Value2;//基于1的数组
var format=GetFormat(usedRange);
sb.追加(数组字符串(参考值,格式));
私有静态对象[,]GetFormat(Microsoft.Office.Interop.Excel.Range)
{
变量行=range.rows.Count;
var cols=range.Columns.Count;
对象[,]VAL=新对象[行,列];

对于(int r=1;r问题的一个解决方案是使用:

Range(XYZ).Value(11) = Range(ABC).Value(11) 
如上所述,这将:

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

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

此外,还解释了Value和Value2之间的差异

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

.Value与.Value2相同,除非单元格格式为货币或日期,否则它会为您提供VBA货币(可能会截断小数点)或VBA日期


尝试以下操作:将格式化的工作表导出到.csv文件。创建新工作表,然后导入(不打开).csv文件。当您执行此操作时,文本导入向导将打开,您将指定每个列为文本。然后,您可以在一个步骤中将此新工作表的UsedRange放入变量数组中。谢谢,但我认为该方法将非常慢且容易出错,因为Excel不是csv最健壮的程序。我永远不会相信EXECl要正确导出包含Unicode字符、前导零、日期、单元格值内的分隔符、单元格值内的换行符等的csv,我没有意识到您对Unicode字符的要求。我只使用了您提供的示例数据,效果很好。谢谢,但正如您所说,NumberFormat仅在整个范围具有同样的格式,否则,您必须逐个查看单元格。如果您的数据在范围段中具有一致的数字格式,并且
Range(XYZ).Value(11) = Range(ABC).Value(11)