787

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

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

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

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

4

43 回答 43

703

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 互操作对象

于 2008-10-01T17:30:41.020 回答
284

您实际上可以干净地释放您的 Excel 应用程序对象,但您必须小心。

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

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

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

例如,假设您有一个名为 的 Range 对象变量、一个名为xlRng的 Worksheet 变量xlSheet、一个名为 Workbook 的变量xlBook和一个名为 的 Excel 应用程序变量xlApp,那么您的清理代码可能如下所示:

// 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 对象的GC.Collect()GC.WaitForPendingFinalizers()调用两次,如下所示:

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

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

我知道明确调用GC.Collect()是一个禁忌(当然两次调用听起来很痛苦),但老实说,没有办法绕过它。通过正常操作,您将生成隐藏的对象,您没有引用这些对象,因此您无法通过调用以外的任何其他方式释放这些对象GC.Collect()

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

我在这里有一个教程:

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

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

于 2008-10-01T19:54:21.410 回答
226

前言:我的答案包含两个解决方案,所以阅读时要小心,不要遗漏任何内容。

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

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

  • 调用 GC.Collect() 和 GC.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 将关闭 =)

于 2009-12-12T14:50:15.970 回答
51

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

我花了一些时间试图解决这个问题,当时 XtremeVBTalk 是最活跃和响应最灵敏的。这是我原来的帖子的链接,Closing an Excel Interop process cleanly, even if your application crashs。以下是帖子的摘要,以及复制到此帖子的代码。

  • 关闭 Interop 进程Application.Quit()Process.Kill()在大多数情况下工作,但如果应用程序发生灾难性崩溃,则会失败。即,如果应用程序崩溃,Excel 进程仍将运行松散。
  • 解决方案是让操作系统使用 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);
    }

}

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

  • 在构造函数中,info.LimitFlags = 0x2000;被调用。0x2000JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE枚举值,这个值由MSDN定义为:

当作业的最后一个句柄关闭时,导致与作业关联的所有进程终止。

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

    [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);
于 2009-08-20T16:00:37.047 回答
45

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

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

我们了解到,当您完成对 Excel COM 对象的每个引用时,将其设置为 null 很重要。这包括单元格、表格和所有内容。

于 2008-10-01T17:30:36.217 回答
38

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

其次,如果要确保在进程结束时清理对进程外 COM 对象的 COM 引用(以便 Excel 进程将关闭),则需要确保垃圾收集器运行。您可以通过调用GC.Collect()和正确地做到这一点GC.WaitForPendingFinalizers()。两次调用它是安全的,并确保循环也被清除(尽管我不确定它是否需要,并且希望有一个显示这一点的示例)。

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

因此,一般模式是:

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 被认为是危险的,以及在调试器下发现引用保持活动状态的问题,这使我之前的测试感到困惑。

于 2016-06-29T22:50:23.497 回答
31

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

你不能这样做:

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

你必须做

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

其次是释放对象。

于 2008-10-01T17:45:45.213 回答
19

找到了一个有用的通用模板,它可以帮助实现 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/

于 2008-12-08T11:10:50.320 回答
17

我不敢相信这个问题已经困扰了世界 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 程序不是您直接控制的一部分。因此,如果链接被释放,它将保持打开状态!

大家编程好~~

于 2010-11-21T03:41:13.067 回答
13

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

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

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

因此,获取当前处理器列表并获取 EXCEL 进程的 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();

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

于 2009-10-16T11:01:36.883 回答
12

这肯定看起来过于复杂了。根据我的经验,让 Excel 正确关闭只有三个关键因素:

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

2:打电话GC.Collect()

3:必须关闭 Excel,要么由用户手动关闭程序,要么由您调用QuitExcel 对象。(请注意,它Quit的功能就像用户试图关闭程序一样,如果有未保存的更改,即使 Excel 不可见,也会显示一个确认对话框。用户可以按取消,然后 Excel 将不会关闭。 )

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

实现这一点的一种方法是用您自己的类包装互操作 Excel 对象,在构造函数中创建互操作实例,并使用 Dispose 实现 IDisposable 看起来像

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

这将从您的程序方面清除 excel。一旦 Excel 关闭(由用户手动或您调用Quit),该过程将消失。如果程序已经关闭,那么该进程将在GC.Collect()调用时消失。

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

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

于 2011-08-31T21:18:17.040 回答
11

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

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

一些主要特点是:

  • 大部分与版本无关(并记录了与版本相关的功能)
  • 无依赖
  • 没有 PIA
  • 没有注册
  • 没有 VSTO

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

于 2013-03-28T14:12:39.593 回答
10

