Return VBA 的 C# dll 中的字符串[]

Return string[] in C# dll for VBA

我使用 UnmanagedExports(作为 NuGet 包获得)编写了这个小的 C# 测试 DLL,它运行良好。但是,我想知道是否可以立即 return 一个 String() 数组,而不是 returning 一个必须是 [=33= 中的 Split() 的字符串] 包装函数。

也就是说,关注点是方法GetFilesWithExtension()。 dll 中的其他方法只是我在弄清楚如何使用正确的编码传递字符串时所做的小测试。

DLL 针对 x64 和 .NET 4.5.2,但您也应该能够为 x86 构建(并相应地更改 VBA 中的函数声明)。

C# class 库 (TestDll.dll):

using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using RGiesecke.DllExport;

namespace TestDll
{
    public class Class1
    {
        [DllExport(nameof(Addition), CallingConvention.StdCall)]
        public static int Addition(int a, int b)
        {
            return a + b + 100;
        }


        [DllExport(nameof(LinqAddition), CallingConvention.StdCall)]
        public static int LinqAddition(int a, int b)
        {
            return new int[] {a, b, 1, 4, 5, 6, 7, 8 }.Sum();
        }

        [DllExport(nameof(LinqAdditionString), CallingConvention.StdCall)]
        [return: MarshalAs(UnmanagedType.AnsiBStr)]
        public static string LinqAdditionString(int a, int b)
        {
            return new int[] { a, b, 1, 4, 5, 6, 7, 8 }.Sum() + "";
        }

        [DllExport(nameof(GetFilesWithExtension), CallingConvention.StdCall)]
        [return: MarshalAs(UnmanagedType.AnsiBStr)]
        public static string GetFilesWithExtension([MarshalAs(UnmanagedType.AnsiBStr)] string folderPath, [MarshalAs(UnmanagedType.AnsiBStr)] string extension, bool includeSubdirectories)
        {
            //Debug
            //File.WriteAllText(@"C:\Users\johanb\Source\Repos\TestDll\output.txt", $"folderPath: {folderPath}, extension: {extension}, includeSubdirectories: {includeSubdirectories}");
            try
            {
                if (!Directory.Exists(folderPath))
                    return "";

                extension = extension.Trim('.');

                return string.Join(";",
                    Directory.GetFiles(folderPath, "*.*",
                            includeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)
                        .Where(
                            f =>
                                Path.GetExtension(f)?
                                    .Trim('.')
                                    .Equals(extension, StringComparison.InvariantCultureIgnoreCase) ?? false)
                        .ToArray());
            }
            catch (Exception ex)
            {
                return ex.ToString();
            }
        }
    }
}

VBA 模块(在 Excel 中测试):

Attribute VB_Name = "TestDll"
Option Explicit

Public Declare PtrSafe Function Addition Lib "C:\Users\johanb\Source\Repos\TestDll\TestDll\bin\Debug\TestDll.dll" (ByVal a As Long, ByVal b As Long) As Long
Public Declare PtrSafe Function LinqAddition Lib "C:\Users\johanb\Source\Repos\TestDll\TestDll\bin\Debug\TestDll.dll" (ByVal a As Long, ByVal b As Long) As Long
Public Declare PtrSafe Function LinqAdditionString Lib "C:\Users\johanb\Source\Repos\TestDll\TestDll\bin\Debug\TestDll.dll" (ByVal a As Long, ByVal b As Long) As String
Public Declare PtrSafe Function GetFilesWithExt Lib "C:\Users\johanb\Source\Repos\TestDll\TestDll\bin\Debug\TestDll.dll" Alias "GetFilesWithExtension" (ByVal folderPath As String, ByVal extension As String, ByVal includeSubdirs As Boolean) As String

Sub Test()
    Dim someAddition As Long
    Dim someLinqAddition As Long
    Dim someLinqAdditionAsString As String
    Dim files() As String
    Dim i As Long

    someAddition = Addition(5, 3)
    Debug.Print someAddition

    someLinqAddition = LinqAddition(5, 3)
    Debug.Print someLinqAddition

    someLinqAdditionAsString = LinqAdditionString(5, 3)
    Debug.Print someLinqAddition

    files = GetFilesWithExtension("C:\Tradostest\Project 4", "sdlxliff", True)
    For i = 0 To UBound(files)
        Debug.Print files(i)
    Next i

End Sub

Function GetFilesWithExtension(folderPath As String, extension As String, includeSubdirs As Boolean) As String()
    GetFilesWithExtension = Split(GetFilesWithExt(folderPath, extension, includeSubdirs), ";")
