有没有办法从 Excel 插件向工作表插入宏?

Is there is a way to insert macro to worksheet from Excel Addin?

我搜索了文档 Office Excel API,但根本找不到任何提及如何实现它的内容。

问题:

API 功能的运气或某些错误可以通过 VBA 宏轻松解决。

但是,要做到这一点,我们只有两种可能:

  1. 将宏手动插入 sheet,这将捕获 sheet 上的一些事件,插件中的 JS 将触发该事件(此解决方案来自非常古老的论坛(如果我会找到 link,我会把它插入这里))。

  2. 在addin运行时通过JS代码向sheet(context.workbook.worksheet?)插入宏,甚至无法被addin执行,但是在这个如果最终用户根本不需要管理宏 - 我们可以使用相同的逻辑使宏 运行ning 的过程更加顺利(JS 将宏插入 sheet,更改 [=152= 中的一些值] 然后它触发一些宏,然后(例如在 JS 中的某个超时后或仅通过 JS 中的另一个事件,我们可以使用此宏轻松删除整个 sheet))。

所以问题是,是否有可能使用 Excel API 以某种方式做出类似于解决方案 2 的东西? (来自插件的 insert/delete VBA 代码的某种功能)

我将不胜感激!

举例说明问题

我正在使用 JavaScript API,但不幸的是 API 没有涵盖 VBA 中已经存在的全部功能(我希望如此) .让我用一个简单的例子来解释一下:

想象一个任务:

  1. 我们需要从工作簿 1sheet1 中复制一些信息

  2. 然后我们需要创建一本书并将值放入新工作簿 2

  3. 然后我们需要向用户建议它(新工作簿 2)需要保存在哪里。

  4. 然后我们需要保存并关闭工作簿 2。

VBA 很容易解决问题,但如果是 JS API - 这个问题没有完整的解决方案(没有第三方应用程序)。

您可以通过以下 link 来比较 API JS 和 VBA:

Workbooks JS API

Workbooks VBA API

所以我想做的 - 是在 JavaScript 中编写实际的 VBA 宏并将此 VBA 宏插入 sheet 以使宏可执行。

如何通过 sheet 上的某些值更改使宏可执行?

我发现,如果您调用 select 方法直接应用于单元格并使用 VBA 中的 Worksheet_SelectionChange 捕获选择更改 - 它完美地工作。

不幸的是,直接设置值到单元格不会触发 VBA Worksheet_change

为什么我要用JS API

目前我已经有一个用于类似任务的 VBA 项目,但是随着项目的成长和发展 - 这里有一些功能,这里有一些功能,我看到插件 - 是解决关键问题的最佳解决方案 -它更易于维护、管理、开发、推送更新、安装,而且看起来更好——因为插件只是一个简单的网站

更新 2019/09/20 - 可能的解决方法

首先,非常感谢@DecimalTurn,是他让这个解决方法成为可能。参见

我稍微修改了它并添加了额外的 JS 脚本和 VBA 脚本来使这个解决方案完整。所以:

  1. 在从 JS 对 ThisWorkbook 模块执行任何操作之前,需要插入以下 VBA 宏:

1.1。 VBA 宏,它将处理我们将传输的所有 VBA 代码

Private Sub Workbook_NewSheet(ByVal Sh As Object)
    On Error GoTo endline
    Const SheetName As String = "_WorksheetSheetWorker"

    CheckIfVBAAccessIsOn

    If InStr(1, Sh.name, SheetName, vbBinaryCompare) >= 0 Then
        If Sh.Range("$A") <> vbNullString Then

            Const ModuleName As String = "m_TempMacroJS"

            Dim ws As Worksheet
            Set ws = ThisWorkbook.Sheets(SheetName)
            'We will take MacroName from sheet which we added from JS
            Dim MacroName As String
            MacroName = ws.Range("A2").Value2

            Dim rng As Range
            Set rng = ws.Range("A1")
            Dim pathToMacroBas As String

            'Export the content of the cell to a .bas file
            pathToMacroBas = ThisWorkbook.path & "\" & ModuleName & ".bas"
            Open pathToMacroBas For Output As #1
            Print #1, "Attribute VB_Name = """ & ModuleName & """ " & vbNewLine & ws.Range("A1").Value2
            Close #1

            'Declare VBProject Object
            Dim vbaProject As VBProject
            Set vbaProject = ThisWorkbook.VBProject

            'Delete pre-existing module with the same name
            On Error Resume Next
            ThisWorkbook.VBProject.VBComponents.Remove ThisWorkbook.VBProject.VBComponents(ModuleName)
                On Error GoTo 0

                'Load the code as a new Module
                vbaProject.VBComponents.Import ThisWorkbook.path & "\" & ModuleName & ".bas"
                Dim vbaModule As VBIDE.VBComponent
                Set vbaModule = vbaProject.VBComponents(ModuleName)

                'Run the code and transfer working sheet to macro
                'You can use this worksheet to transfer values to macro as JSON
                Application.Run ModuleName & "." & MacroName, ws

                'Cleanup
                ThisWorkbook.VBProject.VBComponents.Remove vbaModule 
                'Optional
                Kill pathToMacroBas
                Application.DisplayAlerts = False
                ws.Delete
                Application.DisplayAlerts = True
            End If
        End If
        Exit Sub
      endline:
      End Sub

