簡體   English   中英

如何正確清理 Excel 互操作對象?

[英]How do I properly clean up Excel interop objects?

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

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

盡管這種工作有效,但即使我關閉 Excel, Excel.exe進程仍然在后台。 它只有在我的應用程序被手動關閉后才會被釋放。

我做錯了什么,或者有沒有其他方法可以確保正確處理互操作對象?

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

您實際上可以干凈地釋放您的 Excel Application 對象,但您必須小心。

為您訪問的絕對每個 COM 對象維護一個命名引用,然后通過Marshal.FinalReleaseComObject()顯式釋放它的建議在理論上是正確的,但不幸的是,在實踐中很難管理。 如果有人在任何地方滑倒並使用“兩個點”,或者通過for each循環或任何其他類似的命令迭代單元格,那么您將擁有未引用的 COM 對象並有掛起的風險。 在這種情況下,將無法在代碼中找到原因; 您必須親眼檢查所有代碼並希望找到原因,這對於大型項目來說幾乎是不可能的。

好消息是您實際上不必維護對您使用的每個 COM 對象的命名變量引用。 相反,先調用GC.Collect() ,然后GC.WaitForPendingFinalizers()來釋放所有你沒有持有引用的(通常是次要的)對象,然后顯式釋放你持有命名變量引用的對象。

您還應該按重要性的相反順序釋放您的命名引用:首先是范圍對象,然后是工作表、工作簿,最后是您的 Excel 應用程序對象。

例如,假設你有一個名為Range對象變量xlRng ,命名工作表變量xlSheet ,命名工作簿變量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.WaitForPendingFinalizers() ,如下所示:

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

但是,這應該不是必需的,除非您使用的是 Visual Studio Tools for Office (VSTO),它使用終結器導致在終結隊列中提升整個對象圖。 這樣的對象直到下一次垃圾收集才會被釋放。 但是,如果您沒有使用 VSTO,您應該能夠只調用GC.Collect()GC.WaitForPendingFinalizers()一次。

我知道顯式調用GC.Collect()GC.Collect()當然,這樣做兩次聽起來很痛苦),但老實說,沒有辦法繞過它。 通過正常操作,您將生成沒有引用的隱藏對象,因此除了調用GC.Collect()之外,您無法通過任何其他方式釋放這些對象。

這是一個復雜的話題,但這就是它的全部內容。 一旦你為你的清理過程建立了這個模板,你就可以正常編碼,而無需包裝器等:-)

我在這里有一個教程:

使用 VB.Net / COM Interop 自動化辦公程序

它是為 VB.NET 編寫的,但不要被它推遲,原理與使用 C# 時完全相同。

前言:我的回答包含兩個解決方案,所以閱讀時要小心,不要錯過任何內容。

關於如何使 Excel 實例卸載有不同的方法和建議,例如:

  • 使用 Marshal.FinalReleaseComObject() 顯式釋放每個 com 對象(不要忘記隱式創建的 com 對象)。 要釋放每個創建的 com 對象,您可以使用這里提到的 2 點規則:
    如何正確清理 Excel 互操作對象?

  • 調用 GC.Collect() 和 GC.WaitForPendingFinalizers() 使 CLR 釋放未使用的 com-objects *(實際上是有效的,詳見我的第二個解決方案)

  • 檢查 com-server-application 是否可能會顯示一個等待用戶回答的消息框(雖然我不確定它是否可以阻止 Excel 關閉,但我聽說過幾次)

  • 向主 Excel 窗口發送 WM_CLOSE 消息

  • 在單獨的 AppDomain 中執行適用於 Excel 的函數。 有些人認為卸載 AppDomain 時 Excel 實例將被關閉。

  • 殺死在我們的 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) 嘗試在 Release 模式下運行您的項目並檢查 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 將關閉 =)

更新:添加了 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; 叫做。 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);

這適用於我正在從事的項目:

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

我們了解到,在完成對 Excel COM 對象的每個引用后,將其設置為 null 非常重要。 這包括單元格、表格和所有內容。

