关于C#:如何正确清理Excel Interop对象?

How do I properly clean up Excel interop objects?

我正在使用c(ApplicationClass中的excel interop,并在finally子句中放置了以下代码:

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

虽然这类工作是可行的,但即使在我关闭Excel之后,Excel.exe过程仍处于后台。只有当我的应用程序被手动关闭时,它才会被释放。

我做错了什么,或者是否有其他方法来确保正确处理interop对象?


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

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

对于我来说,它是Excelapp.Worksheets对象,我直接使用它,而不将其分配给变量:

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

我不知道在内部C为工作表COM对象创建了一个包装器,它没有被我的代码释放(因为我不知道),这也是Excel没有被卸载的原因。

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

Never use two dots with COM objects.

因此,有了这些知识,正确的方法是:

1
2
3
4
5
Worksheets sheets = excelApp.Worksheets; // <-- The important part
Worksheet sheet = sheets.Open(...);
...
Marshal.ReleaseComObject(sheets);
Marshal.ReleaseComObject(sheet);

尸检后更新:

我希望每个读者都能非常仔细地阅读HansPassant的答案,因为它解释了我和许多其他开发人员遇到的陷阱。几年前,当我写下这个答案时,我不知道调试器对垃圾收集器的影响,并得出了错误的结论。出于历史的考虑,我的答案是不变的,但请阅读此链接,不要走"两点"的道路:了解.NET中的垃圾收集并使用IDisposable清理Excel Interop对象。


实际上,您可以完全释放Excel应用程序对象,但您必须小心。

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
// 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()调用的次数是以下两次:

1
2
3
4
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();

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

我知道明确地称呼GC.Collect()是一种否定(当然,这样做两次听起来很痛苦),但老实说,没有办法解决这个问题。通过正常的操作,您将生成隐藏的对象,您对此没有任何参考,因此,您不能通过调用GC.Collect()以外的任何其他方式释放这些对象。

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

我在这里有一个教程:

使用vb.net/com互操作自动运行Office程序

它是为vb.net编写的,但不要被它所耽误,它的原理与使用c_时完全相同。


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

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

  • 显式释放每个COM对象与Marshal.FinalReleaseComObject()一起使用(不要忘记已创建COM对象)。释放每个创建的COM对象,您可以使用这里提到的两点规则:如何正确清理Excel Interop对象?

  • 调用gc.collect()和gc.waitForPendingFinalizers()生成clr释放未使用的COM对象*(实际上,它是有效的,有关详细信息,请参阅我的第二个解决方案)

  • 正在检查COM服务器应用程序可能会显示一个消息框等待要回答的用户(尽管我不是当然可以阻止Excel结束了,但我听说了一些时代)

  • 向主服务器发送wm_close消息Excel窗口

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

  • 终止在Excel互操作代码启动后实例化的所有Excel实例。

但是!有时所有这些选择都不起作用或不合适!

例如,昨天我发现在我的一个函数(与Excel一起工作)中,Excel在函数结束后继续运行。我什么都试过了!我彻底检查了整个函数10次,并为所有内容添加了marshal.finalReleaseComObject()!我还有gc.collect()和gc.waitForPendingFinalizers()。我检查了隐藏的信息框。我试图将wm_关闭消息发送到主Excel窗口。我在单独的AppDomain中执行了我的函数并卸载了该域。没什么帮助!关闭所有Excel实例的选项是不合适的,因为如果用户手动启动另一个Excel实例,在执行与Excel一起工作的函数期间,该实例也将由我的函数关闭。我敢打赌用户肯定不会高兴的!所以,老实说,这是一个蹩脚的选择(没有冒犯的家伙)。所以,我花了几个小时才找到一个好的(在我看来)解决方案:通过主窗口的hwnd杀死excel进程(这是第一个解决方案)。

下面是简单的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
[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的代码可以如下所示:

1
2
3
4
5
6
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的方法。所以,不是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void GenerateWorkbook(...)
{
  ApplicationClass xl;
  Workbook xlWB;
  try
  {
    xl = ...
    xlWB = xl.Workbooks.Add(...);
    ...
  }
  finally
  {
    ...
    Marshal.ReleaseComObject(xlWB)
    ...
    GC.Collect();
    GC.WaitForPendingFinalizers();
  }
}

你写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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将关闭=)


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

我花了一段时间试图解决这个问题,当时Xtremevbtalk是最活跃和反应最迅速的。这里有一个链接指向我最初的文章,它可以清晰地关闭ExcelInterop进程,即使您的应用程序崩溃了。下面是文章的摘要,以及复制到此文章的代码。

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

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

Windows作业代码

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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定义为:

Causes all processes associated with the job to terminate when the
last handle to the job is closed.

获取进程ID(PID)的额外win32 api调用

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

使用代码

1
2
3
4
5
    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);


