Если макрос делается не для одного документа, то рано или поздно придется столкнуться с задачей его обновления. Пришла в голову умная мысль, улучшили макрос в одном из документов и возникает вопрос - а что делать с остальными? В них-то тоже хорошо бы провести изменение. Иногда задача еще усложняется - документы могут быть разбросаны по разным машинам и даже по разным организациям.

Конечно все можно сделать вручную. Разослать всем сообщения с указанием, что нужно изменить, и пусть меняют сами. Если не умеют, то придется сопроводить более или менее подробной инструкцией. Пользователям это обычно не нравится.

Предвидя такую проблему, иногда стараются текст программы отделить от документа - поместить либо в Add-in, либо в специальный "программный" трафарет. В этом случае обновление проходит для пользователей менее болезненно, но появляются другие неудобства. Add-in посложнее в разработке, да и в трафарет код тоже переносится не один к одному.

Здесь я хочу коснуться еще одной технологии, которой тоже можно воспользоваться для изменения макросов. Представим себе, что имеется некая программа, которая может открыть документ Visio и изменить в нем текст макросов. Тогда для обновления достаточно будет доставить такую программу конечным пользователям и попросить запустить ее. В качестве такой программы удобно использовать скрипт VBS. Язык очень похож на VBA. Исполняется в любой Windows системе. Текст обычно короткий и понятный.

Рассмотрим небольшой пример:

If WScript.Arguments.Count > 0 Then
    ' The file was Drag&Dropped to VBS script
    docName = WScript.Arguments(0)
    docPath = Mid(docName,1,InStrRev(docName,"\"))
    MsgBox docName
    MsgBox docPath
Else    
    WScript.Quit
End If

    Dim vApp
    Dim vDoc
    Set vApp = WScript.CreateObject("Visio.Application")
    Set vDoc = vApp.Documents.Open(docName)

    Set objVBComp = vDoc.VBProject.VBComponents.Add(1)
    Set objCodeMod = objVBComp.CodeModule
    objCodeMod.Name = "Hyper"
    lLineNum = objCodeMod.CountOfLines + 1
    s = "Sub Test()" & vbCrLf & _
        "    MsgBox ""Hello, World""" & vbCrLf & _
        "End Sub"
    objCodeMod.InsertLines lLineNum, s
    vDoc.VBProject.VBComponents.Import docPath & "Module1.bas"

    vDoc.SaveAsEx docPath & "Dg2.vsd", 6
    vDoc.Close
    vApp.Quit

Первая фаза - вычисление пути к документу Visio. Рассчитано на Drag&Drop. То есть пользователь видит файл скрипта в проводнике или ярлык скрипта на рабочем столе и "набрасывает" на него файл документа Visio, в который нужно внести изменения. В этом случае скрипт получает командную строку, в которой первым аргументом идет полный путь к документу, включая имя файла. Довольно удобная технология, не требующая использования диалоговых окон.

Вторая фаза - обработка документа. Открывается Visio, открывается найденный документ. Далее скрипт работает с VBA проектом (VBProject).

В этом примере к проекту добавляется модуль типа CodeModule, ему присваивается имя (иначе по умолчанию будет "Module1").

Вычисление lLineNum не обязательно, но в некоторых приложениях VBA имеет привычку сам подставлять пару строк в модуль при его создании. Нам желательно позиционироваться после них.

Потом создается строка с текстом программы (в данном случае процедура из трех программных строк) и записывается в модуль с помощью InsertLines.

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

В примере VBComponents.Import создает еще один модуль из файла "Module1.bas", который лежит рядом с модифицируемым документом.
Затем документ с внедренным макросом сохраняется под новым именем и все закрывается.

Исполнение

Таким образом для обновления макросов у пользователя нужно прислать ему такой скрипт (пусть это будет "Updater.vbs" и файл "Module1.bas". Пользователь положит их в папку с целевым документом и "набросит" файл на скрипт. Рядом образуется новый файл документа Visio, в который будут добавлены два программных модуля.

Если бы модуль "Module1" уже был в целевом файле, то перед импортом нового скрипт должен содержать строку для удаления имеющегося модуля.

Не все модули доступны для удаления. Например, нельзя удалить "ThisDocument". Значит для модификации этого модуля следует использовать не импорт из файла, а удаление/вставку строк. Обычно такая необходимость возникает при добавлении обработчиков событий. Они ведь размещаются именно в ThisDocument.

Осложнения

Для того, чтобы скрипт мог редактировать код макросов в Visio должен быть разрешен доступ к VBA проекту. В Visio 2007 это делается в Trust Center / Macro Settings / Developer Macro Settings установкой галочки в Trust access to the VBA project object model. В целях общей безопасности желательно это разрешение давать только на время проведения изменений, а потом снимать.

При желании разрешение можно установить программно, например, тем же скриптом. Это делается через реестр. Для Visio 2007 разрешение можно установить так

Set objShell = CreateObject("WScript.Shell")
objShell.RegWrite _
 "HKEY_CURRENT_USER\Software\Microsoft\Office\12.0\Visio\Security\AccessVBOM", 1, "REG_DWORD"

Для снятия разрешения нужно записывать 0.

Нужно помнить о том, что ключ реестра содержит версию приложения (в данном случае 12.0). Для других версий Visio это значение будет другим.

Если программное изменение реестра нежелательно по политическим соображениям, то может быть полезно скриптом уточнить значение ключа и при необходимости изменения просто вывести пользователю соответствующую рекомендацию. Пусть изменит значение стандартным способом и перезапустит скрипт.

В дополнение - некоторое отклонение от темы. Во-первых, аналогичная технология применима не только к Visio, но и к другим офисным программам. Во-вторых (в частности для Excel) попался совет по перезапуску приложения, что тоже в каких-то случаях может пригодиться.

Sub Enable_AccessVBOM_and_Macro()
    On Error Resume Next
    Key$ = "HKEY_CURRENT_USER\Software\Microsoft\Office\" & Application.Version & _
           "\Excel\Security\"
    ' включаем программный доступ к объектной модели проекта VBA
    'CreateObject("WScript.Shell").RegWrite Key$ & "AccessVBOM", 1, "REG_DWORD"
    ' ставим низкий уровень безопасности (применится после перезапуска Excel)
    CreateObject("WScript.Shell").RegWrite Key$ & "VBAWarnings", 1, "REG_DWORD"
    ' Перезапускаем Excel
    Shell "cmd.exe /c" & "ping -n 2 localhost > null&&start C:\Temp\resume_3.xlw&&del C:\Temp\resume_3.xlw -f"
    Application.DisplayAlerts = False
    Application.Save Filename:="C:\Temp\resume_3.xlw"
    Application.Quit
End Sub

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

    Set objVBComp = vDoc.VBProject.VBComponents("ThisDocument")

Если макрос добавляется в файл .vsdx, его придется переименовать в .vsdm. Например, так:

  newName = Mid(docName,1,InStrRev(docName,".")) & "vsdm"
  vDoc.SaveAsEx newName, 6