如何正确清理 Excel 互操作对象?

How do I properly clean up Excel interop objects?

提问人:HAdes 提问时间:10/2/2008 最后编辑:Peter MortensenHAdes 更新时间:10/28/2023 访问量:346709

问:

我在 C# () 中使用 Excel 互操作,并将以下代码放在我的 finally 子句中:ApplicationClass

while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { }
excelSheet = null;
GC.Collect();
GC.WaitForPendingFinalizers();

尽管这种工作有效,但即使在我关闭Excel之后,该过程仍在后台进行。只有在手动关闭我的应用程序后,它才会发布。Excel.exe

我做错了什么,或者是否有其他方法可以确保正确处置互操作对象?

C# Excel 互操作 com-interop

评论

3赞 Bryant 10/2/2008
您是否尝试在不关闭应用程序的情况下关闭 Excel.exe?不确定我是否完全理解您的问题。
4赞 HAdes 10/2/2008
我正在尝试确保正确处置非托管互操作对象。这样,即使用户完成了我们从应用程序创建的 Excel 电子表格,也不会有 Excel 进程徘徊。
4赞 Jeremy Thompson 4/25/2012
如果可以尝试通过生成 XML Excel 文件来执行此操作,否则请考虑 VSTO 非托管内存管理: jake.ginnivan.net/vsto-com-interop
0赞 Paul C 11/26/2012
这是否能很好地转化为 Excel?
2赞 Arjan 2/25/2015
请参阅(除了下面的答案)Microsoft 的这篇支持文章,其中专门提供了此问题的解决方案: support.microsoft.com/kb/317109

答:

46赞 Philip Fourie 10/2/2008 #1

这适用于我正在从事的一个项目:

excelApp.Quit();
Marshal.ReleaseComObject (excelWB);
Marshal.ReleaseComObject (excelApp);
excelApp = null;

我们了解到,在完成对 Excel COM 对象的每个引用时,将它设置为 null 非常重要。这包括单元格、工作表和所有内容。

711赞 VVS 10/2/2008 #2

Excel 不会退出,因为应用程序仍保留对 COM 对象的引用。

我猜您正在调用 COM 对象的至少一个成员,而没有将其分配给变量。

对我来说,它是excelApp.Worksheets对象,我直接使用它,而没有将其分配给变量:

Worksheet sheet = excelApp.Worksheets.Open(...);
...
Marshal.ReleaseComObject(sheet);

我不知道 C# 在内部为 Worksheets COM 对象创建了一个包装器,该包装器没有被我的代码释放(因为我不知道它),并且是 Excel 未卸载的原因。

在这个页面上找到了我问题的解决方案,它也有一个很好的规则,用于在 C# 中使用 COM 对象:

切勿对 COM 对象使用两个点。


因此,有了这些知识,执行上述操作的正确方法是:

Worksheets sheets = excelApp.Worksheets; // <-- The important part
Worksheet sheet = sheets.Open(...);
...
Marshal.ReleaseComObject(sheets);
Marshal.ReleaseComObject(sheet);

事后更新:

我希望每个读者都能非常仔细地阅读 Hans Passant 的这个答案,因为它解释了我和许多其他开发人员偶然陷入的陷阱。当我几年前写下这个答案时,我不知道调试器对垃圾回收器的影响,并得出了错误的结论。为了历史起见,我保持我的答案不变,但请阅读此链接,不要走“两个点”的道路:了解 .NET 中的垃圾回收使用 IDisposable 清理 Excel 互操作对象

评论

18赞 user7116 12/19/2009
然后我建议不要从 COM 使用 Excel,省去所有的麻烦。Excel 2007 格式无需打开 Excel,即可使用,华丽。
6赞 A9S6 1/27/2010
我不明白“两个点”是什么意思。你能解释一下吗?
23赞 VVS 2/4/2010
这意味着,您不应该使用模式 comObject.Property.PropertiesProperty(您看到两个点了吗?而是将 comObject.Property 分配给变量,并使用和释放该变量。上述规则的更正式版本可能是 sth。例如“在使用变量之前将 com 对象分配给变量。这包括作为另一个 com 对象属性的 com 对象。
5赞 VVS 10/18/2010
@Nick:实际上,你不需要任何形式的清理,因为垃圾收集器会为你做这件事。您唯一需要做的就是将每个 COM 对象分配给其自己的变量,以便 GC 知道它。
14赞 CodingBarfield 8/11/2011
@VSS这是笨拙的,但 GC 会清理所有内容,因为包装变量是由 .net 框架制作的。GC 可能需要很长时间才能清理它。调用 GC.在繁重的互操作后收集并不是一个坏主意。
30赞 MagicKat 10/2/2008 #3

Excel 命名空间中的任何内容都需要释放。时期

你不能做:

Worksheet ws = excel.WorkBooks[1].WorkSheets[1];

你必须做

Workbooks books = excel.WorkBooks;
Workbook book = books[1];
Sheets sheets = book.WorkSheets;
Worksheet ws = sheets[1];

然后是对象的释放。

评论

3赞 HAdes 10/2/2008
例如,如何aobut xlRange.Interior.Color。
0赞 MagicKat 10/2/2008
内部需要发布(它在命名空间中)...另一方面,颜色没有(因为它来自 System.Drawing.Color、iirc)
2赞 Anonymous Type 3/3/2010
实际上,Color 是 Excel 颜色,而不是 .Net 颜色。你只是通过一个长。还需要发布工作簿定义,工作表......不那么重要。
0赞 Dietrich Baumgarten 9/29/2019
这不是真的,两点坏,一点好的规则是无稽之谈。此外,您不必发布工作簿、工作表、范围,这是 GC 的工作。您永远不能忘记的唯一一件事就是对唯一的 Excel.Application 对象调用 Quit。调用 Quit 后,将 Excel.Application 对象清空并调用 GC。收集();气相色谱。WaitForPendingFinalizers();两次。
1赞 bill_the_loser 10/2/2008 #4

我认为其中一些只是框架处理 Office 应用程序的方式,但我可能是错的。在某些日子里,一些应用程序会立即清理进程,而在其他日子里,它似乎要等到应用程序关闭。一般来说,我不再关注细节,只是确保在一天结束时没有任何额外的流程。

另外,也许我把事情简单化了,但我认为你可以......

objExcel = new Excel.Application();
objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing));
DoSomeStuff(objBook);
SaveTheBook(objBook);
objBook.Close(false, Type.Missing, Type.Missing);
objExcel.Quit();

就像我之前说的,我不太关注 Excel 进程何时出现或消失的细节,但这通常对我有用。我也不喜欢在最短的时间内保留 Excel 流程,但我可能只是对此感到偏执。

4赞 Joe 10/2/2008 #5

正如其他人所指出的,您需要为使用的每个 Excel 对象创建一个显式引用,并在该引用上调用 Marshal.ReleaseComObject,如本知识库文章中所述。还需要使用 try/finally 来确保始终调用 ReleaseComObject,即使引发异常也是如此。即代替:

Worksheet sheet = excelApp.Worksheets(1)
... do something with sheet

你需要做一些事情,比如:

Worksheets sheets = null;
Worksheet sheet = null
try
{ 
    sheets = excelApp.Worksheets;
    sheet = sheets(1);
    ...
}
finally
{
    if (sheets != null) Marshal.ReleaseComObject(sheets);
    if (sheet != null) Marshal.ReleaseComObject(sheet);
}

如果要关闭 Excel,还需要在释放 Application 对象之前调用 Application.Quit。

正如你所看到的,一旦你尝试做任何稍微复杂的事情,这很快就会变得非常笨拙。我已经成功地开发了带有一个简单的包装类的 .NET 应用程序,该包装类包装了 Excel 对象模型的一些简单操作(打开工作簿、写入 Range、保存/关闭工作簿等)。包装类实现 IDisposable,在它使用的每个对象上仔细实现 Marshal.ReleaseComObject,并且不会向应用的其余部分公开任何 Excel 对象。

但这种方法不能很好地扩展更复杂的要求。

这是 .NET COM 互操作的一大缺陷。对于更复杂的方案,我会认真考虑用 VB6 或其他非托管语言编写 ActiveX DLL,您可以将与 Out-proc COM 对象(如 Office)的所有交互委托给该 DLL。然后,您可以从 .NET 应用程序引用此 ActiveX DLL,事情会容易得多,因为您只需要发布此引用。

287赞 8 revs, 2 users 94%Mike Rosenblum #6

实际上,您可以干净地释放 Excel Application 对象,但必须小心。

建议为您访问的每个 COM 对象维护一个命名引用,然后通过显式发布它,这在理论上是正确的,但不幸的是,在实践中很难管理。如果有人滑到任何地方并使用“两个点”,或者通过循环或任何其他类似类型的命令迭代单元格,那么您将拥有未引用的 COM 对象并面临挂起的风险。在这种情况下,无法在代码中找到原因;您必须通过眼睛检查所有代码,并希望找到原因,这对于大型项目来说几乎是不可能的任务。Marshal.FinalReleaseComObject()for each

好消息是,您实际上不必维护对使用的每个 COM 对象的命名变量引用。相反,调用 Dand 然后释放所有(通常是次要)对象,而不持有引用,然后显式释放持有命名变量引用的对象。GC.Collect()GC.WaitForPendingFinalizers()

您还应该按重要性的相反顺序发布命名引用:首先是范围对象,然后是工作表、工作簿,最后是 Excel 应用程序对象。

例如,假设您有一个名为 Range 对象变量、一个名为 Worksheet 的变量、一个名为 的工作簿变量和一个名为 的 Excel 应用程序变量,则您的清理代码可能如下所示:xlRngxlSheetxlBookxlApp

// Cleanup
GC.Collect();
GC.WaitForPendingFinalizers();

Marshal.FinalReleaseComObject(xlRng);
Marshal.FinalReleaseComObject(xlSheet);

xlBook.Close(Type.Missing, Type.Missing, Type.Missing);
Marshal.FinalReleaseComObject(xlBook);

xlApp.Quit();
Marshal.FinalReleaseComObject(xlApp);

