65.9K
CodeProject 正在变化。 阅读更多。
Home

VBA 代码模块的解构

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (24投票s)

2013年8月20日

CPOL

12分钟阅读

viewsIcon

112931

downloadIcon

4007

本文旨在通过重点介绍 VBA 代码模块的反向工程来扩展可扩展性知识库。

引言

在使用 VBA 时,一个有用的工具是用于创建工作提取的模块。如果您构建框架,所有组件和过程的完整列表将有助于在模型中保持一致性。自 Visual Basic 编辑器集成到 Microsoft Office 应用程序套件以来,这是可以实现的。

本文旨在通过重点介绍 VBA 代码模块的反向工程来扩展可扩展性知识库。目标是报告活动 VBA 项目中的所有过程。本文附带了一个标准的 VBA 模块,该模块采用了反向工程技术,并且是一个完整的提取和报告包。

请注意,要使用本文中的代码库,任何目标 Office 应用程序都必须支持VBE 接口

 

Visual Basic 编辑器 (VBE)

Visual Basic 编辑器是一个用于创建、修改和维护 Visual Basic for Applications 对象的接口。大多数 Microsoft Office 应用程序都包含一个内置的 VBE 接口,用于访问底层的 Microsoft Visual Basic for Applications Extensibility 库 (VBIDE)。接口是 VBE,其实现是 VBIDE,代码是 VBA。

在运行时,VBIDE 的实例会通过 Application 对象的 VBE 属性自动可用。使用该属性或其暴露的接口不需要其他类型库。唯一的要求是目标 Office 应用程序支持该接口。

因此,本项目不进行早期绑定到可扩展性类型库。所有可扩展性对象都声明为 Object,并且任何必需的常量都已在本文提供的模块中手动重新创建。

在必需的对象就位后,焦点便可以转移到 VBA 代码模块和 VBE CodeModule 对象本身的反向工程。

CodeModule 对象

VBComponent 是一个容器,用于区分 VBA 项目中的代码对象。组件可以是标准模块、类模块、用户窗体或文档。每个 VBComponent 包含一个代码模块,CodeModule 对象提供对它的访问。

将 CodeModule 对象与 VBA 的功能结合起来,两者就成为一个强大的编辑器。与大多数编辑器一样,可以插入、删除和替换行。该对象包括查找-查找下一个功能,并且可以检索行(或行块)。在本项目的范围内,行被索引、计数,有时也会被检索。

CodeModule 对象按行解析 VBA 模块。Lines 集合从第 1 行开始,直到达到 CountOfLines,即模块的最后一行。可以使用该集合检索任何单行或多行。

模块还可以分为声明和过程。声明部分从第 1 行开始,直到达到 CountOfDeclarationLines,即模块中最后一个非过程声明。过程部分从 CountOfDeclarationLines + 1 开始,直到达到 CountOfLines

 

如果模块中没有过程,则所有行都属于声明部分。如果模块中没有声明,则所有行都属于过程部分,前提是存在过程。

以下示例将提取活动项目中的所有代码,并在立即窗口中显示。声明和过程已分配了各自的迭代器,以便能够分段处理模块。它还使用 Lines 集合显示索引所代表的单行。

Public Sub ListCode()

    Dim Component As Object
    Dim Index As Long

    For Each Component In Application.VBE.ActiveVBProject.VBComponents

        With Component.CodeModule

            'The Declarations
            For Index = 1 To .CountOfDeclarationLines
                Debug.Print .Lines(Index, 1)
            Next Index

            'The Procedures
            For Index = .CountOfDeclarationLines + 1 To .CountOfLines
                Debug.Print .Lines(Index, 1)
            Next Index

        End With

    Next Component

End Sub

过程块

过程由开发人员定义。可用的过程类型内置于编程语言中。CodeModule 对象将 Property Get、Property Let 和 Property Set 语句识别为过程类型。它还识别 SubFunction,但不区分两者,而是将它们统称为 Proc

对于 CodeModule 对象而言,过程是一系列具有位置和长度的行。它将位置定义为 ProcStartLine,将长度定义为 ProcCountLines。它还定义了 ProcBodyLine,这是过程声明在块内的位置。

