Как правильно очистить объекты взаимодействия Excel?

Я использую взаимодействие Excel в C# (ApplicationClass) и разместили следующий код в моем предложении finally:

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

хотя этот вид работ,Excel.exe процесс все еще находится в фоновом режиме даже после закрытия Excel. Он выпускается только после того, как мое приложение вручную закрыто.

что я делаю неправильно, или есть ли альтернатива для обеспечения взаимодействия объектов утилизировать?

30 ответов


Excel не завершает работу, потому что приложение по-прежнему содержит ссылки на COM-объекты.

Я думаю, вы вызываете по крайней мере один член COM-объекта, не назначая его переменной.

для меня это был excelApp.Листы объект, который я непосредственно использовал, не назначая его переменной:

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

Я не знал, что внутренне C# создал оболочку для листы COM-объект, который не был выпущен моим кодом (потому что я не знал об этом) и был причиной, почему Excel не был выгружен.

Я нашел решение моей проблемы на на этой странице, который также имеет хорошее правило для использования com-объектов в C#:

никогда не используйте две точки с COM-объектами.


таким образом, с этим знанием правильный способ сделать вышеизложенное:

Worksheets sheets = excelApp.Worksheets; // <-- The important part
Worksheet sheet = sheets.Open(...);
...
Marshal.ReleaseComObject(sheets);
Marshal.ReleaseComObject(sheet);

вы можете на самом деле освободить объект приложения Excel чисто, но вы должны заботиться.

совет поддерживать именованную ссылку для абсолютно каждого COM-объекта, к которому вы обращаетесь, а затем явно освободить его через Marshal.FinalReleaseComObject() правильно в теории, но, к сожалению, очень сложно управлять на практике. Если один когда-либо скользит в любом месте и использует "две точки" или повторяет ячейки через for each loop, или любой другой подобный вид команды, тогда вы будете иметь unreferenced COM объекты и риск повесить. В этом случае не было бы возможности найти причину в коде; вам пришлось бы просмотреть весь свой код на глаз и, надеюсь, найти причину, задача, которая может быть почти невозможна для большого проекта.

хорошей новостью является то, что вам фактически не нужно поддерживать именованную ссылку на переменную для каждого COM-объекта, который вы используете. Вместо этого звоните GC.Collect() а то GC.WaitForPendingFinalizers() чтобы освободить все (обычно незначительные) объекты, на которые вы не держите ссылку, а затем явным образом отпустите объекты, на которые имеется ссылка именованной переменной.

вы также должны освободить именованные ссылки в обратном порядке важности: сначала объекты диапазона, затем рабочие листы, книги, а затем, наконец, объект приложения Excel.

например, предполагая, что у вас есть переменная объекта диапазона с именем 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);

в большинстве примеров кода Вы увидите для очистки COM-объектов из .NET,GC.Collect() и 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().

это сложная тема, но это действительно все. Как только вы установите этот шаблон для вашей процедуры очистки, вы можете кодировать нормально, без необходимости в обертках и т. д. :-)

у меня есть учебник здесь:

автоматизация офисных программ с VB.Net / COM Interop

это написано для VB.NET, но не пугайтесь этого, принципы точно такие же, как при использовании C#.


предисловие: мой ответ содержит два решения, так что будьте осторожны при чтении и ничего не пропустите.

существуют различные способы и советы, как сделать экземпляр Excel выгрузить, например:

  • освобождение каждого com-объекта явно с маршалом.FinalReleaseComObject() (не забывая о неявном создал com-объектов). Выпускать каждый созданный com-объект можно использовать Правило 2 точек, упомянутое здесь:
    как сделать Я правильно очищаю объекты взаимодействия Excel?

  • вызов GC.Собирать и СБОРЩИК МУСОРА.WaitForPendingFinalizers (), чтобы сделать CLR выпускает неиспользуемые com-объекты * (на самом деле, он работает, см. мое второе решение для деталей)

  • проверка com-server-application возможно, показывает окно сообщения, ожидающее пользователь, чтобы ответить (хотя я не конечно, это может предотвратить Excel от закрытия, но я слышал об этом несколько раз)

  • отправка Сообщение WM_CLOSE главному Окно Excel

  • выполнение функции, которая работает в Excel в отдельном AppDomain. Некоторые люди считают, экземпляр Excel будет закрыт, когда AppDomain выгруженный.

  • убиваешь всех экземпляров Excel, который был инстанцирован после нашей программы Excel-interoping код начался.