在大多数代码示例中,你将看到从 .NET 中清理 COM 对象时,将进行两次 and 调用,如下所示:GC.Collect()GC.WaitForPendingFinalizers()

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();

但是,除非您使用的是 Visual Studio Tools for Office (VSTO),否则不应要求这样做,因为 Visual Studio Tools for Office (VSTO) 使用终结器,这些终结器会导致在终结队列中提升整个对象图。在下一次垃圾回收之前,此类对象不会被释放。但是,如果不使用 VSTO,则应该能够调用一次。GC.Collect()GC.WaitForPendingFinalizers()

我知道明确打电话是禁忌(当然做两次听起来很痛苦),但老实说,这是没有办法的。通过正常操作,您将生成隐藏对象,这些对象没有引用,因此,除了调用 .GC.Collect()GC.Collect()

这是一个复杂的话题,但这确实是它的全部内容。一旦你为你的清理过程建立了这个模板,你就可以正常编码了,而不需要包装器等。

我这里有一个关于这个的教程:

使用 VB.Net/COM 互操作实现 Office 程序自动化

它是为 VB.NET 编写的,但不要因此而推迟,其原理与使用 C# 时完全相同。

评论

3赞 Mike Rosenblum 1/24/2009
可以在 ExtremeVBTalk .NET Office Automation 论坛上找到相关讨论,网址为:xtremevbtalk.com/showthread.php?t=303928
2赞 Mike Rosenblum 2/8/2009
如果所有其他方法都失败了,那么 Process.Kill() 可以(作为最后的手段)使用,如下所述:stackoverflow.com/questions/51462/...
1赞 Mike Rosenblum 2/14/2011
很高兴它奏效了,理查德。:-)这里有一个微妙的例子,简单地避免“两个点”不足以防止出现问题:stackoverflow.com/questions/4964663/......
0赞 Mike Rosenblum 4/24/2011
以下是Microsoft的Misha Shneerson在2010年10月7日的评论日期内对该主题的看法。(blogs.msdn.com/b/csharpfaq/archive/2010/09/28/...)
0赞 Brett Green 5/20/2015
我希望这有助于解决我们遇到的一个持续存在的问题。在 finally 块内进行垃圾回收和释放调用是否安全?
6赞 Christian Mogensen 11/6/2008 #7

您需要知道,Excel 对您运行的区域性也非常敏感。

您可能会发现,在调用 Excel 函数之前,需要将区域性设置为 EN-US。 这并不适用于所有功能,但适用于其中的某些功能。

    CultureInfo en_US = new System.Globalization.CultureInfo("en-US"); 
    System.Threading.Thread.CurrentThread.CurrentCulture = en_US;
    string filePathLocal = _applicationObject.ActiveWorkbook.Path;
    System.Threading.Thread.CurrentThread.CurrentCulture = orgCulture;

即使使用 VSTO,这也适用。

详情请见:http://support.microsoft.com/default.aspx?scid=kb;en-us;Q320369

19赞 Edward Wilde 12/8/2008 #8

我发现了一个有用的泛型模板,可以帮助实现 COM 对象的正确处置模式,当它们超出范围时需要调用 Marshal.ReleaseComObject:

用法:

using (AutoReleaseComObject<Application> excelApplicationWrapper = new AutoReleaseComObject<Application>(new Application()))
{
    try
    {
        using (AutoReleaseComObject<Workbook> workbookWrapper = new AutoReleaseComObject<Workbook>(excelApplicationWrapper.ComObject.Workbooks.Open(namedRangeBase.FullName, false, false, missing, missing, missing, true, missing, missing, true, missing, missing, missing, missing, missing)))
        {
           // do something with your workbook....
        }
    }
    finally
    {
         excelApplicationWrapper.ComObject.Quit();
    } 
}

模板:

public class AutoReleaseComObject<T> : IDisposable
{
    private T m_comObject;
    private bool m_armed = true;
    private bool m_disposed = false;

    public AutoReleaseComObject(T comObject)
    {
        Debug.Assert(comObject != null);
        m_comObject = comObject;
    }

#if DEBUG
    ~AutoReleaseComObject()
    {
        // We should have been disposed using Dispose().
        Debug.WriteLine("Finalize being called, should have been disposed");

        if (this.ComObject != null)
        {
            Debug.WriteLine(string.Format("ComObject was not null:{0}, name:{1}.", this.ComObject, this.ComObjectName));
        }

        //Debug.Assert(false);
    }
#endif

    public T ComObject
    {
        get
        {
            Debug.Assert(!m_disposed);
            return m_comObject;
        }
    }

    private string ComObjectName
    {
        get
        {
            if(this.ComObject is Microsoft.Office.Interop.Excel.Workbook)
            {
                return ((Microsoft.Office.Interop.Excel.Workbook)this.ComObject).Name;
            }

            return null;
        }
    }

    public void Disarm()
    {
        Debug.Assert(!m_disposed);
        m_armed = false;
    }

    #region IDisposable Members

    public void Dispose()
    {
        Dispose(true);
#if DEBUG
        GC.SuppressFinalize(this);
#endif
    }

    #endregion

    protected virtual void Dispose(bool disposing)
    {
        if (!m_disposed)
        {
            if (m_armed)
            {
                int refcnt = 0;
                do
                {
                    refcnt = System.Runtime.InteropServices.Marshal.ReleaseComObject(m_comObject);
                } while (refcnt > 0);

                m_comObject = default(T);
            }

            m_disposed = true;
        }
    }
}

参考:

http://www.deez.info/sengelha/2005/02/11/useful-idisposable-class-3-autoreleasecomobject/

评论

3赞 Anonymous Type 3/3/2010
是的,这个很好。我认为现在可以更新此代码,但现在 FinalReleaseCOMObject 可用。
0赞 Henrik 6/27/2012
为每个对象设置一个使用块仍然很烦人。请参阅此处 stackoverflow.com/questions/2191489/...以获取替代方案。
51赞 joshgo 8/21/2009 #9

更新:添加了 C# 代码,并链接到 Windows 作业

我花了一些时间试图弄清楚这个问题,当时 XtremeVBTalk 是最活跃和响应最快的。这是我的原始帖子的链接,即使您的应用程序崩溃,也要干净地关闭 Excel 互操作进程。以下是该帖子的摘要,以及复制到此帖子的代码。

  • 关闭 Interop 进程 with 并在大多数情况下有效,但如果应用程序灾难性崩溃,则失败。也就是说,如果应用程序崩溃,Excel 进程仍将松散运行。Application.Quit()Process.Kill()
  • 解决方案是让操作系统使用 Win32 调用通过 Windows 作业对象处理进程的清理。当您的主应用程序死亡时,关联的进程(即 Excel)也将终止。

我发现这是一个干净的解决方案,因为操作系统正在做真正的清理工作。您所要做的就是注册 Excel 进程。

Windows 作业代码

包装 Win32 API 调用以注册互操作进程。

public enum JobObjectInfoType
{
    AssociateCompletionPortInformation = 7,
    BasicLimitInformation = 2,
    BasicUIRestrictions = 4,
    EndOfJobTimeInformation = 6,
    ExtendedLimitInformation = 9,
    SecurityLimitInformation = 5,
    GroupInformation = 11
}

[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
    public int nLength;
    public IntPtr lpSecurityDescriptor;
    public int bInheritHandle;
}

[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
    public Int64 PerProcessUserTimeLimit;
    public Int64 PerJobUserTimeLimit;
    public Int16 LimitFlags;
    public UInt32 MinimumWorkingSetSize;
    public UInt32 MaximumWorkingSetSize;
    public Int16 ActiveProcessLimit;
    public Int64 Affinity;
    public Int16 PriorityClass;
    public Int16 SchedulingClass;
}

[StructLayout(LayoutKind.Sequential)]
struct IO_COUNTERS
{
    public UInt64 ReadOperationCount;
    public UInt64 WriteOperationCount;
    public UInt64 OtherOperationCount;
    public UInt64 ReadTransferCount;
    public UInt64 WriteTransferCount;
    public UInt64 OtherTransferCount;
}

[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
    public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
    public IO_COUNTERS IoInfo;
    public UInt32 ProcessMemoryLimit;
    public UInt32 JobMemoryLimit;
    public UInt32 PeakProcessMemoryUsed;
    public UInt32 PeakJobMemoryUsed;
}

public class Job : IDisposable
{
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
    static extern IntPtr CreateJobObject(object a, string lpName);

    [DllImport("kernel32.dll")]
    static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);

    private IntPtr m_handle;
    private bool m_disposed = false;

    public Job()
    {
        m_handle = CreateJobObject(null, null);

        JOBOBJECT_BASIC_LIMIT_INFORMATION info = new JOBOBJECT_BASIC_LIMIT_INFORMATION();
        info.LimitFlags = 0x2000;

        JOBOBJECT_EXTENDED_LIMIT_INFORMATION extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
        extendedInfo.BasicLimitInformation = info;

        int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
        IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length);
        Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false);

        if (!SetInformationJobObject(m_handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length))
            throw new Exception(string.Format("Unable to set information.  Error: {0}", Marshal.GetLastWin32Error()));
    }

    #region IDisposable Members

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    #endregion

    private void Dispose(bool disposing)
    {
        if (m_disposed)
            return;

        if (disposing) {}

        Close();
        m_disposed = true;
    }

    public void Close()
    {
        Win32.CloseHandle(m_handle);
        m_handle = IntPtr.Zero;
    }

    public bool AddProcess(IntPtr handle)
    {
        return AssignProcessToJobObject(m_handle, handle);
    }

}

关于构造函数代码的注意事项

  • 在构造函数中,调用 。 是枚举值,MSDN 将此值定义为:info.LimitFlags = 0x2000;0x2000JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE

导致与作业关联的所有进程在 作业的最后一个句柄已关闭。

用于获取进程 ID (PID) 的额外 Win32 API 调用

    [DllImport("user32.dll", SetLastError = true)]
    public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

使用代码

    Excel.Application app = new Excel.ApplicationClass();
    Job job = new Job();
    uint pid = 0;
    Win32.GetWindowThreadProcessId(new IntPtr(app.Hwnd), out pid);
    job.AddProcess(Process.GetProcessById((int)pid).Handle);

