如何从Excel VBA调用.NET方法?

33

我找到了一种方法,可以直接从VBA代码中调用.NET 2代码:

Dim clr As mscoree.CorRuntimeHost
Set clr = New mscoree.CorRuntimeHost
clr.Start
Dim domain As mscorlib.AppDomain
clr.GetDefaultDomain domain
Dim myInstanceOfDotNetClass As Object
Set myInstanceOfDotNetClass = domain.CreateInstanceFrom("SomeDotNetAssembly.dll", "Namespace.Typename").Unwrap
Call myInstanceOfDotNetClass.ExecuteSomeDotNetMethod

我使用“工具”->“引用”为Excel VBA添加了对mscoree.tlb和mscorlib.tlb的引用。

这适用于.NET CLR 2程序集,最高支持.NET框架版本3.5。

我需要让它在.NET 4上运行。

我知道.NET CLR4引入了一种不受版本影响的创建运行时实例的方式,并且我找到了一个用C++编写的代码示例: http://dev.widemeadows.de/2014/02/04/hosting-the-net-4-runtime-in-a-native-process/

我的Excel VBA技能不足以翻译那几行代码。


1
你是否已经安装了Visual Studio Tools for Office Runtime - Tim
TLB在CLR4中仍然存在(位于C:\Windows\Microsoft.NET\Framework\v4.0.30319)。问题到底是什么? - Simon Mourier
我认为这与dll的注册有关。看一下吧。希望能帮到你。http://www.geeksengine.com/article/register-dll.html - Mauro Sampietro
2
@SimonMourier,问题在于即使设置了.NET 4引用,仍然会在禁用.NET 3.5(和2)的情况下运行时出现自动化错误。只有在PC上启用.NET 2才能正常工作。 这个问题旨在确定如何实例化驻留在.NET 4程序集中的COM对象,而无需使用regasm.exe注册dll。 - Jon Fournier
4个回答

29

这是一个关于从Excel(或VBA)调用.NET的三种主要方法的规范化答案。

这三种方式都适用于.NET 4.0。

1. XLLs

第三方供应商Add-In Express提供了XLL功能,但它是免费的且易于使用的Excel-DNA作者在此处https://stackoverflow.com/users/44264

以下是来自Excel-DNA页面的摘录:https://excel-dna.net/

简介

Excel-DNA是一个独立项目,用于将.NET集成到Excel中。使用Excel-DNA,您可以使用C#,Visual Basic.NET或F#制作本机(.xll)Excel插件,提供高性能的用户定义函数(UDF),自定义功能区界面等。您的整个插件可以打包到单个.xll文件中,无需安装或注册。

入门指南

如果您正在使用支持NuGet包管理器的Visual Studio版本(包括Visual Studio 2012 Express for Windows Desktop),则制作Excel-DNA插件的最简单方法是:

在Visual Basic、C#或F#中创建新的类库项目。 使用管理NuGet包对话框或包管理器控制台安装Excel-DNA包:

PM> Install-Package Excel-DNA

