VBA-识别从其他子程序中调用的子程序和函数

时间:2018-07-18 17:52:23

标签: excel vba excel-vba

努力为此标题命名。我有一个包含大约20个表单/ 10个代码和类模块的项目。该项目具有多种功能,可以很好地组合在一起以解决特定目的,但也可以进行细分以满足其他项目中的类似需求。这些其他项目可能不一定需要其他组件。

这样的一个例子可能是:项目的一部分可以创建一个Powerpoint,而项目的另一部分则可以发送电子邮件。该项目同时使用这两个功能,但是另一个项目可能只需要其中一个功能。

我已尽力将这些功能分成各自独立的模块。但是,我将常用的函数放在了单独的模块(“全部”)中,以便能够使用相同的代码而无需在各个地方重复。我是自学成才的,所以如果这不是最佳做法,请告诉我。

问题是我现在希望能够将某些模块从一个项目导出到另一个项目。我正在构建一个工具,使我可以将这些模块作为功能齐全的软件包进行分发,而无需在目标文件上进行任何设置即可使用。为了做到这一点,我需要了解我的子功能所在。我的模块如何互连?如果我导出了,可以说PowerPoint模块,该模块是否尝试访问我的“全部”模块中的内容?如果是这样,我还需要将任何相关代码从该模块导出到新工作簿。

如果第一层过程是目标模块中列出的过程,那么第二层过程将是通过T1过程调用的过程。这意味着从T2过程调用T3过程,依此类推...

我该如何编写需要深入研究的Loop函数?当所有T1程序都经过充分研究后,退出条件肯定是肯定的。

下面的代码只是为我提供了目标模块中的过程列表,并将它们添加到数组中。

Sub getProcs()

Dim VBProj As VBIDE.VBProject
Dim VBComp As VBIDE.VBComponent
Dim VBMod As VBIDE.CodeModule
Dim lineNum As Integer
Dim lineText As String
Dim lineCount As Integer
Dim procArr As Variant
Dim procName As String
Dim ProcKind As VBIDE.vbext_ProcKind

'Set Target Objects
Set VBProj = ThisWorkbook.VBProject
Set VBComp = VBProj.VBComponents(2)
Set VBMod = VBComp.CodeModule
With VBMod
    If .CountOfLines > 0 Then
        lineNum = .CountOfDeclarationLines + 1
        Do Until lineNum >= .CountOfLines
            procName = .ProcOfLine(lineNum, ProcKind)
            procArr = all_dataArray(procArr, procName)
            lineNum = .ProcStartLine(procName, ProcKind) + _
                    .ProcCountLines(procName, ProcKind) + 1
        Loop
    End If
End With
End Sub

Function all_dataArray(arr As Variant, arrVal As Variant) As Variant
'Add item to array
If IsEmpty(arr) Or Not IsArray(arr) Then
    arr = Array(arrVal)
Else
    ReDim Preserve arr(0 To UBound(arr) + 1)
    arr(UBound(arr)) = arrVal
End If
all_dataArray = arr
End Function

也许我将这一切弄错了,很高兴听到任何建议。 感谢您的宝贵时间。

2 个答案:

答案 0 :(得分:2)

识别模块的成员/过程只是一个步骤,而VBIDE API会有所帮助。

下一步是确定过程调用在哪里,为了成功执行此操作,您需要获取模块的文本,然后以某种方式确定什么是过程调用以及哪个过程调用该过程被调用:为了可靠地执行此操作,您将需要“以编程方式理解” VBA代码以及VBA本身。

唯一的方法是将文本转换成 tokens 的流(这就是 lexer 的作用),然后将这些令牌流转换成树结构(这是 parser 所做的事情),然后遍历这些树并填充一个符号表,其中包含有关在什么范围中声明了什么以及哪些范围可以“看到”它的信息。然后,您需要再次遍历解析树,找到代表过程调用的节点,在符号表中找到名称,并根据VBA的语言范围划分规则对特定符号的标识符引用 resolve

标记化步骤需要考虑讨厌的烦人的事情,例如注释,行继续和预编译器指令。

一旦解析了标识符引用,您要做的就是迭代对符号表中每个过程的引用,并列出它们各自的父作用域/过程-这样您就知道谁在呼叫谁。

我想不出任何其他可靠的方式来做到这一点……而且我也无法想象在VBA中做到这一点。 Rubberduck的解析器位于45,000行C#代码的范围内,这些C#代码是通过处理针对VBA语言规范设计的Antlr4语法而生成的。而且,甚至连预处理程序和解析器部件也有其自身的挑战和复杂性。


也就是说,“全部”模块不是最佳实践。实际上,这是获取任何内容的可靠方法:如果模块(或类)是FooUtilsFooManagerFooHelper或其他模糊的内容范围的松散术语,基本上代表“我无法在其他地方放置的任何东西”,这是设计中的问题。

