在C#和VBA之间进行通信

8
在我的老板的要求下,我创建了一组小脚本,用于定期监测某些设备和进程的状态。然后使用一个相对复杂的VBA模块处理这些信息,收集所有信息,应用公式,设置范围并生成图表等。
然而,存在两个问题: 我是一名业余程序员,所以我的VBA例程效率相当低。对我来说这不是什么大问题(我只需起身去喝杯咖啡),但对其他用户来说可能会很麻烦,因为他们不知道为什么需要这么长时间。我想要一个进度的图形表示。 应用程序的配置是通过文本文件完成的。我想提供一个良好的GUI来设置配置。
为了实现这一点,我正在寻找一种让我的C# WinForms应用程序与我的VBA应用程序通信的方法。我知道如何从我的C#应用程序运行VBA例程,但我不知道如何让它们实时通信。
以下是我特别想要实现的两件事:
1. 我已经有一个在VBA例程结束时保存的日志文件。但我想要将日志/调试消息实时发送到我的C#应用程序(不仅仅是在应用程序结束时),以便可以在我的GUI应用程序中显示VBA应用程序生成的消息。
2. 我还希望VBA应用程序向我的GUI应用程序发送有关实时进度的信息,以便我可以在我的GUI应用程序中创建一个图形进度条。
我已经考虑过通过标准输出进行通信。我知道如何使用C#/.Net从标准输出读取,但我不确定如何使用VBA写入到StdOut流。
我相信很多人会指出我正在尝试实现的是愚蠢和老式的(或者完全没有必要的),但对我来说,作为一名业余程序员,这似乎是一个非常具有挑战性和有趣的项目,可以教我一些东西。

2
我建议你把时间花在让VBA应用程序变得更好上。此外,你也可以在VBA中实现进度条和日志记录,没有理由引入额外的复杂性,如果不必要的话。 - Jonathon Reinhart
首先,我不认为我能够创建一个更快或更高效的应用程序,因为我已经几乎使用了我所有关于VBA的知识。正如我所说,我是一个绝对的业余爱好者。其次:我希望“Excel层”在工作簿完全生成之前是不可见的。第三:我在VBA中创建表单完全没有经验,而在C#中有一些经验。第四:就像我说的,这似乎是一件非常有趣的事情。 - romatthe
1
你是说VBA实际上只是另一个脚本的外壳吗?如果是这样,你应该从C#调用该脚本。或者,为什么不修改你的C#前端以定期轮询日志文件,并显示状态栏呢? - Nick.McDermaid
你是从C#程序中启动VBA例程,并尝试从VBA返回进度吗?这可能会变得非常复杂。@ElectricLlama的解决方案可能是最简单的方法;否则,您将不得不使用命名管道或TCP/IP。在两种情况下,我认为您需要在C#应用程序中实现多线程,因为从C#调用VBA例程将冻结您的主线程(至少如果您正在使用Eval()或CallByName()来从C#调用它)。 - transistor1
在你的C#代码中捕获VBA错误,也可以参考这个 - transistor1
显示剩余3条评论
4个回答

10

创建一个C# COM可见类非常容易,而且(正如您在评论中所说)它还很有趣。这是一个小示例。

在一个新的C#库项目中添加:

using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace CSharpCom
{
    [ComVisible(true)]
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    //The 3 GUIDs in this file need to be unique for your COM object.
    //Generate new ones using Tools->Create GUID in VS2010
    [Guid("18C66A75-5CA4-4555-991D-7115DB857F7A")] 
    public interface ICSharpCom
    {
        string Format(string FormatString, [Optional]object arg0, [Optional]object arg1, [Optional]object arg2, [Optional]object arg3);
        void ShowMessageBox(string SomeText);
    }

    //TODO: Change me!
    [Guid("5D338F6F-A028-41CA-9054-18752D14B1BB")] //Change this 
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    interface ICSharpComEvents
    {
        //Add event definitions here. Add [DispId(1..n)] attributes
        //before each event declaration.
    }

    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.None)]
    [ComSourceInterfaces(typeof(ICSharpComEvents))]
    //TODO: Change me!
    [Guid("C17C5EAD-AA14-464E-AD32-E9521AC17134")]
    public sealed class CSharpCom : ICSharpCom
    {
        public string Format(string FormatString, [Optional]object arg0, [Optional]object arg1, [Optional]object arg2, [Optional]object arg3)
        {
            return string.Format(FormatString, arg0, arg1, arg2, arg3);   
        }

        public void ShowMessageBox(string SomeText)
        {
            MessageBox.Show(SomeText);
        }
    }
}

您需要进入项目属性,在"签名"选项卡中勾选"使用强名称来签署程序集",并创建一个新的"强名称密钥文件"。这将有助于避免在注册DLL时出现版本问题。

编译后,在Visual Studio命令提示符中使用regasm注册DLL。根据您使用的Office版本选择32位或64位的regasm...您可以在以下路径中找到RegAsm:C:\windows\Microsoft.NET\FrameworkC:\windows\Microsoft.NET\Framework64 (分别对应32位和64位):

