VBA Excel 二维数组

Posted

技术标签:

【中文标题】VBA Excel 二维数组【英文标题】:VBA Excel 2-Dimensional Arrays 【发布时间】:2011-03-09 21:01:42 【问题描述】:

我试图找出如何声明二维数组,但到目前为止我发现的所有示例都是用集合整数声明的。我正在尝试创建一个程序,该程序将利用两个二维数组,然后对这些数组执行简单的操作(例如查找差异或百分比)。数组由 Excel 工作表中的数字填充(一组数字在 Sheet1 上,另一组在 Sheet2 上,两组数字的行数和列数相同)。

由于我不知道有多少行或列,我打算使用变量。

Dim s1excel As Worksheet
Dim s2excel As Worksheet
Dim s3excel As Worksheet
Dim firstSheetName As String
Dim secondSheetName As String
Dim totalRow As Integer
Dim totalCol As Integer
Dim iRow As Integer
Dim iCol As Integer

Set s1excel = ThisWorkbook.ActiveSheet

' Open the "Raw_Data" workbook
Set wbs = Workbooks.Open(file_path & data_title)
wbs.Activate
ActiveWorkbook.Sheets(firstSheetName).Select
Set s2excel = wbs.ActiveSheet

' Find totalRow, totalColumn (assumes there's values in Column A and Row 1 with no blanks)
totalRow = ActiveSheet.Range("A1").End(xlDown).Row
totalCol = ActiveSheet.Range("A1").End(xlToRight).Column

Dim s2Array(totalRow, totalCol)
Dim s3Array(totalRow, totalCol)

For iRow = 1 To totalRow
    For iCol = 1 To totalCol
        s2Array(iRow, iCol) = Cells(iRow, iCol)
    Next iCol
Next iRow

ActiveWorkbook.Sheets(secondSheetName).Select
Set s3excel = wbs.ActiveSheet

For iRow = 1 To totalRow
    For iCol = 1 To totalCol
        s3Array(iRow, iCol) = Cells(iRow, iCol)
    Next iCol
Next iRow

当我尝试运行它时,我在Dim s2Array(totalRow, totalCol) 处收到一个编译时错误,说需要一个常量表达式。如果我将其更改为Dim s2Array(1 To totalRow, 1 To totalCol),则会发生相同的错误。因为我从一开始就不知道尺寸是多少,所以我不能像Dim s2Array(1, 1) 那样声明它,因为那样我会得到一个越界异常。

谢谢,

杰西·斯莫莫

【问题讨论】:

按照 iDevlop 的回答使用变体数组。它们是为 Excel 中的范围制作的。 【参考方案1】:

事实上,我不会使用任何 REDIM,也不会使用循环将数据从工作表传输到数组:

dim arOne()
arOne = range("A2:F1000")

甚至

arOne = range("A2").CurrentRegion

就是这样,你的数组填充速度比循环快得多,没有 redim。

【讨论】:

+1 我同意这是一种更好的方法。我天真地处理了语法错误。 这是最好的办法,需要指出的是dim语句默认使用Variant。 我不知道宏开始运行之前的尺寸是多少。感谢您的回复 在我的身上测试过,它至少和 ReDim 做的一样,而且看起来确实更快。快速提问,我可以使用这个 CurrentRegion 来用数组填充特定空间吗?所以它可能看起来像 Range("A2").CurrentRegion = arTwo 其中 arTwo 是一些数组。谢谢 我在 CurrentRegion 上阅读了更多内容,我是否正确,它只是突出显示该区域还是将值存储到数组中(这里需要一个二维数组)?【参考方案2】:

你需要ReDim:

m = 5
n = 8
Dim my_array()
ReDim my_array(1 To m, 1 To n)
For i = 1 To m
  For j = 1 To n
    my_array(i, j) = i * j
  Next
Next

For i = 1 To m
  For j = 1 To n
    Cells(i, j) = my_array(i, j)
  Next
Next

正如其他人所指出的,使用范围可以更好地解决您的实际问题。你可以试试这样的:

Dim r1 As Range
Dim r2 As Range
Dim ws1 As Worksheet
Dim ws2 As Worksheet