请添加您的代码 (C#、Visual Basic.NET 或 F#):

using ExcelDna.Integration;
public static class MyFunctions
{
    [ExcelFunction(Description = "My first .NET function")]
    public static string SayHello(string name)
    {
        return "Hello " + name;
    }
}

编译、加载并在Excel中使用您的函数:

=SayHello("World!")

2. 自动化插件

这篇由Eric Carter撰写的文章向我们展示了如何做到这一点,但是文章缺少大量图片,因此我将整篇文章复制/粘贴,并重新创建了图片以进行保存。

参考资料:https://blogs.msdn.microsoft.com/eric_carter/2004/12/01/writing-user-defined-functions-for-excel-in-net/

Excel允许创建可在 Excel 公式中使用的用户定义函数。开发人员必须创建一种称为 XLL 的特殊类型的 DLL。Excel还允许您在 VBA 中编写自定义函数,这些函数可在 Excel 公式中使用。不幸的是,Excel 不支持或建议编写使用托管代码的 XLL。如果您愿意冒险,那么有一些解决方案可实现此场景——在网络上搜索“托管XLL”。

幸运的是,有一种更简单的方法可以创建不需要创建 XLL dll 的用户定义函数。Excel XP、Excel 2003 和 Excel 2007 支持称为自动化插件的东西。自动化插件可以很容易地在 C# 或 VB.NET 中创建。我将在本例中向您展示 C# 的一个示例。

首先,启动 Visual Studio 并创建名为 AutomationAddin 的新 C# 类库项目。

然后,在您的 Class1.cs 文件中,输入下面显示的代码。使用 Visual Studio 的“工具”菜单中的“生成 GUID”创建自己的 GUID 来替换 GUID。

using System;
using System.Runtime.InteropServices;
using Microsoft.Win32;

namespace AutomationAddin
{

  // Replace the Guid below with your own guid that
  // you generate using Create GUID from the Tools menu
  [Guid("A33BF1F2-483F-48F9-8A2D-4DA68C53C13B")] 
  [ClassInterface(ClassInterfaceType.AutoDual)]
  [ComVisible(true)]
  public class MyFunctions
  {
    public MyFunctions()
    {

    }

    public double MultiplyNTimes(double number1, double number2, double timesToMultiply)
    {
      double result = number1;
      for (double i = 0; i < timesToMultiply; i++)
      {
        result = result * number2;
      }
      return result;
    }

    [ComRegisterFunctionAttribute]
    public static void RegisterFunction(Type type)
    {
      Registry.ClassesRoot.CreateSubKey(GetSubKeyName(type, "Programmable"));
      RegistryKey key = Registry.ClassesRoot.OpenSubKey(GetSubKeyName(type, "InprocServer32"), true);
      key.SetValue("", System.Environment.SystemDirectory + @"\mscoree.dll",RegistryValueKind.String);
    }

    [ComUnregisterFunctionAttribute]
    public static void UnregisterFunction(Type type)
    {
      Registry.ClassesRoot.DeleteSubKey(GetSubKeyName(type, "Programmable"), false);
    }

    private static string GetSubKeyName(Type type, string subKeyName)
    {
      System.Text.StringBuilder s = new System.Text.StringBuilder();
      s.Append(@"CLSID\{");
      s.Append(type.GUID.ToString().ToUpper());
      s.Append(@"}\");
      s.Append(subKeyName);
      return s.ToString();
    }  
  }
}
完成代码后,在解决方案浏览器中双击项目下的属性节点,显示项目的属性。点击“生成”选项卡,勾选“注册COM互操作”。如果您在Windows Vista或更高版本上运行,还需要执行额外的步骤。必须使用管理员特权运行Visual Studio才能注册COM互操作。保存项目并退出Visual Studio。然后在开始菜单中找到Visual Studio并右键单击它,选择“以管理员身份运行”。重新打开Visual Studio中的项目,然后选择“生成”来构建插件。

enter image description here

现在启动Excel,按照以下步骤打开自动化服务器对话框:

  1.  Launch Excel and click the Microsoft Office button in the top left corner of the window. 
    
  2.  Choose Excel Options.
    
  3.  Click the Add-Ins tab in the Excel Options dialog. 
    
  4.  Choose Excel Add-Ins from the combo box labeled Manage.  Then click the Go button.
    
  5.  Click the Automation button in the Add-Ins dialog.
    

要找到您创建的类,请在Automation插件列表中查找AutomationAddin.MyFunctions:

enter image description here

现在,让我们尝试在Excel中使用函数“MultiplyNTimes”。首先创建一个简单的电子表格,其中包含一个数字,第二个数字是要将第一个数字乘以的数字,第三个数字是要将第一个数字乘以第二个数字的次数。以下是一个示例电子表格:

enter image description here

单击工作簿中数字下方的空单元格,然后单击公式栏中的“插入函数”按钮。从可用公式对话框中,展开“或选择一个类别”下拉框,并选择“AutomationAddin.MyFunctions”。

enter image description here

然后单击“MultiplyNTimes”函数,如下所示:

enter image description here

单击“确定”按钮后,Excel会弹出一个对话框,帮助您从电子表格中获取函数参数,如下所示:

enter image description here

最后,单击“确定”,查看您的最终电子表格,如下所示,在单元格C3中使用自定义公式。

enter image description here


3. 从Excel VBA调用.Net

使用Automation.AddIn项目中的代码,我们可以轻松地从Excel VBA调用MultiplyNTimes函数。

首先,在Excel中添加对DLL的引用,为此,您需要在VB编辑器中。按Alt + F11,然后点击工具菜单和引用:

enter image description here

选择AutomationAddin DLL:

enter image description here

添加VBA代码以调用.Net DLL:

Sub Test()

Dim dotNetClass As AutomationAddIn.MyFunctions
Set dotNetClass = New AutomationAddIn.MyFunctions

Dim dbl As Double
dbl = dotNetClass.MultiplyNTimes(3, 2, 5)

End Sub

神奇的是!

enter image description here


请注意,如果您在C#中使用类,您需要使用ClassInterface标记它们,使用一个带有ComVisible = true标记的接口:如何在Excel VBA中使用CLR类?

最后,一些由“Andrew Whitechapel”撰写的关于Excel和.Net的优秀的MSDN文章- 谷歌搜索它们


2
我非常感谢您为此答案所付出的努力,但它并没有包含与原始问题展示的方法相同的工作方式。虽然问题没有明确说明,但使用该方法的主要原因是可以在不将其注册为VBA引用的情况下调用程序集。您展示的所有方法都需要将DLL注册并作为引用可用。 - Jon Fournier
1
我错过了你想要不带参考进行操作的信息,你仍然可以在没有管理员权限的情况下完成操作,但不幸的是需要使用regasm。 我使用Excel-DNA进行过优秀的体验和项目。我自责忘记在这个答案中提到Robert Giesecke的工作。 - Jeremy Thompson
很棒的答案。你知道如何处理更复杂的类型,比如数组吗? 我需要在VBA和DLL之间传递数组,而不仅仅是整数或字符串。 - Soufien Hajji
我搜索了一下,但没有找到一个通信数据是以JSON或XML格式的示例。我只想在.NET中计算一个二维数组,然后将其发送到VBA,以便在Excel中显示它。如果您有任何想法,请告诉我。 - Soufien Hajji
请提出一个新问题,把链接留在这里,谢谢。我并不是要表现得很生气,但你可以通过谷歌搜索“.net VBA 2d arrays”来解决问题。 - Jeremy Thompson
显示剩余3条评论

13

默认策略是阻止CLR 4执行来自CLR 2的旧代码:

Set clr = New mscoree.CorRuntimeHost

为了启用旧版执行,您可以在excel.exe所在的文件夹中创建文件excel.exe.config:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup useLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntime version="v4.0"/>
  </startup>
</configuration>

或者你可以调用本地函数CorBindToRuntimeEx,而不是New mscoree.CorRuntimeHost

Private Declare PtrSafe Function CorBindToRuntimeEx Lib "mscoree" ( _
    ByVal pwszVersion As LongPtr, _
    ByVal pwszBuildFlavor As LongPtr, _
    ByVal startupFlags As Long, _
    ByRef rclsid As Long, _
    ByRef riid As Long, _
    ByRef ppvObject As mscoree.CorRuntimeHost) As Long

Private Declare PtrSafe Function VariantCopy Lib "oleaut32" (dest, src) As Long


''
' Creates a .Net object with the CLR 4 without registration.  '
''
Function CreateInstance(assembly As String, typeName As String) As Variant
  Const CLR$ = "v4.0.30319"

  Static domain As mscorlib.AppDomain
  If domain Is Nothing Then
    Dim host As mscoree.CorRuntimeHost, hr&, T&(0 To 7)
    T(0) = &HCB2F6723: T(1) = &H11D2AB3A: T(2) = &HC000409C: T(3) = &H3E0AA34F
    T(4) = &HCB2F6722: T(5) = &H11D2AB3A: T(6) = &HC000409C: T(7) = &H3E0AA34F

    hr = CorBindToRuntimeEx(StrPtr(CLR), 0, 3, T(0), T(4), host)
    If hr And -2 Then err.Raise hr

    host.Start
    host.GetDefaultDomain domain
  End If

  VariantCopy CreateInstance, domain.CreateInstanceFrom(assembly, typeName).Unwrap
End Function

1
那似乎是唯一真正回答问题的答案,所以谢谢你并点赞!然而,我认为你的答案会受益于更多的解释:什么是 T 的魔数?为什么我们需要使用 VariantCopy 而不是简单的 SetCorBindToRuntimeEx 在 .NET 4 中已被弃用,为什么我们还要使用它呢? - Heinzi
2
@Heinzi,T 是一个缓冲区,保存了 CorBindToRuntimeEx 使用的 2 个 CLSID。使用 VariantCopy 而不是 Set,这样就不会增加变量的引用计数。我在这里使用 CorBindToRuntimeEx,因为我认为没有其他办法可以解决它,并且我怀疑这种方法不会很快被删除。 - Florent B.
“_config file_” 方法同样适用于 _Microsoft Access_,只需将文件命名为 msaccess.exe.config 即可。 - Gustav

10

以下是经Soraco Technologies测试过的解决方案,适用于.NET 2.0和.NET 4.0,32位和64位。

下面提出的解决方案使用了后期绑定,不需要注册.NET程序集。

声明

将以下声明添加至您的项目中:

#If VBA7 Then
Private Declare PtrSafe Function GetShortPathName Lib “Kernel32.dll” Alias “GetShortPathNameW” (ByVal LongPath As LongPtr, ByVal ShortPath As LongPtr, ByVal Size As Long) As Long
Private Declare PtrSafe Function SetDllDirectory Lib “Kernel32.dll” Alias “SetDllDirectoryW” (ByVal Path As LongPtr) As Long
Private Declare PtrSafe Sub LoadClr_x64 Lib “QlmCLRHost_x64.dll” (ByVal clrVersion As String, ByVal verbose As Boolean, ByRef CorRuntimeHost As IUnknown)
Private Declare PtrSafe Sub LoadClr_x86 Lib “QlmCLRHost_x86.dll” (ByVal clrVersion As String, ByVal verbose As Boolean, ByRef CorRuntimeHost As IUnknown)
#Else
Private Declare Function GetShortPathName Lib “Kernel32.dll” Alias “GetShortPathNameW” (ByVal LongPath As Long, ByVal ShortPath As Long, ByVal Size As Long) As Long
Private Declare Function SetDllDirectory Lib “Kernel32.dll” Alias “SetDllDirectoryW” (ByVal Path As Long) As Long
Private Declare Sub LoadClr_x64 Lib “QlmCLRHost_x64.dll” (ByVal clrVersion As String, ByVal verbose As Boolean, ByRef CorRuntimeHost As IUnknown)
Private Declare Sub LoadClr_x86 Lib “QlmCLRHost_x86.dll” (ByVal clrVersion As String, ByVal verbose As Boolean, ByRef CorRuntimeHost As IUnknown)
#End If ‘ WinAPI Declarations

' Declare variables
Dim m_myobject As Object
Dim m_homeDir As String

初始化

您必须将 m_homeDir 变量初始化为 .NET 程序集所在的路径。

例如,如果您将 .NET 程序集安装在与 Excel 或 MS-Access 文件相同的文件夹中,则应将 m_homeDir 初始化为:

Excel:m_homeDir = ThisWorkbook.Path

Access:m_homeDir = CurrentProject.Path

.NET 对象创建

将以下代码添加到您的项目中。

Private Function GetMyObject(dllPath As String, dllClass As String) As Object
    Dim LongPath As String
    Dim ShortPath As String

    LongPath = “\\?\” & m_homeDir
    ShortPath = String$(260, vbNull)

    PathLength = GetShortPathName(StrPtr(LongPath), StrPtr(ShortPath), 260)
    ShortPath = Mid$(ShortPath, 5, CLng(PathLength – 4))

    Call SetDllDirectory(StrPtr(ShortPath))
    Dim clr As mscoree.CorRuntimeHost

    If Is64BitApp() Then
        Call LoadClr_x64(“v4.0”, False, clr)
    Else
        Call LoadClr_x86(“v4.0”, False, clr)
    End If

    Call clr.Start

    Dim domain As mscorlib.AppDomain
    Call clr.GetDefaultDomain(domain)

    Dim myInstanceOfDotNetClass As Object
    Dim handle As mscorlib.ObjectHandle

    Set handle = domain.CreateInstanceFrom(dllPath, dllClass)

    Dim clrObject As Object
    Set GetMyObject = handle.Unwrap

    Call clr.Stop
End Function

Private Function Is64BitApp() As Boolean

    #If Win64 Then
        Is64BitApp = True
    #End If
End Function

实例化.NET对象

现在您已经准备好实例化您的.NET对象并开始使用它。将以下代码添加到您的应用程序中:

m_homeDir = ThisWorkbook.Path 

m_myobject = GetMyObject(m_homeDir & “\yourdotnet.dll”, “namespace.class”)

第一个参数是指向.NET DLL的完整路径。

第二个参数是所请求类型的完全限定名称,包括命名空间但不包括程序集,由Type.FullName属性返回。

所需DLL

此解决方案需要部署两个DLL,这些DLL负责托管.NET CLR。这些DLL应该部署在与您的Excel或MS-Access文件相同的文件夹中。

这些DLL可以从Soraco的网站下载:https://soraco.co/products/qlm/QLMCLRHost.zip

LGPL-2.1许可

我们特此授予您使用我们的DLL的权利,只要您的应用程序不直接或间接地与Quick License Manager竞争。您可以在商业或非商业应用程序中使用这些DLL。


5
你是否在" Soraco Technologies "工作?如果是,你应该披露你的关联。请参阅回答中自我推广的限制 - Liam

7
我不确定这是巧合还是因为我发布了相关问题,SO向我展示了您的问题,我认为我也可以做出一些贡献。当使用VBA和DLL时,目前大多数解决方案都告诉我要注册DLL并使其可见,如果您在自己的PC上进行操作,那完全没问题,但如果您要分发VBA应用程序,您不希望在他们的系统中安装DLL。您可能没有权限或者不想经历安装/卸载过程或处理引用问题。然而,您可以使用一些Windows API动态加载dlls。现在的问题是如何从vba访问.NET dll?如果您的客户端具有混合的OS架构x86 x64,则需要相应地处理。假设我们正在使用32位office/Excel。
如果您创建了一个.NET dll,并希望从VBA中访问它,它会抛出类似于“找不到dll入口点”的错误消息。幸运的是,Robert Giesecke 创建了一个抽象包装器,允许您创建简单的DLL,可通过VBA使用。

这里可以找到一个模板

您需要做的只有:

  1. 在Visual Studio中创建一个新的类项目
  2. 将项目平台设置为32位或其他
  3. 在主类中创建您的方法
  4. 创建另一个类,它将返回您的主类作为对象(返回给VBA)
  5. (按照他网站上的模板操作)

假设您已经按照他的模板创建了一个测试方法,如下所示。

[ComVisible(true), ClassInterface(ClassInterfaceType.AutoDual)]
public class YOUR_MAIN_CLASS
{
    [return: MarshalAs(UnmanagedType.BStr)]
    public string FN_RETURN_TEXT(string iMsg)
    {

        return "You have sent me: " + iMsg + "...";
    }
}

和您的未托管导出类:

static class UnmanagedExports
{
    [DllExport]
    [return: MarshalAs(UnmanagedType.IDispatch)]
    static object YOUR_DLL_OBJECT()
    {
        return new YOUR_MAIN_CLASS();
    }
}

准备从VBA端访问dll

将DLL添加到您的根文件夹中:

#If VBA7 Then 
    Public Declare PtrSafe Function LoadLibrary Lib "kernel32" Alias "LoadLibraryA" (ByVal lpLibFileName As String) As LongPtr
    Public Declare PtrSafe Function YOUR_DLL_OBJECT Lib "YOUR_DLL.dll" () As Object
#Else
    Public Declare Function LoadLibrary Lib "kernel32" Alias "LoadLibraryA" (ByVal strFilePath As String) As Long
    Public Declare Function YOUR_DLL_OBJECT Lib "YOUR_DLL.dll" () As Object
#End If

现在重点是加载dll并在vba中创建和访问对象。 这将是:
LoadLibrary (FN_APP_GET_BASE_PATH & "YOUR_DLL.dll")
dim mObj as object
set mObj = YOUR_DLL_OBJECT()
debug.print mObj.FN_RETURN_TEXT("Testing ..")

输出应该是:
"You have sent me: Testing ....."

优点 个人不喜欢安装和引用dll文件。通过上述模板,您无需引用任何内容,也无需安装任何内容,只需加载并自由地使用您的DLL。

注意:我假设dll/.net代码是您自己的,并且您可以再次使用上述模板进行编译。

我成功地使用了上述模板,并为vba创建了一个.NET非阻塞通知,您可以在这里查看:非阻塞类似“toast”的Microsoft Access(VBA)通知


你能检查一下你的例子吗?我用自己的类尝试了一下,但在 MS Access 中一直出现“Bad DLL calling convention”错误。因此,我基于你的示例创建了一个全新的 DLL,在这一行“set mObj = YOUR_DLL_OBJECT()”处也遇到了同样的错误。无论是否勾选“Make assembly COM-visible”,都会发生这种情况。平台为 x86。 - Nathaniel Roark
或者,如果您再次将VBA_TOOLS的源代码上传到GitHub上,我可以亲自查看您是如何构建您的库的。 - Nathaniel Roark
1
我找到问题了!条件声明遵循VBA7路径并使用PtrSafe声明,但它不起作用。我将其删除,因此它使用传统的声明,这起作用了。虽然我应该在VBA7上运行,但我查看了声明并在末尾添加了“ As Object”。现在它可以工作了! - Nathaniel Roark
3F/DllExport 如果您在2022年查看此内容- Giesecke先生的NuGet软件包已不再维护(或不再兼容最近的VS版本);工作似乎已经转移到Denis Kusmin的DllExport项目中。您需要搜索有关VBA的问题:DllExport VBA issues - unbob

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接