ProcStartLineProcBodyLine 是从第 1 行计算的行号。ProcCountLines 是从 ProcStartLineProcCountLines(包括)之间的行数。

 

一个过程包括其声明之上的所有空格和注释,并以其终止块结束。例外情况是模块中的最后一个过程,因为它还包括模块末尾的所有空格和注释。

位置和长度属性可用于索引过程,但要使用这些属性,需要过程的 Name 和过程的 Kind

在此项目中,ProcOfLine 属性用于在给定行号处确定过程的名称和类型。要使用 ProcOfLine,需要提供行号和一个长变量名。ProcOfLine 将检查该行并返回拥有它的过程的名称。它还将使用过程的类型填充长变量。

一旦确定了过程名称和类型,就可以使用位置和长度属性来索引单个过程,或遍历模块中的所有过程。

在以下示例中,索引从 CountOfDeclarationLines + 1 开始,这是过程部分的起始位置。调用 ProcOfLine 并使用索引会填充 Name 和 Kind 变量的值。拥有这两个值后,就可以使用 ProcStartLineProcCountLines 来计算当前过程的结束位置(ProcStartLine + ProcCountLines),然后将索引移到下一个过程的开始位置(+1)。

Public Sub ListNames()

    Dim Component As Object
    Dim Name As String
    Dim Kind As Long
    Dim Index As Long

    For Each Component In Application.VBE.ActiveVBProject.VBComponents

        With Component.CodeModule

            'The Procedures
            Index = .CountOfDeclarationLines + 1

            Do While Index < .CountOfLines
                Name = .ProcOfLine(Index, Kind)
                Debug.Print Component.Name & "." & Name
                Index = .ProcStartLine(Name, Kind) + .ProcCountLines(Name, Kind) + 1
            Loop

        End With

    Next Component

End Sub

ListNames 将按模块显示活动项目中的所有过程。但是,列表中可能包含单个模块中的重复项。重复项属于具有 GetLetSet 定义的属性。要生成模块中过程的唯一列表,过程的 Kind 应包含在结果集中。

数据转换

在此项目中,为了报告目的,数据从值转换为有意义的名称。已从 VBIDE 库中提取了常量值,用于定义组件类型、过程类型和引用类型。除了过程类型外,使用标准 case 语句将定义的对象从值转换为名称。

'VBIDE.vbext_ComponentType
Private Const vbext_ct_ActiveXDesigner As Long = 11
Private Const vbext_ct_ClassModule As Long = 2
Private Const vbext_ct_Document As Long = 100
Private Const vbext_ct_MSForm As Long = 3
Private Const vbext_ct_StdModule As Long = 1

'VBIDE.vbext_ProcKind
Private Const vbext_pk_Get As Long = 3
Private Const vbext_pk_Let As Long = 1
Private Const vbext_pk_Set As Long = 2
Private Const vbext_pk_Proc As Long = 0

'VBIDE.vbext_RefKind
Private Const vbext_rk_Project As Long = 1
Private Const vbext_rk_TypeLib As Long = 0

正如您可能还记得在过程讨论中,CodeModule 对象不区分 SubFunction。该对象将它们统称为 Proc,或者实际上是 VBIDE.vbext_ProcKind.vbext_pk_Proc。如果需要区分 SubFunction,则需要采用一种变通方法。

以下示例执行从 vbext_ProcKind 到有意义名称的数据转换。该函数接收 vbext_ProcKind enum 的长值以及来自 ProcBodyLine 的声明文本。一个“最佳猜测”变通方法会检查声明文本是否包含“Function-Space”一词。如果匹配,则假定为 Function,否则假定为 Sub

Public Function GetProcKind(Kind As Long, Declaration As String) As String

    'Transform the procedure kind to text
    Select Case Kind

        Case vbext_pk_Get
            GetProcKind = "Get"

        Case vbext_pk_Let
            GetProcKind = "Let"

        Case vbext_pk_Set
            GetProcKind = "Set"

        'Best Guess
        Case vbext_pk_Proc
            If InStr(1, Declaration, "Function ", vbBinaryCompare) > 0 Then
                GetProcKind = "Func"
            Else
                GetProcKind = "Sub"
            End If

        Case Else
            GetProcKind = "Undefined"

    End Select