我不知道您的“常用”功能是什么,但是您可能需要的只是进一步模块化。将这些帮助程序方法提取到更专业的模块/类中-ListObjectExtensions模块清楚地说明了它的作用范围,因此您不应期望找到例如里面有一个Get1DArrayFromRange函数。

答案 1 :(得分:1)

我过去已经实现了它,但是没有完美的结果。它帮助我获得了不完整(或有时重载)的依赖关系列表,其余的让编译器抱怨要修复...

还要付出巨大的努力-很少需要它。实际上,如果您在编程中做得很好,则可以尝试将每个功能拆分为多个独立的小功能。因此,最后,当您要求单个功能的可靠性时,事实证明答案通常是几乎整个项目!

此代码是一个完整的模块,引用了其他模块的自定义通用功能,用于提取令牌列表,合并数组等。

无论如何,我都会发布一些我使用过的功能的摘要。我不知道是否有帮助。

祝你好运!

1。

Function GetComponentProcDependences(Optional ComponentName As String = "Module1", Optional ProjectName As String = "")
[...]
objProject = VBAEditor.VBProjects.item(ProjectName)
[...]
PutativeDependences = Quicksort(ProjectFunctionList(objProject, False, False), True)
[...]
allreadyincluded = ComponentMethodsList(ReferencesComponent, False, False)
[...]
For Each item In allreadyincluded 
Call FindProcDependences(CStr(item), ProjectName, PutativeDependences, allreadyincluded)
Next
GetComponentProcDependences = allreadyincluded
End Function

2。

Function ProjectFunctionList(Optional objProject As VBIDE.VBProject = Nothing, Optional Titles As Boolean = True, Optional args As Boolean = True)
[...]
For Each objComponent In objProject.VBComponents
    cs = ComponentMethodsList(objComponent, Titles, args)
[...]
 ProjectFunctionList= ArrayConcatenate(ex, cs)
[...]
End Function

3。

    Public Function ComponentMethodsList(objComponent As VBIDE.VBComponent, Optional inclTitle As Boolean = True, Optional args As Boolean = True)
    [...]
     Dim objCode As VBIDE.CodeModule
    [...]
    Do While iLine < objCode.CountOfLines
    sProcName = objCode.ProcOfLine(iLine, pk)
    [...]
                    pclc = objCode.ProcCountLines(sProcName, pk)
                    pblc = objCode.ProcBodyLine(sProcName, pk)
    [...]
   pcl = Replace(objCode.Lines(pblc, decllines), " _", "") 'etc
    [...]
     Call AddToListSub(IIf(inclTitle, objComponent.Name & ".", "") & sProcName & 
    IIf(args, "#" & pcl, ""), res)
    [...]
    iLine = iLine + pclc
    [...]
    Loop
    ComponentMethodsList = res
    End Function

4。

Sub FindProcDependences(ProcName As String, ProjectName As String, PutativeDependences, Optional allreadyincluded = Nothing)


compcode = GetProcCode(ProcName, ProjectName) '
residue = Array("=", """", "-", "+", ";", "(", ")", "&", " ", ",", vbTab, vbCr, vbLf, ".")
compcode = MassReplace(compcode, "+", residue)
Do
i0 = Len(compcode)
compcode = Replace(compcode, "++", "+")
Loop While i0 <> Len(compcode)
tokens = tokenizer(compcode, False, "+", allreadyincluded)
Dim temparis()
For Each item In tokens
If ArrayContains(item, PutativeDependences, "bool") And Not ArrayContains(item, allreadyincluded) Then
Call AddToArrayListIfUnique(item, temparis)
End If
Next

[...]

allreadyincluded = ArrayConcatenate(allreadyincluded, temparis)

For Each item In temparis
Call FindProcDependences(CStr(item), ProjectName, PutativeDependences, allreadyincluded)  'Recursive calls
Next

End Sub

5。

Public Function GetProcCode(ProcName As String, Optional ProjectName As String = "")
Dim objComponent As VBIDE.VBComponent
Dim objCode As VBIDE.CodeModule
Set objComponent = FindComponentContainsProc(ProcName, ProjectName)
Set objCode = objComponent.CodeModule
iLine = objComponent.CodeModule.ProcStartLine(ProcName, vbext_pk_Proc)
pclc = objCode.ProcCountLines(ProcName, pk)
GetProcCode = objCode.Lines(iLine, pclc)
Set objComponent = Nothing
Set objCode = Nothing
End Function

6。

Function FindComponentContainsProc(ProcName As String, Optional ProjectName As String = "", Optional ProcKind As VBIDE.vbext_ProcKind = vbext_pk_Proc) As VBIDE.VBComponent
[...]
Dim objComponent As VBIDE.VBComponent
For Each objComponent In objProject.VBComponents
pupProcLine = 0
Dim objCode As VBIDE.CodeModule
Set objCode = objComponent.CodeModule
On Error Resume Next
pupProcLine = objCode.ProcStartLine(ProcName, ProcKind)
If pupProcLine > 0 Then Exit For
Next
[...]
End Function