VBA Excel 二维数组

12
我试图找出如何声明一个二维数组,但迄今为止我找到的所有示例都是使用设置整数进行声明的。我正试图创建一个程序,将利用两个二维数组,然后对这些数组执行简单的操作(例如查找差异或百分比)。数组是由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)那样声明数组,因为那样会得到一个索引超出范围的异常。

1
使用iDevlop的答案中提到的变体数组。它们是为Excel中的范围而设计的。 - Lance Roberts
4个回答

28
实际上,我不会使用任何REDIM,也不会使用循环将数据从工作表传输到数组:
dim arOne()
arOne = range("A2:F1000")

甚至可以

arOne = range("A2").CurrentRegion

就是这样,使用此方法填充数组比通过循环进行操作更快,无需Redim。


1
+1 这是更好的做法,我同意。我天真地只是处理了语法错误。 - David Heffernan
这是最好的方法,应该指出的是,dim语句默认使用Variant。 - Lance Roberts
我不知道宏开始运行之前尺寸是多少。谢谢您的回复。 - Jesse Smothermon
我在我的电脑上测试了一下,至少它和ReDim做的事情是一样的,而且似乎更快。快速问题,我能否使用这个CurrentRegion将一个数组填充到特定的空间中?所以它可能看起来像Range("A2").CurrentRegion = arTwo,其中arTwo是某个数组。谢谢。 - Jesse Smothermon
@iDevlop 一旦我将值设置到数组中,如何对它们执行操作?因此,s2Array具有一组值,s3Array具有另一组值,而answerArray将是这些数组之间的差异。s2Array和s3Array位于不同的Excel工作表中。我尝试执行answerArray = s2Array - s3Array,但出现类型不匹配的错误。谢谢。 - Jesse Smothermon
显示剩余4条评论

8
你需要使用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 Smothermon
@Jesse 请尝试我的编辑,将其放入空工作表中的代码中,然后按F5! - David Heffernan
@David 我遇到了一个奇怪的错误。我的测试网格从1到324,尺寸为9乘36(第二个网格从324到1)。我将你的代码严格改编以适应我的代码,除了名称不同之外没有什么大问题。目前看来,它似乎只是复制了我的第一个网格,然后弹出一个消息框,显示“无法执行所请求的操作”。谢谢。 - Jesse Smothermon
@Jesse 嗯,我猜不出是什么原因导致的,可能是你的代码有问题。无论如何,你应该按照 @iDevelop 的方式去做,这样会更容易、更快捷! - David Heffernan
@Jesse 说句实话,我已经更新了我的答案,加入了基于范围的版本。 - David Heffernan
显示剩余2条评论

5
这是一个通用的VBA数组转区域函数,可以通过一次性将数组写入工作表来提高速度,比在循环中逐个单元格地写入数据要快得多... 但是,需要进行一些必要的清理工作,以正确指定目标区域的大小。
这种“清理工作”看起来很繁琐,可能也相对较慢:但这是写入表格的“最后一英里”代码,而且与在VBA中读取或写入工作表相比,任何事情都比写入工作表快得多,甚至可以说是立即完成的。因此,在访问工作表之前,您应尽可能使用代码完成所有操作。
其中一个主要组成部分是错误捕获,我曾经到处都能看到它。我讨厌重复编码:我已经把所有东西都编写在这里了,希望您永远不再需要编写它。
一个VBA“数组到区域”的函数。
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

此外:我建议您将该声明保密。如果您必须在另一个模块中公开它作为公共子程序,请在模块头中插入Option Private Module语句。您真的不希望用户调用任何带有CopyMemory操作和指针算术的函数。

2
在编程方面,以下是翻译的内容:+1 是因为说“在睡觉前,你应该尽可能地做所有事情”,并且+1 是因为在 SO 上我见过的最大的帖子,至少在 VBA 部分。 - Anonymous Type
@iDevlop ...没错,全包括在内。部分原因是VBA不是一种简洁的语言;大部分是我们没有把参数传递给由开发人员编写的明确定义的函数,而是与用户界面进行交互——一个可以包含任何数据类型和各种错误的网格——这就要求进行广泛的防御性编程。 - Nigel Heffernan
@Nile:我只是开玩笑。我真的不觉得这有什么附加价值,与我提供的“一行解决方案”相比。https://dev59.com/CW435IYBdhLWcg3wuikn#5252452 - iDevlop
@iDevlop - 如果你只做一次,那么增加的价值是负面的。如果你每年开发超过一百个战术工具,并且随处倾泻数据,而且你必须支持它们,那么内置弹性的代码价值是相当大的。对于战术开发人员来说,最重要的生产力指标不是行输出,而是每行代码的持续工作量。 - Nigel Heffernan

2
对于这个例子,您需要创建自己的类型,即数组。然后创建一个更大的数组,其中元素是您刚刚创建的类型。
要运行我的示例,您需要在Sheet1中填写A列和B列中的一些值。然后运行test()。它将读取前两行并将值添加到BigArr中。然后它将检查您有多少行数据并将它们全部读取,从它停止读取的地方,即第三行。
在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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接