1.2 VBA 宏,它将以编程方式启用 Trust access to the VBA project object model。请注意:您还需要启用 Microsoft Visual Basic for Applications Extensibility 5.3

我在 link here 上找到了解决方案并稍作修改 - 宏创建 VBScript 并直接在寄存器中启用 Trust access to the VBA project object model。我还不能处理的问题是延迟。保存和关闭现有工作簿需要延迟时间。

Sub CheckIfVBAAccessIsOn()

    '[HKEY_LOCAL_MACHINE/Software/Microsoft/Office/10.0/Excel/Security]
    '"AccessVBOM"=dword:00000001

    Dim strRegPath As String
    strRegPath = "HKEY_CURRENT_USER\Software\Microsoft\Office\" & Application.Version & "\Excel\Security\AccessVBOM"

    If TestIfKeyExists(strRegPath) = False Then
      MsgBox "A change has been introduced into your registry configuration. All changes will be saved. Please reopen book."
      WriteVBS
      ThisWorkbook.Save
      Application.Quit
    End If

  End Sub

  Function TestIfKeyExists(ByVal path As String)
    Dim WshShell As Object
    Set WshShell = CreateObject("WScript.Shell")
    On Error Resume Next
    Dim RegValue As Boolean
    RegValue = WshShell.RegRead(path)
    If RegValue = True Then
      TestIfKeyExists = True
    Else
      TestIfKeyExists = False
    End If
    On Error GoTo 0
  End Function

  Sub WriteVBS()
    Dim objFile         As Object
    Dim objFSO          As Object
    Dim codePath        As String
    codePath = Me.path & "\reg_setting.vbs"

    Set objFSO = CreateObject("Scripting.FileSystemObject")
    Set objFile = objFSO.OpenTextFile(codePath, 2, True)

    objFile.WriteLine (" On Error Resume Next")
    objFile.WriteLine ("")
    objFile.WriteLine ("Dim WshShell")
    objFile.WriteLine ("Set WshShell = CreateObject(""WScript.Shell"")")
    objFile.WriteLine ("")
    objFile.WriteLine ("MsgBox ""Please wait until Excel will closes! Click OK to complete the setup process.""")
    objFile.WriteLine ("")
    objFile.WriteLine ("Dim strRegPath")
    objFile.WriteLine ("Dim Application_Version")
    objFile.WriteLine ("Application_Version = """ & Application.Version & """")
    objFile.WriteLine ("strRegPath = ""HKEY_CURRENT_USER\Software\Microsoft\Office\"" & Application_Version & ""\Excel\Security\AccessVBOM""")
    objFile.WriteLine ("WScript.echo strRegPath")
    objFile.WriteLine ("WshShell.RegWrite strRegPath, 1, ""REG_DWORD""")
    objFile.WriteLine ("")
    objFile.WriteLine ("If Err.Code <> o Then")
    objFile.WriteLine ("   MsgBox ""Error"" & Chr(13) & Chr(10) & Err.Source & Chr(13) & Chr(10) & Err.Message")
    objFile.WriteLine ("End If")
    objFile.WriteLine ("")
    objFile.WriteLine ("WScript.Quit")

    objFile.Close
    Set objFile = Nothing
    Set objFSO = Nothing

    'run the VBscript code
    ' > The macro will fail to execute the VB script if you use a
    '   [codepath] which contains blanks!
    '
    ' > To fix this issue, we add a pair of double quotes (" ") around
    '   [codepath];
    Shell "cscript " & Chr(34) & codePath & Chr(34), vbNormalFocus

  End Sub
  1. 我根据@DecimalTurn 的建议编写的第二部分在 JS 中创建 sheet 然后从 VBA 捕获此事件并将整个代码包装在一个 JS 实例中:
    const VBAWorker = function(){
      /* This is a name of tempurary sheet to execute macro */
      this._executedMacroName = "JSSubRunner"
      /* This is the name of sheet worker*/
      this._WorksheetSheetWorkerName = "_WorksheetSheetWorker"
      /* These options can be applied to already existed sheet*/
      this._worksheetExistenceDecisionOptions = {
        replaceSheet : "replaceSheet",
        findNewAvailableName : "findNewAvailableName"
      }
    }


    /**
     * Function to run macro using sheet worker
     * @param {String} VBAMacro is a code which will be executed
     * @param {String} transferredValues (optional) are a values which we need 
     * to place into executable macro
     * @param {String} worksheetDesicion (optional) is a desicion which we will if the worker worksheet exists
     * default = "replaceSheet", possible = "findNewAvailableName"
     */
    VBAWorker.prototype.run= async function(VBAMacro, transferredValues = "", worksheetDesicion = "replaceSheet"){
      const defaultWorksheetName = this._WorksheetSheetWorkerName
      let worksheetName = defaultWorksheetName
      const preparedVBAMacro = this._changeMacroName(VBAMacro) 
      await Excel.run(async (context) => {
        /* First we need to check out existence of sheet worker*/
        let sheets = context.workbook.worksheets;
        sheets.load("items/name");

        await context.sync()
        /**
         *  In this case we will deside what to do 
         *  if we will find sheet with the same name
         * */ 
        const isSheetExists = this._checkWorksheetExistence(sheets)
        const decisionOptions = this._worksheetExistenceDecisionOptions
        if (isSheetExists){
          switch (worksheetDesicion){
            case decisionOptions.replaceSheet:
              let sheetToReplace = sheets.getItem(worksheetName)
              sheetToReplace.delete()
              await context.sync()
            break;
            case decisionOptions.findNewAvailableName:
              worksheetName = this._changeNameOfWorkerWorksheet(sheets) 
            break;
          }
        } else {
          /* we will keep worksheetName as default */
        }

        let sheet = sheets.add(worksheetName);
        let macroExeCell = sheet.getCell(0,0)
        let macroNameCell = sheet.getCell(1,0)
        let macroValuesCell = sheet.getCell(0,1)
        macroExeCell.values = preparedVBAMacro
        macroNameCell.values = this._executedMacroName
        let preparedValues = []
        const limit = 32700 
        const lengthOfString = transferredValues.length
        // console.log(transferredValues.length)
        // console.log(transferredValues.length / limit)
          if (lengthOfString > limit) {
            try {
              let done = false

              /* during cell lenght limit we will slice string to many*/
              let lastStep = false
              let current = limit
              let oldcurrent = 0

              do {
                let end = current
                let start = oldcurrent
                /* Check that the next simbol not equals to "=" */
                if(transferredValues.slice(end, end + 1) == "="){
                  current += 1
                  end = current
                }

                if (lengthOfString < start ){
                  start = lengthOfString
                }  
                if (lengthOfString < end){
                  end = lengthOfString
                  lastStep = true
                }

                preparedValues.push(transferredValues.slice(start, end))

                if (lastStep){
                  done = true
                } else {
                  oldcurrent = current
                  current += limit
                }
              } while (done == false)
              /* Write values to sheet*/
              await preparedValues.forEach(async (el, i)=>{
                macroValuesCell = sheet.getCell(0 + i,1)
                macroValuesCell.values = [[el]]
              })
            } catch (error) {
              console.log(error)
            }
          } else {
            /* If string.length is less then limit we just put it directly to one cell*/
            macroValuesCell.values = [[transferredValues]]
          }
        return await context.sync();
      });
    }

    /**
     * Function to search available name of sheet and return it
     * @param {Array} sheets - worksheet items with 
     * returns suggestedName (string)
     */
    VBAWorker.prototype._changeNameOfWorkerWorksheet = function(sheets){
      try {
        let suggestCounter = 0
        let suggestedName; 
        let suggestedNameIsFree = false;
        let worksheetName = this._WorksheetSheetWorkerName
        do {
          suggestedName = worksheetName + suggestCounter 
          suggestCounter = suggestCounter +1
          suggestedNameIsFree = !this._checkWorksheetExistence(sheets)
        } while (suggestedNameIsFree = false);
        return suggestedName

      } catch (error) {
        console.log(error)
      }
    }

    /**
     * Function to check worksheet name existence
     * @param {Array} sheets - worksheet items with names
     * returns true or false
     */
    VBAWorker.prototype._checkWorksheetExistence = function(sheets){
      let isSheetExists = false
      sheets.items.forEach(el=>{
        if(el.name == this._WorksheetSheetWorkerName){
          isSheetExists = true
          return;
        }
      }) 
      return isSheetExists
    }

    /**
     * Function to change name of running macro
     * @param {String} VBAMacro is a string that contains executed macro
     * The name of running sub will be changed to "_JSSubRunner"
     */
    VBAWorker.prototype._changeMacroName =function(VBAMacro){
      const regex = /(Sub\s+)(.*)([(])/i
      const renamedVBAMacro = VBAMacro.replace(regex, `Sub ${this._executedMacroName} (`)
      return renamedVBAMacro
    }

    export default VBAWorker

如何使用?

您可以将其用作调用 VBAWorker 的简单实例:

  const VBAWorkerInst = new VBAWorker()
  await VBAWorkerInst.run(
    "your VBA code goes here",
    "your values in string (JSON for example) goes here",
    "optional option:) - a name of decision what we need to do, if sheet already existed"
  )

您的宏可以有任何名称,因为这个 VBAWorker 会处理它并更改该名称以统一它。

请注意: 因为 Excel 是异步的,我们需要等到所有承诺都解决!所以上面的代码必须用异步函数包装,否则你可以捕获 promise 回调。

我还没有测试过,但我认为可以 运行 多个宏,我们可以使用与值相同的策略来编写更有用的代码。

现在就这些:) 真的希望随着时间的推移会有更简单的解决方案..