这对我正在做的一个项目很有用:

1
2
3
4
excelApp.Quit();
Marshal.ReleaseComObject (excelWB);
Marshal.ReleaseComObject (excelApp);
excelApp = null;

我们了解到,在处理完Excel COM对象后,将每个引用都设置为空是很重要的。这包括单元格、工作表和所有内容。


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

你不能这样做:

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

你必须这么做

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

接着是物体的释放。


我找到了一个有用的通用模板,它可以帮助实现COM对象的正确处置模式,当超出范围时需要调用marshal.releaseComObject:

用途:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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();
    }
}

模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
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/utility-idisposable-class-3-autoreleaseComObject/


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

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

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

因此,一般模式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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和堆栈溢出的文章(尤其是这个问题!).

最后说服我仔细研究并找出正确的建议的是blog post marshal.releaseComObject,它被认为是危险的,同时发现了这个问题,其中引用在调试器下保持活跃,这让我之前的测试很困惑。


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

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

关闭时

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

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

很好的编程每个人~~


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

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

好啊。让no挑战并开始销毁它,但考虑不要销毁并行运行的其他实例OS Excel。

因此,获取当前处理器的列表并获取Excel进程的PID,然后一旦您的工作完成,我们就有了一个新的具有唯一PID的来宾进程列表,找到并销毁该列表。

1
2
3
4
5
6
7
8
9
10
11
12
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();

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


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

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

2:呼叫GC.Collect()

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

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

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

1
2
3
4
5
if (!mDisposed) {
   mExcel = null;
   GC.Collect();
   mDisposed = true;
}

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

(我不确定它有多重要,但您可能需要在GC.Collect()调用之后进行GC.WaitForPendingFinalizers()调用,但不必严格取消Excel过程。)

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


这里接受的答案是正确的,但也要注意,不仅需要避免"双点"引用,还需要避免通过索引检索的对象。您也不需要等到程序完成后才能清理这些对象,最好创建函数,在可能的情况下,一旦完成这些对象,就可以清理它们。下面是我创建的一个函数,它为名为xlStyleHeader的样式对象分配一些属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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]设置为一个变量,以便清除它(不是因为这两个点引用了一个不需要释放的枚举,而是因为我引用的对象实际上是一个需要释放的边界对象)。

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

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