но! иногда все эти варианты просто не помогают или не могут быть подходящими!

для например, вчера я узнал, что в одной из моих функций (которая работает с excel) Excel продолжает работать после завершения функции. Я все перепробовал! Я тщательно проверил всю функцию 10 раз и добавил Маршал.FinalReleaseComObject() для все! Я тоже была ГК.Collect () и GC.WaitForPendingFinalizers(). Я проверил, нет ли скрытых сообщений. Я попытался отправить сообщение WM_CLOSE в Главное окно Excel. Я выполнил свою функцию в отдельном AppDomain и выгрузил этот домен. Ничего не помогало! Опция с закрытием всех экземпляров excel неуместна, потому что если пользователь запускает другой экземпляр Excel вручную, во время выполнения моей функции, которая также работает с Excel, то этот экземпляр также будет закрыт моей функцией. Держу пари, пользователь не будет счастлив! Так что, честно говоря, это хромой вариант (без обид, ребята). Поэтому я провел пару часов, прежде чем нашел хороший (по моему скромному мнению) решение: убить процесс excel по hWnd его главного окна (это первое решение).

вот простой код:

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

/// <summary> Tries to find and kill process by hWnd to the main window of the process.</summary>
/// <param name="hWnd">Handle to the main window of the process.</param>
/// <returns>True if process was found and killed. False if process was not found by hWnd or if it could not be killed.</returns>
public static bool TryKillProcessByMainWindowHwnd(int hWnd)
{
    uint processID;
    GetWindowThreadProcessId((IntPtr)hWnd, out processID);
    if(processID == 0) return false;
    try
    {
        Process.GetProcessById((int)processID).Kill();
    }
    catch (ArgumentException)
    {
        return false;
    }
    catch (Win32Exception)
    {
        return false;
    }
    catch (NotSupportedException)
    {
        return false;
    }
    catch (InvalidOperationException)
    {
        return false;
    }
    return true;
}

/// <summary> Finds and kills process by hWnd to the main window of the process.</summary>
/// <param name="hWnd">Handle to the main window of the process.</param>
/// <exception cref="ArgumentException">
/// Thrown when process is not found by the hWnd parameter (the process is not running). 
/// The identifier of the process might be expired.
/// </exception>
/// <exception cref="Win32Exception">See Process.Kill() exceptions documentation.</exception>
/// <exception cref="NotSupportedException">See Process.Kill() exceptions documentation.</exception>
/// <exception cref="InvalidOperationException">See Process.Kill() exceptions documentation.</exception>
public static void KillProcessByMainWindowHwnd(int hWnd)
{
    uint processID;
    GetWindowThreadProcessId((IntPtr)hWnd, out processID);
    if (processID == 0)
        throw new ArgumentException("Process has not been found by the given main window handle.", "hWnd");
    Process.GetProcessById((int)processID).Kill();
}

как вы можете видеть, я предоставил два метода в соответствии с шаблоном Try-Parse (я думаю, что это уместно здесь): один метод не вызывает исключения, если процесс не может быть убит (например, процесс больше не существует), а другой метод вызывает исключение, если процесс не был убит. Единственным слабым местом в этом коде являются разрешения безопасности. Теоретически, пользователь может не иметь разрешений на убийство процесс, но в 99.99% случаев пользователь имеет соответствующие разрешения. Я также протестировал его с гостевой учетной записью-он отлично работает.

