【问题标题】:Copying an array to Range.Value2 SafeArray.pvData successful, but Excel fails to update将数组复制到 Range.Value2 SafeArray.pvData 成功,但 Excel 更新失败
【发布时间】:2016-10-11 03:09:13
【问题描述】:

编辑

确实,没有直接的方法可以编辑内存中的范围值。 感谢@AndASM 的详细回答和卡尔;好预感,它是正确的。那一刻我一定是太纠结于所有的倒车,忘记了Value2只是一个属性。

同时,我用 OllyDbg 进行了一些其他测试和调试,并发现了一些有趣的事情:

  1. 细胞排列在 16 x 1024 的区域中。持有的结构 区域很可能是工作表,但我还不能确认;
  2. 每次调用Value 属性时,绝对表 偏移量(行,列)用于查找相应的区域和 然后在该区域内进行一些索引以获得实际值;
  3. 创建了一个 VARIANT 类型的 2D SAFEARRAY;
  4. 值不是在连续块中检索,而是单独检索。 这意味着范围(行,列)中的每个“点”都被发送到 索引过程为其返回一个值(显然是变体) 对应的 SAFEARRAY 元素;
  5. 由于上述原因,每次您通过 Range.Value2(row,col),整个过程都重复一遍 范围内的值。想象一下,如果你这样做,性能会受到影响 在一个过程中多次,或者更糟糕的是,在一个循环中。 只是不要;你最好创建一个Value2 的副本和 通过索引解决它;
  6. 最后但并非最不重要的一点是内部值的分布 SAFEARRAY.pvData 是基于列的 (col,row),而不是基于行的,它 可能会发现违反直觉并且与 VBA 索引不一致 模式,即(row,col)。如果您需要,这可能会派上用场 直接在内存中访问 pvData 并保持维度一致性。 例如,如下所示的范围

        1, 2, 3, 4  
        5, 6, 7, 8
    

    将按以下顺序存储在pvData

        1, 5, 2, 6, 3, 7, 4, 8
    

我希望以上内容有所帮助。 总而言之,在 Excel 中没有任何此类导出函数的情况下,最好的解决方法是创建 Value2 的副本,对其进行排序/操作以达到所需的结果并将其分配回 range 属性。


我最近完成了 QuickSort 的一个变体,并打算在 Excel 中实现。该算法是有效的,并且如果不是为了将数组值放入范围所花费的额外时间,它确实会作为加载项带来价值。转置仅适用于小于 65537,而 'paste variant-array to range 在大型排序中需要很长时间。

所以,我编写了一些程序,允许将范围内的二维值复制到一维数组中(排序需要一维)并(在排序完成后)将它们放回原处,所有这些都基于 SAFEARRAY 和 MemCopy( RtlMoveMemory),或者,WriteProcessMemory。

就内存操作而言,一切正常: - 范围值被复制到数组中(从一个 SafeArray.pvData 到另一个); - 数组值(运行排序算法后)成功复制到 Range.Value2 SafeArray.pvData。

尽管如此,范围并没有更新,因为它似乎回到了旧值(下面的代码中有更多内容)。 为什么“Range.Value2 = SomeOther2dArray”可以工作而不直接修改内存中的数据?我有一种感觉,我在这里错过了一些东西。是否也需要公式排序/更新?

这里是主要程序:

    Public Sub XLSORT_Array2()
    With Application
        screenUpdateState = .ScreenUpdating
        statusBarState = .DisplayStatusBar
        calcState = .Calculation
        eventsState = .EnableEvents

        .ScreenUpdating = False
        .DisplayStatusBar = False
        .Calculation = xlCalculationManual
        .EnableEvents = False
    End With

    Dim rngSort As Range
    Dim arrSort() As Variant
    Dim arrTemp As Variant
    Dim i As Long
    Dim dblTime As Double
    Dim dblInitTime As Double: dblInitTime = Timer

    Set rngSort = Selection

    If Not rngSort Is Nothing Then
        If rngSort.Cells.Count > 1 And rngSort.Areas.Count = 1 Then
            dblTime = Timer
            ReDim arrSort(1 To rngSort.Cells.Count)
            Debug.Print Timer - dblTime & vbTab & "(Redim)"

            'just testing Excel memory location
            'Debug.Print VarPtr(rngSort.Value2(1, 1))

            dblTime = Timer
            SA_Duplicate arrSort, rngSort.Value2
            Debug.Print Timer - dblTime & vbTab & "(Copy)"

            dblTime = Timer
            SORTVAR_QSWrapper arrSort, 1, rngSort.Cells.Count
            Debug.Print Timer - dblTime & vbTab & "(Sort)"

            'this would be the fastest method
            'variants are copied to memory
            'yet the range does not update with the new values
            SA_Duplicate rngSort.Value2, arrSort

            'dblTime = Timer
            'looping = too slow
            'For i = 1 To rngSort.Cells.Count
            '    rngSort.Cells(i).Value = arrSort(i)
            'Next

            'this works, but it's too slow, as well
            'If rngSort.Cells.Count > 65536 Then
            '    ReDim arrTemp(LBound(rngSort.Value2, 1) To UBound(rngSort.Value2, 1), LBound(rngSort.Value2, 2) To UBound(rngSort.Value2, 2))
            '    SA_Duplicate arrTemp, arrSort
            '    rngSort.Value2 = arrTemp
            'Else
            '    rngSort.Value2 = WorksheetFunction.Transpose(arrSort)
            '    Debug.Print "Transposed"
            'End If
            'Debug.Print Timer - dblTime & vbTab & "(Paste)"
        End If

    End If

    With Application
        .ScreenUpdating = screenUpdateState
        .DisplayStatusBar = statusBarState
        .Calculation = calcState
        .EnableEvents = eventsState
    End With

    Debug.Print VarPtr(rngSort.Value2(1, 1)) & vbTab & Mem_ReadHex(ByVal VarPtr(rngSort.Value2(1, 1)), rngSort.Cells.Count * 16)
    Set rngSort = Nothing
    Debug.Print Timer - dblInitTime & vbTab & "(Total Time)" & vbNewLine