试过之后

  • 按相反顺序释放COM对象
  • 最后加GC.Collect()GC.WaitForPendingFinalizers()两次
  • 不超过两个点
  • 关闭工作簿并退出应用程序
  • 在释放模式下运行
  • 对我有效的最后一个解决方案是移动一组

    1
    2
    GC.Collect();
    GC.WaitForPendingFinalizers();

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    private void FunctionWrapper(string sourcePath, string targetPath)
    {
        try
        {
            FunctionThatCallsExcel(sourcePath, targetPath);
        }
        finally
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }
    }


    要添加Excel不关闭的原因,即使在读取、创建时直接引用每个对象,也是"for"循环。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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


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

    NetOffice完全替代了Office PIA,完全不可知版本。它是一个托管COM包装器的集合,可以处理在.NET中与Microsoft Office一起工作时经常导致此类头痛的清理。

    一些主要功能包括:

    • 主要是与版本无关的(并且记录了与版本相关的特性)
    • 无依赖关系
    • 无软脑膜
    • 不注册
    • 没有VSTO

    我绝不与这个项目有任何关联;我只是真诚地欣赏明显减少的头痛。


    是什么意思????拍Excel程序和嚼泡泡糖????γo

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    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);
    }

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

    在Excel应用程序类被实例化后,我就获得了刚刚创建的Excel进程的控制权。

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

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

    1
    2
    if (!process.HasExited)
       process.Kill();

    "不要将两个点用于COM对象"是避免COM引用泄漏的一个重要经验法则,但Excel PIA可以以比乍一看更明显的方式导致泄漏。

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

    例如,订阅应用程序类的WorkbookOpen事件。

    关于COM事件的一些理论

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

    Excel PIA如何公开COM事件

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

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

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

    对COM实例引用计数的影响

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

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

    解决方案

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

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


    你需要知道,Excel对你所处的文化非常敏感。

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

    1
    2
    3
    4
        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


    正如其他人指出的,您需要为您使用的每个Excel对象创建一个显式引用,并根据该引用调用marshal.releaseComObject,如本文中所述。您还需要使用Try/Finally来确保始终调用ReleaseComObject,即使在抛出异常时也是如此。即代替:

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

    你需要做如下的事情:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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.quit,然后释放application对象。

    正如你所看到的,一旦你尝试做任何更为复杂的事情,这很快就会变得非常笨拙。我已经成功地用一个简单的包装类开发了.NET应用程序,该类包装了Excel对象模型的几个简单操作(打开一个工作簿,写入一个区域,保存/关闭工作簿等)。Wrapper类实现IDisposable,在它使用的每个对象上小心地实现marshal.releaseComObject,并且不会将任何Excel对象公开给应用程序的其余部分。

    但是这种方法不能很好地扩展到更复杂的需求中。

    这是.NET COM互操作的一个巨大缺陷。对于更复杂的场景,我会认真考虑用vb6或其他非托管语言编写一个ActiveX dll,您可以将所有与out-proc-com对象(如office)的交互委托给它。然后,您可以从.NET应用程序引用此ActiveX DLL,这样做会更容易,因为您只需要释放此引用。


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

    1
    2
    3
    4
    5
    6
    app.workbooks.Close();
    Thread.Sleep(500); // adjust, for me it works at around 300+
    app.Quit();

    ...
    FinalReleaseComObject(app);


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

    我花了几个小时尝试几种方法。所有的想法都很好,但我最终发现了我的错误:如果你不释放所有的物体,上面的任何一种方法都不能像我这样帮助你。确保释放所有对象,包括范围1!

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

    选项在这里一起。


    两点法则对我不起作用。在我的例子中,我创建了一个清理资源的方法,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    private static void Clean()
    {
        workBook.Close();
        Marshall.ReleaseComObject(workBook);
        excel.Quit();
        CG.Collect();
        CG.WaitForPendingFinalizers();
    }

    使用Word/Excel互操作应用程序时应该非常小心。在尝试了所有的解决方案之后,我们仍然有许多"winword"进程在服务器上保持打开状态(拥有2000多个用户)。

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

    因此,我想这个问题没有绝对的解决方案,而是转向其他方法,比如OfficeOpenXML开发。


    关于发布COM对象的一篇伟大的文章是2.5发布COM对象(msdn)。

    我建议的方法是将excel.interop引用为空(如果它们是非局部变量),然后调用GC.Collect()GC.WaitForPendingFinalizers()两次。本地作用域的互操作变量将自动处理。

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

    以下是本文中的一个示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    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();
        }
    }

    这些词直接来自文章:

    In almost all situations, nulling the RCW reference and forcing a garbage collection will clean up properly. If you also call GC.WaitForPendingFinalizers, garbage collection will be as deterministic as you can make it. That is, you'll be pretty sure exactly when the object has been cleaned up—on the return from the second call to WaitForPendingFinalizers. As an alternative, you can use Marshal.ReleaseComObject. However, note that you are very unlikely to ever need to use this method.


    这个公认的回答对我不起作用。析构函数中的以下代码完成了该任务。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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(); }
    }


    正如有些人可能已经写过的,关闭Excel(对象)的方式不仅重要,而且重要的是打开Excel的方式以及项目的类型。

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

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

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

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

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


    用途:

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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);
        }
    }

    我的解决方案

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    [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();
    }

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

    还有,也许我过于简单化了,但我认为你可以…

    1
    2
    3
    4
    5
    6
    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流程只停留在最短的时间内,但我可能只是有点多疑。


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

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

    我在正在开发的外接程序退出之前运行下面的方法,因为它会触发卸载事件。它每次都会删除任何挂起的Excel实例。老实说,我不完全确定这是为什么有效,但它对我很有效,可以放在任何Excel应用程序的末尾,而不必担心双点、marshal.releaseComObject或杀戮过程。我会非常感兴趣的任何建议,为什么这是有效的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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;
                        }
                    }
                }
    }

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

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

    2:呼叫gc.collect()。

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

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

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

    这将清除程序方面的Excel。关闭Excel后(由用户手动或通过调用Quit),进程将消失。如果程序已经关闭,那么该进程将在gc.collect()调用中消失。

    (我不确定它有多重要,但您可能需要在gc.collect()调用之后调用gc.waitForPendingFinalizers(),但不必严格取消Excel进程。)

    多年来这对我来说毫无疑问。请记住,虽然这项工作,但实际上你必须优雅地关闭它才能工作。如果在清除Excel之前中断程序(通常是在调试程序时单击"停止"),则仍将积累excel.exe进程。


    使用Microsoft Excel 2016测试

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

    参考文献请参见:https://stackoverflow.com/a/1307180/10442623

    到vb.net参考,请参见:https://stackoverflow.com/a/54044646/10442623

    1包括班级作业

    2实现类来处理Excel进程的适当处置


    我有个主意,试着终止你打开的Excel进程:

  • 打开Excel应用程序之前,请获取名为oldProcessID的所有进程ID。
  • 打开Excel应用程序。
  • 现在获取所有名为nowprocessid的ExcelApplication进程ID。
  • 当需要退出时,杀死oldprocessid和nowprocessid之间的except id。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    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;
            }

        }

  • 到目前为止,所有的答案似乎都涉及到以下几个方面:

  • 终止进程
  • 使用gc.collect()。
  • 跟踪每个COM对象并正确释放它。
  • 这让我明白这个问题有多困难:)

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

    我并没有直接在interop提供的接口上进行编写,而是使扩展方法变得更简单。类似于applicationhelpers.createExcel()或workbook.createWorksheet("将要验证的mySheetNames")。当然,任何创建的东西都可能在稍后的清理中导致问题,所以我实际上倾向于将此过程作为最后的手段。然而,正确的清理(第三种选择)可能是破坏性最小、控制能力最强的。

    因此,在这种情况下,我想知道这样做是否最好:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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);
        }
    }

    我用"可释放"来避免与一次性的混淆。不过,将其扩展到IDisposable应该很容易。

    这样的实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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对象做类似的事情。

    在工厂方法中:

    1
    2
    3
    4
    5
    6
    7
        public static Application CreateExcelApplication(bool hidden = false)
        {
            var excel = new ApplicationContainer().Releasible;
            excel.Visible = !hidden;

            return excel;
        }

    我希望每个容器都能被GC正确地销毁,从而自动调用QuitMarshal.FinalReleaseComObject

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


    下面是一个非常简单的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [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();

    只需在这里列出的另一个解决方案,使用C++/ATL自动化(我想你可以使用类似于VB/C语言的东西)。?)

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

    这对我来说很有魅力…


    这是唯一真正适合我的方法

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

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

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

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