Итак, ваш код, работающий с Excel, может выглядеть так:

int hWnd = xl.Application.Hwnd;
// ...
// here we try to close Excel as usual, with xl.Quit(),
// Marshal.FinalReleaseComObject(xl) and so on
// ...
TryKillProcessByMainWindowHwnd(hWnd);

вуаля! В Excel прекращается! :)

Хорошо, давайте вернемся ко второму решению, как я обещал в начале поста. второе решение-вызвать GC.Collect () и GC.WaitForPendingFinalizers(). Да, они действительно работают, но вам нужно быть осторожнее!
Многие люди говорят (и я сказал), что вызов GC.Collect () не помогает. Но причина в том, что это не поможет, если все еще есть ссылки на COM-объекты! Одна из самых популярных причин для GC.Collect () не помогает при запуске проекта в режиме отладки. В режиме отладки объекты, на которые больше не ссылаются, не будут собираться мусора до конца метода.
Итак, если вы попробовали GC.Collect () и GC.WaitForPendingFinalizers() и это не помогло, попробуйте сделайте следующее:

1) Попробуйте запустить проект в режиме выпуска и проверьте, правильно ли Excel закрыт

2) оберните метод, работающий с Excel, в отдельный метод. Итак, вместо чего-то вроде этого:

void GenerateWorkbook(...)
{
  ApplicationClass xl;
  Workbook xlWB;
  try
  {
    xl = ...
    xlWB = xl.Workbooks.Add(...);
    ...
  }
  finally
  {
    ...
    Marshal.ReleaseComObject(xlWB)
    ...
    GC.Collect();
    GC.WaitForPendingFinalizers();
  }
}

вы пишете:

void GenerateWorkbook(...)
{
  try
  {
    GenerateWorkbookInternal(...);
  }
  finally
  {
    GC.Collect();
    GC.WaitForPendingFinalizers();
  }
}

private void GenerateWorkbookInternal(...)
{
  ApplicationClass xl;
  Workbook xlWB;
  try
  {
    xl = ...
    xlWB = xl.Workbooks.Add(...);
    ...
  }
  finally
  {
    ...
    Marshal.ReleaseComObject(xlWB)
    ...
  }
}

теперь Excel закроется =)


обновление: добавлен код C# и ссылка на задания Windows

