通常,当我对VBA文件进行更改时,我希望对其进行编译以确保所做的更改不会破坏任何内容:
但是在具有不同版本Office的不同机器上进行编译将导致不同的结果,有时将编译,有时则不会...发生like this或maybe this的事情。在excel的每个版本中都显示出各种things can be different(尽管这是最常见的问题,但不仅仅是引用)。
我将如何自动执行VBA代码的编译?我希望能够在Excel,PowerPoint和Word等多种产品中做到这一点,我希望能够在2010、2013、2016等版本中分别编译为32位和64位。
是的,这仍然是一个主要的痛点,现在我有一系列的手动测试人员(人员)根据我们的发布时间表审查了各种不同配置上的所有相关文件,有别忘了有更好的方法< / 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这样的东西有关。
答案 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;
}
}
答案 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 need至worry about。
我将C#控制台应用程序和一些示例excel文件拼凑在一起,我认为这是测试此示例的一个很好的起点。我最终将使它适应于在MSTest框架中运行,并将其集成到我的CD管道中。当然,有一些重要的先决条件:
查看代码将证明我还没有消除所有较小的问题。我最终会解决这个问题,但是在此期间,这确实适用:
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();
}
}
}
}