Set ws1 = Worksheets("Sheet1")
Set ws2 = Worksheets("Sheet2")
totalRow = ws1.Range("A1").End(xlDown).Row
totalCol = ws1.Range("A1").End(xlToRight).Column

Set r1 = ws1.Range(ws1.Cells(1, 1), ws1.Cells(totalRow, totalCol))
Set r2 = ws2.Range(ws2.Cells(1, 1), ws2.Cells(totalRow, totalCol))
r2.Value = r1.Value

【讨论】:

谢谢,它似乎在工作,但我无法确定,因为我实际上看不到里面有什么。您知道如何将值打印到单元格中吗?这是我到目前为止所拥有的: ActiveWorkbook.Sheets(firstSheetName).Copy Before:=ActiveWorkbook.Sheets(firstSheetName) For iRow = 1 To totalRow For iCol = 1 To totalCol ActiveSheet.Cells(iRow, iCol) = answerArray(iRow, iCol ) Next iCol Next iRow 但这实际上并没有输出任何东西 @Jesse 尝试我的编辑,将 if 填充到空工作表中的代码中,然后按 F5! @David 遇到了一个奇怪的错误。我的测试网格从 1 变为 324,尺寸为 9 x 36(第二个网格从 324 变为 1)。我严格更改您的代码以适合我的代码,所以除了名称不同之外没有什么大的不同。到目前为止,它似乎只是复制了我的第一个网格,然后有一个消息框显示“无法执行请求的操作”。谢谢 @Jesse 好吧,我猜不出是什么原因,一定是你的代码有问题。无论如何,你应该按照@iDevelop的方式去做,它会更容易和更快! @Jesse 对于它的价值,我已经更新了我的答案以包含一个基于范围的版本。【参考方案3】:

这是一个通用的 VBA Array To Range 函数,它在对工作表的一次“点击”中将一个数组写入工作表。这比在行和列的循环中一次一个单元格地将数据写入工作表中要快很多...但是,有一些内务工作要做,因为您必须指定目标的大小范围正确。

这个“家务管理”看起来工作量很大,而且可能相当慢:但这是写入工作表的“最后一英里”代码,一切都比写入工作表更快。或者至少,与读取或写入工作表(即使在 VBA 中)相比,它实际上是瞬时的快得多,并且您应该在点击工作表之前在代码中做所有可能的事情。

其中一个主要组成部分是错误捕获,我过去常常看到它无处不在。我讨厌重复的编码:我已经在这里全部编码了,而且 - 希望 - 你再也不用写了。

VBA 'Array to Range' 函数

Public Sub ArrayToRange(rngTarget As Excel.Range, InputArray As Variant)
' Write an array to an Excel range in a single 'hit' to the sheet
' InputArray must be a 2-Dimensional structure of the form Variant(Rows, Columns)

' The target range is resized automatically to the dimensions of the array, with
' the top left cell used as the start point.

' This subroutine saves repetitive coding for a common VBA and Excel task.