End Function

使用格式化的“最佳猜测”方法是成功的,但很少会内联注释过程声明。但是,作为开发人员,知道要寻找什么,使得诸如“最佳猜测”之类的代码片段易于修改。

过程报告

此时,似乎应该结束对 CodeModule 对象这一简短讨论,并附上一份过程报告。最后一个示例生成了活动项目中所有过程的唯一列表,并附带行数。行数是计算过程声明与其终止块之间的所有行的简单方法。

该示例还采用了 GetProcKind 转换,并开始压缩变量名。如果您理解了这段代码,那么您就基本掌握了编写自己的可扩展性报告的技能。其余的就留给您自己去完善了。

Public Sub ReportProcNames()

    Dim Component As Object
    Dim Name As String
    Dim Kind As Long
    Dim Start As Long
    Dim Body As Long
    Dim Length As Long
    Dim BodyLines As Long
    Dim Declaration As String
    Dim ProcedureType As String
    Dim Index As Long

    For Each Component In Application.VBE.ActiveVBProject.VBComponents

        With Component.CodeModule

            'The Procedures
            Index = .CountOfDeclarationLines + 1

            Do While Index < .CountOfLines

                Name = .ProcOfLine(Index, Kind)
                Start = .ProcStartLine(Name, Kind)
                Body = .ProcBodyLine(Name, Kind)
                Length = .ProcCountLines(Name, Kind)
                BodyLines = Length - (Body - Start)
                Declaration = Trim(.Lines(Body, 1))
                ProcedureType = GetProcKind(Kind, Declaration)

                Debug.Print Component.Name & "." & Name & "." & _
                    ProcedureType & "." & CStr(BodyLines)

                Index = Start + Length + 1

            Loop

        End With

    Next Component

End Sub

MProject 设置

MProject 是一个标准的 VBA 模块,随本文提供。它包含一个名为 ExportProject 的公共过程,用于提取活动 VBA 项目中的所有代码。