End Sub

假设范围内的值是 4、3、2 和 1。 在SA_Duplicate arrSort, rngSort.Value2 之前,内存读取:

130836704   05000000 00000000 00000000 00001040 05000000 00000000 00000000 00000840 05000000 00000000 00000000 00000040 05000000 00000000 00000000 0000F03F 
129997032   00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

其中130836704Range.Value2 SafeArray.pvData129997032SortArray SafeArray.pvData。每个 16 字节批次代表从内存读取的变体实际数据(无 LE 转换,仅以十六进制表示),前 2 个字节指示 VarType。在这种情况下,vbDouble。

复制后,正如预期的那样,内存读取:

130836704   05000000 00000000 00000000 00001040 05000000 00000000 00000000 00000840 05000000 00000000 00000000 00000040 05000000 00000000 00000000 0000F03F 
129997032   05000000 00000000 00000000 00001040 05000000 00000000 00000000 00000840 05000000 00000000 00000000 00000040 05000000 00000000 00000000 0000F03F

排序完成后,SortArray SafeArray.pvData 读取:

129997032   05000000 00000000 00000000 0000F03F 05000000 00000000 00000000 00000040 05000000 00000000 00000000 00000840 05000000 00000000 00000000 00001040

执行SA_Duplicate rngSort.Value2, arrSort后,内存显示 Range.Value2 SafeArray.pvData 已更新:

129997032   05000000 00000000 00000000 0000F03F 05000000 00000000 00000000 00000040 05000000 00000000 00000000 00000840 05000000 00000000 00000000 00001040
130836704   05000000 00000000 00000000 0000F03F 05000000 00000000 00000000 00000040 05000000 00000000 00000000 00000840 05000000 00000000 00000000 00001040

到目前为止,一切看起来都很好,除了 Debug.Print VarPtr(rngSort.Value2(1, 1)) & vbTab & Mem_ReadHex[...] 显示值翻转回初始顺序:

130836704   05000000 00000000 00000000 00001040 05000000 00000000 00000000 00000840 05000000 00000000 00000000 00000040 05000000 00000000 00000000 0000F03F

请分享您认为有效的任何想法或方法。任何帮助表示赞赏。不得不等待 Excel 大约 4 秒(对 1,000,000 + 单元格进行排序),这令人沮丧,而即使是最具挑战性的排序也比这花费的时间更少。

提前致谢!

【问题讨论】:

  • 我认为了解 SA_Duplicate 的作用可能很有见地,但由于只是将其结果分配给 range.Value2 才有效,我认为新数据是正确的?因此,我怀疑 Excel 在内部将其数据保存在安全数组之外的其他结构中,并且 .Value/.Value2 属性只是执行转换的外壳;这意味着直接对这些属性返回的变体进行内存操作是没有意义的。

标签: arrays vba excel


【解决方案1】:

嗯,你没有提供几个重要部分的工作实现,尤其是SA_Duplicate,所以这主要是猜测工作。但是,我认为答案可能很简单。

Range.Value2 是一个属性,而不是一个变量。所以在幕后它实际上是两个函数,我们称它们为Range.let_Value2Range.get_Value2

也就是说,您认为SA_Duplicate rngSort.Value2, arrSort 的调用会如何工作?因为我看到的是SA_Duplicate rngSort.get_Value2, arrSort。我假设rngSort.get_Value2 正在创建一个新的SafeArray,然后将数据从Excel 的内部数据结构复制到该SafeArray。如果我是正确的,那么您将数据写入临时缓冲区,VBA 稍后会丢弃该缓冲区并且 Excel 已经忘记了。

您需要使用rngSort.let_Value2 arrSort,通常称为rngSort.Value2 = arrSort

附带说明,如果get_Value2 像我认为的那样分配了一个新数组,则两个SA_Duplicate 调用都是不必要的,您也许可以对返回的数组进行适当的排序。请记住在完成后将数组变量分配回属性,将其传递给let_Value2

【讨论】:

    猜你喜欢
    • 2014-10-13
    • 2014-11-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-28
    • 1970-01-01
    相关资源
    最近更新 更多