я провел некоторое время, пытаясь выяснить эту проблему, и в то время XtremeVBTalk был самым активным и отзывчивым. Вот ссылка на мой оригинальный пост, закрытие процесса взаимодействия Excel чисто, даже если ваше приложение аварийно завершает работу. Ниже приведено резюме поста и код, скопированный на этот пост.

  • закрытие процесса взаимодействия с Application.Quit() и Process.Kill() строительство по большей части, но терпит неудачу, если приложения аварийно завершают работу. Т. е. если приложение падает, процесс Excel останутся на свободе.
  • решение состоит в том, чтобы позволить ОС обрабатывать очистку ваших процессов через Объекты Заданий Windows использование вызовов Win32. Когда ваше основное приложение умирает, связанные процессы (например, 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 значение enum, и это значение определяется MSDN as:

причины все процессы, связанные с заданием, завершаются, когда последний дескриптор задания закрыт.

дополнительный вызов Win32 API для получения идентификатора процесса (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;

мы узнали, что важно ставить ссылка на COM-объект Excel на null, когда вы закончили с ним. Сюда входили клетки, простыни и все остальное.


все, что находится в пространстве имен 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-объектов, которые нуждаются в маршале.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 не является частью вашего прямого управления. Поэтому он останется открытым, если ссылка выпущен!

хорошее Программирование всех~~


Первый - вы никогда нужно позвонить Marshal.ReleaseComObject(...) или Marshal.FinalReleaseComObject(...) при выполнении взаимодействия Excel. Это запутанный анти-шаблон, но любая информация об этом, в том числе от Microsoft, которая указывает, что вам нужно вручную освободить com-ссылки из .NET, неверна. Дело в том, что среда выполнения .NET и сборщик мусора правильно отслеживают и очищают com-ссылки. Для вашего кода, это означает, что вы можете удалить все пока (...) петля вверху.

во-вторых, если вы хотите убедиться, что com-ссылки на COM-объект вне процесса очищаются, когда ваш процесс заканчивается (так что процесс Excel закроется), необходимо убедиться, что сборщик мусора работает. Вы делаете это правильно с помощью вызовов GC.Collect() и GC.WaitForPendingFinalizers(). Вызов этого дважды безопасен и гарантирует, что циклы определенно очищены (хотя я не уверен, что это необходимо, и был бы признателен за пример, который показывает это).

в-третьих, при запуске под отладчиком, местные ссылки будут искусственно сохранены до конца метода (так что локальная проверка переменных будет работать). Так что GC.Collect() вызовы не эффективны для очистки объекта, такого как rng.Cells из того же метода. Вы должны разделить код, выполняющий com-взаимодействие из очистки GC, на отдельные методы. (Это было ключевым открытием для меня, из одной части ответа, опубликованного здесь @nightcoder.)

таким образом, общая картина будет:

Sub WrapperThatCleansUp()

    ' NOTE: Don't call Excel objects in here... 
    '       Debugger would keep alive until end, preventing GC cleanup

    ' Call a separate function that talks to Excel
    DoTheWork()

    ' Now let the GC clean up (twice, to clean up cycles too)
    GC.Collect()    
    GC.WaitForPendingFinalizers()
    GC.Collect()    
    GC.WaitForPendingFinalizers()

End Sub

Sub DoTheWork()
    Dim app As New Microsoft.Office.Interop.Excel.Application
    Dim book As Microsoft.Office.Interop.Excel.Workbook = app.Workbooks.Add()
    Dim worksheet As Microsoft.Office.Interop.Excel.Worksheet = book.Worksheets("Sheet1")
    app.Visible = True
    For i As Integer = 1 To 10
        worksheet.Cells.Range("A" & i).Value = "Hello"
    Next
    book.Save()
    book.Close()
    app.Quit()

    ' NOTE: No calls the Marshal.ReleaseComObject() are ever needed
End Sub

есть много ложных информация и путаница по этому вопросу, включая многие сообщения на MSDN и переполнение стека (и особенно этот вопрос!).

что, наконец, убедил меня поближе и выяснить, правильный совет был блог Маршал.ReleaseComObject Считается Опасным вместе с поиском проблемы со ссылками, сохраненными в живых под отладчиком, который сбивал с толку мое предыдущее тестирование.


общие разработчики, ни одно из решений работал для меня, поэтому я решил реализовать новый фишка.

сначала укажите "какова наша цель?"=>"Не видеть объект excel после нашей работы в диспетчере задач"

Ok. Пусть нет, чтобы бросить вызов и начать уничтожать его, но не считают, чтобы уничтожить другой экземпляр os Excel, которые работают параллельно.

итак, получите список текущих процессоров и получите PID процессов 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();

Это решает мою проблему, надеюсь, ваша тоже.


принятый ответ здесь правильный, но также обратите внимание, что необходимо избегать не только ссылок "две точки", но и объектов, которые извлекаются с помощью индекса. Вам также не нужно ждать, пока вы закончите с программой, чтобы очистить эти объекты, лучше всего создавать функции, которые будут очищать их, как только вы закончите с ними, когда это возможно. Вот функция, которую я создал, которая назначает некоторые свойства объекта Style xlStyleHeader:

public Excel.Style xlStyleHeader = null;

private void CreateHeaderStyle()
{
    Excel.Styles xlStyles = null;
    Excel.Font xlFont = null;
    Excel.Interior xlInterior = null;
    Excel.Borders xlBorders = null;
    Excel.Border xlBorderBottom = null;

    try
    {
        xlStyles = xlWorkbook.Styles;
        xlStyleHeader = xlStyles.Add("Header", Type.Missing);

        // Text Format
        xlStyleHeader.NumberFormat = "@";

        // Bold
        xlFont = xlStyleHeader.Font;
        xlFont.Bold = true;

        // Light Gray Cell Color
        xlInterior = xlStyleHeader.Interior;
        xlInterior.Color = 12632256;

        // Medium Bottom border
        xlBorders = xlStyleHeader.Borders;
        xlBorderBottom = xlBorders[Excel.XlBordersIndex.xlEdgeBottom];
        xlBorderBottom.Weight = Excel.XlBorderWeight.xlMedium;
    }
    catch (Exception ex)
    {
        throw ex;
    }
    finally
    {
        Release(xlBorderBottom);
        Release(xlBorders);
        Release(xlInterior);
        Release(xlFont);
        Release(xlStyles);
    }
}

private void Release(object obj)
{
    // Errors are ignored per Microsoft's suggestion for this type of function:
    // http://support.microsoft.com/default.aspx/kb/317109
    try
    {
        System.Runtime.InteropServices.Marshal.ReleaseComObject(obj);
    }
    catch { } 
}

обратите внимание, что я должен был установить xlBorders[Excel.XlBordersIndex.xlEdgeBottom] переменной, чтобы очистить это (не из-за двух точек, которые ссылаются на перечисление, которое не нужно освобождать, но потому, что объект, на который я ссылаюсь, на самом деле является пограничным объектом, который должен быть освобожден).

этот вид вещи действительно не необходим в стандартных применениях, которые делают большую работу очищать вверх после себя, но в ASP.NET приложения, если вы пропустите хотя бы один из них, независимо от того, как часто вы вызываете сборщик мусора, Excel все равно будет работать на вашем сервере.

это требует много внимания к деталям и многих тестовых исполнений во время мониторинга Диспетчера задач при написании этого кода, но это избавляет вас от хлопот отчаянного поиска по страницам кода, чтобы найти один экземпляр, который вы пропустили. Это особенно важно при работе в циклах, где необходимо освободить каждый экземпляр объекта, даже если он использует одно и то же имя переменной при каждом цикле.


чтобы добавить к причинам, почему 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

это, конечно, кажется, что это было слишком сложно. По моему опыту, есть только три ключевые вещи, чтобы заставить Excel правильно закрыть:

1: Убедитесь, что нет оставшихся ссылок на приложение excel, которое вы создали (вы все равно должны иметь только один; установите его в null)

2: вызов GC.Collect()

3: Excel должен быть закрыт, либо когда пользователь закрывает программу, или ты называешь Quit на объекте Excel. (Обратите внимание, что Quit будет работать так же, как если бы пользователь попытался закрыть программу, и представит диалоговое окно подтверждения, если есть несохраненные изменения, даже если Excel не отображается. Пользователь может нажать кнопку Отмена, а затем Excel не будет закрыт.)

1 должно произойти до 2, но 3 может произойти в любое время.

один из способов реализовать это-обернуть объект interop Excel вашим собственным классом, создать экземпляр interop в конструкторе и реализовать IDisposable с помощью Dispose ищу что-то вроде

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

это очистит excel от стороны вашей программы. Как только Excel будет закрыт (вручную пользователем или вами, вызывающим Quit) процесс уйдет. Если программа уже закрыта, то процесс исчезнет на GC.Collect() звонок.

(Я не уверен, насколько это важно, но вы можете хотеть GC.WaitForPendingFinalizers() звонить после GC.Collect() вызов, но это не обязательно, чтобы избавиться от Excel процесс.)

это работает для меня без проблем в течение многих лет. Имейте в виду, что, хотя это работает, вы на самом деле должны закрыть изящно, чтобы он работал. Вы все равно получите накопление excel.exe-процессы, если вы прерываете свою программу до того, как 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();
    }
}

Я традиционно следовал совету, найденному в ответ ВВС. Однако, чтобы держать этот ответ в курсе последних вариантов, я думаю, что все мои будущие проекты будут использовать библиотеку "NetOffice".

NetOffice полная замена для офиса PIAs и вполне версия-агностик. Это коллекция управляемых оболочек COM, которые могут обрабатывать очистку, которая часто вызывает такие головные боли при работе с Microsoft Office в - ... Сеть.

ключевые особенности:

  • в основном версии независимых (и версии зависимые функции документируются)
  • никаких зависимостей
  • нет PIA
  • Регистрация
  • нет VSTO

Я никоим образом не связан с проектом; я просто искренне ценю резкое сокращение головных болей.


° º ø ø "стрелять 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);
}

вы должны знать, что Excel очень чувствителен к культуре, под которой вы работаете.

вы можете обнаружить, что вам нужно установить культуру EN-US перед вызовом функций Excel. Это относится не ко всем функциям, но к некоторым из них.

    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


я следовал этому точно... Но я все еще сталкивался с проблемами 1 из 1000 раз. Кто знает, почему. Пора вытащить молоток...

сразу после создания экземпляра класса приложения Excel я получаю доступ к Только что созданному процессу Excel.

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

затем, как только я сделал все вышеперечисленные очистки COM, я удостоверяюсь, что этот процесс не запущен. Если он все еще работает, убейте его!

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

