【问题标题】:A Way to Automate the "Compile" Function of MS Office's VBA Code一种自动化 MS Office VBA 代码“编译”功能的方法
【发布时间】:2018-12-04 11:21:07
【问题描述】:

通常,当我对 VBA 文件进行更改时,我喜欢编译它以确保我的更改不会破坏任何内容:

但是在不同的机器上用不同版本的office编译会导致不同的结果,有时会编译,有时不会……like this的事情可能会发生,或者maybe this。结果在每个版本的 excel 中都有各种 things can be different(不只是引用,尽管这是最常见的问题)。

如何自动编译我的 VBA 代码?我希望能够在 Excel、PowerPoint 和 Word 等多种产品中执行此操作,我希望能够编译为 2010、2013、2016 等 32 位和 64 位...

更新 1

是的,这仍然是一个主要的痛点,现在我有一系列手动测试人员(人员)根据我们的发布时间表审查各种不同配置的所有相关文件,必须有更好的方法 这样做。

我更喜欢某种 PowerShell 脚本/.Net 项目(C#, VB.NET) 来实现这一点,即使我必须设置一个带有多个 Office 版本的服务器,我认为它也是如此值得投资。

我想,最坏的情况是,您可以将所有这些不同的版本安装到各种 VM 上,然后使用 AutoHotKey 加上某种 PowerShell 脚本来编译它们。宏在宏的乐趣之上......

这次冒险向我强调了 VBA 开发的难度。我真的是第一个在不同版本的 excel 之间遇到问题的人吗?问能compile under different versions是不是不合理?

MS may love it,但对我来说,这种语言几乎没有 long term plan 过去只是 supporting legacy code。它只是 continues to exist 没有任何重大的官方未来迭代或考虑,因为它与 core development challenges 相关,例如这个。

【问题讨论】:

  • 也许作为一个更简单的解决方案,您可以省去自己的麻烦并全面标准化版本?有必要有那么多不同的版本吗?
  • 是的,任何用 VBA 编码并发送到第三方的应用程序都将在他们安装的任何版本的 Office 上运行。因此,如果您想在 VBA 中进行开发,并且希望您的应用程序可以在每个人的系统上运行,您需要针对您希望支持的所有可能版本进行开发。对我来说,这是 2010+ 32/64 位。我希望我可以简化它,但这似乎是应用程序的本质 :( 你需要为多个平台编写代码。
  • 您的问题在于库引用更改。您可以在 VBA 中自动注册 dll,请参阅 this SO post
  • @SiyonDP 嗯,这是一个问题,但我的问题不仅限于此,在不同版本的 excel 之间有很多变化,尤其是在 32 位和 64 位版本之间。我想以某种自动化的方式捕捉所有这些问题。
  • 我会将所有模块导出为文本,然后使用目标 Office 版本的 COM API 来重建新工作簿、导入代码并运行嵌入式测试宏。 COM API 可通过多种语言访问,并且有大量示例。

标签: c# vba powershell ms-office


【解决方案1】:

你需要去 Excel -> 文件 -> 选项 -> 信任中心 -> 信任中心设置并检查选项Trust access to the VBA project object model(如果你不检查它下面的代码将引发运行时错误1004对 Visual Basic 项目的编程访问不受信任)。

Sub Compiler()
Dim objVBECommandBar As Object
Set objVBECommandBar = Application.VBE.CommandBars
    Set compileMe = objVBECommandBar.FindControl(Type:=msoControlButton, ID:=578)
    compileMe.Execute
End Sub

在 C 类似的东西上,不要忘记将 excel 包添加到命名空间。

void Main()
{
    var oExcelApp = (Microsoft.Office.Interop.Excel.Application)System.Runtime.InteropServices.Marshal.GetActiveObject("Excel.Application");
    try{
        var WB = oExcelApp.ActiveWorkbook;
        var WS = (Worksheet)WB.ActiveSheet;
        //((string)((Range)WS.Cells[1,1]).Value).Dump("Cell Value"); //cel A1 val
        oExcelApp.Run("Compiler").Dump("macro");
    }
    finally{
        if(oExcelApp != null)
            System.Runtime.InteropServices.Marshal.ReleaseComObject(oExcelApp);
        oExcelApp = null;
    }
}

还有look here123

【讨论】:

    【解决方案2】:

    我认为您可以使用一些 VBA IDE 自动化来完成。您可以使用多种语言来执行此过程,但是,出于熟悉,我选择了 Autohotkey。

    我认为您不能使用 VBA 来完成此操作,因为我认为您无法在运行其他 VBA 代码的同时编译其他代码(这里可能完全错了!),因此您需要另一个进程才能使其正常工作。您需要信任 Excel 中的 VBA 项目对象模型。

    此代码首先创建一个新的 Excel 应用程序对象并打开所需的工作簿。接下来,它通过导航 CommandBars 找到 DebugButton,然后调用 Execute 方法,该方法调用 Compile 操作。

    AHK 代码

    xl := ComObjCreate("Excel.Application")
    xl.Visible := True
    wb := xl.Workbooks.Open("C:\Users\Ryan\Desktop\OtherWB.xlsb")
    DebugButton := wb.VBProject.Collection.VBE.CommandBars("Menu Bar").Controls("&Debug").Controls("Compi&le VBAProject")
    
    if (isObject(DebugButton) && DebugButton.Enabled){
        DebugButton.execute()
    }
    wb.Close(SaveChanges:=True)
    

    【讨论】:

    • 是的,这会起作用,但自动化很棘手,PowerShell 会更容易,但我不确定这是否可行。我仍然认为这是一个选择......
    • 我知道您正在寻找方法,不一定与特定语言相关联。如果这是您所追求的,那么此方法应该很容易移植到 PowerShell。 PowerShell 可以处理 COM 对象。
    【解决方案3】:

    这是一场巨大的斗争,结果证明这是在多个方面的挑战。不过,我非常感谢所提供的所有帮助,使用@DmitrijHolkin 的建议,我使用“Microsoft.Office.Interop.Excel”库作为起点。

    我之前不明白的是,您可以从 C# 调用“编译”函数,然后在单独的 Excel 窗口中运行。现在这在理论上似乎很容易实现,但脚本/应用程序的实现却是一个挑战。 sorts of thingsyou needworry about都有。

    我拼凑了一个 C# 控制台应用程序,以及一些我认为是测试它的良好起点的示例 excel 文件。我最终会将它调整为在 MSTest 框架内运行,并将其集成到我的 CD 管道中。当然还有一些重要的先决条件:

    1. 您需要安装要测试的 Excel 版本。
    2. 能够容忍窗口弹出/关闭(即需要在未使用的用户帐户/计算机上运行)。

    查看代码将证明我还没有解决所有小问题。我最终会开始这样做,但在此期间,这确实有效:

    XXX.XLSM (VBA)

    Public Function Compiler()
        On Error GoTo ErrorHandler
    
        Compiler = "Successfully Compiled"
    
        Dim compileMe As Object
        Set compileMe = Application.VBE.CommandBars.FindControl(Type:=msoControlButton, ID:=578)
    
        If compileMe.Enabled Then
            compileMe.Execute
        End If
    
        Exit Function
    
    ErrorHandler:
    
        Compiler = "Unable to Compile - " & Err.Description
    
    End Function
    

    YYY.XLSM (VBA)

    (与 XXX 相同,但包含一个单独的方法,其中包含一堆乱码,旨在导致 VBA 文件的编译失败)

    TestVBA编译 - C#

    (注意:您需要从 NuGet 安装“Microsoft.Office.Interop.Excel”库)

    using Microsoft.Office.Interop.Excel;
    using Microsoft.Win32;
    using System;
    using System.Diagnostics;
    using System.Runtime.InteropServices;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace TestVBACompilation
    {
        internal class TestVBACompilationMain
        {
            private static void Main(string[] args)
            {
                Console.WriteLine(TestMainFile("Excel 2010 32-bit", @"C:\Program Files (x86)\Microsoft Office\Office14\EXCEL.EXE", @"C:\Users\LocalAdmin\Downloads\XXX.xlsm"));
                Console.WriteLine(TestMainFile("Excel 2016 32-bit", @"C:\Program Files (x86)\Microsoft Office\root\Office16\EXCEL.EXE", @"C:\Users\LocalAdmin\Downloads\XXX.xlsm"));
    
                Console.WriteLine(TestMainFile("Excel 2010 32-bit", @"C:\Program Files (x86)\Microsoft Office\Office14\EXCEL.EXE", @"C:\Users\LocalAdmin\Downloads\YYY.xlsm"));
                Console.WriteLine(TestMainFile("Excel 2016 32-bit", @"C:\Program Files (x86)\Microsoft Office\root\Office16\EXCEL.EXE", @"C:\Users\LocalAdmin\Downloads\YYY.xlsm"));
    
                Console.ReadLine();
            }
    
            /// <summary>
            /// Call this method with each version of the file and the version of excel you wish to test with
            /// </summary>
            /// <param name="pathToFileToTest"></param>
            /// <param name="pathToTheVersionOfExcel"></param>
            /// <param name="excelVersionFriendlyText"></param>
            /// <returns></returns>
            private static string TestMainFile(string excelVersionFriendlyText,
                string pathToTheVersionOfExcel,
                string pathToFileToTest
                )
            {
                TestVBACompilationMain program = new TestVBACompilationMain();
                string returnText = "";
    
                program.UpdateRegistryKey();
                program.KillAllExcelFileProcesses();
    
                //A compromise: https://stackoverflow.com/questions/25319484/how-do-i-get-a-return-value-from-task-waitall-in-a-console-app
                string compileFileResults = "";
                using (Task results = new Task(() => compileFileResults = program.CompileExcelFile(excelVersionFriendlyText, pathToTheVersionOfExcel, pathToFileToTest)))
                {
                    results.Start();
                    results.Wait(30000); //May need to be adjusted depending on conditions
    
                    returnText = "Test: " + (results.IsCompleted ? compileFileResults : "FAILED: File not compiled due to timeout error");
    
                    program.KillAllExcelFileProcesses();
                    results.Wait();
                }
    
                return returnText;
            }
    
            /// <summary>
            /// This should be run in a task with a timeout, can be dangerous as if excel prompts for something this will run forever...
            /// </summary>
            /// <param name="pathToTheVersionOfExcel"></param>
            /// <param name="pathToFileToTest"></param>
            /// <param name="amountOfTimeToWaitForFailure">I've played around with it, depends on what plugins you have installed, for me 10 seconds seems to work good</param>
            /// <returns></returns>
            private string CompileExcelFile(string excelVersionFriendlyText,
                string pathToTheVersionOfExcel,
                string pathToFileToTest,
                int amountOfTimeToWaitForFailure = 10000)
            {
                string returnValue = "";
                _Application oExcelApp = null;
                Workbook mainWorkbook = null;
    
                try
                {
                    //TODO: I still need to figure out how to run specific versions of excel using the "pathToTheVersionOfExcel" variable, right now it just runs the default one installed
                    //In the future I will add support to run multiple versions on one machine
                    //These are ways that don't seem to work
                    //oExcelApp = new Microsoft.Office.Interop.Excel.Application();
                    //oExcelApp = (Microsoft.Office.Interop.Excel.Application)Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application.14"));
    
                    Process process = new Process();
                    process.StartInfo.FileName = pathToTheVersionOfExcel;
                    process.Start();
    
                    Thread.Sleep(amountOfTimeToWaitForFailure);
    
                    oExcelApp = (_Application)Marshal.GetActiveObject("Excel.Application");
    
                    mainWorkbook = oExcelApp.Workbooks.Open(pathToFileToTest);
    
                    Workbook activeWorkbook = oExcelApp.ActiveWorkbook;
                    Worksheet activeSheet = (Worksheet)activeWorkbook.ActiveSheet;
    
                    //Remember the following code needs to be present in your VBA file
                    //https://stackoverflow.com/a/55613985/2912011
                    dynamic results = oExcelApp.Run("Compiler");
                    Thread.Sleep(amountOfTimeToWaitForFailure);
    
                    //This could be improved, love to have the VBA method tell me what failed, that's still outstanding: https://stackoverflow.com/questions/55621735/vba-method-to-detect-compilation-failure
                    if (Process.GetProcessesByName("EXCEL")[0].MainWindowTitle.Contains("Microsoft Visual Basic for Applications"))
                    {
                        returnValue = "FAILED: \"Microsoft Visual Basic for Applications\" has popped up, this file failed to compile.";
                    }
                    else
                    {
                        returnValue = "PASSED: File Compiled Successfully: " + (string)results;
                    }
                }
                catch (Exception e)
                {
                    returnValue = "FAILED: Failed to start excel or run the compile method. " + e.Message;
                }
                finally
                {
                    try
                    {
                        if (mainWorkbook != null)
                        {
                            //This will typically fail if the compiler failed and is prompting the user for something
                            mainWorkbook.Close(false, null, null);
                        }
    
                        if (oExcelApp != null)
                        {
                            oExcelApp.Quit();
                        }
    
                        if (oExcelApp != null)
                        {
                            System.Runtime.InteropServices.Marshal.ReleaseComObject(oExcelApp);
                        }
                    }
                    catch (Exception innerException)
                    {
                        returnValue = "FAILED: Failed to close the excel file, typically indicative of a compilation error - " + innerException.Message;
                    }
                }
    
                return excelVersionFriendlyText + " - " + returnValue;
            }
    
            /// <summary>
            /// This is reponsible for verifying the correct excel options are enabled, see https://stackoverflow.com/a/5301556/2912011
            /// </summary>
            private void UpdateRegistryKey()
            {
                //Office 2010
                //https://stackoverflow.com/a/3267832/2912011  
                RegistryKey myKey2010 = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Office\14.0\Excel\Security", true);
                if (myKey2010 != null)
                {
                    myKey2010.SetValue("AccessVBOM", 1, RegistryValueKind.DWord);
                    myKey2010.Close();
                }
    
                //Office 2013
                RegistryKey myKey2013 = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Office\15.0\Excel\Security", true);
                if (myKey2013 != null)
                {
                    myKey2013.SetValue("AccessVBOM", 1, RegistryValueKind.DWord);
                    myKey2013.Close();
                }
    
                //Office 2016
                RegistryKey myKey2016 = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Office\16.0\Excel\Security", true);
                if (myKey2016 != null)
                {
                    myKey2016.SetValue("AccessVBOM", 1, RegistryValueKind.DWord);
                    myKey2016.Close();
                }
            }
    
            /// <summary>
            /// Big hammer, just kill everything and start the specified version of excel
            /// </summary>
            private void KillAllExcelFileProcesses()
            {
                //TODO: We could tune this to just the application that we opened/want to use
                foreach (Process process in Process.GetProcessesByName("EXCEL"))
                {
                    process.Kill();
                }
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-12-29
      • 1970-01-01
      • 2018-10-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多