【发布时间】:2016-10-11 03:09:13
【问题描述】:
编辑
确实,没有直接的方法可以编辑内存中的范围值。
感谢@AndASM 的详细回答和卡尔;好预感,它是正确的。那一刻我一定是太纠结于所有的倒车,忘记了Value2只是一个属性。
同时,我用 OllyDbg 进行了一些其他测试和调试,并发现了一些有趣的事情:
- 细胞排列在 16 x 1024 的区域中。持有的结构 区域很可能是工作表,但我还不能确认;
- 每次调用
Value属性时,绝对表 偏移量(行,列)用于查找相应的区域和 然后在该区域内进行一些索引以获得实际值; - 创建了一个 VARIANT 类型的 2D SAFEARRAY;
- 值不是在连续块中检索,而是单独检索。 这意味着范围(行,列)中的每个“点”都被发送到 索引过程为其返回一个值(显然是变体) 对应的 SAFEARRAY 元素;
- 由于上述原因,每次您通过
Range.Value2(row,col),整个过程都重复一遍 范围内的值。想象一下,如果你这样做,性能会受到影响 在一个过程中多次,或者更糟糕的是,在一个循环中。 只是不要;你最好创建一个Value2的副本和 通过索引解决它; -
最后但并非最不重要的一点是内部值的分布
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
其中130836704 是Range.Value2 SafeArray.pvData,129997032 是SortArray 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 属性只是执行转换的外壳;这意味着直接对这些属性返回的变体进行内存操作是没有意义的。