添加 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
于 2010-12-06T13:08:35.833 回答
9

这里接受的答案是正确的,但还要注意不仅需要避免“两点”引用,还需要避免通过索引检索的对象。你也不需要等到你完成程序来清理这些对象,最好创建函数,一旦你完成它们就会清理它们,如果可能的话。这是我创建的一个函数,它分配了一个名为的 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 { } 
}

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

这种事情在标准应用程序中并不是真正需要的,它们可以很好地自行清理,但在 ASP.NET 应用程序中,如果你错过了其中一个,无论你多久调用一次垃圾收集器,Excel 都会仍在您的服务器上运行。

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

于 2009-12-17T20:15:04.830 回答
9

尝试后

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

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

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

我们在函数的末尾添加了一个包装器,如下所示:

private void FunctionWrapper(string sourcePath, string targetPath)
{
    try
    {
        FunctionThatCallsExcel(sourcePath, targetPath);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}
于 2013-11-18T22:58:12.140 回答
8

我完全遵循了这一点......但我仍然遇到了 1000 次中的问题。谁知道为什么。是时候拿出锤子了……

在实例化 Excel 应用程序类之后,我立即获得了刚刚创建的 Excel 进程。

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

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

if (!process.HasExited)
   process.Kill();
于 2014-02-25T03:35:56.367 回答
7
于 2013-06-14T15:16:58.820 回答
6

您需要知道 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

于 2008-11-06T15:21:25.463 回答
6

“永远不要对 COM 对象使用两个点”是避免 COM 引用泄漏的一个很好的经验法则,但 Excel PIA 可能导致泄漏的方式比乍看之下更明显。

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

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

关于 COM 事件的一些理论

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

Excel PIA 如何公开 COM 事件

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

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

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

对 COM 实例引用计数的影响

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

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

解决方案

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

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

于 2012-06-25T06:52:30.430 回答
4

正如其他人指出的那样,您需要为您使用的每个 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 对象模型的一些简单操作(打开工作簿、写入范围、保存/关闭工作簿等)。包装类实现 IDisposable,在它使用的每个对象上小心地实现 Marshal.ReleaseComObject,并且不会公开向应用程序的其余部分公开任何 Excel 对象。

但是这种方法不能很好地适应更复杂的需求。

这是.NET COM Interop 的一大缺陷。对于更复杂的场景,我会认真考虑用 VB6 或其他非托管语言编写一个 ActiveX DLL,您可以将所有与进程外 COM 对象(如 Office)的交互委托给它。然后您可以从您的 .NET 应用程序中引用这个 ActiveX DLL,并且事情会容易得多,因为您只需要发布这个引用。

于 2008-10-01T18:26:09.857 回答
3

当上面的所有东西都不起作用时,尝试给 Excel 一些时间来关闭它的工作表:

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

...
FinalReleaseComObject(app);
于 2011-06-18T12:02:35.710 回答
3

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

我花了几个小时尝试了几种方法。都是好主意,但我终于发现了我的错误:如果您不释放所有对象,那么上述任何方法都无法帮助您喜欢我的情况。确保释放所有对象,包括范围一!

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

选项都在这里

于 2012-08-08T18:38:33.667 回答
3

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

我提倡的方法是将您的 Excel.Interop 引用设为空,如果它们是非局部变量,然后调用GC.Collect()andGC.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。但是,请注意,您不太可能需要使用此方法。

于 2013-01-11T01:46:22.340 回答
2

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

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

所以我猜这个问题没有绝对的解决办法,而是改用其他方法比如Office Open XML开发。

于 2012-03-14T15:47:35.893 回答
2

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

private static void Clean()
{
    workBook.Close();
    Marshall.ReleaseComObject(workBook);
    excel.Quit();
    CG.Collect();
    CG.WaitForPendingFinalizers();
}
于 2013-12-12T22:37:19.883 回答
2

我的解决方案

[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();
}
于 2014-06-18T09:34:16.183 回答
1

我认为其中一些只是框架处理 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 进程,但我可能只是对此感到偏执。

于 2008-10-01T17:47:22.913 回答
1

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

在 WPF 应用程序中,基本上相同的代码在没有问题或很少有问题的情况下工作。

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

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

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

底线是:您还必须检查初始化代码,特别是如果您有很多类等。

于 2013-04-11T14:01:14.757 回答
1

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

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(); }
}
于 2014-09-05T11:42:45.330 回答
1

我目前正在研究办公自动化,并且偶然发现了一个每次都适用于我的解决方案。它很简单,不涉及杀死任何进程。