首先 - 在執行 Excel 互操作時,您永遠不必調用Marshal.ReleaseComObject(...)Marshal.FinalReleaseComObject(...) 這是一個令人困惑的反模式,但是任何關於此的信息(包括來自 Microsoft 的信息)表明您必須從 .NET 手動釋放 COM 引用都是不正確的。 事實是 .NET 運行時和垃圾收集器正確地跟蹤和清理 COM 引用。 對於您的代碼,這意味着您可以刪除頂部的整個 `while (...) 循環。

其次,如果要確保在進程結束時清除對進程外 COM 對象的 COM 引用(以便 Excel 進程將關閉),則需要確保垃圾收集器運行。 您可以通過調用GC.Collect()GC.WaitForPendingFinalizers()正確執行此操作。 兩次調用它是安全的,並確保循環也絕對被清理(盡管我不確定是否需要它,並且希望有一個顯示這一點的示例)。

第三,在調試器下運行時,局部引用將人為地保持活動狀態直到方法結束(以便局部變量檢查工作)。 因此GC.Collect()調用對於從同一方法清除像rng.Cells這樣的對象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 被認為是危險的,同時找到了在調試器下保持活動引用的問題,這讓我之前的測試感到困惑。

Excel 命名空間中的任何內容都需要釋放。 時期

你不能這樣做:

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

你必須做

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

然后釋放對象。

找到了一個有用的通用模板,它可以幫助為 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/

我不敢相信這個問題已經困擾世界 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 程序不是您直接控制的一部分。 因此,如果鏈接被釋放,它將保持打開狀態!

大家編程好~~

普通的開發人員,你們的解決方案都不適合我,所以我決定實施一個新技巧

首先讓我們明確“我們的目標是什么?” =>“在我們在任務管理器中工作后看不到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();

這解決了我的問題,希望你也是。

這肯定看起來過於復雜了。 根據我的經驗,讓 Excel 正確關閉只有三個關鍵的事情:

1:確保沒有對您創建的 excel 應用程序的剩余引用(無論如何您應該只有一個;將其設置為null

2:調用GC.Collect()

3:Excel 必須關閉,要么由用戶手動關閉程序,要么通過在 Excel 對象上調用Quit (請注意, Quit功能就像用戶試圖關閉程序一樣,如果有未保存的更改,即使 Excel 不可見,也會顯示一個確認對話框。用戶可以按取消,然后 Excel 將不會被關閉.)

1 需要在 2 之前發生,但 3 可以隨時發生。

實現這一點的一種方法是用您自己的類包裝互操作 Excel 對象,在構造函數中創建互操作實例,並使用 Dispose 實現 IDisposable,看起來像

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

這將從您的程序方面清理 excel。 一旦 Excel 關閉(由用戶手動或通過您調用Quit ),該過程將消失。 如果程序已經關閉,那么該進程將在GC.Collect()調用中消失。

(我不確定它有多重要,但您可能需要在GC.Collect()調用之后調用GC.WaitForPendingFinalizers()但並不是絕對需要擺脫 Excel 進程。)

多年來,這對我來說一直沒有問題。 請記住,雖然這有效,但您實際上必須優雅地關閉它才能工作。 如果在清理 Excel 之前中斷程序(通常是在調試程序時點擊“停止”),您仍然會積累 excel.exe 進程。

傳統上,我一直遵循VVS 的回答中的建議。 但是,為了使這個答案與最新選項保持同步,我認為我未來的所有項目都將使用“NetOffice”庫。

NetOffice是 Office PIA 的完全替代品,並且完全與版本無關。 它是托管 COM 包裝器的集合,可以處理在 .NET 中使用 Microsoft Office 時經常導致此類問題的清理工作。

一些主要功能是:

  • 主要與版本無關(並且記錄了與版本相關的功能)
  • 無依賴
  • 沒有 PIA
  • 無需注冊
  • 沒有 VSTO

我與該項目沒有任何關系; 我真的很感激頭痛的明顯減少。

為了增加 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

這里接受的答案是正確的,但還要注意,不僅需要避免“兩點”引用,還需要避免通過索引檢索的對象。 您也不需要等到程序完成后才清理這些對象,最好創建函數,以便在完成后立即清理它們,如果可能的話。 這是我創建的一個函數,它為名為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]設置為一個變量以清除它(不是因為兩個點,它指的是不需要釋放的枚舉,而是因為對象 I' m 所指的實際上是一個需要釋放的 Border 對象)。

這類事情在標准應用程序中並不是真正必要的,這些應用程序可以很好地完成自己的清理工作,但是在 ASP.NET 應用程序中,如果您錯過了其中一個,無論您調用垃圾收集器的頻率如何,Excel 都會仍在您的服務器上運行。

在編寫此代碼時,它需要在監視任務管理器的同時對細節和許多測試執行進行大量關注,但這樣做可以為您省去拼命搜索代碼頁面以找到您錯過的一個實例的麻煩。 這在循環中尤其重要,您需要釋放對象的每個實例,即使它每次循環使用相同的變量名。

嘗試后

  1. 以相反的順序釋放 COM 對象
  2. 最后添加GC.Collect()GC.WaitForPendingFinalizers()兩次
  3. 不超過兩個點
  4. 關閉工作簿並退出應用程序
  5. 在發布模式下運行

對我有用的最終解決方案是移動一組

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

我們添加到函數末尾的包裝器,如下所示:

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

我完全遵循了這一點......但我仍然遇到了千分之一的問題。 誰知道為什么。 是時候拿出錘子了……

在 Excel Application 類被實例化之后,我就掌握了剛剛創建的 Excel 進程。

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

然后,一旦我完成了上述所有 COM 清理工作,我就會確保該進程沒有運行。 如果它仍在運行,請殺死它!

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

¨°º¤ø„¸ Shoot Excel proc 和嚼泡泡糖¸„ø¤º°¨

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

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

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

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

“永遠不要對 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 接口。

您需要注意 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

正如其他人所指出的,您需要為您使用的每個 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,事情會容易得多,因為您只需要釋放這個引用。

確保釋放所有與 Excel 相關的對象!

我花了幾個小時嘗試了幾種方法。 都是好主意,但我終於發現了我的錯誤:如果您不釋放所有對象,那么在我的情況下,上述任何一種方法都無法幫助您 確保釋放所有對象,包括范圍一!

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

選項都在這里

關於釋放 COM 對象的一篇很棒的文章是2.5 釋放 COM 對象(MSDN)。

我提倡的方法是,如果 Excel.Interop 引用是非局部變量,則將它們歸零,然后調用GC.Collect()GC.WaitForPendingFinalizers()兩次。 本地范圍的互操作變量將被自動處理。

這消除了為每個COM 對象保留命名引用的需要。

這是文章中的一個例子:

public class Test {

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

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

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

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

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

這些話直接來自文章:

在幾乎所有情況下,清空 RCW 引用並強制進行垃圾收集都會正確清理。 如果您還調用 GC.WaitForPendingFinalizers,垃圾收集將盡可能確定。 也就是說,您將非常確定對象何時被清除——從第二次調用 WaitForPendingFinalizers 返回。 作為替代,您可以使用 Marshal.ReleaseComObject。 但是,請注意,您不太可能需要使用此方法。

當上述所有內容都不起作用時,請嘗試給 Excel 一些時間來關閉其工作表:

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

...
FinalReleaseComObject(app);

兩點規則對我不起作用。 就我而言,我創建了一個方法來清理我的資源,如下所示:

private static void Clean()
{
    workBook.Close();
    Marshall.ReleaseComObject(workBook);
    excel.Quit();
    CG.Collect();
    CG.WaitForPendingFinalizers();
}

我的解決方案

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

使用 Word/Excel 互操作應用程序時應該非常小心。 在嘗試了所有解決方案之后,我們仍然有很多“WinWord”進程在服務器上打開(有 2000 多個用戶)。

在解決這個問題幾個小時后,我意識到如果我同時在不同線程上使用Word.ApplicationClass.Document.Open()打開多個文檔,IIS 工作進程 (w3wp.exe) 會崩潰,所有 WinWord 進程都打開!

所以我猜這個問題沒有絕對的解決辦法,只能改用Office Open XML開發等其他方式。

我認為其中一些只是框架處理 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 進程保留在任何地方,但我可能只是對此感到偏執。

正如一些人可能已經寫過的那樣,關閉Excel(對象)的方式不僅重要; 如何打開它以及項目類型也很重要。

在 WPF 應用程序中,基本上相同的代碼可以正常工作,沒有或幾乎沒有問題。

我有一個項目,其中針對不同的參數值對同一個 Excel 文件進行了多次處理 - 例如,根據通用列表中的值對其進行解析。

我把所有與Excel相關的函數放到基類中,解析器放到一個子類中(不同的解析器使用常用的Excel函數)。 我不希望為通用列表中的每個項目再次打開和關閉 Excel,所以我只在基類中打開它一次並在子類中關閉它。 將代碼移動到桌面應用程序時遇到問題。 我已經嘗試了許多上述解決方案。 GC.Collect()之前已經實現了,是建議的兩倍。

然后我決定將打開 Excel 的代碼移到子類中。 現在我不是只打開一次,而是創建一個新對象(基類)並為每個項目打開 Excel 並在最后關閉它。 有一些性能損失,但基於幾個測試 Excel 進程關閉沒有問題(在調試模式下),因此臨時文件也被刪除。 如果我得到一些更新,我將繼續測試並寫更多。

底線是:你還必須檢查初始化代碼,特別是如果你有很多類等。

接受的答案對我不起作用。 析構函數中的以下代碼完成了這項工作。

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

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

'這肯定看起來過於復雜了。 根據我的經驗,讓 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 進程。

這是一個非常簡單的方法:

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

我的回答晚了,它的唯一目的是通過一個完整的例子來支持 Govert、Porkbutts 和 Dave Cousineau 提出的解決方案。 正如我們在德語中所說,自動化來自與 COM 無關的 .NET 世界的 Excel 或其他 COM 對象是一個“硬核”,您很容易發瘋。 我依靠以下步驟:

  1. 對於與 Excel 的每次交互,獲取 Application 接口的一個且僅一個本地實例ExcelApp ,並創建ExcelApp所在的范圍。 這是必要的,因為在對 Excel 的任何引用超出范圍之前,CLR 不會釋放 Excel 的資源。 一個新的 Excel 進程在后台啟動。

  2. 通過使用ExcelApp通過集合屬性生成新對象(如 Workbook(s)、Worksheet(s) 和 Cell(s))來實現執行任務的函數。 在這些函數中,不關心伏都教的一個點好,兩個點壞的規則,不要試圖為每個隱式創建的對象獲取引用,也不要Marshall.ReleaseComObject任何東西。 這就是垃圾收集的工作。

  3. ExcelApp的范圍內,調用這些函數並傳遞引用ExcelApp

  4. 在加載 Excel 實例時,不允許任何用戶操作會繞過再次卸載此實例的Quit函數。

  5. 完成 Excel 后,在為 Excel 處理而創建的范圍內調用單獨的Quit函數。 這應該是此范圍內的最后一條語句。

在運行我的應用程序之前,打開任務管理器並在“進程”選項卡中查看后台進程中的條目。 當您啟動程序時,列表中會出現一個新的 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 實例。 這就是為什么我更喜歡在不再需要時超出范圍的本地引用。

用:

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

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

  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 正確銷毀,因此會自動調用QuitMarshal.FinalReleaseComObject

注釋? 或者這是對第三類問題的回答?

只是為這里列出的許多解決方案添加另一個解決方案,使用 C++/ATL 自動化(我想你可以使用 VB/C# 中的類似的東西??)

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

這對我來說就像一個魅力......

我有一個想法,嘗試終止您打開的 excel 進程:

  1. 在打開一個 excel 應用程序之前,獲取所有名為 oldProcessIds 的進程 ID。
  2. 打開excel應用程序。
  3. 現在獲取所有名為 nowProcessIds 的 excelapplication 進程 ID。
  4. 當需要退出時,殺死 oldProcessIds 和 nowProcessIds 之間的 except id。

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

使用 Microsoft Excel 2016 進行測試

一個真正經過測試的解決方案。

C# 參考請參見: https : //stackoverflow.com/a/1307180/10442623

VB.net參考請看: https : //stackoverflow.com/a/54044646/10442623

1 包括班級作業

2 實現類來處理excel過程的適當處理

在我的 VSTO AddIn 中更新 Application 對象后,我在關閉 PowerPoint 時遇到了同樣的問題。 我在這里嘗試了所有答案,但成功有限。

這是我為我的案例找到的解決方案 - 不要使用“新應用程序”,ThisAddIn 的 AddInBase 基類已經擁有“應用程序”的句柄。 如果您在需要的地方使用該句柄(如果必須,請將其設為靜態),那么您無需擔心清理它,PowerPoint 也不會掛起。

在其他答案中考慮的三種一般策略中,殺死excel進程顯然是一種黑客行為,而調用垃圾收集器是一種野蠻的霰彈槍方法,旨在補償COM對象的不正確釋放。 經過大量實驗並在我的版本不可知和后期綁定包裝器中重寫COM對象的管理后,我得出的結論是,准確及時地調用Marshal.ReleaseComObject()是最有效和最優雅的策略。 不,您永遠不需要FinalReleaseComObject() ,因為在編寫良好的程序中,每個COM 都獲取一次,因此需要對引用計數器進行一次遞減。

應確保釋放每個COM對象,最好在不再需要時立即釋放。 但是完全有可能在退出Excel應用程序后立即釋放所有內容,唯一的代價是更高的內存使用量。 只要不松動或忘記釋放COM對象, Excel就會按預期關閉。

該過程中最簡單和最明顯的幫助是將每個互操作對象包裝到一個實現IDisposable.NET類中,其中Dispose()方法在其互操作對象上調用ReleaseComObject() 在析構函數中執行此操作是沒有意義的,因為析構函數是不確定的。

下面顯示的是我們的包裝器的方法, WorkSheet繞過中間的Cells成員從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

我真的很喜歡事情在他們自己之后清理……所以我做了一些包裝類來為我做所有的清理工作! 這些被進一步記錄下來。

最終代碼非常可讀且易於訪問。 在我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()Quit()調用,但如果您正在寫入 Excel 文檔,您至少需要Save() 當對象超出范圍(方法返回)時,類終結器將自動啟動並進行任何清理。 只要您注意變量的范圍,從 Worksheet COM 對象到 COM 對象的任何引用都將被自動管理和清除,例如,僅在存儲對 COM 對象的引用時,將變量保持在當前范圍內。 如果需要,您可以輕松地將需要的值復制到 POCO,或者創建額外的包裝類,如下所述。

為了管理這一切,我創建了一個類DisposableComObject ,它充當任何 COM 對象的包裝器。 它實現了IDisposable接口,並且還為那些不喜歡using .

Dispose()方法調用Marshal.ReleaseComObject(ComObject) ,然后將ComObjectRef屬性設置為 null。

當私有ComObjectRef屬性為 null 時,對象處於ComObjectRef狀態。

如果ComObject屬性被ComObject后訪問,則拋出ComObjectAccessedAfterDisposeException異常。

Dispose()方法可以手動調用。 它也被終結器調用,在using塊結束時,以及在該變量作用域結束時using var

來自Microsoft.Office.Interop.ExcelApplicationWorkbookWorksheet的頂級類獲得自己的包裝類,其中每個類都是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);
    }
}

ExcelWorkbookExcelWorkbook DisposableComObject<Workbook>並用於打開工作表。

Worksheet()方法返回ExcelWorksheet ,您猜對了,它也是DisposableComObject<Workbook>的子類。

Dispose()方法被覆蓋並在調用基礎Dispose()之前關閉工作表。

注意:我添加了一些用於迭代Workbook.Worksheets擴展方法。 如果出現編譯錯誤,這就是原因。 我會在最后添加擴展方法。

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

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

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

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

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

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

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

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

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

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

最后, ExcelWorksheet

UsedRows()只是返回一個可枚舉的未包裝的Microsoft.Office.Interop.Excel.Range對象。 我還沒有遇到過從Microsoft.Office.Interop.Excel.Worksheet對象的屬性訪問的 COM 對象需要手動包裝的情況,就像ApplicationWorkbookWorksheet 這些似乎都會自動清理它們。 大多數情況下,我只是迭代范圍並獲取或設置值,所以我的特定用例並不像可用功能那樣先進。

在這種情況下,沒有覆蓋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包裝工作簿時ExcelApplication.OpenWorkbook()

一些有用的擴展方法:

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

這是唯一對我有用的方法

        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 函數或子函數(通過Application.Run ),然后執行任何所需的處理。 此外 - 在調用Application.Run - 請務必注意指示 excel 繁忙的異常並重試調用Application.Run

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM