VBA在c# dll中返回字符串[]

本文关键字:字符串 返回 dll VBA | 更新日期: 2023-09-27 18:13:08

我使用UnmanagedExports(作为NuGet包获得)编写了这个小的c#测试DLL,它工作良好。然而,我想知道是否,如果是这样,如何,有可能立即返回一个String()数组,而不是返回一个必须在VBA包装函数中Split()的字符串。

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

DLL的目标是x64和。net 4.5.2,但你也应该能够为x86构建(并相应地改变VBA中的函数声明)。

c#类库(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

VBA在c# dll中返回字符串[]

我从来没有得到返回一个对象到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));
            }
        }
    }
}

我可以使用我的DLL在Excel如下,使用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,并使用标准的微软互操作库或NetOffice处理它们,并且我已经设法编写了一个方法,通过c# lambda表达式的字符串表示过滤VBA字符串数组,这听起来可能对许多人有用:

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上找到整个项目