评论

3赞 3/16/2015
显式终止进程外 COM 服务器(可能为其他 COM 客户端提供服务)是一个可怕的想法。如果您诉诸于此,那是因为您的 COM 协议已损坏。不应将 COM 服务器视为常规窗口化应用
13赞 Mohsen Afshin 10/16/2009 #10

普通的开发人员,你的解决方案都不适合我, 所以我决定实施一个新技巧

首先,让我们指定 “我们的目标是什么?” => “在任务管理器中完成工作后看不到 excel 对象”

还行。不要挑战并开始破坏它,但要考虑不要破坏并行运行的其他实例 os Excel。

因此,获取当前处理器列表并获取 EXCEL 进程的 PID,然后一旦您的工作完成,我们在进程列表中有一个具有唯一 PID 的新访客,找到并销毁该 PID。

<请记住,在 Excel 作业期间,任何新的 Excel 进程都将被检测为新的进程并销毁> < 更好的解决方案是捕获新创建的 excel 对象的 PID 并销毁它>

Process[] prs = Process.GetProcesses();
List<int> excelPID = new List<int>();
foreach (Process p in prs)
   if (p.ProcessName == "EXCEL")
       excelPID.Add(p.Id);

.... // your job 

prs = Process.GetProcesses();
foreach (Process p in prs)
   if (p.ProcessName == "EXCEL" && !excelPID.Contains(p.Id))
       p.Kill();

这解决了我的问题,希望你也解决了。

评论

2赞 whytheq 6/7/2016
....这有点像雪橇锤的方法?- 它类似于我也在使用的:(但需要改变。使用这种方法的问题在于,通常在打开excel时,在运行此方法的机器上,它会在左侧栏中显示警报,显示“Excel已错误关闭”之类的内容:不是很优雅。我认为这个线程中较早的建议之一是首选。
0赞 Anton Shepelev 12/3/2020
投了反对票,因为这是一个丑陋的黑客,而不是一个解决方案。
231赞 nightcoder 12/12/2009 #11

前言:我的答案包含两个解决方案,所以阅读时要小心,不要错过任何东西。

关于如何使Excel实例卸载,有不同的方法和建议,例如:

  • 显式释放 EVERY com 对象 使用 Marshal.FinalReleaseComObject() (不要忘记隐含 创建的 com 对象)。释放 每个创建的 COM 对象,都可以使用 这里提到的 2 点规则:
    如何正确清理 Excel 互操作对象?

  • 调用 GC.Collect() 和 气相色谱。WaitForPendingFinalizers() 使 CLR 发布未使用的 com-objects *(实际上,它有效,有关详细信息,请参阅我的第二个解决方案)

  • 检查 com-server-application 可能显示一个消息框等待 要回答的用户(虽然我不是 确保它可以防止 Excel 关闭,但我听说过一些 次)

  • 将WM_CLOSE消息发送到主 Excel 窗口

  • 执行有效的函数 在单独的 AppDomain 中使用 Excel。 有些人相信Excel实例 当 AppDomain 为 卸载。

  • 杀死所有在我们的 excel 互操作代码启动后实例化的 excel 实例。

但!有时所有这些选项都无济于事或不合适!

例如,昨天我发现在我的一个函数(与 excel 一起使用)中,Excel 在函数结束后继续运行。我什么都试过了!我彻底检查了整个函数 10 次,并为所有内容添加了 Marshal.FinalReleaseComObject()!我也有GC。Collect() 和 GC.WaitForPendingFinalizers()。我检查了隐藏的消息框。我尝试将WM_CLOSE消息发送到 Excel 主窗口。我在单独的 AppDomain 中执行了我的函数并卸载了该域。没有任何帮助!关闭所有 excel 实例的选项是不合适的,因为如果用户在执行我的函数期间手动启动另一个 Excel 实例,该函数也适用于 Excel,那么该实例也将被我的函数关闭。我敢打赌用户不会高兴!所以,老实说,这是一个蹩脚的选择(没有冒犯的家伙)。所以我花了几个小时才找到一个好的(以我的拙见)解决方案通过主窗口的 hWnd 杀死 excel 进程(这是第一个解决方案)。

下面是简单的代码:

[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

/// <summary> Tries to find and kill process by hWnd to the main window of the process.</summary>
/// <param name="hWnd">Handle to the main window of the process.</param>
/// <returns>True if process was found and killed. False if process was not found by hWnd or if it could not be killed.</returns>
public static bool TryKillProcessByMainWindowHwnd(int hWnd)
{
    uint processID;
    GetWindowThreadProcessId((IntPtr)hWnd, out processID);
    if(processID == 0) return false;
    try
    {
        Process.GetProcessById((int)processID).Kill();
    }
    catch (ArgumentException)
    {
        return false;
    }
    catch (Win32Exception)
    {
        return false;
    }
    catch (NotSupportedException)
    {
        return false;
    }
    catch (InvalidOperationException)
    {
        return false;
    }
    return true;
}

/// <summary> Finds and kills process by hWnd to the main window of the process.</summary>
/// <param name="hWnd">Handle to the main window of the process.</param>
/// <exception cref="ArgumentException">
/// Thrown when process is not found by the hWnd parameter (the process is not running). 
/// The identifier of the process might be expired.
/// </exception>
/// <exception cref="Win32Exception">See Process.Kill() exceptions documentation.</exception>
/// <exception cref="NotSupportedException">See Process.Kill() exceptions documentation.</exception>
/// <exception cref="InvalidOperationException">See Process.Kill() exceptions documentation.</exception>
public static void KillProcessByMainWindowHwnd(int hWnd)
{
    uint processID;
    GetWindowThreadProcessId((IntPtr)hWnd, out processID);
    if (processID == 0)
        throw new ArgumentException("Process has not been found by the given main window handle.", "hWnd");
    Process.GetProcessById((int)processID).Kill();
}

正如你所看到的,我提供了两种方法,根据Try-Parse模式(我认为它在这里是合适的):如果进程无法被杀死(例如,进程不再存在),一种方法不会引发异常,另一种方法如果进程没有被杀死,则抛出异常。此代码中唯一的弱点是安全权限。从理论上讲,用户可能没有终止进程的权限,但在 99.99% 的情况下,用户拥有这样的权限。我还用访客帐户测试了它 - 它运行良好。

因此,使用 Excel 的代码可能如下所示:

int hWnd = xl.Application.Hwnd;
// ...
// here we try to close Excel as usual, with xl.Quit(),
// Marshal.FinalReleaseComObject(xl) and so on
// ...
TryKillProcessByMainWindowHwnd(hWnd);

瞧!Excel 已终止!:)

好的,让我们回到第二个解决方案,正如我在文章开头所承诺的那样。 第二种解决方案是调用 GC。Collect() 和 GC.WaitForPendingFinalizers()。是的,它们确实有效,但您需要在这里小心!
很多人说(我说过)打电话给GC。Collect() 无济于事。但它无济于事的原因是,如果仍然有对 COM 对象的引用!GC最受欢迎的原因之一。Collect() 没有帮助是在调试模式下运行项目。在调试模式下,在方法结束之前,不再真正引用的对象不会被垃圾回收。
所以,如果你尝试过GC。Collect() 和 GC.WaitForPendingFinalizers() 并且没有帮助,请尝试执行以下操作:

1)尝试在发布模式下运行项目,并检查Excel是否正确关闭

2)将使用Excel的方法包装在一个单独的方法中。 所以,而不是这样的东西:

void GenerateWorkbook(...)
{
  ApplicationClass xl;
  Workbook xlWB;
  try
  {
    xl = ...
    xlWB = xl.Workbooks.Add(...);
    ...
  }
  finally
  {
    ...
    Marshal.ReleaseComObject(xlWB)
    ...
    GC.Collect();
    GC.WaitForPendingFinalizers();
  }
}

你写:

void GenerateWorkbook(...)
{
  try
  {
    GenerateWorkbookInternal(...);
  }
  finally
  {
    GC.Collect();
    GC.WaitForPendingFinalizers();
  }
}

private void GenerateWorkbookInternal(...)
{
  ApplicationClass xl;
  Workbook xlWB;
  try
  {
    xl = ...
    xlWB = xl.Workbooks.Add(...);
    ...
  }
  finally
  {
    ...
    Marshal.ReleaseComObject(xlWB)
    ...
  }
}

现在,Excel 将关闭 =)

评论