' If you think you won't need the code that works around common errors (long strings
' and objects in the array, etc) then feel free to comment them out.

On Error Resume Next

'
' Author: Nigel Heffernan
' HTTP://Excellerando.blogspot.com
'
' This code is in te public domain: take care to mark it clearly, and segregate
' it from proprietary code if you intend to assert intellectual property rights
' or impose commercial confidentiality restrictions on that proprietary code

Dim rngOutput As Excel.Range

Dim iRowCount   As Long
Dim iColCount   As Long
Dim iRow        As Long
Dim iCol        As Long
Dim arrTemp     As Variant
Dim iDimensions As Integer

Dim iRowOffset  As Long
Dim iColOffset  As Long
Dim iStart      As Long


Application.EnableEvents = False
If rngTarget.Cells.Count > 1 Then
    rngTarget.ClearContents
End If
Application.EnableEvents = True

If IsEmpty(InputArray) Then
    Exit Sub
End If


If TypeName(InputArray) = "Range" Then
    InputArray = InputArray.Value
End If

' Is it actually an array? IsArray is sadly broken so...
If Not InStr(TypeName(InputArray), "(") Then
    rngTarget.Cells(1, 1).Value2 = InputArray
    Exit Sub
End If


iDimensions = ArrayDimensions(InputArray)

If iDimensions < 1 Then

    rngTarget.Value = CStr(InputArray)

ElseIf iDimensions = 1 Then

    iRowCount = UBound(InputArray) - LBound(InputArray)
    iStart = LBound(InputArray)
    iColCount = 1

    If iRowCount > (655354 - rngTarget.Row) Then
        iRowCount = 655354 + iStart - rngTarget.Row
        ReDim Preserve InputArray(iStart To iRowCount)
    End If

    iRowCount = UBound(InputArray) - LBound(InputArray)
    iColCount = 1

    ' It's a vector. Yes, I asked for a 2-Dimensional array. But I'm feeling generous.
    ' By convention, a vector is presented in Excel as an arry of 1 to n rows and 1 column.
    ReDim arrTemp(LBound(InputArray, 1) To UBound(InputArray, 1), 1 To 1)
    For iRow = LBound(InputArray, 1) To UBound(InputArray, 1)
        arrTemp(iRow, 1) = InputArray(iRow)
    Next

    With rngTarget.Worksheet
        Set rngOutput = .Range(rngTarget.Cells(1, 1), rngTarget.Cells(iRowCount + 1, iColCount))
        rngOutput.Value2 = arrTemp
        Set rngTarget = rngOutput
    End With

    Erase arrTemp

ElseIf iDimensions = 2 Then

    iRowCount = UBound(InputArray, 1) - LBound(InputArray, 1)
    iColCount = UBound(InputArray, 2) - LBound(InputArray, 2)

    iStart = LBound(InputArray, 1)

    If iRowCount > (65534 - rngTarget.Row) Then
        iRowCount = 65534 - rngTarget.Row
        InputArray = ArrayTranspose(InputArray)
        ReDim Preserve InputArray(LBound(InputArray, 1) To UBound(InputArray, 1), iStart To iRowCount)
        InputArray = ArrayTranspose(InputArray)
    End If


    iStart = LBound(InputArray, 2)
    If iColCount > (254 - rngTarget.Column) Then
        ReDim Preserve InputArray(LBound(InputArray, 1) To UBound(InputArray, 1), iStart To iColCount)
    End If



    With rngTarget.Worksheet

        Set rngOutput = .Range(rngTarget.Cells(1, 1), rngTarget.Cells(iRowCount + 1, iColCount + 1))

        Err.Clear
        Application.EnableEvents = False
        rngOutput.Value2 = InputArray
        Application.EnableEvents = True

        If Err.Number <> 0 Then
            For iRow = LBound(InputArray, 1) To UBound(InputArray, 1)
                For iCol = LBound(InputArray, 2) To UBound(InputArray, 2)
                    If IsNumeric(InputArray(iRow, iCol)) Then
                        ' no action
                    Else
                        InputArray(iRow, iCol) = "" & InputArray(iRow, iCol)
                        InputArray(iRow, iCol) = Trim(InputArray(iRow, iCol))
                    End If
                Next iCol
            Next iRow
            Err.Clear
            rngOutput.Formula = InputArray
        End If 'err<>0

        If Err <> 0 Then
            For iRow = LBound(InputArray, 1) To UBound(InputArray, 1)
                For iCol = LBound(InputArray, 2) To UBound(InputArray, 2)
                    If IsNumeric(InputArray(iRow, iCol)) Then
                        ' no action
                    Else
                        If Left(InputArray(iRow, iCol), 1) = "=" Then
                            InputArray(iRow, iCol) = "'" & InputArray(iRow, iCol)
                        End If
                        If Left(InputArray(iRow, iCol), 1) = "+" Then
                            InputArray(iRow, iCol) = "'" & InputArray(iRow, iCol)
                        End If
                        If Left(InputArray(iRow, iCol), 1) = "*" Then
                            InputArray(iRow, iCol) = "'" & InputArray(iRow, iCol)
                        End If
                    End If
                Next iCol
            Next iRow
            Err.Clear
            rngOutput.Value2 = InputArray
        End If 'err<>0

        If Err <> 0 Then
            For iRow = LBound(InputArray, 1) To UBound(InputArray, 1)
                For iCol = LBound(InputArray, 2) To UBound(InputArray, 2)

                    If IsObject(InputArray(iRow, iCol)) Then
                        InputArray(iRow, iCol) = "[OBJECT] " & TypeName(InputArray(iRow, iCol))
                    ElseIf IsArray(InputArray(iRow, iCol)) Then
                        InputArray(iRow, iCol) = Split(InputArray(iRow, iCol), ",")
                    ElseIf IsNumeric(InputArray(iRow, iCol)) Then
                        ' no action
                    Else
                        InputArray(iRow, iCol) = "" & InputArray(iRow, iCol)
                        If Len(InputArray(iRow, iCol)) > 255 Then
                            ' Block-write operations fail on strings exceeding 255 chars. You *have*
                            ' to go back and check, and write this masterpiece one cell at a time...
                            InputArray(iRow, iCol) = Left(Trim(InputArray(iRow, iCol)), 255)
                        End If
                    End If
                Next iCol
            Next iRow
            Err.Clear
            rngOutput.Text = InputArray
        End If 'err<>0

        If Err <> 0 Then
            Application.ScreenUpdating = False
            Application.Calculation = xlCalculationManual
            iRowOffset = LBound(InputArray, 1) - 1
            iColOffset = LBound(InputArray, 2) - 1
            For iRow = 1 To iRowCount
                If iRow Mod 100 = 0 Then
                    Application.StatusBar = "Filling range... " & CInt(100# * iRow / iRowCount) & "%"
                End If
                For iCol = 1 To iColCount
                    rngOutput.Cells(iRow, iCol) = InputArray(iRow + iRowOffset, iCol + iColOffset)
                Next iCol
            Next iRow
            Application.StatusBar = False
            Application.ScreenUpdating = True


        End If 'err<>0


        Set rngTarget = rngOutput   ' resizes the range This is useful, *most* of the time

    End With

End If

End Sub

您需要 ArrayDimensions 的来源:

模块头中需要此 API 声明:

Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
                   (Destination As Any, _
                    Source As Any, _
                    ByVal Length As Long)

...这是函数本身:

Private Function ArrayDimensions(arr As Variant) As Integer
  '-----------------------------------------------------------------
  ' will return:
  ' -1 if not an array
  ' 0  if an un-dimmed array
  ' 1  or more indicating the number of dimensions of a dimmed array
  '-----------------------------------------------------------------


  ' Retrieved from Chris Rae's VBA Code Archive - http://chrisrae.com/vba
  ' Code written by Chris Rae, 25/5/00

  ' Originally published by R. B. Smissaert.
  ' Additional credits to Bob Phillips, Rick Rothstein, and Thomas Eyde on VB2TheMax

  Dim ptr As Long
  Dim vType As Integer

  Const VT_BYREF = &H4000&

  'get the real VarType of the argument
  'this is similar to VarType(), but returns also the VT_BYREF bit
  CopyMemory vType, arr, 2

  'exit if not an array
  If (vType And vbArray) = 0 Then
    ArrayDimensions = -1
    Exit Function
  End If

  'get the address of the SAFEARRAY descriptor
  'this is stored in the second half of the
  'Variant parameter that has received the array
  CopyMemory ptr, ByVal VarPtr(arr) + 8, 4

  'see whether the routine was passed a Variant
  'that contains an array, rather than directly an array
  'in the former case ptr already points to the SA structure.
  'Thanks to Monte Hansen for this fix

  If (vType And VT_BYREF) Then
    ' ptr is a pointer to a pointer
    CopyMemory ptr, ByVal ptr, 4
  End If

  'get the address of the SAFEARRAY structure
  'this is stored in the descriptor

  'get the first word of the SAFEARRAY structure
  'which holds the number of dimensions
  '...but first check that saAddr is non-zero, otherwise
  'this routine bombs when the array is uninitialized

  If ptr Then
    CopyMemory ArrayDimensions, ByVal ptr, 2
  End If

End Function

另外:我建议您将该声明保密。如果您必须将其设为另一个模块中的公共 Sub,请在模块标题中插入 Option Private Module 语句。您真的不希望您的用户使用 CopyMemory 操作和指针算法调用任何函数。

【讨论】:

+1 表示“你应该在击中工作表之前尽一切可能”和 +1 表示我在 SO 上看到的最庞大的帖子.. 至少在 VBA 部分. @iDevlop ...是的,所有的东西。部分原因是 VBA 不是一种简洁的语言。大多数情况下,我们不是将参数传递给由开发人员编写的定义良好的函数,而是与用户界面交互 - 一个可以包含任何数据类型和各种错误的网格 - 这需要大量的防御性编码。跨度> @Nile:我在开玩笑。与我给***.com/a/5252452/78522 的“单线解决方案”相比,我没有真正看到附加值 @iDevlop - 如果你只做过一次,附加值是负的。如果您每年开发超过一百种战术工具,将数据转储到任何地方的范围内,并且您必须支持它们,具有内置弹性的代码的价值是相当可观的。对于战术开发人员来说,最重要的生产力指标不是行输出,而是每行代码的持续工作量。【参考方案4】:

对于本例,您需要创建自己的类型,即数组。然后你创建一个更大的数组,其中的元素是你刚刚创建的类型。

要运行我的示例,您需要用一些值填充 Sheet1 中的 AB 列。然后运行 ​​test()。它将首先读取两行并将值添加到BigArr。然后它会检查你有多少行数据,并从它停止读取的地方,即第 3 行读取它们。

在 Excel 2007 中测试。

Option Explicit
Private Type SmallArr
  Elt() As Variant
End Type

Sub test()
    Dim x As Long, max_row As Long, y As Long
    '' Define big array as an array of small arrays
    Dim BigArr() As SmallArr
    y = 2
    ReDim Preserve BigArr(0 To y)
    For x = 0 To y
        ReDim Preserve BigArr(x).Elt(0 To 1)
        '' Take some test values
        BigArr(x).Elt(0) = Cells(x + 1, 1).Value
        BigArr(x).Elt(1) = Cells(x + 1, 2).Value
    Next x
    '' Write what has been read
    Debug.Print "BigArr size = " & UBound(BigArr) + 1
    For x = 0 To UBound(BigArr)
        Debug.Print BigArr(x).Elt(0) & " | " & BigArr(x).Elt(1)
    Next x
    '' Get the number of the last not empty row
    max_row = Range("A" & Rows.Count).End(xlUp).Row

    '' Change the size of the big array
    ReDim Preserve BigArr(0 To max_row)

    Debug.Print "new size of BigArr with old data = " & UBound(BigArr)
    '' Check haven't we lost any data
    For x = 0 To y
        Debug.Print BigArr(x).Elt(0) & " | " & BigArr(x).Elt(1)
    Next x

    For x = y To max_row
        '' We have to change the size of each Elt,
        '' because there are some new for,
        '' which the size has not been set, yet.
        ReDim Preserve BigArr(x).Elt(0 To 1)
        '' Take some test values
        BigArr(x).Elt(0) = Cells(x + 1, 1).Value
        BigArr(x).Elt(1) = Cells(x + 1, 2).Value
    Next x

    '' Check what we have read
    Debug.Print "BigArr size = " & UBound(BigArr) + 1
    For x = 0 To UBound(BigArr)
        Debug.Print BigArr(x).Elt(0) & " | " & BigArr(x).Elt(1)
    Next x

End Sub

【讨论】:

以上是关于VBA Excel 二维数组的主要内容,如果未能解决你的问题,请参考以下文章

从工作表将二维数组传递给 VBA/UDF 函数

如何在 Excel 中显示来自 Access 的二维数组记录集?

vba 代码中有二维动态数组S,第1次重定义为50行,但由于只用到j行

vba 赋值给二维数组?

vba中怎么选取二维数组中的某一行或某一列?

VBA:将二维数组传递给集合函数[重复]