"никогда не используйте две точки с COM-объектами" - отличное эмпирическое правило, чтобы избежать утечки com-ссылок, но Excel PIA может привести к утечке больше способов, чем кажется на первый взгляд.

одним из этих способов является подписка на любое событие, предоставляемое любым из COM-объектов объектной модели Excel.

например, подписка на событие WorkbookOpen класса приложения.

некоторая теория о COM-событиях

классы COM предоставляют группу событий через интерфейсы обратного вызова. Чтобы подписаться на события, клиентский код может просто зарегистрировать объект, реализующий интерфейс обратного вызова, и COM-класс вызовет свои методы в ответ на определенные события. Поскольку интерфейс обратного вызова является com-интерфейсом, в обязанности реализующего объекта входит уменьшение количества ссылок на любой COM-объект, который он получает (в качестве параметра) для любого из обработчиков событий.

как Excel PIA выставить COM события

Excel PIA предоставляет COM-события класса приложения Excel как обычные события .NET. Всякий раз, когда клиентский код подписывается на событие .NET (ударение на "a"), PIA создает an экземпляр класса, реализующего интерфейс обратного вызова, и регистрирует его в Excel.

следовательно, ряд объектов обратного вызова регистрируются в Excel в ответ на различные запросы подписки из кода .NET. Один объект обратного вызова на подписку на событие.

обратный звонок интерфейс для обработки событий означает, что PIA должен подписаться на все события интерфейса для каждого запроса подписки на событие .NET. Он не может выбирать. При получении обратного вызова события объект обратного вызова проверяет, заинтересован ли связанный обработчик событий .NET в текущем событии или нет, а затем либо вызывает обработчик, либо молча игнорирует обратный вызов.

влияние на количество ссылок экземпляра COM

все эти объекты обратного вызова не уменьшают количество ссылок на любой из COM-объектов, которые они получают (как параметры) для любого из методов обратного вызова (даже для тех, которые молча игнорируются). Они полагаются исключительно на CLR сборщик мусора, чтобы освободить COM-объекты.

поскольку GC run является недетерминированным, это может привести к задержке процесса Excel на более длительный срок, чем требуется, и создать впечатление "утечки памяти".

решение

единственное решение на данный момент чтобы избежать поставщика событий PIA для COM-класса и написать свой собственный поставщик событий, который детерминированно освобождает COM-объекты.

