Способ автоматизации функции компиляции кода VBA MS Office - PullRequest
0 голосов
/ 25 июня 2018

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

enter image description here

НоКомпиляция на разных машинах с разными версиями офиса приведет к разным результатам, иногда это скомпилируется, иногда нет ... Вещи , как это может произойти, или , может быть, это .Оказывается, в каждой версии Excel все виды могут отличаться (не только ссылки, хотя это самая распространенная проблема).

Как бы я автоматизировал компиляцию своего кода VBA?Я хотел бы иметь возможность делать это в нескольких продуктах, таких как Excel, PowerPoint и Word, я хотел бы иметь возможность компилировать как 32- и 64-разрядные, с 2010, 2013, 2016 и т. Д ...

Обновление 1

Да, это все еще основная проблема, сейчас у меня есть серия тестировщиков (людей), которые рассматривают все соответствующие файлы в различных конфигурациях на основе нашего графика выпуска, получилчтобы быть лучшим способом сделать это.

То, что я бы предпочел, это какой-то сценарий PowerShell / проект .Net ( C # , VB.NET), который бы достиг этого, даже если бы мне пришлось настроить сервер с кучейверсии Office, я думаю, что это будет стоить инвестиций.

Я думаю, в худшем случае вы могли бы установить все эти разные версии на различные виртуальные машины, а затем использовать AutoHotKey плюс некоторыесвоего рода сценарий PowerShell для их компиляции.Макрос - это веселье Макроса ...

Эта одиссея просто подчеркивает мне, насколько сложна разработка VBA.Я действительно первый человек, у которого есть проблемы между различными версиями Excel?Разумно ли просить, чтобы можно было компилировать под разные версии ?

MS , возможно, это понравится , но для меня это почти так, как будто у этого языка на самом деле нет долгосрочного плана после просто , поддерживающего устаревший код .Он просто продолжает существовать без каких-либо крупных официальных будущих итераций или соображений, поскольку он относится к core development вызовам , таким как этот.

Ответы [ 3 ]

0 голосов
/ 10 апреля 2019

Вам нужно перейти в Excel -> Файл -> Параметры -> Центр управления безопасностью -> Настройки центра управления безопасностью и проверить параметр Trust access to the VBA project object model (если вы не проверите его, приведенный ниже код вызовет ошибку времени выполнения 1004программный доступ к Visual Basic Project не является доверенным).

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;
    }
}

Также смотрите здесь и 1 2 3

0 голосов
/ 11 апреля 2019

Это была массивная борьба, оказалось, что реализация этого сложна во многих отношениях.Я действительно ценю всю помощь, которая была предоставлена, хотя, используя предложение @DmitrijHolkin, я использовал библиотеку «Microsoft.Office.Interop.Excel» в качестве отправной точки для этого.

Прежде я не понимал, что вы можете вызывать функцию «Компиляция» из C # и запускать ее в отдельном окне Excel.Теперь, когда это кажется простым в теории, реализация скрипта / приложения оказалась сложной задачей.Есть все всякого рода вещи вам нужно до беспокоиться о .

Я собрал консольное приложение 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 #

(Примечание. Вам потребуется установить библиотеку «Microsoft.Office.Interop.Excel» из NuGet)

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();
            }
        }
    }
}
0 голосов
/ 10 апреля 2019

Я думаю, что вы можете достичь с помощью некоторой автоматизации VBA IDE.Вы можете сделать этот процесс с несколькими языками, однако я выбрал Autohotkey из-за знакомства.

Я не думаю, что вы можете использовать VBA для этого, так как я не думаю, что вы можете скомпилировать другой код во время выполнения другого кода VBA (здесь может быть совершенно неправильно!), Поэтому вам нужен другой процесс, чтобы получитьэто работает.Вам нужно будет доверять объектной модели проекта VBA в Excel.

Этот код работает, сначала создав новый объект приложения Excel и открыв нужную рабочую книгу.Затем он находит DebugButton, перемещаясь по CommandBars, затем вызывает метод 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)
...