21赞 chiccodoro 5/5/2010
可悲的是,这个线程已经太老了,以至于你的优秀答案出现在那么远的地方,我认为这是它没有被点赞的唯一原因......
17赞 Mark 8/27/2010
我不得不承认,当我第一次读到你的回答时,我以为这是一个巨大的问题。经过大约 6 个小时的搏斗(一切都被释放了,我没有双点等),我现在认为你的答案是天才。
3赞 BBlake 10/19/2010
谢谢你。在我发现它之前,我一直在与一个无论如何都不会关闭的 Excel 搏斗。非常好。
2赞 Mike Rosenblum 12/30/2011
@nightcoder:很棒的答案,非常详细。你对调试模式的评论非常真实,你指出了这一点。在发布模式下使用相同的代码通常没问题。但是,Process.Kill 仅在使用自动化时才有效,而不适用于程序在 Excel 中运行时,例如,作为托管 COM 加载项。
2赞 Mike Rosenblum 12/30/2011
@DanM:你的方法是100%正确的,永远不会失败。通过将所有变量保留在方法的本地位置,.NET 代码实际上无法访问所有引用。这意味着,当您的 finally 部分调用 GC.Collect(),所有 COM 对象的终结器都将被确定调用。然后,每个终结器在要终结的对象上调用 Marshal.FinalReleaseComObject。因此,您的方法简单且万无一失。不要害怕使用它。(唯一需要注意的是:如果使用 VSTO,我怀疑您是,则需要调用 GC。Collect() 和 GC。WaitForPendingFinalizers 两次。
9赞 Chris McGrath 12/18/2009 #12

这里公认的答案是正确的,但也要注意,不仅需要避免“两点”引用,还需要避免通过索引检索的对象。您也不需要等到完成程序来清理这些对象,最好创建函数,以便在您完成它们后立即清理它们,如果可能的话。这是我创建的一个函数,它分配了 Style 对象的一些属性,称为:xlStyleHeader

public Excel.Style xlStyleHeader = null;

private void CreateHeaderStyle()
{
    Excel.Styles xlStyles = null;
    Excel.Font xlFont = null;
    Excel.Interior xlInterior = null;
    Excel.Borders xlBorders = null;
    Excel.Border xlBorderBottom = null;

    try
    {
        xlStyles = xlWorkbook.Styles;
        xlStyleHeader = xlStyles.Add("Header", Type.Missing);

        // Text Format
        xlStyleHeader.NumberFormat = "@";

        // Bold
        xlFont = xlStyleHeader.Font;
        xlFont.Bold = true;

        // Light Gray Cell Color
        xlInterior = xlStyleHeader.Interior;
        xlInterior.Color = 12632256;

        // Medium Bottom border
        xlBorders = xlStyleHeader.Borders;
        xlBorderBottom = xlBorders[Excel.XlBordersIndex.xlEdgeBottom];
        xlBorderBottom.Weight = Excel.XlBorderWeight.xlMedium;
    }
    catch (Exception ex)
    {
        throw ex;
    }
    finally
    {
        Release(xlBorderBottom);
        Release(xlBorders);
        Release(xlInterior);
        Release(xlFont);
        Release(xlStyles);
    }
}

private void Release(object obj)
{
    // Errors are ignored per Microsoft's suggestion for this type of function:
    // http://support.microsoft.com/default.aspx/kb/317109
    try
    {
        System.Runtime.InteropServices.Marshal.ReleaseComObject(obj);
    }
    catch { } 
}

请注意,我必须设置一个变量才能清理它(不是因为两个点,它们指的是不需要释放的枚举,而是因为我所指的对象实际上是一个需要释放的 Border 对象)。xlBorders[Excel.XlBordersIndex.xlEdgeBottom]

在标准应用程序中,这种事情并不是真正必要的,这些应用程序可以很好地自行清理,但是在 ASP.NET 应用程序中,即使您错过了其中之一,无论您调用垃圾回收器的频率如何,Excel 仍将在您的服务器上运行。

在编写此代码时,在监视任务管理器时,它需要非常注意细节和许多测试执行,但这样做可以省去拼命搜索代码页以查找遗漏的一个实例的麻烦。这在循环中工作时尤其重要,因为在循环中,您需要释放对象的每个实例,即使它每次循环时都使用相同的变量名称。

评论

0赞 Gerhard Powell 2/27/2015
在 Release 中,检查 Null(Joe 的答案)。这将避免不必要的 null 异常。我已经测试了很多方法。这是有效释放 Excel 的唯一方法。
18赞 Colin 11/21/2010 #13

我简直不敢相信这个问题已经困扰了世界 5 年......如果已创建应用程序,则需要先将其关闭,然后再删除链接。

objExcel = new Excel.Application();  
objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing)); 

关闭时

objBook.Close(true, Type.Missing, Type.Missing); 
objExcel.Application.Quit();
objExcel.Quit(); 

当您新建 excel 应用程序时,它会在后台打开一个 excel 程序。在释放链接之前,您需要命令该 excel 程序退出,因为该 excel 程序不是您直接控制的一部分。因此,如果链接被释放,它将保持打开状态!

大家编程不错~~

评论

0赞 downwitch 3/20/2015
否,您不需要,尤其是当您希望允许用户与 COM 应用程序交互时(在本例中为 Excel,OP 不指定是否打算进行用户交互,但未提及将其关闭)。您只需要让调用方松开,用户就可以在关闭单个文件时退出 Excel。
0赞 mcmillab 11/17/2015
这对我来说非常有效 - 当我只有 objExcel.Quit() 时崩溃,但是当我事先也做了 objExcel.Application.Quit() 时,干净地关闭了。
0赞 Alexander Høst 2/10/2022
objExcel.Application.Quit()并且是相同的调用,因为 .因此,您打电话给哪一个并不重要。只需做一次。第二次这样做(不管是哪一次),它已经退出了。objExcel.Quit()objExcel == objExcel.Application
10赞 Grimfort 12/6/2010 #14

为了增加 Excel 不关闭的原因,即使您在读取时创建对每个对象的直接引用,创建也是“For”循环。

For Each objWorkBook As WorkBook in objWorkBooks 'local ref, created from ExcelApp.WorkBooks to avoid the double-dot
   objWorkBook.Close 'or whatever
   FinalReleaseComObject(objWorkBook)
   objWorkBook = Nothing
Next 

'The above does not work, and this is the workaround:

For intCounter As Integer = 1 To mobjExcel_WorkBooks.Count
   Dim objTempWorkBook As Workbook = mobjExcel_WorkBooks.Item(intCounter)
   objTempWorkBook.Saved = True
   objTempWorkBook.Close(False, Type.Missing, Type.Missing)
   FinalReleaseComObject(objTempWorkBook)
   objTempWorkBook = Nothing
Next

评论

0赞 Grimfort 12/6/2010
另一个原因是,在不首先释放前一个值的情况下重用引用。
0赞 Chad Braun-Duin 1/14/2011
谢谢。我还使用了“针对每个”的解决方案,您的解决方案对我有用。
0赞 AjV Jsy 10/11/2014
+1 谢谢。另外,请注意,如果您有一个 Excel 范围的对象,并且想要在对象的生存期内更改范围,我发现在重新分配它之前我必须 ReleaseComObject,这使得代码有点不整洁!
3赞 spiderman 6/18/2011 #15

当上述所有内容都不起作用时,请尝试给 Excel 一些时间来关闭其工作表:

app.workbooks.Close();
Thread.Sleep(500); // adjust, for me it works at around 300+
app.Quit();

...
FinalReleaseComObject(app);

评论

3赞 dFlat 7/14/2011
我讨厌任意的等待。但是 +1,因为您需要等待工作簿关闭是正确的。另一种方法是在循环中轮询工作簿集合,并使用任意等待作为循环超时。
0赞 Alexander Høst 2/10/2022
我非常怀疑这不会阻止调用,即我认为在工作簿实际上关闭之前不会继续执行。如果该方法返回,然后您可以枚举工作簿的集合并且它不是空的,那么在我看来这将是一个严重的错误。我宁愿遍历工作簿并调用,然后.Close()Close().Close(false)Quit()
-1赞 quixver 6/19/2011 #16

Excel 不是为通过 C++ 或 C# 编程而设计的。COM API 专门设计用于 Visual Basic、VB.NET 和 VBA。

此外,此页上的所有代码示例都不是最佳的,原因很简单,即每个调用都必须跨越托管/非托管边界,并进一步忽略了以下事实:Excel COM API 可以自由地失败任何调用,并带有指示 RPC 服务器繁忙的神秘 HRESULT

在我看来,自动化 Excel 的最佳方法是将您的数据收集到尽可能大/可行的数组中,并将其发送到 VBA 函数或子(通过),然后执行任何所需的处理。此外,调用时,请务必注意指示 excel 繁忙的异常,然后重试调用 。Application.RunApplication.RunApplication.Run

评论

4赞 Dominic Zukiewicz 5/2/2012
C# 和 VB.NET 都在 CLR 下运行,因此它不能为 VB.NET 而不是 C# 设计。它们最终是一回事,只是构建应用程序的语言语义不同。
0赞 quixver 5/18/2012
请尝试从 C#(版本 3.0 及更早版本)或 C++ 中的 excel Application 对象调用 Run 方法。然后尝试从 VB.net 做同样的事情。在你传入的第 10 个或第 12 个缺失参数之后 - 你可能会明白我的意思:]
0赞 quixver 5/18/2012
此外,正如您提到的,C# 和 VB.net 都是 CLR 语言。它们为程序员提供了 CLR 功能的不同子集以及各种语法糖。碰巧的是,VB.net 提供的子集使 com 和 excel 编程更容易。
0赞 Dominic Zukiewicz 5/18/2012
但同样,这只是语言的语义(v3.5 之前)。我确实感受到了VB的痛苦,甚至在.NET之前。Microsoft.Office.Tools DLL的托管库使得在没有任何COM清理的情况下轻松完成所有这些操作,因为它的所有托管包装器。Application.Run(Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value,,Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value);
1赞 Jeremy Thompson 9/27/2012
Excel is not designed to be programmed via C++ or C#- 信息不正确。
13赞 Dave Cousineau 9/1/2011 #17

这似乎过于复杂了。根据我的经验,只有三个关键因素可以使 Excel 正确关闭:

1:确保没有对您创建的 Excel 应用程序的剩余引用(无论如何都应该只有一个;将其设置为null)

2:调用GC.Collect()

