Excel VBA代码模块的源代码控制

26

我想要对我的Excel电子表格的VBA模块进行源代码控制(当前正在使用Excel 2003 SP3),以便于我可以共享和管理被一堆不同电子表格使用的代码,因此当打开电子表格时,我希望能够重新从文件中加载它们。

我有一个名为Loader.bas的模块,我用它来完成大部分工作(加载和卸载任何其它所需模块) - 我希望能够在电子表格打开时立即从文件中加载它。

我已将以下代码附加到Workbook_Open事件中(在ThisWorkbook类中)。

Private Sub Workbook_Open()
    Call RemoveLoader
    Call LoadLoader
End Sub

RemoveLoader (也在ThisWorkbook类中) 包含以下代码:

Private Sub RemoveLoader()
    Dim y As Integer
    Dim OldModules, NumModules As Integer
    Dim CompName As String

    With ThisWorkbook.VBProject
        NumModules = ThisWorkbook.VBProject.VBComponents.Count
        y = 1
        While y <= NumModules
            If .VBComponents.Item(y).Type = 1 Then
                CompName = .VBComponents.Item(y).Name
                If VBA.Strings.InStr(CompName, "Loader") > 0 Then
                    OldModules = ThisWorkbook.VBProject.VBComponents.Count
                    .VBComponents.Remove .VBComponents(CompName)
                    NumModules = ThisWorkbook.VBProject.VBComponents.Count
                    If OldModules - NumModules = 1 Then
                        y = 1
                    Else
                        MsgBox ("Failed to remove " & CompName & " module from VBA project")
                    End If
                End If
            End If
            y = y + 1
        Wend
    End With
End Sub

这可能过于复杂和粗糙了,但我正在尝试找到所有能让它加载外部模块的方法!

经常情况下,当我打开电子表格时,RemoveLoader函数会发现VBA项目中已经包含了一个名为"Loader1"的模块,它无法删除,并且还无法从文件中加载新的Loader模块。

有没有想法,我所尝试的是否可行?Excel似乎非常喜欢在这些模块名称后面添加数字1 - 无论是在加载还是删除时(我不确定哪种情况)。


我忘了补充一点,我思考了一下并想到了这个:http://grumpyop.wordpress.com/2009/04/20/version-control-for-excel-workbooks-part-2/ 请注意评论中提到的VBAMaven,它似乎是一些外部服务,可能会有所帮助。 - Mike Woodhouse
6个回答

20

在此处有一个解决VBA版本控制问题的绝佳方案:https://github.com/hilkoc/vbaDeveloper

它的好处是,只要保存工作簿,它就会自动导出您的代码,并且当您打开一个工作簿时,它会自动导入代码。

您不需要运行任何构建脚本或Maven命令,也不需要对工作簿进行任何更改。它适用于所有工作簿。

它还解决了导入问题,例如将模块(如ModName)导入为重复模块中的ModName1。即使多次进行导入,导入也能正常工作。

额外的好处是,它附带了一个简单的代码格式化工具,允许您在VBA编辑器中编写代码时进行格式化。


2
我刚刚经历了整个构建过程,有两个问题,我想问一下你是否已经解决:
  1. Add-Ins选项卡没有显示vbaDeveloper菜单,除非我运行Menu模块中提供的refreshMenu()方法。
  2. 代码的导出不会自动发生,而是必须手动触发该操作。
- Marcus Mangelsdorf
4
  1. 菜单将通过“workbook_open"事件创建。为了使其工作,需要构建、保存和关闭加载项。然后再次打开加载项,"workbook_open"事件将被触发,除非你手动禁用它:Application.EnableEvents = False。
  2. 自动导入/导出默认情况下被禁用:https://github.com/hilkoc/vbaDeveloper/issues/8
- MathKid