可能的解决方法

作为免责声明,此方法可能会在您使用的 Excel 文件中引入一些漏洞,因此您必须谨慎使用宏、模块和 sheet 的唯一名称名称以确保没有加载项将 运行 VBA 代码未经您的同意。

我们的想法是创建一个新作品sheet并将宏代码写入该作品中的一个单元格sheet(假设单元格 A1)。然后,VBA 模块中已经存在一个 VBA 事件过程,它将完成繁重的工作,使您的宏 运行.

假设 Trust access to the VBA project object model 已启用并且您已将 Microsoft Visual Basic for Applications Extensibility 5.3 库添加到您的工作簿,您可以在 ThisWorkbook 中包含以下 VBA 事件过程:

Private Sub Workbook_NewSheet(ByVal Sh As Object)
    If Sh.Name = "NewSheet" Then
        If Sh.Range("$A") <> vbNullString Then

            Const ModuleName As String = "MacroJs"
            Const MacroName As String = "YourMacroName"
            Const SheetName As String = "NewSheet"

            Dim ws As Worksheet
            Set ws = ThisWorkbook.Sheets(SheetName)

            Dim rng As Range
            Set rng = ws.Range("A1")

            'Export the content of the cell to a .bas file
            Open ThisWorkbook.Path & "\" & ModuleName & ".bas" For Output As #1
            Print #1, "Attribute VB_Name = """ & ModuleName & """ " & vbNewLine & ws.Range("A1").Value2
            Close #1

            'Declare VBProject Object
            Dim vbaProject As VBProject
            Set vbaProject = ThisWorkbook.VBProject

            'Delete pre-existing module with the same name
            On Error Resume Next
            ThisWorkbook.VBProject.VBComponents.Remove ThisWorkbook.VBProject.VBComponents(ModuleName)
            On Error GoTo 0

            'Load the code as a new Module
            vbaProject.VBComponents.Import ThisWorkbook.Path & "\" & ModuleName & ".bas"
            Dim vbaModule As VBIDE.VBComponent
            Set vbaModule = vbaProject.VBComponents(ModuleName)

            'Run the code
            Application.Run ModuleName & "." & MacroName

            'Cleanup
            ThisWorkbook.VBProject.VBComponents.Remove vbaModule 'Optional
            Application.DisplayAlerts = False
                ws.Delete
            Application.DisplayAlerts = True
        End If
    End If
End Sub

此过程将由您的 Office-JS 代码创建 sheet 触发。

请注意,我还建议添加一些错误处理,以确保清理部分将 运行 以防在 运行 时出现 运行 时间错误代码。

然后您的 JavaScript 代码将如下所示:

var sheets = context.workbook.worksheets;
var sheet = sheets.add("NewSheet");
sheet.getRange("A1").values = 'sub YourMacroName() \n Msgbox "Test" \n End sub';

您好,今天 Office.js API 不存在此功能。我会 post 在 Office 加载项 User Voice 网站上提出要求:https://officespdev.uservoice.com。谢谢。