【问题标题】:object life-cycle for COM objects passed from .NET into VBA从 .NET 传递到 VBA 的 COM 对象的对象生命周期
【发布时间】:2025-12-01 04:30:01
【问题描述】:

我的组织有时需要使用 Excel 来生成一堆格式化的报表(在文档的意义上说“您的帐户余额是 $X”),将它们打印成 PDF,然后将它们组合成一个大 PDF。通常使用的方法涉及由索引单元驱动的单个工作表和另一个工作表上的人员/数据列表。 VBA 宏从 1 到 N 迭代索引单元格,然后每次使用 Adob​​e Distiller API 打印格式化的工作表并组合结果。

出于各种原因,我想在我们的 VSTO Excel 插件中用 C# 实现这个宏的大部分逻辑,以便将过程的 VBA 端减少到几行。

我决定公开一个大致如下所示的 API:

AcroPDDoc PdfBegin(Worksheet worksheet, string filename);
void PdfAddPage(AcroPDDoc pdf, Worksheet worksheet);
void PdfComplete(AcroPDDoc pdf);

你的想法是写 VBA 的形式:

Sub PrintToPdf()
    Dim obj As IMySharedObject
    Set obj = Application.COMAddIns("MyAddIn").Object

    Dim pdf As Acrobat.AcroPDDoc

    Dim i As Long
    For i = 1 To 10
        Range("counter").Value = i

        If i = 1 Then
            Set pdf = obj.PdfBegin(Sheets("Statement"), "C:\myFile.pdf")
        Else
            PdfAddPage pdf, Sheets("Statement")
        End If
    Next i

    PdfComplete pdf
End Sub

我对@9​​87654323@ 对象的生命周期以及打开文件句柄、Acrobat.exe 进程等感到好奇/担心,以防宏遇到错误或在执行过程中终止。不用超级担心,因为“关闭 Excel 并重新打开它”是一种可以接受的解决方案(如果需要)。我用 C# 编写了以下代码:

internal static class Printing
{
    private static WeakReference weakref;

    public static AcroPDDoc PdfBegin(Worksheet worksheet, string filename)
    {
        SetAdobeOutputFile(filename);
        worksheet.PrintOut(ActivePrinter: "Adobe PDF");

        AcroPDDoc pdf = new AcroPDDoc();
        pdf.Open(filename);
        weakref = new WeakReference(pdf);

        return pdf;
    }

    public static void GC()
    {
        System.GC.Collect();
    }

    public static void test(AcroPDDoc pdf)
    {
        if (weakref != null) {
            System.Diagnostics.Debug.WriteLine("IsAlive pre: " + weakref.IsAlive);
            if (weakref.IsAlive) System.Diagnostics.Debug.WriteLine("ReferenceEquals: " + Object.ReferenceEquals(pdf, weakref.Target));
        }

        GC.Collect();

        if (weakref != null) System.Diagnostics.Debug.WriteLine("IsAlive post: " + weakref.IsAlive);
    }
}

我省略了一堆额外的Debug.WriteLines 和一些其他无关的代码。我使用以下 VBA 对其进行了测试:

Sub foo()
    Dim obj As IUDFSharedObject
    Set obj = Application.COMAddIns("MyAddIn").Object

    Dim pdf As Acrobat.AcroPDDoc
    Set pdf = obj.PdfBegin(Sheets("Statement"), "C:\myFile.pdf")
    'obj.GC
    'obj.test pdf
End Sub

我发现,一般来说,.NET 不包括在其垃圾收集的引用计数中发送到 VBA-land 的引用。

例如,如果我只取消注释 obj.GCobj.test pdf,我会被告知 weakref 不存在。

但是,如果我只取消注释 obj.test pdfweakref 在之前和之后都是有效的(并且我发出“ReferenceEquals: true”)。

请注意,pdf一直都在 VBA 的范围内。我最初测试了如果你让pdf 也逃逸 VBA 范围会发生什么,但事实证明这并不重要。

这对我来说是一个比资源链接大得多的问题。除了永久存储在List 中生成的每个AcroPDDoc 对象以保持引用计数高于零之外,是否有任何解决方案?