End Function

我永远无法将对象返回到 Excel 以正常工作,但是通过引用来回传递对象就可以了。不管出于什么原因,我不得不使用关键字 ref 而不是 out,否则 Excel 会崩溃。

我还必须对字符串使用 UnmanagedType.AnsiBstr 才能获得正确的编码,但对于字符串数组,我让它工作的唯一方法是将其声明为对象并在 运行 方法开始时的时间。

using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using RGiesecke.DllExport;

namespace TestDll
{
    public class FolderHandling
    {
        [DllExport(nameof(GetFilesByExtensions), CallingConvention.StdCall)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static bool GetFilesByExtensions(
            ref object arrayOfFiles,                                  //out doesn't work
            [MarshalAs(UnmanagedType.AnsiBStr)] string folderPath,
            object extensions,                                       //type safety breaks it..somehow
            [MarshalAs(UnmanagedType.Bool)] bool includeSubdirectories)
        {
            try
            {
                if (!Directory.Exists(folderPath))
                {
                    arrayOfFiles = new[] { $"Parameter {nameof(folderPath)} ({folderPath}) is not a folder" };
                    return false;
                }

                if (!(extensions is string[]))
                {
                    arrayOfFiles = new[] { $"Parameter {nameof(extensions)} is not a string array" };
                    return false;
                }

                var exts = ((string[])extensions).Select(e => e.Trim('.').ToLowerInvariant()).ToArray();

                var files = Directory.GetFiles(folderPath, "*.*",
                        includeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)
                    .Where(f => exts.Contains(Path.GetExtension(f)?.Trim('.').ToLowerInvariant() ?? ";;;"))
                    .ToArray();


                //normalize ANSI just in case
                General.NormalizeANSI(ref files);

                arrayOfFiles = files;

                return true;
            }
            catch (Exception ex)
            {
                arrayOfFiles = new[] { "Exception: " + ex };
                return false;
            }
        }
    }
}


using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

namespace TestDll
{
    static class General
    {
        public static void NormalizeANSI(ref string[] files)
        {
            for (int i = 0; i < files.Length; i++)
            {
                files[i] = string.Concat(files[i].Normalize(NormalizationForm.FormD).Where(c => CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark));
            }
        }
    }
}

我可以在 Excel 中使用我的 DLL,如下所示,使用 LoadLibrary(),这样我就不必将它放在用户的系统文件夹中或注册 COM。使用 FreeLibrary() 的好处是它允许我在不关闭的情况下重新编译 C# 项目 Excel.

Public Declare PtrSafe Function GetFilesByExtensions Lib "TestDll.dll" (ByRef filesRef, ByVal folderPath As String, ByVal extensions, ByVal includeSubdirs As Boolean) As Boolean

Private Declare PtrSafe Function FreeLibrary Lib "kernel32" (ByVal hLibModule As Long) As Long
Private Declare PtrSafe Function LoadLibraryA Lib "kernel32" (ByVal lpLibFileName As String) As Long

Private Function LoadLibrary(dllName As String) As Long
    Dim path As String
    path = ThisWorkbook.path & "\" & dllName
    LoadLibrary = LoadLibraryA(path)
End Function

Sub TestFolderFiltering()
    Dim files() As String
    Dim i As Long
    Dim moduleHandle As Long

    On Error GoTo restore

    moduleHandle = LoadLibrary("TestDll.dll")

    If GetFilesByExtensions(files, "C:\Tradostest\Project 4", Split("sdlxliff", ";"), True) Then
        For i = 0 To UBound(files)
            Debug.Print " - " & files(i)
        Next i
    Else
        Debug.Print "ERROR: " & files(0)
    End If

restore:
    If moduleHandle <> 0 Then
        Call FreeLibrary(moduleHandle)
    End If

End Sub

也可以像这样将 COM 对象从 VBA 传递到 DLL,并使用标准的 Microsoft Interop 库或 NetOffice 处理它们,我已经设法编写了一个过滤 VBA string arrays by the string representation of a C# lambda expression,这听起来对很多人来说可能会派上用场:

If FilterStringArray(myArr, "s => s.ToUpperInvariant().Equals(s, StringComparison.CurrentCulture)") Then
    For i = 0 To UBound(myArr)
        Debug.Print " - " & myArr(i)
    Next i
Else
    Debug.Print "ERROR: " & myArr(0)
End If

您可以在 GitLab 上找到整个项目。