似乎仅仅通过循环当前的活动进程,并以任何方式“访问”一个打开的 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;
                    }
                }
            }
}
于 2016-08-02T14:32:38.500 回答
1

'这肯定看起来过于复杂了。根据我的经验,让 Excel 正确关闭只有三个关键因素:

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

2:调用 GC.Collect()

3:必须关闭 Excel,要么由用户手动关闭程序,要么由您在 Excel 对象上调用 Quit。(请注意,Quit 的功能就像用户试图关闭程序一样,如果有未保存的更改,即使 Excel 不可见,也会显示一个确认对话框。用户可以按取消,然后 Excel 将不会关闭.)

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

实现这一点的一种方法是用您自己的类包装互操作 Excel 对象,在构造函数中创建互操作实例,并使用 Dispose 实现 IDisposable 看起来像

这将从您的程序方面清除 excel。一旦 Excel 关闭(由用户手动或通过您调用 Quit),该过程将消失。如果程序已经关闭,那么该进程将在 GC.Collect() 调用时消失。

(我不确定它有多重要,但您可能希望在 GC.Collect() 调用之后调用 GC.WaitForPendingFinalizers() ,但摆脱 Excel 进程并不是绝对必要的。)

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

于 2017-03-10T04:18:09.717 回答
1

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

[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();
于 2019-04-02T20:24:30.487 回答
1

我的回答迟了,它的唯一目的是通过一个完整的例子来支持 Govert、Porkbutts 和 Dave Cousineau 提出的解决方案。正如我们用德语所说,从与 COM 无关的 .NET 世界中自动化 Excel 或其他 COM 对象是一个“棘手的问题”,您很容易发疯。我依赖以下步骤:

  1. 对于与 Excel 的每次交互,获取一个且只有一个ExcelApp应用程序接口的本地实例,并创建一个存在于其中的范围ExcelApp。这是必要的,因为在对 Excel 的任何引用超出范围之前,CLR 不会释放 Excel 的资源。一个新的 Excel 进程在后台启动。

  2. 实现执行任务的函数,ExcelApp通过 Collection 属性生成新对象,如 Workbook(s)、Worksheet(s) 和 Cell(s)。在这些函数中,不关心 voodoo one-dot-good, two-dot-bad 规则,不要尝试为每个隐式创建的对象获取引用,也不要做 Marshall.ReleaseComObject任何事情。这就是垃圾收集器的工作。

  3. 在范围内ExcelApp,调用这些函数并传递引用ExcelApp

  4. 加载 Excel 实例时,不允许任何会绕过Quit再次卸载此实例的函数的用户操作。

  5. 使用完 Excel 后,Quit在 Excel 处理范围内调用单独的函数。这应该是此范围内的最后一条语句。

在运行我的应用程序之前,打开任务管理器并在“进程”选项卡中查看后台进程中的条目。当您启动程序时,一个新的 Excel 进程条目会出现在列表中并一直停留在那里,直到线程在 5 秒后再次唤醒。之后,将调用 Quit 函数,停止优雅地从后台进程列表中消失的 Excel 进程。

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

namespace GCTestOnOffice
{
  class Program
  {
    //Don't: private  static Excel.Application ExcelApp = new Excel.Application();

    private static void DoSomething(Excel.Application ExcelApp)
    {
        Excel.Workbook Wb = ExcelApp.Workbooks.Open(@"D:\Aktuell\SampleWorkbook.xlsx");
        Excel.Worksheet NewWs = Wb.Worksheets.Add();

        for (int i = 1; i < 10; i++)
        {
            NewWs.Cells[i, 1] = i;
        }
        Wb.Save();
    }

    public static void Quit(Excel.Application ExcelApp)
    {
        if (ExcelApp != null)
        {
            ExcelApp.Quit(); //Don't forget!!!!!
            ExcelApp = null;
        }
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }

    static void Main(string[] args)
    {
      {
        Excel.Application ExcelApp = new Excel.Application();
        Thread.Sleep(5000);
        DoSomething(ExcelApp);
        Quit(ExcelApp);
        //ExcelApp goes out of scope, the CLR can and will(!) release Excel
      }

      Console.WriteLine("Input a digit: ");
      int k = Console.Read(); 
    }
  }
}

如果我将 Main 函数更改为

static void Main(string[] args)
{
  Excel.Application ExcelApp = new Excel.Application();
  DoSomething(ExcelApp);

  Console.WriteLine("Input a digit: ");
  int k = Console.Read(); 
  Quit(ExcelApp);
}

用户可以不输入数字,而是点击控制台的关闭按钮,我的 Excel 实例从此过上幸福的生活。因此,如果您的 Excel 实例仍然顽固地加载,您的清理功能可能不会出错,但会被不可预见的用户操作绕过。

如果 Program 类有 Excel 实例的成员,CLR 不会在应用程序终止之前卸载 Excel 实例。这就是为什么我更喜欢在不再需要时超出范围的本地引用。

于 2019-09-29T06:46:15.000 回答
0

采用:

[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);
    }
}
于 2013-08-27T10:11:24.290 回答
0

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

  1. 杀死进程
  2. 使用 GC.Collect()
  3. 跟踪每个 COM 对象并正确释放它。