要使用 MProject,最低要求是 Office 2003 或更高版本。此外,任何目标 Office 应用程序都必须支持 VBE 接口。(有关详细信息,请参阅支持部分

要将 MProject 添加到您的 Office 应用程序中,请按照以下步骤操作...

  1. 创建一个文件夹
  2. 将您的 Office 项目移动到该文件夹
  3. 将 MProject 模块导入到 Office 项目中
  4. 编译并保存项目

必须将 Office 项目移至其自己的文件夹。调用 ExportProject 会在 Office 项目文件的当前位置自动创建一个名为“VBA”的子文件夹结构。VBA 文件夹将包含 CodeReport 子文件夹,如下面的图像所示

        MyOfficeProject
          VBA 
            Code
            Report

在每次提取运行时开始时,CodeReport 文件夹将被创建或清除所有文件。然后将组件提取到 Code 文件夹。如果请求了工作簿报告,则将其保存到 Report 文件夹。ExportProject 不会更改任何其他文件夹或文件。

ExportProject 用法

使用 ExportProject 非常简单。在立即窗口中键入 ExportProject 并按 Enter 键。应该会出现一个立即窗口报告,详细说明运行结果。报告显示 VBA 基本文件夹位置以及十个看起来最有用的摘要值。

ExportProject
BaseFolder 	    C:\Users\Mark Regal\Desktop\MyOfficeProject\VBA
Extracts 	    6
References 	    5
Components 	    5
Procedures 	    11
UniqueNames 	    11
Declarations 	    41
CodeLines 	    250
Comments 	    235
TotalCode 	    291
TotalLines 	    526
Done...

通常,提取计数和组件计数会不同。MProject 模块始终与活动项目一起提取,但是,除非要求,否则它不包含在计数和计算中。(有关详细信息,请参阅语法部分

摘要定义如下...

BaseFolder   当前 Office 项目的 VBA 文件夹
Extracts   所有代码模块提取的数量
参考文献   所有项目和类型库引用的数量
Components   用于计算和计数的组件总数
流程   所有过程的数量
UniqueNames   来自所有模块的唯一过程名称的数量
声明   所有声明的数量
CodeLines   所有代码行的数量
注释   所有注释和空格的数量
TotalCode   CodeLines + 声明的总和
TotalLines   CodeLines + 声明 + 注释的总和

请注意,应将选择性地注释(启用或禁用)报告行视为常规做法。报告行是简单的 Debug.Print 语句,存在于 MProject.ExportProject 过程中。

工作簿报告

如果请求,ExportProject 将在提取运行结束时创建一个 Excel 工作簿。该工作簿包含活动项目中过程、组件和引用的详细视图。它还包括一个提取的组件及其文件和路径信息的列表。

MProject 设置中所述,工作簿始终保存在 VBA 文件夹结构中的 Report 文件夹中。如果未请求工作簿,则应预期一个空的 Report 文件夹,因为它会在每次提取运行时开始时被清除。

需要注意的是,工作簿和立即报告都使用相同的数据,但显示细节的程度不同。代码行和声明被计算为实际代码行。注释和空格被计算为注释,总行数是两者的摘要。

ExportProject 语法

ExportProject([DisplayWorkbook [,OpenBaseFolder [,ExcludeModule]]])

DisplayWorkbook 可选的布尔值

True 在成功运行完成后显示 Excel 工作簿报告
False(或无值)不创建报告
默认值为 False

OpenBaseFolder 可选的布尔值

True 在成功运行完成后打开基础提取文件夹
False(或无值)不执行任何操作
默认值为 False

ExcludeModule 可选的布尔值

True(或无值)将在所有计数和计算中排除 MProject
False 将 MProject 包含在所有计数和计算中
无论此设置如何,MProject 始终是代码提取的一部分
默认值为 True

*请注意,如果重命名 MProject 模块,则必须将 Module_Name 常量的值更改为与新名称匹配。

版本历史

  版本      发布日期    
  2.0      2013.11.12    
  1.0      2013.08.20    

发布说明 - 版本 2.0

  • 添加了唯一过程名称工作表
  • 向过程工作表添加了范围列
  • 重新格式化了过程工作表
  • 重新格式化了消息

发布说明 - 版本 1.0

  • 初始发布

Office VBE 支持

要使用本项目中的代码库,任何目标 Office 应用程序都必须支持 VBE 接口。确定 Office 应用程序是否支持该接口的简单方法是使用以下步骤...

  1. 创建要测试的任何 Office 应用程序的新实例
  2. 打开新实例的 VBA IDE 窗口
  3. 导航到立即窗口并键入 Application.VBE

应出现 IntelliSense 弹出窗口,如果 VBE 包含在列表中,那么此项目在该应用程序和版本中很有可能有效。如果 VBE 未出现在列表中,则此项目在该应用程序的测试版本中将无效。

故障排除技巧

在提取运行时,任何错误都会生成详细消息,并且程序将优雅地退出。错误消息应提供足够的信息来确定根本原因。解决错误后,只需重新运行 ExportProject 即可。

以下是一些可能有用的提示...

  • 确保项目编译没有错误
  • 在提取运行之前关闭所有提取文件和工作簿报告
  • 验证是否可以读写项目文件夹

如果需要,请确认已启用“信任对 VBA 项目对象模型的访问”。在 Office 2003 中,它显示为宏安全性设置。在 Office 2007 及更高版本中,它是信任中心设置的一部分。有关更多信息,请参阅 Microsoft 知识库文章 KB282830

项目引用

自 VBA 语言发布以来我一直在使用它进行编程,但即便如此,仍有许多信息丰富的网站可以改进对 CodeModule 对象的讨论。Microsoft 在线文档越来越出色,是我最喜欢的。

以下网站已用作本文的参考文献

最终想法

最重要的是,应从本文中掌握两个关键概念。一,CodeModule 对象是 VBA 代码的编辑器;二,它可以用于在运行时对 VBA 代码模块进行数据挖掘。通过数据挖掘和一点点创造力,就可以实现许多高级编码技术。例如解析枚举等技术。

VBA 一直是一门具有挑战性的语言,因为它是一种轻量级的面向对象语言。但它易于使用以及实际证明的实用性始终令人惊叹。

© . All rights reserved.