3:必须关闭 Excel,要么由用户手动关闭程序,要么通过调用 Excel 对象。(请注意,这将像用户尝试关闭程序一样起作用,如果有未保存的更改,即使 Excel 不可见,也会显示确认对话框。用户可以按取消,然后 Excel 将不会关闭。QuitQuit

1 需要在 2 之前发生,但 3 可以随时发生。

实现此目的的一种方法是将互操作 Excel 对象包装到您自己的类中,在构造函数中创建互操作实例,然后使用 Dispose 实现 IDisposable,如下所示

if (!mDisposed) {
   mExcel = null;
   GC.Collect();
   mDisposed = true;
}

这将从程序方面清理 excel。一旦关闭Excel(由用户手动或由您调用),该过程将消失。如果程序已经关闭,则该过程将在通话中消失。QuitGC.Collect()

(我不确定它有多重要,但您可能希望在通话后进行通话,但并非绝对有必要摆脱 Excel 流程。GC.WaitForPendingFinalizers()GC.Collect()

多年来,这对我毫无问题。请记住,虽然这有效,但您实际上必须优雅地关闭它才能工作。如果您在清理 Excel 之前中断程序(通常通过在调试程序时点击“停止”),您仍然会累积 excel.exe 进程。

评论

1赞 drew.cuthbert 2/26/2015
这个答案是有效的,而且比公认的答案要方便得多。不用担心 COM 对象的“两点”,不用 .Marshal
2赞 Saber 3/14/2012 #18

使用 Word/Excel 互操作应用程序时应非常小心。在尝试了所有解决方案之后,我们仍然在服务器上(拥有超过 2000 个用户)上留下了许多“WinWord”进程。

在解决了这个问题几个小时后,我意识到如果我同时在不同的线程上使用多个文档,IIS 工作进程 (w3wp.exe) 将崩溃,使所有 WinWord 进程都处于打开状态!Word.ApplicationClass.Document.Open()

所以我想这个问题没有绝对的解决方案,而是切换到其他方法,例如 Office Open XML 开发。

6赞 Amit Mittal 6/25/2012 #19

“切勿对 COM 对象使用两个点”是避免 COM 引用泄漏的重要经验法则,但 Excel PIA 可能导致泄漏的方式比乍一看要多。

其中一种方法是订阅由任何 Excel 对象模型的 COM 对象公开的任何事件。

例如,订阅 Application 类的 WorkbookOpen 事件。

关于COM事件的一些理论

COM 类通过回调接口公开一组事件。为了订阅事件,客户端代码可以简单地注册一个实现回调接口的对象,COM 类将调用其方法来响应特定事件。由于回调接口是 COM 接口,因此实现对象有责任减少它接收的任何 COM 对象(作为参数)的任何事件处理程序的引用计数。

Excel PIA 如何公开 COM 事件

Excel PIA 将 Excel Application 类的 COM 事件公开为常规 .NET 事件。每当客户端代码订阅 .NET 事件(强调“a”)时,PIA 都会创建一个实现回调接口的类的实例,并将其注册到 Excel。

因此,许多回调对象将注册到 Excel 中,以响应来自 .NET 代码的不同订阅请求。每个事件订阅一个回调对象。

用于事件处理的回调接口意味着,PIA 必须为每个 .NET 事件订阅请求订阅所有接口事件。它不能挑三拣四。收到事件回调时,回调对象会检查关联的 .NET 事件处理程序是否对当前事件感兴趣,然后调用该处理程序或以无提示方式忽略回调。

对 COM 实例引用计数的影响

所有这些回调对象都不会减少它们接收的任何 COM 对象(作为参数)的任何 COM 对象的引用计数,即使是对于无提示忽略的方法。它们仅依靠 CLR 垃圾回收器来释放 COM 对象。

由于 GC 运行是不确定的,这可能会导致 Excel 进程的延迟时间比预期的要长,并造成“内存泄漏”的印象。

溶液

到目前为止,唯一的解决方案是避免 COM 类的 PIA 事件提供程序,并编写自己的事件提供程序,以确定性地释放 COM 对象。

对于 Application 类,可以通过实现 AppEvents 接口,然后使用 IConnectionPointContainer 接口向 Excel 注册实现来完成此操作。Application 类(以及所有使用回调机制公开事件的 COM 对象)实现 IConnectionPointContainer 接口。

3赞 Ned 8/9/2012 #20

请确保释放与 Excel 相关的所有对象!

我花了几个小时尝试了几种方法。这些都是好主意,但我最终发现了我的错误:如果你不释放所有对象,那么在我的情况下,上述任何方法都不能帮助你。确保释放所有对象,包括范围 1!

Excel.Range rng = (Excel.Range)worksheet.Cells[1, 1];
worksheet.Paste(rng, false);
releaseObject(rng);

选项在这里一起。

评论

0赞 Paul C 11/26/2012
很好的观点!我想因为我使用的是 Office 2007,所以代表我进行了一些清理。我已经进一步使用了建议,但没有像您在这里建议的那样存储变量,并且 EXCEL.EXE 确实退出了,但我可能很幸运,如果我有任何其他问题,我一定会查看我的代码的这一部分 =)
5赞 2 revs, 2 users 82%Porkbutts #21

2.5 Releasing COM Objects (MSDN) 是一篇关于释放 COM 对象的好文章。

我提倡的方法是,如果 Excel.Interop 引用是非局部变量,则将其清空,然后调用 and 两次。将自动处理本地范围的互操作变量。GC.Collect()GC.WaitForPendingFinalizers()

这样就不需要为每个 COM 对象保留命名引用。

以下是文章中的示例:

public class Test {

    // These instance variables must be nulled or Excel will not quit
    private Excel.Application xl;
    private Excel.Workbook book;

    public void DoSomething()
    {
        xl = new Excel.Application();
        xl.Visible = true;
        book = xl.Workbooks.Add(Type.Missing);

        // These variables are locally scoped, so we need not worry about them.
        // Notice I don't care about using two dots.
        Excel.Range rng = book.Worksheets[1].UsedRange;
    }

    public void CleanUp()
    {
        book = null;
        xl.Quit();
        xl = null;

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

这些话直接来自文章:

在几乎所有情况下,清空 RCW 引用并强制垃圾回收都将正确清理。如果您还调用 GC.WaitForPendingFinalizers,垃圾回收将尽可能确定。也就是说,您将非常确定对象何时被清理 - 在第二次调用 WaitForPendingFinalizers 时返回。或者,可以使用 Marshal.ReleaseComObject。但是,请注意,您不太可能需要使用此方法。

评论

0赞 emanresu 9/29/2022
就我而言,即使是局部变量也需要在 finally {} 块中设置为 null,以防在 try{} 块中抛出异常。
12赞 BTownTKD 3/28/2013 #22

传统上,我一直遵循 VVS 答案中的建议。然而,为了让这个答案与最新的选项保持同步,我认为我未来的所有项目都将使用“NetOffice”库。

NetOffice 是 Office PIA 的完全替代品,并且完全与版本无关。它是托管 COM 包装器的集合,可以处理在 .NET 中使用 Microsoft Office 时经常导致此类麻烦的清理。

一些主要功能是:

  • 大多数与版本无关(并且记录了与版本相关的功能)
  • 无依赖关系
  • 无 PIA
  • 无需注册
  • 无 VSTO

我与该项目没有任何关联;我只是真的很欣赏头痛的明显减少。

评论

3赞 C. Augusto Proiete 12/3/2015
这应该被标记为答案,真的。NetOffice 将所有这些复杂性抽象化。
2赞 Stas Ivanov 2/10/2017
我已经使用 NetOffice 编写了相当长的时间,它运行良好。唯一需要考虑的是,如果您不显式释放使用的对象,它会在您退出应用程序时进行处理(因为它无论如何都会跟踪它们)。因此,NetOffice的经验法则始终是将“使用”模式用于每个Excel对象,如单元格,范围或工作表等。
1赞 Blaz Brencic 4/11/2013 #23

正如一些人可能已经写过的那样,关闭 Excel(对象)不仅重要;打开它的方式以及项目的类型也很重要。

在 WPF 应用程序中,基本上相同的代码可以正常工作,而不会出现问题或几乎没有问题。

我有一个项目,其中同一个 Excel 文件正在为不同的参数值进行多次处理 - 例如,根据通用列表中的值解析它。

我将所有与 Excel 相关的函数放入基类中,并将解析器放入子类中(不同的解析器使用常见的 Excel 函数)。我不希望为泛型列表中的每个项目再次打开和关闭 Excel,因此我只在基类中打开过一次,然后在子类中关闭了它。在将代码移动到桌面应用程序时,我遇到了问题。我已经尝试了上述许多解决方案。 之前已经实施了,是建议的两倍。GC.Collect()

然后,我决定将用于打开 Excel 的代码移动到子类中。现在,我不再只打开一次,而是创建一个新对象(基类),为每个项目打开 Excel,并在最后关闭它。有一些性能损失,但根据几项测试,Excel 进程关闭时没有问题(在调试模式下),因此也会删除临时文件。如果我能得到一些更新,我将继续测试并写更多。

底线是:您还必须检查初始化代码,尤其是当您有很多类等时。

7赞 Antoine Meltzheim 6/14/2013 #24

̈°º¤ø“ ̧ 拍摄 Excel proc 和咀嚼泡泡糖 ̧”ø¤º° ̈

public class MyExcelInteropClass
{
    Excel.Application xlApp;
    Excel.Workbook xlBook;

    public void dothingswithExcel() 
    {
        try { /* Do stuff manipulating cells sheets and workbooks ... */ }
        catch {}
        finally {KillExcelProcess(xlApp);}
    }

    static void KillExcelProcess(Excel.Application xlApp)
    {
        if (xlApp != null)
        {
            int excelProcessId = 0;
            GetWindowThreadProcessId(xlApp.Hwnd, out excelProcessId);
            Process p = Process.GetProcessById(excelProcessId);
            p.Kill();
            xlApp = null;
        }
    }

    [DllImport("user32.dll")]
    static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);
}
0赞 Shivam Srivastava 8/27/2013 #25

用:

[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

声明它,在块中添加代码:finally

finally
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
    if (excelApp != null)
    {
        excelApp.Quit();
        int hWnd = excelApp.Application.Hwnd;
        uint processID;
        GetWindowThreadProcessId((IntPtr)hWnd, out processID);
        Process[] procs = Process.GetProcessesByName("EXCEL");
        foreach (Process p in procs)
        {
            if (p.Id == processID)
                p.Kill();
        }
        Marshal.FinalReleaseComObject(excelApp);
    }
}
9赞 D.G. 11/19/2013 #26

尝试后

  1. 以相反的顺序释放 COM 对象
  2. 在末尾添加两次GC.Collect()GC.WaitForPendingFinalizers()
  3. 不超过两个点
  4. 关闭工作簿并退出应用程序
  5. 在发布模式下运行

对我有用的最终解决方案是移动一组

GC.Collect();
GC.WaitForPendingFinalizers();

我们添加到函数末尾的包装器中,如下所示:

private void FunctionWrapper(string sourcePath, string targetPath)
{
    try
    {
        FunctionThatCallsExcel(sourcePath, targetPath);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

评论

0赞 Carol 3/2/2016
我发现我不需要取消 ComObjects,只需要 Quit() - Close() 和 FinalReleaseComObject。我简直不敢相信这就是我让它工作所缺少的一切。伟大!
2赞 Hahnemann 12/13/2013 #27

两点规则对我不起作用。就我而言,我创建了一个方法来清理我的资源,如下所示:

private static void Clean()
{
    workBook.Close();
    Marshall.ReleaseComObject(workBook);
    excel.Quit();
    CG.Collect();
    CG.WaitForPendingFinalizers();
}
8赞 craig.tadlock 2/25/2014 #28

我完全按照这个......但是我仍然遇到了 1000 次中的 1 次问题。谁知道为什么。是时候拿出锤子了......

在实例化 Excel Application 类后,我立即掌握了刚刚创建的 Excel 进程。

excel = new Microsoft.Office.Interop.Excel.Application();
var process = Process.GetProcessesByName("EXCEL").OrderByDescending(p => p.StartTime).First();

然后,一旦我完成了上述所有 COM 清理,我就会确保该进程没有运行。如果它仍在运行,请杀死它!

if (!process.HasExited)
   process.Kill();
2赞 Loart 6/18/2014 #29

我的解决方案

[DllImport("user32.dll")]
static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);

private void GenerateExcel()
{
    var excel = new Microsoft.Office.Interop.Excel.Application();
    int id;
    // Find the Excel Process Id (ath the end, you kill him
    GetWindowThreadProcessId(excel.Hwnd, out id);
    Process excelProcess = Process.GetProcessById(id);

try
{
    // Your code
}
finally
{
    excel.Quit();

    // Kill him !
    excelProcess.Kill();
}
1赞 Martin 9/5/2014 #30

接受的答案对我不起作用。析构函数中的以下代码完成了这项工作。

if (xlApp != null)
{
    xlApp.Workbooks.Close();
    xlApp.Quit();
}

System.Diagnostics.Process[] processArray = System.Diagnostics.Process.GetProcessesByName("EXCEL");
foreach (System.Diagnostics.Process process in processArray)
{
    if (process.MainWindowTitle.Length == 0) { process.Kill(); }
}

评论

0赞 akirakonenu 5/4/2015 #31

到目前为止,似乎所有答案都涉及以下一些:

  1. 终止进程
  2. 使用 GC。收集()
  3. 跟踪每个 COM 对象并正确释放它。

这让我意识到这个问题:)是多么困难

我一直在开发一个库来简化对 Excel 的访问,我正在努力确保使用它的人不会留下一团糟(手指交叉)。

我没有直接在 Interop 提供的接口上编写,而是制作了扩展方法,使生活更轻松。如 ApplicationHelpers.CreateExcel() 或工作簿。CreateWorksheet(“mySheetNameThatWillBeValidated”)。当然,任何创建的东西都可能导致以后的清理问题,所以我实际上赞成将终止该过程作为最后的手段。然而,正确清理(第三种选择)可能是破坏性最小和最可控的。

所以,在这种情况下,我想知道做这样的东西是否最好:

public abstract class ReleaseContainer<T>
{
    private readonly Action<T> actionOnT;

    protected ReleaseContainer(T releasible, Action<T> actionOnT)
    {
        this.actionOnT = actionOnT;
        this.Releasible = releasible;
    }

    ~ReleaseContainer()
    {
        Release();
    }

    public T Releasible { get; private set; }

    private void Release()
    {
        actionOnT(Releasible);
        Releasible = default(T);
    }
}

我使用“Releasible”来避免与 Disposable 混淆。不过,将其扩展到 IDisposable 应该很容易。

像这样的实现:

public class ApplicationContainer : ReleaseContainer<Application>
{
    public ApplicationContainer()
        : base(new Application(), ActionOnExcel)
    {
    }

    private static void ActionOnExcel(Application application)
    {
        application.Show(); // extension method. want to make sure the app is visible.
        application.Quit();
        Marshal.FinalReleaseComObject(application);
    }
}

人们可以对各种 COM 对象执行类似操作。

在工厂方法中:

    public static Application CreateExcelApplication(bool hidden = false)
    {
        var excel = new ApplicationContainer().Releasible;
        excel.Visible = !hidden;

        return excel;
    }

我希望每个容器都会被 GC 正确销毁,因此会自动调用 和 。QuitMarshal.FinalReleaseComObject

评论?或者这是对第三类问题的回答?

0赞 dicksters 4/7/2016 #32

只是为了在这里列出的许多解决方案中添加另一个解决方案,使用 C++/ATL 自动化(我想您可以使用 VB/C# 中的类似内容??

Excel::_ApplicationPtr pXL = ...
  :
SendMessage ( ( HWND ) m_pXL->GetHwnd ( ), WM_DESTROY, 0, 0 ) ;

这对我来说就像一个魅力......

41赞 Govert 6/30/2016 #33

首先 - 您永远不必调用或在执行 Excel 互操作时。这是一个令人困惑的反模式,但有关此的任何信息(包括来自 Microsoft 的信息)表明您必须从 .NET 手动释放 COM 引用都是不正确的。事实是,.NET 运行时和垃圾回收器可以正确跟踪和清理 COM 引用。对于您的代码,这意味着您可以删除顶部的整个“while (...)”循环。Marshal.ReleaseComObject(...)Marshal.FinalReleaseComObject(...)

其次,如果要确保在进程结束时清理对进程外 COM 对象的 COM 引用(以便 Excel 进程关闭),则需要确保垃圾回收器运行。通过调用 和 可以正确地执行此操作。调用它两次是安全的,并确保周期也肯定会被清理(尽管我不确定它是否需要,并希望有一个示例来证明这一点)。GC.Collect()GC.WaitForPendingFinalizers()

第三,在调试器下运行时,局部引用将被人为地保持活动状态,直到方法结束(以便局部变量检查起作用)。因此,调用对于从同一方法中清除对象无效。应将执行 GC 清理中的 COM 互操作的代码拆分为单独的方法。(这对我来说是一个关键的发现,来自 @nightcoder 在这里发布的答案的一部分。GC.Collect()rng.Cells

因此,一般模式为:

Sub WrapperThatCleansUp()

    ' NOTE: Don't call Excel objects in here... 
    '       Debugger would keep alive until end, preventing GC cleanup

    ' Call a separate function that talks to Excel
    DoTheWork()

    ' Now let the GC clean up (twice, to clean up cycles too)
    GC.Collect()    
    GC.WaitForPendingFinalizers()
    GC.Collect()    
    GC.WaitForPendingFinalizers()

End Sub

Sub DoTheWork()
    Dim app As New Microsoft.Office.Interop.Excel.Application
    Dim book As Microsoft.Office.Interop.Excel.Workbook = app.Workbooks.Add()
    Dim worksheet As Microsoft.Office.Interop.Excel.Worksheet = book.Worksheets("Sheet1")
    app.Visible = True
    For i As Integer = 1 To 10
        worksheet.Cells.Range("A" & i).Value = "Hello"
    Next
    book.Save()
    book.Close()
    app.Quit()

    ' NOTE: No calls the Marshal.ReleaseComObject() are ever needed
End Sub

关于这个问题有很多虚假信息和混淆,包括MSDN和Stack Overflow上的许多帖子(尤其是这个问题!

最终说服我仔细研究并找出正确建议的是博客文章 Marshal.ReleaseComObject 被认为很危险,同时在调试器下发现引用保持活动状态的问题,这让我之前的测试感到困惑。

评论

8赞 Alan Baljeu 10/13/2018
除了这个答案之外,这个页面上几乎所有的答案都是错误的。它们有效,但工作量太大了。忽略“2 点”规则。让 GC 为您完成工作。来自 .Net 的补充证据 GURU Hans Passant : stackoverflow.com/a/25135685/3852958
2赞 Dietrich Baumgarten 9/29/2019
Govert,找到正确的方法真是令人欣慰。谢谢。
1赞 Tom Brearley 8/2/2016 #34

我目前正在从事 Office 自动化工作,并且偶然发现了一种每次都适合我的解决方案。它很简单,不涉及终止任何进程。

似乎只需循环访问当前活动进程,并以任何方式“访问”打开的 Excel 进程,任何杂散的 Excel 挂起实例都将被删除。下面的代码只是检查名称为“Excel”的进程,然后将进程的 MainWindowTitle 属性写入字符串。与进程的这种“交互”似乎使 Windows 赶上并中止冻结的 Excel 实例。

我在我正在开发的加载项退出之前运行以下方法,因为它触发了它的卸载事件。它每次都会删除任何挂起的 Excel 实例。老实说,我不完全确定为什么这有效,但它对我来说效果很好,可以放在任何 Excel 应用程序的末尾,而不必担心双点、Marshal.ReleaseComObject 或终止进程。我对为什么这样做有效的任何建议都非常感兴趣。

public static void SweepExcelProcesses()
{           
            if (Process.GetProcessesByName("EXCEL").Length != 0)
            {
                Process[] processes = Process.GetProcesses();
                foreach (Process process in processes)
                {
                    if (process.ProcessName.ToString() == "excel")
                    {                           
                        string title = process.MainWindowTitle;
                    }
                }
            }
}
-1赞 Hermes Monteiro 2/14/2017 #35

这是真正对我有用的唯一方法

        foreach (Process proc in System.Diagnostics.Process.GetProcessesByName("EXCEL"))
        {
            proc.Kill();
        }
1赞 Бурда Евгений 3/10/2017 #36

“这似乎太复杂了。根据我的经验,只有三个关键因素可以使 Excel 正确关闭:

1:确保没有对您创建的 Excel 应用程序的剩余引用(无论如何都应该只有一个;将其设置为 null)

2:调用GC。收集()

3:必须关闭 Excel,要么由用户手动关闭程序,要么通过对 Excel 对象调用 Quit。(请注意,“退出”的功能与用户尝试关闭程序一样,如果有未保存的更改,即使 Excel 不可见,也会显示确认对话框。用户可以按取消,然后 Excel 将不会关闭。

1 需要在 2 之前发生,但 3 可以随时发生。

实现此目的的一种方法是将互操作 Excel 对象包装到您自己的类中,在构造函数中创建互操作实例,然后使用 Dispose 实现 IDisposable,如下所示

这将从程序方面清理 excel。关闭 Excel 后(由用户手动关闭或调用 Quit),该过程将消失。如果程序已经关闭,则该过程将在 GC 上消失。Collect() 调用。

(我不确定它有多重要,但你可能想要一个 GC。GC 之后的 WaitForPendingFinalizers() 调用。Collect() 调用,但并非绝对需要摆脱 Excel 进程。

多年来,这对我毫无问题。请记住,虽然这有效,但您实际上必须优雅地关闭它才能工作。如果您在清理 Excel 之前中断程序(通常在调试程序时点击“停止”),您仍然会累积 excel.exe 进程。

0赞 Aloha 12/19/2018 #37

我有个主意,试着杀死你打开的excel进程:

  1. 在打开 excelapplication 之前,获取所有名为 oldProcessIds 的进程 ID。
  2. 打开 ExcelApplication。
  3. 现在获取所有名为 nowProcessIds 的 ExcelApplication 进程 ID。
  4. 当需要退出时,杀死 oldProcessIds 和 nowProcessIds 之间的 except id。

    private static Excel.Application GetExcelApp()
         {
            if (_excelApp == null)
            {
                var processIds = System.Diagnostics.Process.GetProcessesByName("EXCEL").Select(a => a.Id).ToList();
                _excelApp = new Excel.Application();
                _excelApp.DisplayAlerts = false;
    
                _excelApp.Visible = false;
                _excelApp.ScreenUpdating = false;
                var newProcessIds = System.Diagnostics.Process.GetProcessesByName("EXCEL").Select(a => a.Id).ToList();
                _excelApplicationProcessId = newProcessIds.Except(processIds).FirstOrDefault();
            }
    
            return _excelApp;
        }
    
    public static void Dispose()
        {
            try
            {
                _excelApp.Workbooks.Close();
                _excelApp.Quit();
                System.Runtime.InteropServices.Marshal.ReleaseComObject(_excelApp);
                _excelApp = null;
                GC.Collect();
                GC.WaitForPendingFinalizers();
                if (_excelApplicationProcessId != default(int))
                {
                    var process = System.Diagnostics.Process.GetProcessById(_excelApplicationProcessId);
                    process?.Kill();
                    _excelApplicationProcessId = default(int);
                }
            }
            catch (Exception ex)
            {
                _excelApp = null;
            }
    
        }
    
0赞 SamSar 1/5/2019 #38

使用 Microsoft Excel 2016 进行测试

一个真正经过测试的解决方案。

有关 C# 参考,请参阅:https://stackoverflow.com/a/1307180/10442623

要 VB.net 参考,请参阅: https://stackoverflow.com/a/54044646/10442623

1 包括类作业

2 实现类以处理 Excel 过程的适当处置

2赞 tjarrett 4/3/2019 #39

这是一个非常简单的方法:

[DllImport("User32.dll")]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
...

int objExcelProcessId = 0;

Excel.Application objExcel = new Excel.Application();

GetWindowThreadProcessId(new IntPtr(objExcel.Hwnd), out objExcelProcessId);

Process.GetProcessById(objExcelProcessId).Kill();
0赞 MushroomSoda 7/3/2019 #40

在 VSTO AddIn 中更新应用程序对象后,我遇到了同样的问题,让 PowerPoint 关闭。我在这里尝试了所有答案,但收效甚微。

这是我为我的情况找到的解决方案 - 不要使用“new Application”,ThisAddIn 的 AddInBase 基类已经具有“Application”的句柄。如果你在需要的地方使用该句柄(如果需要,请将其设置为静态),那么你不必担心清理它,PowerPoint也不会挂在关闭上。

1赞 Dietrich Baumgarten 9/29/2019 #41

我的回答已经晚了,它的唯一目的是支持 Govert 提出的解决方案。

正如我们在德语中所说的那样,从与 COM 无关的 .NET 世界中自动执行 Excel 或其他 COM 对象是一个“棘手的难题”,您很容易发疯。我依靠以下步骤:

  1. 本机 COM 函数返回一个整数类型,其值 指示函数的成功或失败。在 C# 中,省略了该值,并且本机 COM 函数的参数 是返回值。指示将导致失败的 A 在例外中。因此,必须将关键 COM 函数括起来 在 try/catch 块中。HRESULTHRESULTretvalHRESULT

  2. COM 对象在 C# 中由管理的智能变量表示 他们自己的加载和上传。因此,什么都不做。那是垃圾的工作 收集。对 COM 对象的引用在 C# 中表现为智能指针 在 C++ 中,也没有发布。Marshall.ReleaseComObject

  3. 对于与 Excel 的每次交互,获取一个且只有一个应用程序接口实例,并在 哪个活着。这是必需的,因为 CLR 不会释放 在对 Excel 的任何引用消失之前 Excel 的资源 范围。在后台启动一个新的 Excel 进程。最好的 scope 是 try/catch 块的 try 部分,因为加载了 Excel 可能会失败,在这种情况下会引发异常。ExcelAppExcelApp

  4. 通过 Collection 属性生成新对象,如 Workbook、 工作表和单元格。不要在乎巫毒教的一点好, 两点坏规则,使用任意数量的引用,如前所述 之前什么都不做。Marshall.ReleaseComObject

  5. 使用完 Excel 后,调用 为 Excel 处理量定的范围。这应该是最后一句话 在此范围内。该功能是退出 Excel 所必需的,但 在向此引用变量之前,不会卸载该进程 实例离开作用域。加载 Excel 实例时,请勿 允许任何会绕过该功能的用户操作。QuitQuitQuit

在运行我的应用程序之前,请打开任务管理器,并在“进程”选项卡中查看后台进程中的条目。当您启动程序时,列表中会出现一个新的 Excel 进程条目,并一直保持在那里,直到线程在 5 秒后再次唤醒。调用函数后,变量超出范围,卸载 Excel 进程,该进程将从后台进程列表中正常消失。QuitExcelApp

using System;
using System.Threading;
using Excel = Microsoft.Office.Interop.Excel;

class Program
{
  private static void DoSomething(Excel.Workbook Wb)
  {
    Excel.Worksheet NewWs = Wb.Worksheets.Add();
                        
    for (int i = 1; i < 10; i++)
    {
      NewWs.Cells[i, 1] = i;
    }

    Wb.Save();
    Wb.Close();
    //NewWs goes out of scope and will be released by the GC.
  }

  static void Main(string[] args)
  {
    try
    {
      Excel.Application ExcelApp = new Excel.Application();
      Excel.Workbook Wb = ExcelApp.Workbooks.Open(@"D:\ExcelFiles\\CSharpExample.xlsx");
      DoSomething(Wb);
      ExcelApp?.Quit(); //Don't forget!
      Thread.Sleep(5000);
      //ExcelApp and Wb go out of scope and will be released by the GC.
    }     
    catch (Exception ex)
    {
      Console.WriteLine(ex.Message);
    }
  }
}
0赞 Anton Shepelev 12/3/2020 #42

在其他答案中考虑的三种常规策略中,终止进程显然是一种黑客攻击,而调用垃圾回收器是一种残酷的霰弹枪方法,旨在补偿 COM 对象的错误释放。经过大量实验和重写与版本无关的后期绑定包装中 COM 对象的管理,我得出的结论是,准确和及时地调用 是最有效和最优雅的策略。不,你永远不需要,因为在一个写得很好的程序中,每个COM都获取一次,因此需要引用计数器的单次递减。excelMarshal.ReleaseComObject()FinalReleaseComObject()

应确保释放每个 COM 对象,最好是在不再需要时立即释放。但是,完全有可能在退出 Excel 应用程序后立即释放所有内容,唯一的代价是内存使用率更高。Excel 将按预期关闭,只要没有丢失或忘记释放 COM 对象。

该过程中最简单和最明显的帮助是将每个互操作对象包装到一个 .NET 类实现中,其中该方法在其互操作对象上调用。正如这里所建议的那样,在析构函数中执行此操作是没有意义的,因为析构函数是非确定性的。IDisposableDispose()ReleaseComObject()

下面显示的是我们的包装器方法,它通过绕过中间成员来获得一个单元格。请注意它在使用后处理中间对象的方式:WorkSheetCells

public ExcelRange XCell( int row, int col)
{   ExcelRange anchor, res;
    using( anchor = Range( "A1") )
    {   res = anchor.Offset( row - 1, col - 1 );  }
    return res;
}

下一步可能是一个简单的内存管理器,它将跟踪获得的每个 COM 对象,并确保在 Excel 退出后释放它,如果用户更喜欢用一些 RAM 使用量换取更简单的代码。

延伸阅读

  1. 如何正确释放Excel COM对象
  2. 释放 COM 对象:垃圾回收器与 Marshal.RelseaseComObject
0赞 br3nt 11/19/2021 #43

我真的很喜欢在他们自己之后清理事情的时候......所以我做了一些包装类,为我做所有的清理工作!这些将进一步记录。

最终代码非常可读且易于访问。我还没有发现任何在我工作簿和应用程序之后运行的 Excel 虚拟实例(除了我在过程中调试和关闭应用程序的地方)。Close()Quit()

function void OpenCopyClose() {
  var excel = new ExcelApplication();
  var workbook1 = excel.OpenWorkbook("C:\Temp\file1.xslx", readOnly: true);
  var readOnlysheet = workbook1.Worksheet("sheet1");

  var workbook2 = excel.OpenWorkbook("C:\Temp\file2.xslx");
  var writeSheet = workbook.Worksheet("sheet1");

  // do all the excel manipulation

  // read from the first workbook, write to the second workbook.
  var a1 = workbook1.Cells[1, 1];
  workbook2.Cells[1, 1] = a1

  // explicit clean-up
  workbook1.Close(false);
  workbook2 .Close(true);
  excel.Quit();
}

注意:您可以跳过 和 调用,但如果您要写入 Excel 文档,您至少需要 .当对象超出范围(方法返回)时,类终结器将自动启动并执行任何清理。只要您注意变量的作用域,就会自动管理和清理工作表 COM 对象中对 COM 对象的任何引用,例如,仅在存储对 COM 对象的引用时,才将变量保留在当前范围的本地。如果需要,您可以轻松地将所需的值复制到 POCO,或创建其他包装类,如下所述。Close()Quit()Save()

为了管理所有这些,我创建了一个类 ,它充当任何 COM 对象的包装器。它实现了接口,还包含一个终结器,供那些不喜欢的人使用。DisposableComObjectIDisposableusing

该方法调用该方法,然后将该属性设置为 null。Dispose()Marshal.ReleaseComObject(ComObject)ComObjectRef

当私有属性为 null 时,对象处于释放状态。ComObjectRef

如果在释放属性后访问该属性,则会引发异常。ComObjectComObjectAccessedAfterDisposeException

该方法可以手动调用。它也被终结器调用,在块的末尾,以及在该变量的作用域结束时。Dispose()usingusing var

、 、 和 中的顶级类有自己的包装类,其中每个类都是Microsoft.Office.Interop.ExcelApplicationWorkbookWorksheetDisposableComObject

代码如下:

/// <summary>
/// References to COM objects must be explicitly released when done.
/// Failure to do so can result in odd behavior and processes remaining running after the application has stopped.
/// This class helps to automate the process of disposing the references to COM objects.
/// </summary>
public abstract class DisposableComObject : IDisposable
{
    public class ComObjectAccessedAfterDisposeException : Exception
    {
        public ComObjectAccessedAfterDisposeException() : base("COM object has been accessed after being disposed") { }
    }

    /// <summary>The actual COM object</summary>
    private object ComObjectRef { get; set; }

    /// <summary>The COM object to be used by subclasses</summary>
    /// <exception cref="ComObjectAccessedAfterDisposeException">When the COM object has been disposed</exception>
    protected object ComObject => ComObjectRef ?? throw new ComObjectAccessedAfterDisposeException();

    public DisposableComObject(object comObject) => ComObjectRef = comObject;

    /// <summary>
    /// True, if the COM object has been disposed.
    /// </summary>
    protected bool IsDisposed() => ComObjectRef is null;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); // in case a subclass implements a finalizer
    }

    /// <summary>
    /// This method releases the COM object and removes the reference.
    /// This allows the garbage collector to clean up any remaining instance.
    /// </summary>
    /// <param name="disposing">Set to true</param>
    protected virtual void Dispose(bool disposing)
    {
        if (!disposing || IsDisposed()) return;
        Marshal.ReleaseComObject(ComObject);
        ComObjectRef = null;
    }

    ~DisposableComObject()
    {
        Dispose(true);
    }
}

还有一个方便的泛型子类,使使用起来稍微容易一些。

public abstract class DisposableComObject<T> : DisposableComObject
{
    protected new T ComObject => (T)base.ComObject;

    public DisposableComObject(T comObject) : base(comObject) { }
}

最后,我们可以用来为 Excel 互操作类创建包装类。DisposableComObject<T>

该子类具有对新 Excel 应用程序实例的引用,用于打开工作簿。ExcelApplication

OpenWorkbook()返回一个,它也是 DisposableComObject 的子类。ExcelWorkbook

Dispose()已被重写以在调用基方法之前退出 Excel 应用程序。 是 的别名。Dispose()Quit()Dispose()

public class ExcelApplication : DisposableComObject<Application>
{
    public class OpenWorkbookActionCancelledException : Exception
    {
        public string Filename { get; }

        public OpenWorkbookActionCancelledException(string filename, COMException ex) : base($"The workbook open action was cancelled. {ex.Message}", ex) => Filename = filename;
    }

    /// <summary>The actual Application from Interop.Excel</summary>
    Application App => ComObject;

    public ExcelApplication() : base(new Application()) { }

    /// <summary>Open a workbook.</summary>
    public ExcelWorkbook OpenWorkbook(string filename, bool readOnly = false, string password = null, string writeResPassword = null)
    {
        try
        {
            var workbook = App.Workbooks.Open(Filename: filename, UpdateLinks: (XlUpdateLinks)0, ReadOnly: readOnly, Password: password, WriteResPassword: writeResPassword, );

            return new ExcelWorkbook(workbook);
        }
        catch (COMException ex)
        {
            // If the workbook is already open and the request mode is not read-only, the user will be presented
            // with a prompt from the Excel application asking if the workbook should be opened in read-only mode.
            // This exception is raised when when the user clicks the Cancel button in that prompt.
            throw new OpenWorkbookActionCancelledException(filename, ex);
        }
    }

    /// <summary>Quit the running application.</summary>
    public void Quit() => Dispose(true);

    /// <inheritdoc/>
    protected override void Dispose(bool disposing)
    {
        if (!disposing || IsDisposed()) return;
        App.Quit();
        base.Dispose(disposing);
    }
}

ExcelWorkbook也是子类,用于打开工作表。DisposableComObject<Workbook>

这些方法返回的,你猜对了,它也是 的子类。Worksheet()ExcelWorksheetDisposableComObject<Workbook>

该方法被重写,并在调用基 之前关闭工作表。Dispose()Dispose()

注意:我添加了一些扩展方法,用于迭代。如果出现编译错误,这就是原因。我在末尾添加扩展方法。Workbook.Worksheets

public class ExcelWorkbook : DisposableComObject<Workbook>
{
    public class WorksheetNotFoundException : Exception
    {
        public WorksheetNotFoundException(string message) : base(message) { }
    }

    /// <summary>The actual Workbook from Interop.Excel</summary>
    Workbook Workbook => ComObject;

    /// <summary>The worksheets within the workbook</summary>
    public IEnumerable<ExcelWorksheet> Worksheets => worksheets ?? (worksheets = Workbook.Worksheets.AsEnumerable<Worksheet>().Select(w => new ExcelWorksheet(w)).ToList());
    private IEnumerable<ExcelWorksheet> worksheets;

    public ExcelWorkbook(Workbook workbook) : base(workbook) { }

    /// <summary>
    /// Get the worksheet matching the <paramref name="sheetName"/>
    /// </summary>
    /// <param name="sheetName">The name of the Worksheet</param>
    public ExcelWorksheet Worksheet(string sheetName) => Worksheet(s => s.Name == sheetName, () => $"Worksheet not found: {sheetName}");

    /// <summary>
    /// Get the worksheet matching the <paramref name="predicate"/>
    /// </summary>
    /// <param name="predicate">A function to test each Worksheet for a macth</param>
    public ExcelWorksheet Worksheet(Func<ExcelWorksheet, bool> predicate, Func<string> errorMessageAction) => Worksheets.FirstOrDefault(predicate) ??  throw new WorksheetNotFoundException(errorMessageAction.Invoke());

    /// <summary>
    /// Returns true of the workbook is read-only
    /// </summary>
    public bool IsReadOnly() => Workbook.ReadOnly;

    /// <summary>
    /// Save changes made to the workbook
    /// </summary>
    public void Save()
    {
        Workbook.Save();
    }

    /// <summary>
    /// Close the workbook and optionally save changes
    /// </summary>
    /// <param name="saveChanges">True is save before close</param>
    public void Close(bool saveChanges)
    {
        if (saveChanges) Save();
        Dispose(true);
    }

    /// <inheritdoc/>
    protected override void Dispose(bool disposing)
    {
        if (!disposing || IsDisposed()) return;
        Workbook.Close();
        base.Dispose(disposing);
    }
}

最后,.ExcelWorksheet

UsedRows()仅返回未包装对象的枚举对象。我还没有遇到过从对象属性访问的COM对象需要手动包装的情况,就像需要用、和一样。这些似乎都会自动清理它们。大多数情况下,我只是遍历 Ranges 并获取或设置值,因此我的特定用例并不像可用功能那样先进。Microsoft.Office.Interop.Excel.RangeMicrosoft.Office.Interop.Excel.WorksheetApplicationWorkbookWorksheet

在这种情况下,没有覆盖,因为不需要对工作表执行特殊操作。Dispose()

public class ExcelWorksheet : DisposableComObject<Worksheet>
{
    /// <summary>The actual Worksheet from Interop.Excel</summary>
    Worksheet Worksheet => ComObject;

    /// <summary>The worksheet name</summary>
    public string Name => Worksheet.Name;

    // <summary>The worksheets cells (Unwrapped COM object)</summary>
    public Range Cells => Worksheet.Cells;

    public ExcelWorksheet(Worksheet worksheet) : base(worksheet) { }

    /// <inheritdoc cref="WorksheetExtensions.UsedRows(Worksheet)"/>
    public IEnumerable<Range> UsedRows() => Worksheet.UsedRows().ToList();
}

可以添加更多的包装类。只需根据需要添加其他方法,并在包装类中返回 COM 对象。只需复制我们在通过 和 包装工作簿时所做的操作。ExcelWorksheetExcelApplication.OpenWorkbook()ExcelWorkbook.WorkSheets

一些有用的扩展方法:

public static class EnumeratorExtensions
{
    /// <summary>
    /// Converts the <paramref name="enumerator"/> to an IEnumerable of type <typeparamref name="T"/>
    /// </summary>
    public static IEnumerable<T> AsEnumerable<T>(this IEnumerable enumerator)
    {
        return enumerator.GetEnumerator().AsEnumerable<T>();
    }

    /// <summary>
    /// Converts the <paramref name="enumerator"/> to an IEnumerable of type <typeparamref name="T"/>
    /// </summary>
    public static IEnumerable<T> AsEnumerable<T>(this IEnumerator enumerator)
    {
        while (enumerator.MoveNext()) yield return (T)enumerator.Current;
    }

    /// <summary>
    /// Converts the <paramref name="enumerator"/> to an IEnumerable of type <typeparamref name="T"/>
    /// </summary>
    public static IEnumerable<T> AsEnumerable<T>(this IEnumerator<T> enumerator)
    {
        while (enumerator.MoveNext()) yield return enumerator.Current;
    }
}

public static class WorksheetExtensions
{
    /// <summary>
    /// Returns the rows within the used range of this <paramref name="worksheet"/>
    /// </summary>
    /// <param name="worksheet">The worksheet</param>
    public static IEnumerable<Range> UsedRows(this Worksheet worksheet) =>
        worksheet.UsedRange.Rows.AsEnumerable<Range>();
}