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

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

尽管这种方法有效,但即使在我关闭Excel之后,Excel.exe进程仍处于后台。它只在我的应用程序被手动关闭后发布。

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


当前回答

我目前正在研究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;
                    }
                }
            }
}

其他回答

这是唯一对我有效的方法

        foreach (Process proc in System.Diagnostics.Process.GetProcessesByName("EXCEL"))
        {
            proc.Kill();
        }

我的解决方案

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

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

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

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

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

我不知道C#在内部为WorksheetsCOM对象创建了一个包装器,但我的代码没有发布它(因为我不知道),这也是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互操作对象

这里公认的答案是正确的,但也要注意,不仅需要避免“双点”引用,还需要避免通过索引检索的对象。您也不需要等到程序完成后才能清理这些对象,最好创建函数,以便在可能的情况下,在完成这些对象后立即清理它们。下面是我创建的一个函数,用于分配名为xlStyleHeader的Style对象的一些财产:

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仍将在您的服务器上运行。

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

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

这是我为我的案例找到的解决方案-不要使用“新应用程序”,ThisAddIn的AddInBase基类已经有了“应用程序”的句柄。如果你在需要的地方使用这个手柄(如果必须的话,让它保持静态),那么你不必担心清理它,PowerPoint也不会挂在近处。