【问题讨论】:

  • ++ 用于格式良好的问题。一个问题可能是为什么不完全消除 VBA 的使用而只依赖 .NET 调用。 IE。您可以在 .NET 加载项中初始化并设置对工作表/单元格的引用并从 .NET 调用 PDF API?
  • 其实我是要公开这样一个功能的(虽然上面没有提到)。我也需要这些函数的原因是,我已经知道很多逻辑不能准确地确认这种模式的情况——例如某些索引被跳过,或者有额外的逻辑来决定为每个索引打印出哪个工作表。

标签: c# excel vba pdf vsto


【解决方案1】:

请注意,您在 .Net 中对 AcroPDDoc 的引用实际上是对您通过不同生态系统传递的进程外 COM 对象的包装器,.Net 框架不完全控制底层对象的生命周期,引用计数由COM 服务器,只要有一个对象的 COM 引用(来自 VBA 或来自 .Net),该对象就会保持活动状态。

我相信您会发现这个问题及其答案很有趣: RCW & reference counting when using COM interop in C#

【讨论】:

  • 这实际上是我有点困惑的事情。我一般都知道 COM 对象在 .Net 中是如何工作的,并且知道包装器。我有一半的预期是,我尝试将Acrobat.AcroPDDoc 返回到 VBA-land 会失败,因为返回的对象实际上是一个 .Net 包装器。但是我可以很好地使用返回的对象——例如,我可以在 VBA 中调用pdf.GetFileName(),它的行为与预期的一样。所以我认为 COM 对象在发送到 VBA 之前可能是未包装的,但正如您在 test2 中看到的那样,在 C# -> VBA -> C# 往返之后,该对象仍然是引用相等的。
  • 我认为您正在计算对包装器的引用,而不是对 COM 对象本身的引用
  • (因为我达到了上面的评论长度限制)您提供的链接,就像我在提出这个问题之前发现的其他内容一样,涉及“COM 对象在访问时会发生什么情况。网”。我试图弄清楚从.Net到COM的相反方向的对象会发生什么。通常,要让一个对象完全做到这一点,我必须用[ComVisible] 装饰它的类并注册它等等,但是 Acrobat 已经完成了 COM 注册。
  • re:计算对包装器的引用,这是我一直在考虑的事情,但对我来说并不完全有意义。 ReferenceEquals 测试让我非常确定实际上相同的指针被传递到 VBA 并退出。这意味着 VBA 能够成功调用 GetFileName(),相信它拥有的指针是指向常规 Acrobat.AcroPDDoc 对象的指针,而不是 .Net 包装器对象。
  • 好的,阅读您的链接以及它链接的所有内容,并在谷歌上搜索更多内容,我想我已经弄清楚了。当一个 COM 对象被传递给 C# 时,.NET 实际上会运行一些 RCW 的主列表,它必须查看它们中是否有任何一个已经指向该对象。如果有,则将 C# 交给该 RCW,否则将创建一个新的。现在事情变得更有意义了。
【解决方案2】:

感谢上面的@yms,我已经弄清楚发生了什么,并提出了一个我相当满意的解决方案。一、API稍作修改:

void PdfBegin(AcroPDDoc pdf, Worksheet worksheet, string filename);
void PdfAddPage(AcroPDDoc pdf, Worksheet worksheet);
void PdfComplete(AcroPDDoc pdf);

每个 C# 方法在返回之前都会调用 Mashal.ReleaseComObject(pdf)。我确实读过Marshal.ReleaseComObject considered dangerous,但我已经测试了他所说的特定故障模式,发现它在实践中似乎没有发生。

VBA 现在必须从一开始就提供 AcroPDDoc 对象。因此,典型用法如下所示:

Sub PrintToPdf()
    Dim obj As IMySharedObject
    Set obj = Application.COMAddIns("MyAddIn").Object

    Dim pdf As New AcroPDDoc

    Dim i As Long
    For i = 1 To 10
        Range("counter").Value = i

        If i = 1 Then
            obj.PdfBegin pdf, Sheets("Statement"), "C:\myFile.pdf"
        Else
            obj.PdfAddPage pdf, Sheets("Statement")
        End If
    Next i

    obj.PdfComplete pdf
End Sub

基本上只是声明现在是As New AcroPDDoc,而不是As AcroPDDoc 和后来的Set

测试表明,一旦 AcroPDDoc 的引用计数超出范围或引用设置为 Nothing,VBA 会非常迅速地减少它的引用计数。这包括在子程序中间引发错误并且用户结束执行的情况。

最后,Acrobat.exe 进程也会在其 refcount 达到零时提示自己关闭,即使它打开了一个文件。

【讨论】:

    最近更新 更多