C:\windows\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe /codebase /tlb CSharpCom.dll

这将在Windows中注册您的DLL,因此现在在VBA编辑器中,您应该能够转到工具->引用并找到您的COM的命名空间"CSharpCom"。勾选该框,现在您就可以在VBA中创建您的COM对象:

Sub TestCom()
    Dim c As CSharpCom.CSharpCom
    Set c = New CSharpCom.CSharpCom
    c.ShowMessageBox c.Format("{0} {1}!", "Hello", "World")
End Sub
你可以向你的COM对象添加表单,并在VBA调用特定方法时打开它们;你应该能够使用此方法创建带有进度条的表单。但需要考虑的一件事是,VBA是单线程的。这意味着在您的代码运行时,所有其他操作都会被冻结,因此情况可能会变得有点棘手。
我想到的方法是,在COM可见类中创建3个方法:一个用于打开表单,一个用于更新进度(在VBA调用您的COM对象的“update()”方法后立即调用VBA的DoEvents()方法,以允许Office处理屏幕更新),以及一个用于关闭表单。你应该在表单上调用Dispose()方法;这可以在“close”方法中完成,但如果VBA崩溃并且您的关闭方法从未被调用,这可能会引起内存泄漏/问题,只是另一个需要考虑的问题。

我觉得他正在尝试在C#程序中捕获VB引发的事件,而不是反过来。 - Nick.McDermaid
是的,这正是它的作用...COM-visible C#对象是用C#编写的,并在VBA(或任何地方)中实例化 - 这就是它们的用途。反过来是不行的。 - transistor1
这确实做到了我所要求的,但这是一种相当高级的技术。我玩弄这些不同的解决方案真的很有趣,但最终我(可以预料地)得出结论,需要彻底重新思考我的“应用程序”(如果这样一个名称确实可以应用于我在这里看到的内容)。谢谢你的意见,我确实学到了不少东西! - romatthe
我已经需要这样的东西有一段时间了,因为在长时间运行操作时,Access会冻结。向用户显示某种进度很好。您可以在Access中显示进度,但我发现这样做有点笨拙,并且仍然存在无法解决冻结的情况。我抓住这个机会创建了一个东西,很高兴与您分享:[Google文档链接](https://docs.google.com/open?id=0B02kklF3apJcMGJNVVJoOFczWEk)。阅读包含的README.txt文件以获取详细信息。 - transistor1
另外,需要考虑的一件事是,如果使用这样的东西,.dll 文件需要在您的 VBA 应用程序使用的每台机器上进行注册。可以在不注册它们的情况下使用 .NET DLL,但这需要在 VBA 中进行一些额外的编码。有关更多详细信息,请参见此处:这种技术是我将要分发我的进度条的方式。 - transistor1

5
您可以使用.NET DLL与您的VB6项目,并创建在程序集中定义的任何对象的实例。这是通过为COM互操作(在VB6中使用)注册.NET程序集(.dll)并创建类型库文件(.tlb)来完成的。.tlb文件包含额外的信息,以便您的VB6项目可以使用您的.dll。请注意,您的VB6项目将不会引用.dll文件,而是对应的.tlb文件。您可以使用Regasm.exe工具生成和注册类型库,并注册托管程序集的位置。然后只需创建您的.NET对象的实例,无论是WinForm还是其他一些酷类。=)。
Dim MyDotNetObject As MyDotNetClass

Set MyDotNetObject = New MyDotNetClass
MyDotNetObject.SomeMethod(value1, value2)

''end of execution
Set MyDotNetObject = Nothing

请参考:

请查看:http://support.microsoft.com/default.aspx?scid=kb;en-us;817248


我觉得他试图在C#程序中捕获VB引发的事件,而不是反过来。 - Nick.McDermaid

4
我相信这个链接中的代码大致可以达到您想要的效果:
  • C#代码创建了一个COM对象(在示例中是IE,但根据您的描述,您已经成功创建了VBA例程的实例)

  • 它使用+=将一个.Net事件处理程序附加到由COM对象引发的事件(当标题更改时)

  • 它定义了样本事件处理代码

http://msdn.microsoft.com/en-us/library/66ahbe6y.aspx

我并不打算理解这段代码,我的目标是展示这种解决方案有多复杂!您应该真正地构建一个完整的解决方案。完全的VBA解决方案将更快地开发(您只需要学习易懂的VBA表单),但是这是旧技术。完全的C#解决方案将更慢地开发,但是您将学习C#。

实际上,性能是一个问题。如果现在执行效率低下,当您要处理5倍数量的记录时会发生什么?您应该在出现那么多记录之前解决性能问题。


0

这个问题的其他答案涉及到VBA宏调用外部方法。但有一种更简单的方法。您的GUI应用程序可以附加到工作簿的SheetSelectionChange事件或工作表Change事件,当任何值发生变化时将触发该事件。

因为您可能不想减慢所有操作,我建议添加一个额外的工作表,添加命名范围来描述每个值的含义,然后将工作表更改事件挂钩以拦截这些值发生变化并更新您的GUI。


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