для класса приложений это можно сделать, реализовав интерфейс AppEvents, а затем зарегистрировав реализацию в Excel с помощью интерфейс IConnectionPointContainer. Класс приложения (и, если на то пошло, все COM-объекты, представляющие события с помощью механизма обратного вызова) реализует IConnectionPointContainer взаимодействие.


как указывали другие, вам нужно создать явную ссылку для каждого используемого объекта Excel и вызвать Маршал.ReleaseComObject по этой ссылке, как описано в эта статья KB. Вам также нужно использовать 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.

но этот подход плохо масштабируется для более сложных требований.

Это большой недостаток взаимодействия .NET COM. Для более сложных сценариев я бы серьезно рассмотрел возможность написания библиотеки ActiveX DLL на VB6 или другом неуправляемом языке, на который вы можете делегировать все взаимодействие с внешними COM-объектами, такими как Office. Затем можно ссылаться на эту библиотеку DLL ActiveX из приложения .NET и все будет намного проще, так как вам нужно будет только выпустить эту ссылку.


когда все вышеперечисленное не сработало, попробуйте дать Excel некоторое время, чтобы закрыть его листы:

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

...
FinalReleaseComObject(app);

убедитесь, что вы освобождаете все объекты, связанные с Excel!

Я потратил несколько часов, попробовав несколько способов. Все отличные идеи, но я, наконец, нашел свою ошибку:если вы не отпустите все объекты, ни один из способов выше не может помочь вам как в моем случае. Убедитесь, что вы отпустите все объекты, включая range one!

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

параметры вместе здесь.


правило двух точек не работает для меня. В моем случае я создал метод очистки моих ресурсов следующим образом:

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

вы должны быть очень осторожны, используя приложения Word / Excel interop. После попытки всех решений у нас все еще было много процесса "WinWord", оставленного открытым на сервере (с более чем 2000 пользователями).

после работы над проблемой в течение нескольких часов я понял, что если я открою больше, чем пару документов, используя Word.ApplicationClass.Document.Open() в разных потоках одновременно рабочий процесс IIS (w3wp.exe) будет сбой, оставляя все процессы WinWord открытыми!

поэтому я думаю, что нет абсолютного решения к этой проблеме, но переключение на другие методы, такие как Office Open XML развитие.


отличная статья о выпуске COM-объектов - 2.5 освобождение COM-объектов (MSDN).

метод, который я бы рекомендовал, - это null вашего Excel.Ссылки взаимодействия, если они являются нелокальными переменными, а затем вызовите 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. Однако обратите внимание, что вам вряд ли когда-либо понадобится использовать этот метод.


мое решение

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

принятый ответ не работа для меня. Следующий код в деструкторе сделал свою работу.

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 automation и наткнулся на решение для этого, которое работает каждый раз для меня. Это просто и не включает в себя убийство каких-либо процессов.

Кажется, что просто цикл через текущие активные процессы и любым способом "доступ" к открытому процессу Excel, любой случайный висячий экземпляр Excel будет удален. Приведенный ниже код просто проверяет процессы, где имя "Excel", а затем записывает свойство MainWindowTitle перейти к строке. Это "взаимодействие" с процессом, похоже, заставляет Windows догонять и прерывать замороженный экземпляр Excel.

Я запускаю метод ниже непосредственно перед надстройкой, в которой я разрабатываю выходы, так как он запускает событие разгрузки. Он удаляет все висячие экземпляры Excel каждый раз. Честно говоря, я не совсем уверен, почему это работает, но он хорошо работает для меня и может быть помещен в конце любого приложения Excel, не беспокоясь о двойных точках, Маршал.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;
                    }
                }
            }
}

Я думаю, что некоторые из них-это просто способ, которым платформа обрабатывает приложения 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 закрываются без проблем (в режиме отладки), поэтому также удаляются временные файлы. Я продолжу тестирование и напишу еще несколько, если получу некоторые обновления.

дно строка: вы также должны проверить код инициализации, особенно если у вас много классов и т. д.