Управление исходными кодами модулей Excel VBA

Я хотел бы иметь возможность управлять исходными текстами своих модулей электронной таблицы Excel (в настоящее время с использованием 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 к этим именам модулей - либо при загрузке, либо при удалении (я не уверен, какой).

Ответ 1

Посмотрите на страницу VBAMaven. У меня есть доморощенные решения, которые используют одни и те же понятия. У меня есть общая библиотека с кучей исходного кода, ant build и 'import' VB script. ant управляет сборкой, которая берет чистый файл excel и вставляет в него необходимый код. @Mike абсолютно прав - любые дублирующие определения модулей автоматически будут содержать число, добавленное к имени модуля. Кроме того, классные модули (как и в Листе и этой книге) требуют специального лечения. Вы не можете создавать эти модули, вы должны прочитать входной файл и записать буфер в соответствующий модуль. Это VB script, который я сейчас использую для этого. Раздел, содержащий текст с разделителями (т.е. @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 [email protected]"

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 [email protected]@common [email protected]
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

Ответ 2

Существует отличное решение проблемы управления версиями vba: https://github.com/hilkoc/vbaDeveloper

Приятная часть этого заключается в том, что он автоматически экспортирует ваш код, как только вы сохраните свою книгу. Кроме того, когда вы открываете книгу, она импортирует код.

Вам не нужно запускать скрипты сборки или команды maven, и вам не нужно вносить какие-либо изменения в ваши книги. Он работает для всех.

Он также решил проблему импорта, когда модули, такие как ModName, импортируются как ModName1 в дублирующий модуль. Импорт работает так, как он должен, даже когда делал это несколько раз.

В качестве бонуса он поставляется с простым форматированием кода, который позволяет вам форматировать ваш код VBA, когда вы пишете его в редакторе VBA.

Ответ 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

Если вы сами исцеляете свой код, как я, вам также придется запустить свой код, который перезаписывает код вызова с той же стратегией.

Я еще не проводил подробное тестирование, я в полном режиме торжества, но это меня очень близко к прямому 99,9% кода самовосстановления в автономном файле .xls без каких-либо других трюков.

Ответ 4

Обычно "Loader1" происходит, когда Excel просит импортировать модуль, а модуль уже существует с тем же именем. Поэтому, если вы импортируете "Loader", загрузите его снова, и вы получите "Loader1". Это было бы потому, что Excel не знает (или, может быть, просто не волнует), если это действительно то же самое или новый кусок функциональности, который только что происходит, имеет одно и то же имя модуля, поэтому он все равно импортирует его.

Я не могу придумать идеальное решение, но я думаю, что я был бы склонен попытаться поставить логику загрузки/выгрузки в надстройку - эта работа Workbook_Open выглядит немного уязвимой и ее во всех книгах идет быть огромной болью, если код когда-либо должен измениться (никогда не говори никогда). Логика XLA может быть более сложной (сложнее ловушка для необходимых событий, с одной стороны), но по крайней мере она будет существовать только в одном месте.