使用Range对象
Range对象表示电子表格中的单元格范围。范围可以包含一个单元格,多个连续的单元格,甚至多个不连续的单元格。您可以在Excel中选择时按住Ctrl键选择多个不连续的单元格。
获取特定Cell或Cells范围对象
Excel提供了多种获取Range对象的方法。 Range对象是您要在Excel工作表中处理单元格或单元格范围时使用的对象。在Application对象的描述中提到了两种获取Range对象的方法。 Application.ActiveCell在活动窗口中返回活动选择的左上角单元格。 Application.Selection返回一个表示活动窗口中活动选择的对象。如果活动选择是单元格范围,则可以将Application.Selection转换为Range对象。如果在活动窗口(例如形状或图表)中选择了其他选项,Application.Selection将返回所选对象。
工作表还提供了几种获取Range对象的方法。 Worksheet.get_Range方法是从工作表获取Range对象的最常用方法。此方法接收可以传递字符串的必需对象参数。它具有可以传递第二个字符串的第二个可选参数。您传递的字符串是所谓的A1样式参考格式。解释A1样式参考格式的最简单的方法是给出几个例子。
参考A1指定A列1行的单元格。参考D22指定D列22行的单元格。参考AA11指定行11,列AA(第27列)处的单元格。
参考$ A $ 1也指的是第1行列A中的单元格。如果在A1样式引用中使用$符号,则它们将被忽略。
您可以使用范围运算符(:)来指定单元格范围,其中第一个A1样式引用是范围的左上角,后跟一个冒号运算符,之后是右下角的第二个A1样式引用的范围。参考A1:B1指的是行1,列A和列1,列B的两个单元格。参考A1:AA11是指块中的所有297个单元格,其左上角位于第1列,第A列和下侧右角位于第11列,AA栏(第27列)。
您可以使用联合运算符(,)来指定可能是不连续的多个单元格。例如,参考A1,C4指定了第一个单元格位于第1列,第A列,第二个单元格位于第4列第C列的两个单元格的范围。用户可以通过按住Ctrl键来选择单元格的不连续范围因为他们选择各种细胞。参考A1,C4,C8,C10是指定四个不同单元格的另一个有效的A1样式参考。
交点运算符(一个空格)可以指定单元格的交集。例如,参考A1:A10 A5:A15解析为从行5,列A开始并以行A,列A开始的相交六个单元。参考A1:A10 A5:A15 A5分解为第5行的单个单元格,列A.
您还可以使用您在A1样式参考中在工作表中定义的任何名称。例如,假设您定义了指向单元格A1的名为foo的名称范围。使用您的名称的一些有效的A1样式的引用将包括foo:A2,其引用行1,列A和行2,列A的单元格。引用foo,A5:A6是指第1行,第A列;第5行,A列;和第6列,列A。
如前所述,get_Range方法采用第二个可选参数,您可以传递第二个A1样式的引用字符串。使用范围运算符有效地组合第一个参数和第二个参数。因此,当调用get_Range(“A1”,“A2”)时,get_Range返回的范围等同于调用get_Range(“A1:A2”,Type.Missing)时获得的范围。
获取Range对象的第二种方法是使用Worksheet.Cells属性,该属性返回工作表中所有单元格的范围。然后,您可以在返回的Range对象上使用相同的get_Range方法,并以与使用Worksheet对象中的get_Range相同的方式传递A1样式引用以选择单元格。所以Cells.get_Range(“A1:A2”,Type.Missing)等价于get_Range(“A1:A2”,Type.Missing)。使用Cells属性的更常见的用法是将其与Range的get_Item属性结合使用,该属性将使用行索引和可选的列索引。使用get_Item是一种在不使用A1样式引用的情况下访问特定单元格的方法。所以Cells.get_Item(1,1)相当于get_Range(“A1”,Type.Missing)。
获取Range对象的另一种方法是使用Worksheet.Rows或Worksheet.Columns属性。这些返回一个与其他Range对象不同的范围。例如,如果您采用Column返回的范围并显示范围内的单元格数,则返回256列数。但是如果您在返回的范围内调用Select方法,Excel将在工作表中选择所有16,772,216个单元格。考虑Rows和Columns返回的范围的最简单的方法是它们的行为与列和行标题在Excel中的行为相似。
清单5-27显示了使用get_Range方法和Cells,Rows和Columns属性的几个示例。我们使用范围的Value2属性将范围中的每个单元格设置为指定的字符串值。程序的运行结果如图5-7所示
清单5-27 获取Range对象的VSTO定制
private void Sheet1_Startup(object sender, System.EventArgs e) { Excel.Range r1 = this.get_Range("A1", missing); r1.Value2 = "r1"; Excel.Range r2 = this.get_Range("B7:C9", missing); r2.Value2 = "r2"; Excel.Range r3 = this.get_Range("C1,C3,C5", missing); r3.Value2 = "r3"; Excel.Range r4 = this.get_Range("A1:A10 A5:A15", missing); r4.Value2 = "r4"; Excel.Range r5 = this.get_Range("F4", "G8"); r5.Value2 = "r5"; Excel.Range r6 = this.Rows.get_Item(12, missing) as Excel.Range; r6.Value2 = "r6"; Excel.Range r7 = this.Columns.get_Item(5, missing) as Excel.Range; r7.Value2 = "r7"; }
图5-7 运行结果清单5-27
使用Address
给定一个Range对象,你经常需要确定它所指的单元格。 get_Address方法返回A1样式或R1C1样式范围的地址。 您已经了解了A1样式的引用。 R1C1样式的引用支持与A1样式引用(范围为冒号,联合逗号和交叉空间)讨论的所有相同的运算符。 R1C1样式的引用分别以R和C开头的行和列号。 所以R1C1风格的单元格A4将是R4C1。 图5-8显示了我们在本节中考虑的三个方面的范围。
图5-8 具有三个不连续区域的范围
图5-8中范围的地址以A1样式和R1C1样式显示:
$A$15:$F$28,$H$3:$J$9,$L$1 R15C1:R28C6,R3C8:R9C10,R1C12
获取地址的另一个选择是获取外部引用还是本地引用。 我们已经在图5-8中显示的地址是本地引用。 外部引用包括范围所在的工作簿和工作表的名称。 在图5-8中,与A1风格和R1C1风格的外部参考相同。
[Book1]Sheet1!$A$15:$F$28,$H$3:$J$9,$L$1 [Book1]Sheet1!R15C1:R28C6,R3C8:R9C10,R1C12
对于我们的示例,我们创建的范围的工作簿未保存。 当我们将它保存为Book1.xls时,地址如下所示:
[Book1.xls]Sheet1!$A$15:$F$28,$H$3:$J$9,$L$1 [Book1.xls]Sheet1!R15C1:R28C6,R3C8:R9C10,R1C12
获取地址的另一个选择是使用绝对地址还是使用相对地址。 我们已经考虑过的地址是绝对的。 相对格式(相对于单元格A1)的相同地址如下所示:
R[14]C:R[27]C[5],R[2]C[7]:R[8]C[9],RC[11] A15:F28,H3:J9,L1
对于R1C1样式的地址,您还可以指定希望地址相对的单元格。 如果我们在图5-4中相对于单元格B2获得了R1C1样式,我们得到以下结果:
R[13]C[-1]:R[26]C[4],R[1]C[6]:R[7]C[8],R[-1]C[10]
get_Address方法使用五个可选参数来控制引用的返回方式,如表5-17所示。
表5-17 get_Address的可选参数
Parameter Name |
Type |
What It Does |
---|---|---|
RowAbsolute |
object |
通过TRue将地址的行部分作为绝对引用返回($ A $ 1)。 如果您传递false,行参考将不会是绝对的($ A1)。 默认值为true。 |
ColumnAbsolute |
object |
通过TRue将地址的列部分作为绝对引用返回($ A $ 1)。 如果你传递错误,列参考将不是绝对的(A $ 1)。 默认值为true。 |
ReferenceStyle |
XlReferenceStyle |
通过xlA1返回A1样式的引用。 通过xlR1C1返回R1C1样式的引用。 |
External |
object |
传递真的返回外部引用。 默认值为false。 |
RelativeTo |
object |
传递一个表示您希望R1C1样式引用相对于单元格的Range对象。 与A1样式引用一起使用时不起作用。 |
清单5-28显示了使用示例范围的get_Address的几个示例。
清单5-28 使用get_Address的VSTO自定义
private void Sheet1_Startup(object sender, System.EventArgs e) { Excel.Range range1 = this.get_Range( "$A$15:$F$28,$H$3:$J$9,$L$1", missing); System.Text.StringBuilder sb = new System.Text.StringBuilder(); sb.AppendLine("A1-Style Addresses:"); sb.AppendFormat("Default: {0} ", range1.get_Address( missing, missing, Excel.XlReferenceStyle.xlA1, missing, missing)); sb.AppendFormat("Relative rows: {0} ", range1.get_Address(false, missing, Excel.XlReferenceStyle.xlA1, missing, missing)); sb.AppendFormat("Row & Column Relative: {0} ", range1.get_Address(false, false, Excel.XlReferenceStyle.xlA1, missing, missing)); sb.AppendFormat("External: {0} ", range1.get_Address( missing, missing, Excel.XlReferenceStyle.xlA1, true, missing)); sb.AppendLine(); sb.AppendLine("R1C1-Style Addresses:"); sb.AppendFormat("Default: {0} ", range1.get_Address( missing, missing, Excel.XlReferenceStyle.xlR1C1, missing, missing)); sb.AppendFormat("Row & Column Relative to C5: {0} ", range1.get_Address(false, false, Excel.XlReferenceStyle.xlR1C1, missing, this.get_Range("C5", missing))); sb.AppendFormat("External: {0}", range1.get_Address( missing, missing, Excel.XlReferenceStyle.xlR1C1, true, missing)); MessageBox.Show(sb.ToString()); }
使用运算符方法创建新的范围
我们讨论了可以在地址字符串中使用的几个“运算符”,包括联合运算符(逗号)和交集运算符(空格)。 您还可以通过Application.Union和Application.Intersection方法应用这些操作符。
也可以通过使用get_Offset方法取一个范围并获得一个与之相距一些行和列的新范围。 该方法采用行和列值来偏移给定范围并返回新的偏移范围。 所以在图5-8的示例范围中调用get_Offset(5,5)返回一个这样的A1样式地址的范围:
"$F$20:$K$33,$M$8:$O$14,$Q$6"
清单5-29显示了使用这些运算符的示例。 请注意,联合和交点需要很多可选参数,允许您联合或相交多于两个范围。
清单5-29 使用Union,Intersection和get_Offset的VSTO自定义
private void Sheet1_Startup(object sender, System.EventArgs e) { Excel.Application app = this.Application; Excel.Range range1 = this.get_Range("$A$15:$F$28", missing); Excel.Range range2 = this.get_Range("$H$3:$J$9", missing); Excel.Range range3 = this.get_Range("$L$1", missing); Excel.Range range4 = this.get_Range("$A$11:$G$30", missing); Excel.Range rangeUnion = app.Union(range1, range2, range3, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing); Excel.Range rangeIntersection = app.Intersect(range1, range4, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing, missing); Excel.Range rangeOffset = rangeUnion.get_Offset(5, 5); MessageBox.Show(String.Format("Union: {0}", rangeUnion.get_Address(missing, missing, Excel.XlReferenceStyle.xlA1, missing, missing))); MessageBox.Show(String.Format("Intersection: {0}", rangeIntersection.get_Address(missing, missing, Excel.XlReferenceStyle.xlA1, missing, missing))); MessageBox.Show(String.Format("Offset: {0}", rangeOffset.get_Address(missing, missing, Excel.XlReferenceStyle.xlA1, missing, missing))); }
使用Area
当一个范围内存在多个不连续的单元格范围时,每个不连续的范围称为一个区域。 如果Range中有多个不连续的区域,请使用Areas属性通过Areas集合访问每个区域(作为Range)。 Areas集合具有一个Areas.Count属性和一个Areas.get_Item方法,它将一个表示基于1的索引的int参数作为数组。 清单5-30显示了一个迭代示例范围(有三个区域)并打印每个区域的地址的示例。
清单5-30 适用于区域的VSTO定制
private void Sheet1_Startup(object sender, System.EventArgs e) { Excel.Range range1 = this.get_Range( "$A$15:$F$28,$H$3:$J$9,$L$1", missing); MessageBox.Show(String.Format("There are {0} areas", range1.Areas.Count)); foreach (Excel.Range area in range1.Areas) { MessageBox.Show(String.Format("Area address is {0}", area.get_Address(missing, missing, Excel.XlReferenceStyle.xlA1, missing, missing))); } }
使用Cells
Count属性返回给定范围内的单元格数。 您可以使用get_Item方法获取范围内的特定单元格范围。 get_Item方法接受所需的行索引和可选的列索引。 当范围是单元格的一维数组时,可以省略列索引,因为在这种情况下,它只有一列或一列的单元格,所以称为RowIndex的参数真的像数组索引一样。 如果范围有多个区域,则必须首先获取要处理的区域,get_Item只会返回单元格的范围中的第一个区域。
清单5-31显示了使用get_Item的示例。
清单5-31 使用get_Item的VSTO自定义
private void Sheet1_Startup(object sender, System.EventArgs e) { Excel.Range range1 = this.get_Range("$A$15:$F$28", missing); int rowCount = range1.Rows.Count; int columnCount = range1.Columns.Count; for (int i = 1; i <= rowCount; i++) { for (int j = 1; j <= columnCount; j++) { Excel.Range cell = range1.get_Item(i, j) as Excel.Range; string address = cell.get_Address(missing, missing, Excel.XlReferenceStyle.xlA1, missing, missing); cell.Value2 = String.Format("get_Item({0},{1})", i, j); } } }
使用rows和columns
给定一个Range对象,您可以使用Row和Column属性来确定其第一个区域的左上角的行和列号。行和列号作为int值返回。
您还可以使用“行”和“列”属性来确定第一个区域中的行和列的总数。这些属性返回特殊范围,您可以将其视为对应于与范围相关联的行或列标题。当我们从图5-8中的示例范围获取Rows.Count时,它返回14,Columns.Count返回6.这是有道理的,因为我们选择的第一个区域(A15:F28)跨越6列和14行。
要获取第一个区域右下角的行和列位置,可以使用清单5-32所示的相当尴尬的表达式。清单5-32还说明了使用get_Item,它使用行和列索引(相对于给定范围的顶部),并返回该行和列索引处的单元格(作为范围)。当您获得一个Rows或Columns范围时,这些范围是一维的,在这种情况下,称为RowIndex的参数像数组索引一样。
清单5-32 获取行和列位置的VSTO自定义
private void Sheet1_Startup(object sender, System.EventArgs e) { Excel.Range range1 = this.get_Range( "$A$15:$F$28,$H$3:$J$9,$L$1", missing); Excel.Range area = range1.Areas.get_Item(1); int topLeftColumn = area.Column; int topLeftRow = area.Row; int bottomRightColumn = ((Excel.Range)area.Columns. get_Item(area.Columns.Count, missing)).Column; int bottomRightRow = ((Excel.Range)area.Rows. get_Item(area.Rows.Count, missing)).Row; MessageBox.Show(String.Format( "Area Top Left Column {0} and Row {1}", topLeftColumn, topLeftRow)); MessageBox.Show(String.Format( "Area Bottom Right Column {0} and Row {1}", bottomRightColumn, bottomRightRow)); MessageBox.Show(String.Format( "Total Rows in Area = {0}", area.Rows)); MessageBox.Show(String.Format( "Total Columns in Area = {0}", area.Columns)); }
使用regions
CurrentRegion属性返回一个范围,该范围将扩展为包含所有单元格,直到空白行和空白列。这个扩展的范围被称为一个区域。所以,例如,你可能有一个范围,它包含一个表格中的几个单元格,以获得包含整个表格的范围(假设该表由空白的行和列组成),您将使用较小范围的CurrentRegion属性返回整个桌子
get_End方法是对与Range相关联的区域起作用的方法。 get_End方法接受XlDirection枚举的成员:xlDown,xlUp,xlToLeft或xlToRight。当xlUp传递的方法返回与Range范围左上角的单元格相同的列中的最上面的单元格。当通过xlDown时,它返回与Range的左上角单元格相同的列中的最下面的单元格。当通过xlToLeft时,它返回与Range的左上角单元格相同行中的最左边的单元格。当通过xlToRight时,它将返回与Range的左上角单元格相同行中的最右边的单元格。
选择Range
您可以使用范围上的选择方法使范围当前选择。记住,拨打选择会更改用户的当前选择,这不是一件很好的事情,没有很好的理由。然而,在某些情况下,您希望将用户的注意力吸引到某些情况下,在选择范围的情况下可以做到这一点。
编辑Range的值
通常使用两种方法来获取和设置范围内的值。第一种方法是使用get_Value和set_Value方法。第二种方法是使用属性Value2。 Value2和get_Value的区别在于,Value2属性返回的元素是货币或日期作为双重值。而且,get_Value也接受XlRangeValueDataType类型的可选参数。如果您传递XlRangeValueData.xlRangeValueDefault,您将返回一个表示单个单元格范围单元格值的对象。对于Value2和get_Value,如果Range包含多个单元格,则将返回与Range中单元格相对应的对象数组。
清单5-33显示了使用Value2的几个示例,包括将值数组传递给Value2的示例。通过数组一次设置范围内的单元格的值比通过多个调用单独设置每个单元格更有效。
清单5-33 使用Value2的VSTO定制
private void Sheet1_Startup(object sender, System.EventArgs e) { Excel.Range range1 = this.get_Range("$A$15:$F$28", missing); range1.Value2 = "Test"; int rowCount = range1.Rows.Count; int columnCount = range1.Columns.Count; object[,] array = new object[rowCount, columnCount]; for (int i = 0; i < rowCount; i++) { for (int j = 0; j < columnCount; j++) { array[i, j] = i * j; } } range1.Value2 = array; }
复制,清除和删除Range
Excel提供了一些复制,清除和删除范围的方法。复制方法采用Destination参数,您可以传递复制范围的目的地。 Clear方法清除范围内的单元格的内容和格式。 ClearContents只清除范围内单元格的值,而ClearFormats仅清除格式。删除方法删除单元格的范围,并作为参数移动单元格替换已删除的单元格的方向。该方向作为XlDeleteShiftDirection枚举的成员传递:xlShiftToLeft或xlShiftUp。
在Range内查找文本
Find方法允许您查找范围中的文本,并返回范围内的单元格,其中找到文本。查找方法对应于查找和替换对话框,如图5-9所示。如果在调用Find方法时省略参数,它将使用上次使用Find对话框时用户设置的任何设置。此外,当您指定参数时,指定的设置将在下次用户打开时显示在“查找”对话框中。
图5-9 查找和替换对话框
Find方法采用表5-18中描述的许多参数。 Find返回一个Range对象,如果它成功,如果找不到任何东西,则返回null。 您可以使用FindNext方法找到与您的查找条件匹配的下一个单元格。 FindNext需要一个可选的After参数,您需要传递上一个找到的范围,以确保您不会再一次找到相同的单元格。 清单5-34显示了使用Find和FindNext方法的示例,其中我们搜索包含字符“2”的任何单元格,并加粗这些单元格。
表5-18 查找方法的参数
Parameter Name |
Type |
What It Does |
---|---|---|
What |
object |
Pass the data to search for as a required string. |
After |
object |
Pass a single cell after which you want the search to begin as a Range. The default is the top-left cell if this omitted. |
LookIn |
object |
Pass the type to search. |
LookAt |
XlLookAt |
Pass xlWhole to match the whole cell contents, xlPart to match parts of the cell contents. |
SearchOrder |
XlSearchOrder |
Pass xlByRows to search by rows, xlByColumns to search by columns. |
SearchDirection |
XlSearch-Direction |
Pass xlNext to search forward, xlPrevious to search backward. |
MatchCase |
object |
Pass true to match case. |
MatchByte |
object |
Pass true to have double-byte characters match only double-byte characters. |
SearchFormat |
object |
Set to true if you want the search to respect the FindFormat options. You can change the FindFormat options by using the Application.FindFormat. |
清单5-34 使用Find和FindNext的VSTO定制
private void Sheet1_Startup(object sender, System.EventArgs e) { Excel.Range range1 = this.get_Range("$A$15:$F$28", missing); int rowCount = range1.Rows.Count; int columnCount = range1.Columns.Count; object[,] array = new object[rowCount, columnCount]; for (int i = 0; i < rowCount; i++) { for (int j = 0; j < columnCount; j++) { array[i, j] = i * j; } } range1.Value2 = array; Excel.Range foundRange = range1.Find("2", range1.get_Item(1, 1), missing, Excel.XlLookAt.xlPart, missing, Excel.XlSearchDirection.xlNext, missing, missing, missing); while (foundRange != null) { foundRange.Font.Bold = true; foundRange = range1.FindNext(foundRange); } }
格式化一系列单元格
Excel提供了几种方法和属性来格式化一系列单元格。最有用的是NumberFormat属性,您可以将其设置为格式化与“格式化单元格”对话框的“自定义”类别中的字符串相对应的字符串。例如,您可以将NumberFormat设置为“常规”,不设置特定的数字格式。将NumberFormat设置为m / d / yyyy设置日期格式,0%将格式设置为百分比格式。当使用NumberFormat时,如果您正在构建控制台应用程序或加载项,请务必考虑本章后面的“特殊Excel问题”一节中讨论的区域设置问题,因为读取和设置此字符串可能会在不同的运行时导致问题语言环境。如果您在工作簿或模板项目后面使用VSTO代码,则无需担心语言环境问题。
Font属性返回一个Font对象,可用于将Font设置为各种大小和样式。清单5-34显示了用于加粗单元格字体的Font对象的示例。
Excel还允许您创建与工作簿相关联的样式,并将这些样式应用于范围。您可以使用Workbook.Styles创建样式。清单5-35显示了创建样式并将其应用于Range的示例。
清单5-35 创建和应用样式的VSTO自定义
private void Sheet1_Startup(object sender, System.EventArgs e) { Excel.Range range1 = this.get_Range("$A$15:$F$28", missing); range1.Value2 = "Hello"; Excel.Style style = Globals.ThisWorkbook.Styles.Add( "My Style", missing); style.Font.Bold = true; style.Borders.LineStyle = Excel.XlLineStyle.xlDash; style.Borders.ColorIndex = 3; style.NumberFormat = "General"; range1.Style = "My Style"; }
特殊Excel问题
在.NET中使用Excel对象模型时,需要注意几个特殊的注意事项。 本节将检查两个最重要的内容:使用多个区域设置并使用Excel日期。
自动化可执行文件和COM加载项的Excel区域问题
当使用自动化可执行文件或COM加载项中的托管代码对Excel对象模型进行编程时,根据当前线程的区域设置,Excel方法和属性的行为可能会有所不同。 请注意,使用VSTO构建的文档解决方案后面的代码中不会出现此问题。 例如,如果要为范围设置公式并且位于法语区域设置中,则Excel要求您使用本地化的法语公式名称和格式:
sheet.get_Range("A1", Type.Missing).Formula = "=SOMME(3; 4)";
这种行为与独立于语言环境的解决方案背后的VBA和VSTO代码不同。 VBA和VSTO总是告诉Excel,该语言环境是美国英语(locale id 1033)。 在解决方案中的VBA和VSTO代码中,与Excel进行交谈时,您不必考虑区域设置。 您可以编写此代码,并使其在法国语言环境中工作:
sheet.get_Range("A1", Type.Missing).Formula = "=SUM(3, 4)";
当托管代码调用到Excel对象模型中时,它会告知Excel它正在运行的区域设置(当前线程的区域设置),这将导致Excel期望您将以该区域设置的本地化格式提供公式和其他值。 Excel还将以该区域设置的本地化格式返回公式和其他值。 Excel期望本地化的字符串,例如日期格式,与Range关联的NumberFormat字符串,与NumberFormat字符串相关联的颜色名称和公式名称。
使用DateTime for Dates
作为一个例子,如果你不考虑这个问题,可以考虑下列代码:
sheet.get_Range("A1", Type.Missing).Value2 = "03/11/02";
该值可能由2002年3月11日,2002年11月3日,或2003年11月2日由Excel解释,具体取决于当前线程的区域设置。
对于日期,您有一个明确的解决方法。 不要将日期作为文字字符串传递给Excel。 相反,使用System.DateTime对象构建日期,并使用DateTime的ToOADate方法将其传递给Excel,如代码清单5-36所示。 ToOADate方法将DateTime转换为OLE自动化日期,这是Excel对象模型期望的日期格式。
清单5-36 适当地将日期传递给Excel的VSTO定制
private void Sheet1_Startup(object sender, System.EventArgs e) { Excel.Range range1 = this.get_Range("$A$1", missing); // March 11, 2002 System.DateTime date = new System.DateTime(2002, 3, 11); range1.Value2 = date.ToOADate(); }
将线程区域切换为英文和后退不推荐
您可能会认为与设置或获取Range.NumberFormat和Range.Formula相关的问题的解决方案是保存线程的区域设置,临时将线程的区域设置切换为英语(区域设置ID 1033),执行设置的代码或获取受区域设置影响的属性(如NumberFormat或Formula),然后切换回保存的区域设置。不建议使用此方法,因为它会影响不期望本地交换机的其他加载项。
请考虑以下示例。您的加载项正在法国机器上运行。您的加载项将区域设置切换到1033并设置公式值。另一个加载项是处理Change事件并显示一个对话框。该对话框以英文而不是法语显示。因此,通过更改线程区域设置,您已经改变了另一个加载项的行为,并且一般是不良的Office公民。
使用反思来解决地区问题
遇到区域设置问题的COM加载项或自动化可执行文件的建议解决方法(访问受当前语言环境影响的属性(如NumberFormat或Formula属性)时)是通过反射访问这些属性。反射使您能够指定Excel的英文区域设置,并编写无论当前线程区域设置如何的代码。清单5-37说明了如何使用反射来设置NumberFormat和Formula属性。
清单5-37 使用反思来解决Excel中的区域问题
static void Main(string[] args) { Excel.Application application = new Excel.Application(); application.Visible = true; object missing = Type.Missing; Excel.Workbook workbook = application.Workbooks.Add(missing); Excel.Worksheet sheet = (Excel.Worksheet)workbook.Worksheets.Add(missing, missing, missing, missing); Excel.Range range1 = sheet.get_Range("$A$1", missing); // Set Formula in English (US) using reflection typeof(Excel.Range).InvokeMember("Formula", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.SetProperty, null, range1, new object[] {"=SUM(12, 34)" }, System.Globalization.CultureInfo.GetCultureInfo(1033)); // Set NumberFormat in English (US) using reflection typeof(Excel.Range).InvokeMember("NumberFormat", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.SetProperty, null, rangel, new object[] {"General" }, System.Globalization.CultureInfo.GetCultureInfo(1033)); }
旧格式或无效的类型库错误
Excel语言环境问题进一步复杂化的第二个问题是,在将语言环境设置为非英语区域设置的计算机上的英文Excel安装中使用Excel对象模型时,可能会收到“旧格式或无效类型库”错误。 Excel正在程序文件 Microsoft Office OFFICE11 1033中找到一个名为xllex.dll的文件,它找不到。解决此问题的方法是安装xllex.dll文件或安装Office的MUI语言包。您还可以复制excel.exe,将其重命名为xllex.dll,并将其复制到1033目录。
VSTO和Excel语言环境问题
文档解决方案后的VSTO代码通过使用位于您和Excel对象模型之间的透明代理对象来解决Excel区域设置问题。此代理总是告诉Excel,该语言环境是美国英语(locale id 1033),这有效地使VSTO匹配VBA行为。如果您在文档解决方案后面使用VSTO代码,则会为您解决Excel语言环境问题,您不必再进一步担心。如果您正在为Excel或自动化可执行文件构建托管COM加载项,则问题仍然存在。
VSTO对Excel语言环境问题的解决方案有一些注意事项。 VSTO透明代理可以稍微减慢代码的速度。它也会导致Excel对象在调试器中检查时显示略有不同。最后,如果您使用Equals运算符将代理的Excel对象(如Application)与未使用的Application对象进行比较,则它们将不会被评估为相等。
如果要绕过特定对象的VSTO透明代理,可以使用Microsoft.Office.Tools.Excel.ExcelLocale1033Proxy.Unwrap方法,并传递要绕过代理的Excel对象。此方法将删除代理并返回原始的PIA对象,再次暴露您的区域设置问题。您还可以将VSTO项目的AssemblyInfo.cs文件中的程序集属性ExcelLocale1033设置为false,以便关闭整个Excel解决方案的透明代理。
如果您导航到另一个PIA的对象,然后再次导航回Excel PIA,则可能会丢失透明代理。例如,如果从Application.CommandBars集合中的Microsoft.Office.Core PIA命名空间获取CommandBar对象,然后使用CommandBar.Application属性返回到Excel Application对象,则现在已丢失代理和区域设置问题会再次发生。
最后,如果您从解决方案中的Word VSTO代码创建了一个新的Excel实例,那么您直接与Excel PIA进行交谈,而没有透明的代理对象,并且区域设置问题将继续有效。
将Excel日期转换为DateTime
Excel可以以两种格式表示日期:1900格式或1904格式。 1900格式基于一个系统,当转换为一个数字时,它代表从1900年1月1日起的已过去的天数。1904格式是基于一个系统,当转换为一个数字时,它表示已过的天数自1904年1月1日起,1904年格式由早期的Macintosh计算机引入,因为我们稍后描述的1900格式的问题。您可以通过检查Workbook.Date1904属性来确定工作簿正在使用的格式,如果工作簿使用1904格式,则返回true。
如果Excel工作簿使用1904格式,并将日期从该工作簿转换为DateTime直接,则会得到错误的值。由于DateTime期望1900年的格式,由数字代表的Excel日期的值是1900年1月1日以后,而不是1904年1月1日之前的经过天数,因此将关闭4年和2个闰年。因此,此代码如果您在工作簿中使用1904格式,将会给出错误的日期时间。
object excelDate = myRange.get_value(Type.Missing); DateTime possiblyBadDateIfExcelIsIn1904Mode = (DateTime)excelDate;
要获得1904格式的日期为DateTime格式,您必须添加1904格式日期4年和2个闰天(以弥补1904年在1904年而不是1900年的0)。 所以,如果你编写代码,并使用函数Convert-ExcelDateToDate在清单5-38中,你将得到正确的结果,如果使用1904日期系统。
object excelDate = myRange.get_value(Type.Missing); DateTime goodDate = ConvertExcelDateToDate(excelDate);
清单5-38 将Excel日期转换为DateTime并再次返回
static readonly DateTime march1st1900 = new DateTime(1900, 03, 01); static readonly DateTime december31st1899 = new DateTime(1899, 12, 31); static readonly DateTime january1st1904 = new DateTime(1904, 01, 01); static readonly TimeSpan date1904adjustment = new TimeSpan(4 * 365 + 2, 0, 0,0, 0); static readonly TimeSpan before1stMarchAdjustment = new TimeSpan(1, 0, 0, 0); bool date1904 = ActiveWorkbook.Date1904; object ConvertDateToExcelDate(DateTime date) { LanguageSettings languageSettings = Application.LanguageSettings; int lcid = languageSettings.get_LanguageID( MsoAppLanguageID.msoLanguageIDUI); CultureInfo officeUICulture = new CultureInfo(lcid); DateTimeFormatInfo dateFormatProvider = officeUICulture. DateTimeFormat; string dateFormat = dateFormatProvider.ShortDatePattern; if (date1904) { if (date >= january1st1904) return date - date1904adjustment; else return date.ToString(dateFormat, dateFormatProvider); } if (date >= march1st1900) return date; if (date < march1st1900 && date > december31st1899) return date - before1stMarchAdjustment; return date.ToString(dateFormat, dateFormatProvider); } DateTime ConvertExcelDateToDate(object excelDate) { DateTime date = (DateTime)excelDate; if (date1904) return date + date1904adjustment; if (date < march1st1900) return date + before1stMarchAdjustment; return date; }
清单5-38还对1900格式日期进行了更正。 事实证明,当Lotus 1-2-3写成时,程序员错误地认为1900年是一个闰年。 当Microsoft写Excel时,他们希望确保它们与现有的Lotus 1-2-3电子表格保持兼容,使其能够计算自1899年12月31日以来的天数,而不是1900年1月1日。当DateTime为 写的,它的创作者没有试图回溯到1899年12月31日,它是从1900年1月1日开始计算的。所以为了将1900年3月1日之前的1900年格式的Excel日期正确地转换成DateTime,你必须添加一天。
最后,Excel不能代表1900年1月1日之前1900年格式的日子,1904年1月1日之前的日期,以1904年格式。 因此,当您将DateTime转换为Excel日期时,必须传递一个字符串而不是表示date的数字,因为这些日期不能在Excel中表示为日期(仅作为字符串)。
结论
本章探讨了Excel对象模型中的一些最重要的对象。 我们在后续章节的Excel示例中使用了许多这些对象。 在第21章“在Excel中使用XML”中,我们还考虑了一些用于在Excel中使用XML的其他Excel对象模型对象。
本章描述了由Excel的主要互操作程序集定义的这些对象。 您应该注意到,VSTO扩展了这些对象(Workbook,Worksheet,Range,Chart,ChartObject和ListObject),以添加一些附加功能,如数据绑定支持。 本书第三部分考察了这些扩展。