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

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

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

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


当前回答

正如其他人所指出的,您需要为使用的每个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对象。

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

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

其他回答

这似乎太复杂了。根据我的经验,要让Excel正确关闭,只有三件关键的事情:

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

2:调用GC.Collect()

3:必须通过用户手动关闭程序或通过对Excel对象调用“退出”来关闭Excel。(请注意,“退出”的功能与用户试图关闭程序的功能相同,如果存在未保存的更改,即使Excel不可见,也会显示一个确认对话框。用户可以按“取消”,Excel将不会关闭。)

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

实现这一点的一种方法是用自己的类包装interop Excel对象,在构造函数中创建interop实例,并使用Dispose实现IDisposable

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

这将从程序的方面清理出优秀的东西。一旦Excel关闭(由用户手动或您调用退出),该过程将消失。如果程序已经关闭,那么进程将在GC.Collect()调用中消失。

(我不确定它有多重要,但您可能需要在GC.Collect()调用之后调用GC.WaitForPendingFinalizers(),但这并不是完全需要摆脱Excel进程。)

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

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

我花了一些时间试图解决这个问题,当时XtremeVBTalk是最活跃和响应最快的。这里有一个链接到我的原始文章,“即使应用程序崩溃,也要干净地关闭Excel互操作过程”。下面是文章的摘要,以及复制到本文的代码。

使用Application.Quit()和process.Kill()关闭Interop进程在很大程度上是有效的,但如果应用程序发生灾难性崩溃,则会失败。也就是说,如果应用程序崩溃,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;被调用。0x2000是JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE枚举值,MSDN将此值定义为:

当作业的最后一个句柄关闭。

获取进程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);

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

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

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

拍摄Excel程序并咀嚼泡泡糖

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

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

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

我有一个项目,在该项目中,同一个Excel文件针对不同的参数值被处理了多次-例如,基于通用列表中的值对其进行分析。

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

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

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