一种自动执行MS Office VBA代码“编译”功能的方法

时间:2018-06-25 19:56:14

标签: c# vba powershell ms-office

通常,当我对VBA文件进行更改时,我希望对其进行编译以确保所做的更改不会破坏任何内容:

enter image description here

但是在具有不同版本Office的不同机器上进行编译将导致不同的结果,有时将编译,有时则不会...发生like thismaybe this的事情。在excel的每个版本中都显示出各种things can be different(尽管这是最常见的问题,但不仅仅是引用)。

我将如何自动执行VBA代码的编译?我希望能够在Excel,PowerPoint和Word等多种产品中做到这一点,我希望能够在2010、2013、2016等版本中分别编译为32位和64位。

更新1

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

我希望使用某种PowerShell脚本/.Net项目(C#,VB.NET)可以完成此任务,即使我必须使用一堆Office版本安装服务器,认为这是值得的投资。

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

这次冒险之旅向我强调了VBA开发有多么困难。我真的是第一个在不同版本的excel之间出现问题的人吗?要求能够compile under different versions是否不合理?

MS may love it,但是对我来说,这几乎就像是long term plan过去的supporting legacy code一样。它只是continues to exist,没有任何重大的未来官方迭代或考虑因素,因为它与core development challenges这样的东西有关。

3 个答案:

答案 0 :(得分:7)

您需要转到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 here1 2 3

答案 1 :(得分:3)

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

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

此代码的工作方式是,首先创建一个新的Excel Application对象,然后打开所需的工作簿。接下来,它通过导航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)

答案 2 :(得分:-1)

这是一场艰苦的斗争,事实证明,实施此任务具有多种挑战。我非常感谢提供的所有帮助,使用@DmitrijHolkin的建议,我将“ Microsoft.Office.Interop.Excel”库用作此操作的起点。

我以前不了解的是,您可以从C#调用“编译”功能,然后在单独的excel窗口中运行该功能。从理论上讲,这似乎很容易实现,但是脚本/应用程序的实现却面临一些挑战。从sorts of things you 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文件的编译失败)

TestVBACompilation-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();
            }
        }
    }
}