5
看看VBAMaven页面。我有一个自制解决方案,使用相同的概念。我有一个共同的库,其中包含一堆源代码,一个ant构建和一个“导入”VB脚本。Ant控制构建,它将空白的Excel文件推入所需的代码中。@Mike绝对正确 - 任何重复的模块定义都会自动附加到模块名称后面的数字。此外,类模块(如Sheet和ThisWorkbook)类需要特殊处理。您无法创建这些模块,必须读取输入文件并将缓冲区写入适当的模块中。这是我目前用来执行此操作的VB脚本。包含@分隔文本(即@build file@)的部分是占位符 - ant构建将这些标记替换为有意义的内容。它不完美,但对我有效。
''
' Imports VB Basic module and class files from the src folder
' into the excel file stored in the bin folder. 
'

Option Explicit

Dim pFileSystem, pFolder,  pPath
Dim pShell
Dim pApp, book

Dim pFileName

pFileName = "@build file@"

Set pFileSystem = CreateObject("Scripting.FileSystemObject")

Set pShell = CreateObject("WScript.Shell")
pPath = pShell.CurrentDirectory

If IsExcelFile (pFileName) Then
    Set pApp = WScript.CreateObject ("Excel.Application")
    pApp.Visible = False
    Set book = pApp.Workbooks.Open(pPath & "\build\" & pFileName)
Else
    Set pApp = WScript.CreateObject ("Word.Application")
    pApp.Visible = False
    Set book = pApp.Documents.Open(pPath & "\build\" & pFileName)
End If


'Include root source folder code if no args set
If Wscript.Arguments.Count = 0 Then
    Set pFolder = pFileSystem.GetFolder(pPath & "\src")
    ImportFiles pFolder, book
    '
    ' Get selected modules from the Common Library, if any
    @common path@@common file@
Else
    'Add code from subdirectories of src . . .
    If Wscript.Arguments(0) <> "" Then
        Set pFolder = pFileSystem.GetFolder(pPath & "\src\" & Wscript.Arguments(0))
        ImportFiles pFolder, book
    End If
End If





Set pFolder = Nothing
Set pFileSystem = Nothing
Set pShell = Nothing


If IsExcelFile (pFileName) Then
    pApp.ActiveWorkbook.Save
Else
    pApp.ActiveDocument.Save
End If

pApp.Quit
Set book = Nothing
Set pApp = Nothing


'' Loops through all the .bas or .cls files in srcFolder
' and calls InsertVBComponent to insert it into the workbook wb.
'
Sub ImportFiles(ByVal srcFolder, ByVal obj)
    Dim fileCollection, pFile
    Set fileCollection = srcFolder.Files
    For Each pFile in fileCollection
        If Right(pFile, 3) = "bas _
          Or Right(pFile, 3) = "cls _
          Or Right(pFile, 3) = "frm Then
            InsertVBComponent obj, pFile
        End If
    Next
    Set fileCollection = Nothing
End Sub


'' Inserts the contents of CompFileName as a new component in 
'  a Workbook or Document object.
'
'  If a class file begins with "Sheet", then the code is
'  copied into the appropriate code module 1 painful line at a time.
'
'  CompFileName must be a valid VBA component (class or module) 
Sub InsertVBComponent(ByVal obj, ByVal CompFileName)
    Dim t, mName
    t = Split(CompFileName, "\")
    mName = Split(t(UBound(t)), ".")
    If IsSheetCodeModule(mName(0), CompFileName) = True Then
        ImportCodeModule obj.VBProject.VBComponents(mName(0)).CodeModule, _
                         CompFileName
    Else
        If Not obj Is Nothing Then
            obj.VBProject.VBComponents.Import CompFileName
        Else
            WScript.Echo  "Failed to import " & CompFileName
        End If
    End If 
End Sub

''
' Imports the code in the file fName into the workbook object
' referenced by mName.
' @param target destination CodeModule object in the excel file
' @param fName file system file containing code to be imported
Sub ImportCodeModule (ByVal target, ByVal fName)
    Dim shtModule, code, buf    
    Dim fso
    Set fso = CreateObject("Scripting.FileSystemObject") 
    Const ForReading = 1, ForWriting = 2, ForAppending = 3
    Const TristateUseDefault = -2, TristateTrue = -1, TristateFalse = 0

    Set buf = fso.OpenTextFile(fName, ForReading, False, TristateUseDefault)
    buf.SkipLine
    code = buf.ReadAll

    target.InsertLines 1, code
    Set fso = Nothing
End Sub


''
' Returns true if the code module in the file fName
' appears to be a code module for a worksheet.
Function IsSheetCodeModule (ByVal mName, ByVal fName)
    IsSheetCodeModule = False
    If mName = "ThisWorkbook" Then
       IsSheetCodeModule = False
    ElseIf Left(mName, 5) = "Sheet" And _
       IsNumeric(Mid (mName, 6, 1)) And _
       Right(fName, 3) = "cls Then
       IsSheetCodeModule = True
    End If
End Function

''
' Returns true if fName has a xls file extension
Function IsExcelFile (ByVal fName)
    If Right(fName, 3) = "xls" Then
        IsExcelFile = True
    Else
        IsExcelFile = False
    End If 
End Function

谢谢。我已经查看了您的脚本和vbaMaven,看起来两种方法都会创建全新的电子表格并将宏复制到其中。是这样吗?我本来希望能找到一种在打开电子表格时将宏加载到其中的方法,但我完全无法让其正常工作。也许我正在尝试使用Excel无法完成的操作。现在我开始考虑用一个通过COM访问Excel的外部脚本来替换我的宏 - 这样至少我可以将脚本放在版本控制下。 - Jon Mills
是的,基本思路是复制一个“空白”电子表格并将代码复制到其中。如果我在开始之前知道vbaMaven,我可能会选择它...我选择这种方法的原因是每行代码都可以在源代码控制下。如果您的工作簿中有加载其他代码的代码,则您的代码加载模块与其他所有内容不在同一级别的控制下-它被包装在Excel文件中。 - DaveParillo
此外,在修改Auto_Open或Workbook_Open事件的代码时,您必须非常小心。试图从Auto_Open函数内部编辑ThisWorkbook对象是导致Excel崩溃的快速途径。 - DaveParillo
谢谢,戴夫。总的来说,似乎完全可以自动创建包含外部定义宏的工作簿(使用上述脚本或vbaMaven),然后在Excel中打开这些工作簿——但好像无法在“运行时”将宏导入Excel。 - Jon Mills
如果您可以接受在配置管理模板中存在代码,那么是的,您可以这样做。Chip Pearson对如何动态添加模块有很好的概述:http://www.cpearson.com/Excel/vbe.aspx。我的首选是将Excel工作簿视为二进制可执行文件,因此我使用我回答中概述的“编译”过程。我的最高优先事项是在我的Excel模板中绝对不放置任何代码,并将所有源代码放在版本控制下。希望这可以帮助澄清一些问题。 - DaveParillo

4

我已经花了几个月的时间研究这个问题。 我想我找到了答案。

如果VB项目试图删除包含调用堆栈中某些内容的模块,则会延迟删除,直到调用堆栈弹出要替换的模块。

为避免模块在调用堆栈中,可以使用Application.OnTime启动代码。

Private Sub Workbook_Open()

    'WAS: module_library (1)

    Application.OnTime (Now + TimeValue("00:00:01")), "load_library_kicker_firstiter"

End Sub

如果你像我一样正在进行代码自我修复,那么你还需要启动你的代码,用相同的策略覆盖“调用”代码。

我还没有进行大量测试,但我非常兴奋,因为这让我非常接近在独立的.xls文件中实现直观的99.9%自我修复代码而不需要任何其他技巧。


1
谢谢。这正是我近4年来遇到的确切问题。 - enderland

2
通常情况下,当要求Excel导入一个模块并且已经存在同名的模块时,就会发生"Loader1"的事情。因此,如果您导入"Loader",然后再次加载它,您将得到"Loader1"。这是因为Excel不知道(或者可能仅仅是不关心)它是否真的是同一件事情,或者只是具有相同模块名称的新功能块,所以它仍然会导入它。
我想不出完美的解决方案,但我倾向于尝试在插件中放置加载/卸载逻辑 - Workbook_Open看起来有点脆弱,如果代码需要更改(永远不要说永远),并且它存在于所有工作簿中将是一个巨大的麻烦。 XLA逻辑可能更加复杂(例如更难捕获必要的事件),但至少它只存在于一个地方。

感谢您的答复,迈克。您可能是正确的,AddIn 可能是唯一的解决方法。我试图避免使用 AddIn 方案,因为我希望将宏存储在 SubVersion 中 - 所以我更喜欢将它们存储为文本文件,这样我可以轻松地合并和 diff - 而不是二进制 XLA 文件。 - Jon Mills
也许使用一个小插件来加载模块? - Mike Woodhouse
我不确定使用AddIn如何在这种情况下有所帮助。要将非二进制VBA模块加载到Excel中,我仍然需要在打开工作簿时运行某种代码 - 而且我认为将Loader代码保留在AddIn中并不会改变问题的本质。 - Jon Mills
显然,Addin方法是可行的。请见下面我的回答。他们的插件将所有的导入和导出逻辑都集中在一个地方。它还钩子进了Excel的“打开”和“保存”事件,这样你就不需要在所有工作簿中编写workbook_open()子程序了。 - MathKid

0
以下是一个易于实施的答案,如果你不需要自动导出VBA代码,只需调用以下子程序,它将以文本形式导出当前活动工作簿的VBA代码到名为"VC_nameOfTheWorkBook"的子文件夹中。如果你的项目是.xlam,你需要临时将IsAddin属性设置为false。然后你可以轻松地将新的子文件夹添加到Git中。这是对Steve Jansen在这里找到的代码的轻微修改。对于更完整的解决方案,请参阅Ron de Bruin的帖子
在VBE编辑器中,你需要设置对"Microsoft Visual Basic For Applications Extensibility 5.3"和"Microsoft Scripting Runtime"的引用。
Public Sub ExportVisualBasicCode()
    Const Module = 1
    Const ClassModule = 2
    Const Form = 3
    Const Document = 100
    Const Padding = 24
    Dim VBComponent As Object
    Dim path As String
    Dim directory As String
    Dim extension As String
    Dim fso As New FileSystemObject
    directory = ActiveWorkbook.path & "\VC_" & fso.GetBaseName(ActiveWorkbook.Name)
    If Not fso.FolderExists(directory) Then
        Call fso.CreateFolder(directory)
    End If
    Set fso = Nothing
    For Each VBComponent In ActiveWorkbook.VBProject.VBComponents
        Select Case VBComponent.Type
            Case ClassModule, Document
                extension = ".cls"
            Case Form
                extension = ".frm"
            Case Module
                extension = ".bas"
            Case Else
                extension = ".txt"
        End Select

        On Error Resume Next
        Err.Clear

        path = directory & "\" & VBComponent.Name & extension
        Call VBComponent.Export(path)

        If Err.Number <> 0 Then
            Call MsgBox("Failed to export " & VBComponent.Name & " to " & path, vbCritical)
        Else
            Debug.Print "Exported " & Left$(VBComponent.Name & ":" & Space(Padding), Padding) & path
        End If

        On Error GoTo 0
    Next
End Sub

0

无法对评论进行回复

这里有一个关于VBA版本控制问题的优秀解决方案: https://github.com/hilkoc/vbaDeveloper

试试在Build.bas中使用此XLAM保存自定义VBAProjects:

'===============
Public Sub testImport()
    Dim proj_name As String
    Dim vbaProject As Object

    'proj_name = "VBAProject"
    'Set vbaProject = Application.VBE.VBProjects(proj_name)

    Set vbaProject = Application.VBE.ActiveVBProject
    proj_name = vbaProject.name

    Build.importVbaCode vbaProject
End Sub
'===============
Public Sub testExport()
    Dim proj_name As String
    Dim vbaProject As Object

    'proj_name = "VBAProject"
    'Set vbaProject = Application.VBE.VBProjects(proj_name)

    Set vbaProject = Application.VBE.ActiveVBProject
    proj_name = vbaProject.name

    Build.exportVbaCode vbaProject
End Sub
'===============

这将导出/导入活动的VBA项目。


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