这让我明白这个问题有多么困难:)

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

我没有直接在 Interop 提供的接口上编写代码,而是制作扩展方法以使生活更轻松。像 ApplicationHelpers.CreateExcel() 或 workbook.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 正确销毁,因此会自动调用Quitand Marshal.FinalReleaseComObject

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

于 2015-05-04T13:12:33.870 回答
0

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

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

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

于 2016-04-07T12:29:30.253 回答
0

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

  1. 在打开一个 excel 应用程序之前,获取所有名为 oldProcessIds 的进程 ID。
  2. 打开excel应用程序。
  3. 现在获取所有名为 nowProcessIds 的 excel 应用程序进程 ID。
  4. 当需要退出时,杀死 oldProcessIds 和 nowProcessIds 之间的 except ids。

    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;
            }
    
        }
    
于 2018-12-19T03:06:10.883 回答
0

使用 Microsoft Excel 2016 测试

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

C#参考请看: https ://stackoverflow.com/a/1307180/10442623

VB.net参考请看: https ://stackoverflow.com/a/54044646/10442623

1 包括课堂作业

2 实现类来处理excel过程的适当处理

于 2019-01-04T19:26:25.477 回答
0

在我的 VSTO 插件中更新应用程序对象后,我在关闭 PowerPoint 时遇到了同样的问题。我在这里尝试了所有答案,但成功有限。

这是我为我的案例找到的解决方案 - 不要使用“新应用程序”,ThisAddIn 的 AddInBase 基类已经有一个“应用程序”的句柄。如果您在需要的地方使用该句柄(如果必须将其设为静态),那么您无需担心清理它,PowerPoint 也不会在关闭时挂起。

于 2019-07-02T18:25:26.073 回答
0

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

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

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

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

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
于 2020-12-02T23:00:09.600 回答
0

我真的很喜欢事情自己清理的时候......所以我做了一些包装类来为我做所有的清理工作!这些记录在下面。

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

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();
}

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

为了管理这一切,我创建了一个类,DisposableComObject它充当任何 COM 对象的包装器。它实现了IDisposable接口,还包含一个终结器,用于那些不喜欢using.

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

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

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

Dispose()方法可以手动调用。它也被终结器调用,在using块结束时,以及using var在该变量的范围结束时。

Microsoft.Office.Interop.Excel, Application,Workbook和中的顶级类Worksheet有自己的包装类,其中每个都是DisposableComObject

这是代码:

/// <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) { }
}

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

ExcelApplication子类引用了一个新的 Excel 应用程序实例并用于打开工作簿。

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

Dispose()已被覆盖以在调用基本Dispose()方法之前退出 Excel 应用程序。 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()方法返回,你猜对ExcelWorksheet了,它也是DisposableComObject<Workbook>.

Dispose()方法被覆盖,并在调用 base 之前先关闭工作表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()简单地返回一个可枚举的未包装Microsoft.Office.Interop.Excel.Range对象。我还没有遇到过从对象的属性访问的 COMMicrosoft.Office.Interop.Excel.Worksheet对象需要手动包装的情况,就像使用ApplicationWorkbookWorksheet. 这些似乎都可以自动清理它们。大多数情况下,我只是迭代 Ranges 并获取或设置值,因此我的特定用例不如可用功能先进。

在这种情况下没有覆盖,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();
}

可以添加更多的包装类。只需ExcelWorksheet根据需要添加其他方法并在包装类中返回 COM 对象。只需复制我们在通过ExcelApplication.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>();
}
于 2021-11-19T06:33:47.097 回答
-1

Excel 并非旨在通过 C++ 或 C# 进行编程。COM API 专门设计用于与 Visual Basic、VB.NET 和 VBA 一起使用。

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

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

于 2011-06-19T00:16:53.707 回答
-1

这是唯一对我有用的方法

        foreach (Process proc in System.Diagnostics.Process.GetProcessesByName("EXCEL"))
        {
            proc.Kill();
        }
于 2017-02-14T07:55:42.520 回答