【问题标题】:Source control of Excel VBA code modulesExcel VBA代码模块的源代码控制
【发布时间】:2009-04-01 09:28:53
【问题描述】:

我希望能够对我的 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 附加到这些模块名称 - 无论是在加载还是删除时(我不确定是哪个)。

【问题讨论】:

标签: vba excel


【解决方案1】:

这里有一个很好的解决vba版本控制问题的办法:https://github.com/hilkoc/vbaDeveloper

这样做的好处是,它会在您保存工作簿后自动导出您的代码。此外,当您打开工作簿时,它会导入代码。

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

它还解决了将 ModName 等模块作为 ModName1 导入到重复模块中的导入问题。即使多次导入,导入也会正常进行。

作为奖励,它带有一个简单的代码格式化程序,允许您在 VBA 编辑器中编写您的 vba 代码时对其进行格式化。

【讨论】:

  • 我刚刚完成了整个构建过程,有两个问题,我想问你是否弄清楚:1.加载项功能区没有显示vbaDeveloper菜单,除非我运行菜单模块 2 中提供的 refreshMenu() 方法。导出代码不会自动发生,而是我必须手动触发操作
  • 1 菜单将由 workbook_open 事件创建。为此,构建保存并关闭加载项。然后再次打开它后会触发 workbook_open 事件,除非您手动禁用它: Application.EnableEvents = False 2 默认情况下禁用自动导入/导出:github.com/hilkoc/vbaDeveloper/issues/8
【解决方案2】:

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

''
' 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 的外部脚本替换我的宏 - 所以至少我可以将脚本置于版本控制之下。
  • 是的,基本思想是复制“空白”电子表格并将代码复制到其中。如果我在开始之前就知道 vbaMaven,我可能会选择它......我选择走这条路的原因是每一行代码都可以在源代码控制之下。如果您的工作簿中有加载其他代码的代码,那么您的代码加载模块与其他所有内容不在同一级别的控制之下 - 它被包装在一个 excel 文件中。
  • 另外,您必须非常小心地修改 Auto_Open 或 Workbook_Open 事件的事件代码。尝试从 Auto_Open 函数中编辑 ThisWorkbook 对象是导致 excel 崩溃的捷径。
  • 谢谢戴夫。总之,似乎绝对有可能自动创建包含外部定义的宏的工作簿(使用上面的脚本或 vbaMaven),然后在 Excel 中打开工作簿 - 但似乎不可能拉在“运行时”将宏导入 Excel。
  • 是的,你可以如果你可以在你的配置管理模板中存在代码。 Chip Pearson 很好地概述了如何动态添加模块:cpearson.com/Excel/vbe.aspx。我的偏好是将 excel 工作簿与二进制可执行文件没有区别,因此我使用答案中概述的“编译”过程。我的首要任务是在我的 excel 模板中绝对没有代码,并将所有源代码置于版本控制之下。希望这有助于澄清一点。
【解决方案3】:

我已经为此工作了几个月。我想我想通了。

如果 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% 的自我修复代码,而无需任何其他技巧

【讨论】:

  • 谢谢。这是我将近 4 年后的确切问题。
【解决方案4】:

通常,当要求 Excel 导入模块并且已存在同名模块时,会发生“Loader1”的情况。因此,如果您导入“Loader”,然后再次加载它,您将获得“Loader1”。这可能是因为 Excel 不知道(或者可能只是不关心)它是否真的是相同的东西,或者刚刚发生的新功能块是否具有相同的模块名称,所以无论如何它都会导入它。

我想不出一个完美的解决方案,但我想我会倾向于尝试将加载/卸载逻辑放在加载项中 - Workbook_Open 看起来有点脆弱,并且在所有工作簿中都有它如果代码需要更改(永远不要说永远),那将是一个巨大的痛苦。 XLA 逻辑可能更复杂(一方面,捕获必要的事件更棘手),但至少它只存在于一个地方。

【讨论】:

  • 感谢迈克的回答。您可能是对的,AddIn 可能是唯一的方法。我试图避免使用 AddIn 解决方案,因为我想将宏存储在 SubVersion 中 - 所以我更愿意将它们存储为文本文件,以便我可以轻松合并和区分 - 而不是二进制 XLA 文件。
  • 也许使用一个小插件来加载模块?
  • 我不确定在这种情况下使用 AddIn 会有什么帮助。要将非二进制 VBA 模块加载到 excel 中,我仍然需要在打开工作簿时运行某种代码 - 我不认为将加载程序代码保留在 AddIn 中会改变问题的本质.
  • 显然 Addin 方法确实有效。请看下面我的回答。他们的插件将所有导入导出逻辑保存在一个地方。它还挂钩到 excel 的“打开”和“保存”事件,这样您就不需要在所有工作簿中编写 workbook_open() 子程序。
【解决方案5】:

无法发表评论

vba版本控制问题有一个很好的解决方案 这里:https://github.com/hilkoc/vbaDeveloper

关于使用此 XLAM 保存自定义 VBAProject。 在 Build.bas 中试试这个:

'===============
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 项目。

【讨论】:

    【解决方案6】:

    如果您不需要自动导出 VBA 代码,以下是一个易于实现的答案。只需调用以下子程序,它就会将当前活动工作簿的 VBA 代码导出(作为文本)到名为“VC_nameOfTheWorkBook”的子文件夹中。如果您的项目是 .xlam,则需要临时将 IsAddin 属性设置为 false。然后您可以轻松地将新的子文件夹添加到 Git。这是对 Steve Jansen 发现的 here 代码的轻微修改。如需更完整的解决方案,请参阅 Ron de Bruin post

    您需要在 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
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-01-10
      • 2021-06-12
      • 1970-01-01
      • 1970-01-01
      • 2012-07-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多